| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * |
| * BlueZ - Bluetooth protocol stack for Linux |
| * |
| * Copyright (C) 2020 Intel Corporation. All rights reserved. |
| * Copyright 2023-2024 NXP |
| * |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #define _GNU_SOURCE |
| #include <stdio.h> |
| #include <stdbool.h> |
| #include <inttypes.h> |
| #include <errno.h> |
| #include <unistd.h> |
| #include <stdlib.h> |
| #include <fcntl.h> |
| #include <string.h> |
| #include <sys/ioctl.h> |
| #include <sys/uio.h> |
| #include <wordexp.h> |
| #include <sys/timerfd.h> |
| #include <sys/stat.h> |
| |
| #include <glib.h> |
| |
| #include "gdbus/gdbus.h" |
| |
| #include "lib/bluetooth.h" |
| #include "lib/uuid.h" |
| #include "lib/iso.h" |
| |
| #include "profiles/audio/a2dp-codecs.h" |
| #include "src/shared/lc3.h" |
| |
| #include "src/shared/util.h" |
| #include "src/shared/shell.h" |
| #include "src/shared/io.h" |
| #include "src/shared/queue.h" |
| #include "src/shared/bap-debug.h" |
| #include "print.h" |
| #include "player.h" |
| |
| /* String display constants */ |
| #define COLORED_NEW COLOR_GREEN "NEW" COLOR_OFF |
| #define COLORED_CHG COLOR_YELLOW "CHG" COLOR_OFF |
| #define COLORED_DEL COLOR_RED "DEL" COLOR_OFF |
| |
| #define BLUEZ_MEDIA_INTERFACE "org.bluez.Media1" |
| #define BLUEZ_MEDIA_PLAYER_INTERFACE "org.bluez.MediaPlayer1" |
| #define BLUEZ_MEDIA_FOLDER_INTERFACE "org.bluez.MediaFolder1" |
| #define BLUEZ_MEDIA_ITEM_INTERFACE "org.bluez.MediaItem1" |
| #define BLUEZ_MEDIA_ENDPOINT_INTERFACE "org.bluez.MediaEndpoint1" |
| #define BLUEZ_MEDIA_TRANSPORT_INTERFACE "org.bluez.MediaTransport1" |
| |
| #define BLUEZ_MEDIA_ENDPOINT_PATH "/local/endpoint" |
| |
| #define NSEC_USEC(_t) (_t / 1000L) |
| #define SEC_USEC(_t) (_t * 1000000L) |
| #define TS_USEC(_ts) (SEC_USEC((_ts)->tv_sec) + NSEC_USEC((_ts)->tv_nsec)) |
| #define ROUND_CLOSEST(_x, _y) (((_x) + (_y / 2)) / (_y)) |
| |
| #define EP_SRC_LOCATIONS 0x00000003 |
| #define EP_SNK_LOCATIONS 0x00000003 |
| |
| #define EP_SRC_CTXT 0x000f |
| #define EP_SUPPORTED_SRC_CTXT EP_SRC_CTXT |
| #define EP_SNK_CTXT 0x0fff |
| #define EP_SUPPORTED_SNK_CTXT EP_SNK_CTXT |
| |
| #if __BYTE_ORDER == __LITTLE_ENDIAN |
| |
| struct avdtp_media_codec_capability { |
| uint8_t rfa0:4; |
| uint8_t media_type:4; |
| uint8_t media_codec_type; |
| uint8_t data[0]; |
| } __attribute__ ((packed)); |
| |
| #elif __BYTE_ORDER == __BIG_ENDIAN |
| |
| struct avdtp_media_codec_capability { |
| uint8_t media_type:4; |
| uint8_t rfa0:4; |
| uint8_t media_codec_type; |
| uint8_t data[0]; |
| } __attribute__ ((packed)); |
| |
| #else |
| #error "Unknown byte order" |
| #endif |
| |
| #define BCAST_CODE {0x01, 0x02, 0x68, 0x05, 0x53, 0xf1, 0x41, 0x5a, \ |
| 0xa2, 0x65, 0xbb, 0xaf, 0xc6, 0xea, 0x03, 0xb8} |
| |
| struct endpoint { |
| char *path; |
| char *uuid; |
| uint8_t codec; |
| uint16_t cid; |
| uint16_t vid; |
| struct iovec *caps; |
| struct iovec *meta; |
| uint32_t locations; |
| uint16_t supported_context; |
| uint16_t context; |
| bool auto_accept; |
| uint8_t max_transports; |
| uint8_t iso_group; |
| uint8_t iso_stream; |
| struct queue *acquiring; |
| struct queue *transports; |
| DBusMessage *msg; |
| struct preset *preset; |
| bool broadcast; |
| struct iovec *bcode; |
| }; |
| |
| static DBusConnection *dbus_conn; |
| static GDBusProxy *default_player; |
| static GList *medias = NULL; |
| static GList *players = NULL; |
| static GList *folders = NULL; |
| static GList *items = NULL; |
| static GList *endpoints = NULL; |
| static GList *local_endpoints = NULL; |
| static GList *transports = NULL; |
| static struct queue *ios = NULL; |
| static uint8_t bcast_code[] = BCAST_CODE; |
| |
| struct transport { |
| GDBusProxy *proxy; |
| int sk; |
| uint16_t mtu[2]; |
| char *filename; |
| int fd; |
| struct stat stat; |
| struct io *io; |
| uint32_t seq; |
| struct io *timer_io; |
| int num; |
| }; |
| |
| static void endpoint_unregister(void *data) |
| { |
| struct endpoint *ep = data; |
| |
| bt_shell_printf("Endpoint %s unregistered\n", ep->path); |
| g_dbus_unregister_interface(dbus_conn, ep->path, |
| BLUEZ_MEDIA_ENDPOINT_INTERFACE); |
| } |
| |
| static void disconnect_handler(DBusConnection *connection, void *user_data) |
| { |
| g_list_free_full(local_endpoints, endpoint_unregister); |
| local_endpoints = NULL; |
| } |
| |
| static bool check_default_player(void) |
| { |
| if (!default_player) { |
| bt_shell_printf("No default player available\n"); |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| static char *generic_generator(const char *text, int state, GList *source) |
| { |
| static int index = 0; |
| |
| if (!source) |
| return NULL; |
| |
| if (!state) |
| index = 0; |
| |
| return g_dbus_proxy_path_lookup(source, &index, text); |
| } |
| |
| static char *player_generator(const char *text, int state) |
| { |
| return generic_generator(text, state, players); |
| } |
| |
| static char *item_generator(const char *text, int state) |
| { |
| return generic_generator(text, state, items); |
| } |
| |
| static void play_reply(DBusMessage *message, void *user_data) |
| { |
| DBusError error; |
| |
| dbus_error_init(&error); |
| |
| if (dbus_set_error_from_message(&error, message) == TRUE) { |
| bt_shell_printf("Failed to play: %s\n", error.name); |
| dbus_error_free(&error); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Play successful\n"); |
| |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| static void cmd_play(int argc, char *argv[]) |
| { |
| GDBusProxy *proxy; |
| |
| if (argc > 1) { |
| proxy = g_dbus_proxy_lookup(items, NULL, argv[1], |
| BLUEZ_MEDIA_ITEM_INTERFACE); |
| if (proxy == NULL) { |
| bt_shell_printf("Item %s not available\n", argv[1]); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| } else { |
| if (!check_default_player()) |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| proxy = default_player; |
| } |
| |
| if (g_dbus_proxy_method_call(proxy, "Play", NULL, play_reply, |
| NULL, NULL) == FALSE) { |
| bt_shell_printf("Failed to play\n"); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Attempting to play %s\n", argv[1] ? : ""); |
| } |
| |
| static void pause_reply(DBusMessage *message, void *user_data) |
| { |
| DBusError error; |
| |
| dbus_error_init(&error); |
| |
| if (dbus_set_error_from_message(&error, message) == TRUE) { |
| bt_shell_printf("Failed to pause: %s\n", error.name); |
| dbus_error_free(&error); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Pause successful\n"); |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static void cmd_pause(int argc, char *argv[]) |
| { |
| if (!check_default_player()) |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| |
| if (g_dbus_proxy_method_call(default_player, "Pause", NULL, |
| pause_reply, NULL, NULL) == FALSE) { |
| bt_shell_printf("Failed to play\n"); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Attempting to pause\n"); |
| } |
| |
| static void stop_reply(DBusMessage *message, void *user_data) |
| { |
| DBusError error; |
| |
| dbus_error_init(&error); |
| |
| if (dbus_set_error_from_message(&error, message) == TRUE) { |
| bt_shell_printf("Failed to stop: %s\n", error.name); |
| dbus_error_free(&error); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Stop successful\n"); |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static void cmd_stop(int argc, char *argv[]) |
| { |
| if (!check_default_player()) |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| |
| if (g_dbus_proxy_method_call(default_player, "Stop", NULL, stop_reply, |
| NULL, NULL) == FALSE) { |
| bt_shell_printf("Failed to stop\n"); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Attempting to stop\n"); |
| } |
| |
| static void next_reply(DBusMessage *message, void *user_data) |
| { |
| DBusError error; |
| |
| dbus_error_init(&error); |
| |
| if (dbus_set_error_from_message(&error, message) == TRUE) { |
| bt_shell_printf("Failed to jump to next: %s\n", error.name); |
| dbus_error_free(&error); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Next successful\n"); |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static void cmd_next(int argc, char *argv[]) |
| { |
| if (!check_default_player()) |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| |
| if (g_dbus_proxy_method_call(default_player, "Next", NULL, next_reply, |
| NULL, NULL) == FALSE) { |
| bt_shell_printf("Failed to jump to next\n"); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Attempting to jump to next\n"); |
| } |
| |
| static void previous_reply(DBusMessage *message, void *user_data) |
| { |
| DBusError error; |
| |
| dbus_error_init(&error); |
| |
| if (dbus_set_error_from_message(&error, message) == TRUE) { |
| bt_shell_printf("Failed to jump to previous: %s\n", error.name); |
| dbus_error_free(&error); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Previous successful\n"); |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static void cmd_previous(int argc, char *argv[]) |
| { |
| if (!check_default_player()) |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| |
| if (g_dbus_proxy_method_call(default_player, "Previous", NULL, |
| previous_reply, NULL, NULL) == FALSE) { |
| bt_shell_printf("Failed to jump to previous\n"); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Attempting to jump to previous\n"); |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static void fast_forward_reply(DBusMessage *message, void *user_data) |
| { |
| DBusError error; |
| |
| dbus_error_init(&error); |
| |
| if (dbus_set_error_from_message(&error, message) == TRUE) { |
| bt_shell_printf("Failed to fast forward: %s\n", error.name); |
| dbus_error_free(&error); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("FastForward successful\n"); |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static void cmd_fast_forward(int argc, char *argv[]) |
| { |
| if (!check_default_player()) |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| |
| if (g_dbus_proxy_method_call(default_player, "FastForward", NULL, |
| fast_forward_reply, NULL, NULL) == FALSE) { |
| bt_shell_printf("Failed to jump to previous\n"); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Fast forward playback\n"); |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static void rewind_reply(DBusMessage *message, void *user_data) |
| { |
| DBusError error; |
| |
| dbus_error_init(&error); |
| |
| if (dbus_set_error_from_message(&error, message) == TRUE) { |
| bt_shell_printf("Failed to rewind: %s\n", error.name); |
| dbus_error_free(&error); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Rewind successful\n"); |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static void cmd_rewind(int argc, char *argv[]) |
| { |
| if (!check_default_player()) |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| |
| if (g_dbus_proxy_method_call(default_player, "Rewind", NULL, |
| rewind_reply, NULL, NULL) == FALSE) { |
| bt_shell_printf("Failed to rewind\n"); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Rewind playback\n"); |
| } |
| |
| static void generic_callback(const DBusError *error, void *user_data) |
| { |
| char *str = user_data; |
| |
| if (dbus_error_is_set(error)) { |
| bt_shell_printf("Failed to set %s: %s\n", str, error->name); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } else { |
| bt_shell_printf("Changing %s succeeded\n", str); |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| } |
| |
| static void cmd_equalizer(int argc, char *argv[]) |
| { |
| char *value; |
| DBusMessageIter iter; |
| |
| if (!check_default_player()) |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| |
| if (!g_dbus_proxy_get_property(default_player, "Equalizer", &iter)) { |
| bt_shell_printf("Operation not supported\n"); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| value = g_strdup(argv[1]); |
| |
| if (g_dbus_proxy_set_property_basic(default_player, "Equalizer", |
| DBUS_TYPE_STRING, &value, |
| generic_callback, value, |
| g_free) == FALSE) { |
| bt_shell_printf("Failed to setting equalizer\n"); |
| g_free(value); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Attempting to set equalizer\n"); |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static void cmd_repeat(int argc, char *argv[]) |
| { |
| char *value; |
| DBusMessageIter iter; |
| |
| if (!check_default_player()) |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| |
| if (!g_dbus_proxy_get_property(default_player, "Repeat", &iter)) { |
| bt_shell_printf("Operation not supported\n"); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| value = g_strdup(argv[1]); |
| |
| if (g_dbus_proxy_set_property_basic(default_player, "Repeat", |
| DBUS_TYPE_STRING, &value, |
| generic_callback, value, |
| g_free) == FALSE) { |
| bt_shell_printf("Failed to set repeat\n"); |
| g_free(value); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Attempting to set repeat\n"); |
| } |
| |
| static void cmd_shuffle(int argc, char *argv[]) |
| { |
| char *value; |
| DBusMessageIter iter; |
| |
| if (!check_default_player()) |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| |
| if (!g_dbus_proxy_get_property(default_player, "Shuffle", &iter)) { |
| bt_shell_printf("Operation not supported\n"); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| value = g_strdup(argv[1]); |
| |
| if (g_dbus_proxy_set_property_basic(default_player, "Shuffle", |
| DBUS_TYPE_STRING, &value, |
| generic_callback, value, |
| g_free) == FALSE) { |
| bt_shell_printf("Failed to set shuffle\n"); |
| g_free(value); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Attempting to set shuffle\n"); |
| } |
| |
| static void cmd_scan(int argc, char *argv[]) |
| { |
| char *value; |
| DBusMessageIter iter; |
| |
| if (!check_default_player()) |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| |
| if (!g_dbus_proxy_get_property(default_player, "Shuffle", &iter)) { |
| bt_shell_printf("Operation not supported\n"); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| value = g_strdup(argv[1]); |
| |
| if (g_dbus_proxy_set_property_basic(default_player, "Shuffle", |
| DBUS_TYPE_STRING, &value, |
| generic_callback, value, |
| g_free) == FALSE) { |
| bt_shell_printf("Failed to set scan\n"); |
| g_free(value); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Attempting to set scan\n"); |
| } |
| |
| static char *proxy_description(GDBusProxy *proxy, const char *title, |
| const char *description) |
| { |
| const char *path; |
| |
| path = g_dbus_proxy_get_path(proxy); |
| |
| return g_strdup_printf("%s%s%s%s %s ", |
| description ? "[" : "", |
| description ? : "", |
| description ? "] " : "", |
| title, path); |
| } |
| |
| static void print_media(GDBusProxy *proxy, const char *description) |
| { |
| char *str; |
| |
| str = proxy_description(proxy, "Media", description); |
| |
| bt_shell_printf("%s\n", str); |
| print_property(proxy, "SupportedUUIDs"); |
| |
| g_free(str); |
| } |
| |
| static void print_player(void *data, void *user_data) |
| { |
| GDBusProxy *proxy = data; |
| const char *description = user_data; |
| char *str; |
| |
| str = proxy_description(proxy, "Player", description); |
| |
| bt_shell_printf("%s%s\n", str, |
| default_player == proxy ? "[default]" : ""); |
| |
| g_free(str); |
| } |
| |
| static void cmd_list(int argc, char *arg[]) |
| { |
| g_list_foreach(players, print_player, NULL); |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static void cmd_show_item(int argc, char *argv[]) |
| { |
| GDBusProxy *proxy; |
| |
| proxy = g_dbus_proxy_lookup(items, NULL, argv[1], |
| BLUEZ_MEDIA_ITEM_INTERFACE); |
| if (!proxy) { |
| bt_shell_printf("Item %s not available\n", argv[1]); |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| bt_shell_printf("Item %s\n", g_dbus_proxy_get_path(proxy)); |
| |
| print_property(proxy, "Player"); |
| print_property(proxy, "Name"); |
| print_property(proxy, "Type"); |
| print_property(proxy, "FolderType"); |
| print_property(proxy, "Playable"); |
| print_property(proxy, "Metadata"); |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static void cmd_show(int argc, char *argv[]) |
| { |
| GDBusProxy *proxy; |
| GDBusProxy *folder; |
| GDBusProxy *item; |
| DBusMessageIter iter; |
| const char *path; |
| |
| if (argc < 2) { |
| if (check_default_player() == FALSE) |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| |
| proxy = default_player; |
| } else { |
| proxy = g_dbus_proxy_lookup(players, NULL, argv[1], |
| BLUEZ_MEDIA_PLAYER_INTERFACE); |
| if (!proxy) { |
| bt_shell_printf("Player %s not available\n", argv[1]); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| } |
| |
| bt_shell_printf("Player %s\n", g_dbus_proxy_get_path(proxy)); |
| |
| print_property(proxy, "Name"); |
| print_property(proxy, "Repeat"); |
| print_property(proxy, "Equalizer"); |
| print_property(proxy, "Shuffle"); |
| print_property(proxy, "Scan"); |
| print_property(proxy, "Status"); |
| print_property(proxy, "Position"); |
| print_property(proxy, "Track"); |
| |
| folder = g_dbus_proxy_lookup(folders, NULL, |
| g_dbus_proxy_get_path(proxy), |
| BLUEZ_MEDIA_FOLDER_INTERFACE); |
| if (folder == NULL) |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| |
| bt_shell_printf("Folder %s\n", g_dbus_proxy_get_path(proxy)); |
| |
| print_property(folder, "Name"); |
| print_property(folder, "NumberOfItems"); |
| |
| if (!g_dbus_proxy_get_property(proxy, "Playlist", &iter)) |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| |
| dbus_message_iter_get_basic(&iter, &path); |
| |
| item = g_dbus_proxy_lookup(items, NULL, path, |
| BLUEZ_MEDIA_ITEM_INTERFACE); |
| if (item == NULL) |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| |
| bt_shell_printf("Playlist %s\n", path); |
| |
| print_property(item, "Name"); |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static void cmd_select(int argc, char *argv[]) |
| { |
| GDBusProxy *proxy; |
| |
| proxy = g_dbus_proxy_lookup(players, NULL, argv[1], |
| BLUEZ_MEDIA_PLAYER_INTERFACE); |
| if (proxy == NULL) { |
| bt_shell_printf("Player %s not available\n", argv[1]); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| if (default_player == proxy) |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| |
| default_player = proxy; |
| print_player(proxy, NULL); |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static void change_folder_reply(DBusMessage *message, void *user_data) |
| { |
| DBusError error; |
| |
| dbus_error_init(&error); |
| |
| if (dbus_set_error_from_message(&error, message) == TRUE) { |
| bt_shell_printf("Failed to change folder: %s\n", error.name); |
| dbus_error_free(&error); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("ChangeFolder successful\n"); |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static void change_folder_setup(DBusMessageIter *iter, void *user_data) |
| { |
| const char *path = user_data; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, &path); |
| } |
| |
| static void cmd_change_folder(int argc, char *argv[]) |
| { |
| GDBusProxy *proxy; |
| |
| if (dbus_validate_path(argv[1], NULL) == FALSE) { |
| bt_shell_printf("Not a valid path\n"); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| if (check_default_player() == FALSE) |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| |
| proxy = g_dbus_proxy_lookup(folders, NULL, |
| g_dbus_proxy_get_path(default_player), |
| BLUEZ_MEDIA_FOLDER_INTERFACE); |
| if (proxy == NULL) { |
| bt_shell_printf("Operation not supported\n"); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| if (g_dbus_proxy_method_call(proxy, "ChangeFolder", change_folder_setup, |
| change_folder_reply, argv[1], NULL) == FALSE) { |
| bt_shell_printf("Failed to change current folder\n"); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Attempting to change folder\n"); |
| } |
| |
| struct list_items_args { |
| int start; |
| int end; |
| }; |
| |
| static void list_items_setup(DBusMessageIter *iter, void *user_data) |
| { |
| struct list_items_args *args = user_data; |
| DBusMessageIter dict; |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, |
| DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING |
| DBUS_TYPE_STRING_AS_STRING |
| DBUS_TYPE_VARIANT_AS_STRING |
| DBUS_DICT_ENTRY_END_CHAR_AS_STRING, |
| &dict); |
| |
| if (args->start < 0) |
| goto done; |
| |
| g_dbus_dict_append_entry(&dict, "Start", |
| DBUS_TYPE_UINT32, &args->start); |
| |
| if (args->end < 0) |
| goto done; |
| |
| g_dbus_dict_append_entry(&dict, "End", DBUS_TYPE_UINT32, &args->end); |
| |
| done: |
| dbus_message_iter_close_container(iter, &dict); |
| } |
| |
| static void list_items_reply(DBusMessage *message, void *user_data) |
| { |
| DBusError error; |
| |
| dbus_error_init(&error); |
| |
| if (dbus_set_error_from_message(&error, message) == TRUE) { |
| bt_shell_printf("Failed to list items: %s\n", error.name); |
| dbus_error_free(&error); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("ListItems successful\n"); |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static void cmd_list_items(int argc, char *argv[]) |
| { |
| GDBusProxy *proxy; |
| struct list_items_args *args; |
| |
| if (check_default_player() == FALSE) |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| |
| proxy = g_dbus_proxy_lookup(folders, NULL, |
| g_dbus_proxy_get_path(default_player), |
| BLUEZ_MEDIA_FOLDER_INTERFACE); |
| if (proxy == NULL) { |
| bt_shell_printf("Operation not supported\n"); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| args = g_new0(struct list_items_args, 1); |
| args->start = -1; |
| args->end = -1; |
| |
| if (argc < 2) |
| goto done; |
| |
| errno = 0; |
| args->start = strtol(argv[1], NULL, 10); |
| if (errno != 0) { |
| bt_shell_printf("%s(%d)\n", strerror(errno), errno); |
| g_free(args); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| if (argc < 3) |
| goto done; |
| |
| errno = 0; |
| args->end = strtol(argv[2], NULL, 10); |
| if (errno != 0) { |
| bt_shell_printf("%s(%d)\n", strerror(errno), errno); |
| g_free(args); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| done: |
| if (g_dbus_proxy_method_call(proxy, "ListItems", list_items_setup, |
| list_items_reply, args, g_free) == FALSE) { |
| bt_shell_printf("Failed to change current folder\n"); |
| g_free(args); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Attempting to list items\n"); |
| } |
| |
| static void search_setup(DBusMessageIter *iter, void *user_data) |
| { |
| char *string = user_data; |
| DBusMessageIter dict; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &string); |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, |
| DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING |
| DBUS_TYPE_STRING_AS_STRING |
| DBUS_TYPE_VARIANT_AS_STRING |
| DBUS_DICT_ENTRY_END_CHAR_AS_STRING, |
| &dict); |
| |
| dbus_message_iter_close_container(iter, &dict); |
| } |
| |
| static void search_reply(DBusMessage *message, void *user_data) |
| { |
| DBusError error; |
| |
| dbus_error_init(&error); |
| |
| if (dbus_set_error_from_message(&error, message) == TRUE) { |
| bt_shell_printf("Failed to search: %s\n", error.name); |
| dbus_error_free(&error); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Search successful\n"); |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static void cmd_search(int argc, char *argv[]) |
| { |
| GDBusProxy *proxy; |
| char *string; |
| |
| if (check_default_player() == FALSE) |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| |
| proxy = g_dbus_proxy_lookup(folders, NULL, |
| g_dbus_proxy_get_path(default_player), |
| BLUEZ_MEDIA_FOLDER_INTERFACE); |
| if (proxy == NULL) { |
| bt_shell_printf("Operation not supported\n"); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| string = g_strdup(argv[1]); |
| |
| if (g_dbus_proxy_method_call(proxy, "Search", search_setup, |
| search_reply, string, g_free) == FALSE) { |
| bt_shell_printf("Failed to search\n"); |
| g_free(string); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Attempting to search\n"); |
| } |
| |
| static void add_to_nowplaying_reply(DBusMessage *message, void *user_data) |
| { |
| DBusError error; |
| |
| dbus_error_init(&error); |
| |
| if (dbus_set_error_from_message(&error, message) == TRUE) { |
| bt_shell_printf("Failed to queue: %s\n", error.name); |
| dbus_error_free(&error); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("AddToNowPlaying successful\n"); |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static void cmd_queue(int argc, char *argv[]) |
| { |
| GDBusProxy *proxy; |
| |
| proxy = g_dbus_proxy_lookup(items, NULL, argv[1], |
| BLUEZ_MEDIA_ITEM_INTERFACE); |
| if (proxy == NULL) { |
| bt_shell_printf("Item %s not available\n", argv[1]); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| if (g_dbus_proxy_method_call(proxy, "AddtoNowPlaying", NULL, |
| add_to_nowplaying_reply, NULL, |
| NULL) == FALSE) { |
| bt_shell_printf("Failed to play\n"); |
| return bt_shell_noninteractive_quit(EXIT_FAILURE); |
| } |
| |
| bt_shell_printf("Attempting to queue %s\n", argv[1]); |
| } |
| |
| static const struct bt_shell_menu player_menu = { |
| .name = "player", |
| .desc = "Media Player Submenu", |
| .entries = { |
| { "list", NULL, cmd_list, "List available players" }, |
| { "show", "[player]", cmd_show, "Player information", |
| player_generator}, |
| { "select", "<player>", cmd_select, "Select default player", |
| player_generator}, |
| { "play", "[item]", cmd_play, "Start playback", |
| item_generator}, |
| { "pause", NULL, cmd_pause, "Pause playback" }, |
| { "stop", NULL, cmd_stop, "Stop playback" }, |
| { "next", NULL, cmd_next, "Jump to next item" }, |
| { "previous", NULL, cmd_previous, "Jump to previous item" }, |
| { "fast-forward", NULL, cmd_fast_forward, |
| "Fast forward playback" }, |
| { "rewind", NULL, cmd_rewind, "Rewind playback" }, |
| { "equalizer", "<on/off>", cmd_equalizer, |
| "Enable/Disable equalizer"}, |
| { "repeat", "<singletrack/alltrack/group/off>", cmd_repeat, |
| "Set repeat mode"}, |
| { "shuffle", "<alltracks/group/off>", cmd_shuffle, |
| "Set shuffle mode"}, |
| { "scan", "<alltracks/group/off>", cmd_scan, |
| "Set scan mode"}, |
| { "change-folder", "<item>", cmd_change_folder, |
| "Change current folder", |
| item_generator}, |
| { "list-items", "[start] [end]", cmd_list_items, |
| "List items of current folder" }, |
| { "search", "<string>", cmd_search, |
| "Search items containing string" }, |
| { "queue", "<item>", cmd_queue, "Add item to playlist queue", |
| item_generator}, |
| { "show-item", "<item>", cmd_show_item, "Show item information", |
| item_generator}, |
| {} }, |
| }; |
| |
| static char *local_endpoint_generator(const char *text, int state) |
| { |
| int len = strlen(text); |
| GList *l; |
| static int index = 0; |
| |
| if (!state) |
| index = 0; |
| |
| for (l = g_list_nth(local_endpoints, index); l; l = g_list_next(l)) { |
| struct endpoint *ep = l->data; |
| |
| index++; |
| |
| if (!strncasecmp(ep->path, text, len)) |
| return strdup(ep->path); |
| } |
| |
| return NULL; |
| } |
| |
| static char *endpoint_generator(const char *text, int state) |
| { |
| char *ret; |
| |
| ret = generic_generator(text, state, endpoints); |
| if (ret) |
| return ret; |
| |
| return local_endpoint_generator(text, state); |
| } |
| |
| static void print_endpoint(void *data, void *user_data) |
| { |
| GDBusProxy *proxy = data; |
| const char *description = user_data; |
| char *str; |
| |
| str = proxy_description(proxy, "Endpoint", description); |
| |
| bt_shell_printf("%s\n", str); |
| |
| g_free(str); |
| } |
| |
| static void cmd_list_endpoints(int argc, char *argv[]) |
| { |
| GList *l; |
| |
| if (argc > 1) { |
| if (strcmp("local", argv[1])) { |
| bt_shell_printf("Endpoint list %s not available\n", |
| argv[1]); |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| for (l = local_endpoints; l; l = g_list_next(l)) { |
| struct endpoint *ep = l->data; |
| |
| bt_shell_printf("%s\n", ep->path); |
| } |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| for (l = endpoints; l; l = g_list_next(l)) { |
| GDBusProxy *proxy = l->data; |
| print_endpoint(proxy, NULL); |
| } |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static void confirm_response(const char *input, void *user_data) |
| { |
| DBusMessage *msg = user_data; |
| |
| if (!strcasecmp(input, "y") || !strcasecmp(input, "yes")) |
| g_dbus_send_reply(dbus_conn, msg, DBUS_TYPE_INVALID); |
| else if (!strcasecmp(input, "n") || !strcmp(input, "no")) |
| g_dbus_send_error(dbus_conn, msg, "org.bluez.Error.Rejected", |
| NULL); |
| else |
| g_dbus_send_error(dbus_conn, msg, "org.bluez.Error.Canceled", |
| NULL); |
| } |
| |
| static DBusMessage *endpoint_set_configuration(DBusConnection *conn, |
| DBusMessage *msg, void *user_data) |
| { |
| struct endpoint *ep = user_data; |
| DBusMessageIter args, props; |
| const char *path; |
| |
| dbus_message_iter_init(msg, &args); |
| |
| dbus_message_iter_get_basic(&args, &path); |
| dbus_message_iter_next(&args); |
| |
| dbus_message_iter_recurse(&args, &props); |
| if (dbus_message_iter_get_arg_type(&props) != DBUS_TYPE_DICT_ENTRY) |
| return g_dbus_create_error(msg, |
| "org.bluez.Error.InvalidArguments", |
| NULL); |
| |
| bt_shell_printf("Endpoint: SetConfiguration\n"); |
| bt_shell_printf("\tTransport %s\n", path); |
| print_iter("\t", "Properties", &props); |
| |
| if (!ep->max_transports) { |
| bt_shell_printf("Maximum transports reached: rejecting\n"); |
| return g_dbus_create_error(msg, |
| "org.bluez.Error.Rejected", |
| "Maximum transports reached"); |
| } |
| |
| ep->max_transports--; |
| |
| if (!ep->transports) |
| ep->transports = queue_new(); |
| |
| queue_push_tail(ep->transports, strdup(path)); |
| |
| if (ep->auto_accept) { |
| bt_shell_printf("Auto Accepting...\n"); |
| return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); |
| } |
| |
| bt_shell_prompt_input("Endpoint", "Accept (yes/no):", confirm_response, |
| dbus_message_ref(msg)); |
| |
| return NULL; |
| } |
| |
| #define CODEC_CAPABILITIES(_name, _uuid, _codec_id, _data, _meta) \ |
| { \ |
| .name = _name, \ |
| .uuid = _uuid, \ |
| .codec_id = _codec_id, \ |
| .data = _data, \ |
| .meta = _meta, \ |
| } |
| |
| #define LC3_DATA(_freq, _duration, _chan_count, _len_min, _len_max) \ |
| UTIL_IOV_INIT(0x03, LC3_FREQ, _freq, _freq >> 8, \ |
| 0x02, LC3_DURATION, _duration, \ |
| 0x02, LC3_CHAN_COUNT, _chan_count, \ |
| 0x05, LC3_FRAME_LEN, _len_min, _len_min >> 8, \ |
| _len_max, _len_max >> 8) |
| |
| static const struct capabilities { |
| const char *name; |
| const char *uuid; |
| uint8_t codec_id; |
| struct iovec data; |
| struct iovec meta; |
| } caps[] = { |
| /* A2DP SBC Source: |
| * |
| * Channel Modes: Mono DualChannel Stereo JointStereo |
| * Frequencies: 16Khz 32Khz 44.1Khz 48Khz |
| * Subbands: 4 8 |
| * Blocks: 4 8 12 16 |
| * Bitpool Range: 2-64 |
| */ |
| CODEC_CAPABILITIES("a2dp_src/sbc", A2DP_SOURCE_UUID, A2DP_CODEC_SBC, |
| UTIL_IOV_INIT(0xff, 0xff, 2, 64), |
| UTIL_IOV_INIT()), |
| |
| /* A2DP SBC Sink: |
| * |
| * Channel Modes: Mono DualChannel Stereo JointStereo |
| * Frequencies: 16Khz 32Khz 44.1Khz 48Khz |
| * Subbands: 4 8 |
| * Blocks: 4 8 12 16 |
| * Bitpool Range: 2-64 |
| */ |
| CODEC_CAPABILITIES("a2dp_snk/sbc", A2DP_SINK_UUID, A2DP_CODEC_SBC, |
| UTIL_IOV_INIT(0xff, 0xff, 2, 64), |
| UTIL_IOV_INIT()), |
| |
| /* PAC LC3 Sink: |
| * |
| * Frequencies: 8Khz 11Khz 16Khz 22Khz 24Khz 32Khz 44.1Khz 48Khz |
| * Duration: 7.5 ms 10 ms |
| * Channel count: 3 |
| * Frame length: 26-240 |
| */ |
| CODEC_CAPABILITIES("pac_snk/lc3", PAC_SINK_UUID, LC3_ID, |
| LC3_DATA(LC3_FREQ_ANY, LC3_DURATION_ANY, 3u, 26, |
| 240), |
| UTIL_IOV_INIT()), |
| |
| /* PAC LC3 Source: |
| * |
| * Frequencies: 8Khz 11Khz 16Khz 22Khz 24Khz 32Khz 44.1Khz 48Khz |
| * Duration: 7.5 ms 10 ms |
| * Channel count: 3 |
| * Frame length: 26-240 |
| */ |
| CODEC_CAPABILITIES("pac_src/lc3", PAC_SOURCE_UUID, LC3_ID, |
| LC3_DATA(LC3_FREQ_ANY, LC3_DURATION_ANY, 3u, 26, |
| 240), |
| UTIL_IOV_INIT()), |
| |
| /* Broadcast LC3 Source: |
| * |
| * Frequencies: 8Khz 11Khz 16Khz 22Khz 24Khz 32Khz 44.1Khz 48Khz |
| * Duration: 7.5 ms 10 ms |
| * Channel count: 3 |
| * Frame length: 26-240 |
| */ |
| CODEC_CAPABILITIES("bcaa/lc3", BCAA_SERVICE_UUID, LC3_ID, |
| LC3_DATA(LC3_FREQ_ANY, LC3_DURATION_ANY, 3u, 26, |
| 240), |
| UTIL_IOV_INIT()), |
| |
| /* Broadcast LC3 Sink: |
| * |
| * Frequencies: 8Khz 11Khz 16Khz 22Khz 24Khz 32Khz 44.1Khz 48Khz |
| * Duration: 7.5 ms 10 ms |
| * Channel count: 3 |
| * Frame length: 26-240 |
| */ |
| CODEC_CAPABILITIES("baa/lc3", BAA_SERVICE_UUID, LC3_ID, |
| LC3_DATA(LC3_FREQ_ANY, LC3_DURATION_ANY, 3u, 26, |
| 240), |
| UTIL_IOV_INIT()), |
| }; |
| |
| struct codec_preset { |
| char *name; |
| const struct iovec data; |
| struct bt_bap_qos qos; |
| uint8_t target_latency; |
| }; |
| |
| #define SBC_PRESET(_name, _data) \ |
| { \ |
| .name = _name, \ |
| .data = _data, \ |
| } |
| |
| static struct codec_preset sbc_presets[] = { |
| /* Table 4.7: Recommended sets of SBC parameters in the SRC device |
| * Other settings: Block length = 16, Allocation method = Loudness, |
| * Subbands = 8. |
| * A2DP spec sets maximum bitrates as follows: |
| * This profile limits the available maximum bit rate to 320kb/s for |
| * mono, and 512kb/s for two-channel modes. |
| */ |
| SBC_PRESET("MQ_MONO_44_1", |
| UTIL_IOV_INIT(0x28, 0x15, 2, SBC_BITPOOL_MQ_MONO_44100)), |
| SBC_PRESET("MQ_MONO_48", |
| UTIL_IOV_INIT(0x18, 0x15, 2, SBC_BITPOOL_MQ_MONO_48000)), |
| SBC_PRESET("MQ_STEREO_44_1", |
| UTIL_IOV_INIT(0x21, 0x15, 2, |
| SBC_BITPOOL_MQ_JOINT_STEREO_44100)), |
| SBC_PRESET("MQ_STEREO_48", |
| UTIL_IOV_INIT(0x11, 0x15, 2, |
| SBC_BITPOOL_MQ_JOINT_STEREO_48000)), |
| SBC_PRESET("HQ_MONO_44_1", |
| UTIL_IOV_INIT(0x28, 0x15, 2, SBC_BITPOOL_HQ_MONO_44100)), |
| SBC_PRESET("HQ_MONO_48", |
| UTIL_IOV_INIT(0x18, 0x15, 2, SBC_BITPOOL_HQ_MONO_48000)), |
| SBC_PRESET("HQ_STEREO_44_1", |
| UTIL_IOV_INIT(0x21, 0x15, 2, |
| SBC_BITPOOL_HQ_JOINT_STEREO_44100)), |
| SBC_PRESET("HQ_STEREO_48", |
| UTIL_IOV_INIT(0x11, 0x15, 2, |
| SBC_BITPOOL_HQ_JOINT_STEREO_48000)), |
| /* Higher bitrates not recommended by A2DP spec, it dual channel to |
| * avoid going above 53 bitpool: |
| * |
| * https://habr.com/en/post/456476/ |
| * https://gitlab.freedesktop.org/pulseaudio/pulseaudio/-/issues/1092 |
| */ |
| SBC_PRESET("XQ_DUAL_44_1", UTIL_IOV_INIT(0x24, 0x15, 2, 43)), |
| SBC_PRESET("XQ_DUAL_48", UTIL_IOV_INIT(0x14, 0x15, 2, 39)), |
| /* Ultra high bitpool that fits in 512 kbps mandatory bitrate */ |
| SBC_PRESET("UQ_STEREO_44_1", UTIL_IOV_INIT(0x21, 0x15, 2, 64)), |
| SBC_PRESET("UQ_STEREO_48", UTIL_IOV_INIT(0x11, 0x15, 2, 58)), |
| }; |
| |
| #define LC3_PRESET_LL(_name, _data, _qos) \ |
| { \ |
| .name = _name, \ |
| .data = _data, \ |
| .qos = _qos, \ |
| .target_latency = 0x01, \ |
| } |
| |
| #define LC3_PRESET(_name, _data, _qos) \ |
| { \ |
| .name = _name, \ |
| .data = _data, \ |
| .qos = _qos, \ |
| .target_latency = 0x02, \ |
| } |
| |
| #define LC3_PRESET_HR(_name, _data, _qos) \ |
| { \ |
| .name = _name, \ |
| .data = _data, \ |
| .qos = _qos, \ |
| .target_latency = 0x03, \ |
| } |
| |
| #define LC3_PRESET_B(_name, _data, _qos) \ |
| { \ |
| .name = _name, \ |
| .data = _data, \ |
| .qos = _qos, \ |
| .target_latency = 0x00, \ |
| } |
| |
| static struct codec_preset lc3_ucast_presets[] = { |
| /* Table 4.43: QoS configuration support setting requirements */ |
| LC3_PRESET("8_1_1", LC3_CONFIG_8_1, LC3_QOS_8_1_1), |
| LC3_PRESET("8_2_1", LC3_CONFIG_8_2, LC3_QOS_8_2_1), |
| LC3_PRESET("16_1_1", LC3_CONFIG_16_1, LC3_QOS_16_1_1), |
| LC3_PRESET("16_2_1", LC3_CONFIG_16_2, LC3_QOS_16_2_1), |
| LC3_PRESET("24_1_1", LC3_CONFIG_24_1, LC3_QOS_24_1_1), |
| LC3_PRESET("24_2_1", LC3_CONFIG_24_2, LC3_QOS_24_2_1), |
| LC3_PRESET("32_1_1", LC3_CONFIG_32_1, LC3_QOS_32_1_1), |
| LC3_PRESET("32_2_1", LC3_CONFIG_32_2, LC3_QOS_32_1_1), |
| LC3_PRESET("44_1_1", LC3_CONFIG_44_1, LC3_QOS_44_1_1), |
| LC3_PRESET("44_2_1", LC3_CONFIG_44_2, LC3_QOS_44_2_1), |
| LC3_PRESET("48_1_1", LC3_CONFIG_48_1, LC3_QOS_48_1_1), |
| LC3_PRESET("48_2_1", LC3_CONFIG_48_2, LC3_QOS_48_2_1), |
| LC3_PRESET("48_3_1", LC3_CONFIG_48_3, LC3_QOS_48_3_1), |
| LC3_PRESET("48_4_1", LC3_CONFIG_48_4, LC3_QOS_48_4_1), |
| LC3_PRESET("48_5_1", LC3_CONFIG_48_5, LC3_QOS_48_5_1), |
| LC3_PRESET("48_6_1", LC3_CONFIG_48_6, LC3_QOS_48_6_1), |
| /* QoS Configuration settings for high reliability audio data */ |
| LC3_PRESET_HR("8_1_2", LC3_CONFIG_8_1, LC3_QOS_8_1_2), |
| LC3_PRESET_HR("8_2_2", LC3_CONFIG_8_2, LC3_QOS_8_2_2), |
| LC3_PRESET_HR("16_1_2", LC3_CONFIG_16_1, LC3_QOS_16_1_2), |
| LC3_PRESET_HR("16_2_2", LC3_CONFIG_16_2, LC3_QOS_16_2_2), |
| LC3_PRESET_HR("24_1_2", LC3_CONFIG_24_1, LC3_QOS_24_1_2), |
| LC3_PRESET_HR("24_2_2", LC3_CONFIG_24_2, LC3_QOS_24_2_2), |
| LC3_PRESET_HR("32_1_2", LC3_CONFIG_32_1, LC3_QOS_32_1_2), |
| LC3_PRESET_HR("32_2_2", LC3_CONFIG_32_2, LC3_QOS_32_2_2), |
| LC3_PRESET_HR("44_1_2", LC3_CONFIG_44_1, LC3_QOS_44_1_2), |
| LC3_PRESET_HR("44_2_2", LC3_CONFIG_44_2, LC3_QOS_44_2_2), |
| LC3_PRESET_HR("48_1_2", LC3_CONFIG_48_1, LC3_QOS_48_1_2), |
| LC3_PRESET_HR("48_2_2", LC3_CONFIG_48_2, LC3_QOS_48_2_2), |
| LC3_PRESET_HR("48_3_2", LC3_CONFIG_48_3, LC3_QOS_48_3_2), |
| LC3_PRESET_HR("48_4_2", LC3_CONFIG_48_4, LC3_QOS_48_4_2), |
| LC3_PRESET_HR("48_5_2", LC3_CONFIG_48_5, LC3_QOS_48_5_2), |
| LC3_PRESET_HR("48_6_2", LC3_CONFIG_48_6, LC3_QOS_48_6_2), |
| /* QoS configuration support setting requirements for the UGG and UGT */ |
| LC3_PRESET_LL("16_1_gs", LC3_CONFIG_16_1, LC3_QOS_16_1_GS), |
| LC3_PRESET_LL("16_2_gs", LC3_CONFIG_16_2, LC3_QOS_16_2_GS), |
| LC3_PRESET_LL("32_1_gs", LC3_CONFIG_32_1, LC3_QOS_32_1_GS), |
| LC3_PRESET_LL("32_2_gs", LC3_CONFIG_32_2, LC3_QOS_32_2_GS), |
| LC3_PRESET_LL("48_1_gs", LC3_CONFIG_48_1, LC3_QOS_48_1_GS), |
| LC3_PRESET_LL("48_2_gs", LC3_CONFIG_48_2, LC3_QOS_48_2_GS), |
| LC3_PRESET_LL("32_1_gr", LC3_CONFIG_32_1, LC3_QOS_32_1_GR), |
| LC3_PRESET_LL("32_2_gr", LC3_CONFIG_32_2, LC3_QOS_32_2_GR), |
| LC3_PRESET_LL("48_1_gr", LC3_CONFIG_48_1, LC3_QOS_48_1_GR), |
| LC3_PRESET_LL("48_2_gr", LC3_CONFIG_48_2, LC3_QOS_48_2_GR), |
| LC3_PRESET_LL("48_3_gr", LC3_CONFIG_48_3, LC3_QOS_48_3_GR), |
| LC3_PRESET_LL("48_4_gr", LC3_CONFIG_48_4, LC3_QOS_48_4_GR), |
| LC3_PRESET_LL("32_1_gr_l+r", LC3_CONFIG_32_1_AC(2), |
| LC3_QOS_32_1_GR_AC(2)), |
| LC3_PRESET_LL("32_2_gr_l+r", LC3_CONFIG_32_2_AC(2), |
| LC3_QOS_32_2_GR_AC(2)), |
| LC3_PRESET_LL("48_1_gr_l+r", LC3_CONFIG_48_1_AC(2), |
| LC3_QOS_48_1_GR_AC(2)), |
| LC3_PRESET_LL("48_2_gr_l+r", LC3_CONFIG_48_2_AC(2), |
| LC3_QOS_48_2_GR_AC(2)), |
| LC3_PRESET_LL("48_3_gr_l+r", LC3_CONFIG_48_3_AC(2), |
| LC3_QOS_48_3_GR_AC(2)), |
| LC3_PRESET_LL("48_4_gr_l+r", LC3_CONFIG_48_4_AC(2), |
| LC3_QOS_48_4_GR_AC(2)), |
| }; |
| |
| static struct codec_preset lc3_bcast_presets[] = { |
| /* Table 6.4: Broadcast Audio Stream configuration support requirements |
| * for the Broadcast Source and Broadcast Sink |
| */ |
| LC3_PRESET_B("8_1_1", LC3_CONFIG_8_1, LC3_QOS_8_1_1_B), |
| LC3_PRESET_B("8_2_1", LC3_CONFIG_8_2, LC3_QOS_8_2_1_B), |
| LC3_PRESET_B("16_1_1", LC3_CONFIG_16_1, LC3_QOS_16_1_1_B), |
| LC3_PRESET_B("16_2_1", LC3_CONFIG_16_2, LC3_QOS_16_2_1_B), |
| LC3_PRESET_B("24_1_1", LC3_CONFIG_24_1, LC3_QOS_24_1_1_B), |
| LC3_PRESET_B("24_2_1", LC3_CONFIG_24_2, LC3_QOS_24_2_1_B), |
| LC3_PRESET_B("32_1_1", LC3_CONFIG_32_1, LC3_QOS_32_1_1_B), |
| LC3_PRESET_B("32_2_1", LC3_CONFIG_32_2, LC3_QOS_32_2_1_B), |
| LC3_PRESET_B("44_1_1", LC3_CONFIG_44_1, LC3_QOS_44_1_1_B), |
| LC3_PRESET_B("44_2_1", LC3_CONFIG_44_2, LC3_QOS_44_2_1_B), |
| LC3_PRESET_B("48_1_1", LC3_CONFIG_48_1, LC3_QOS_48_1_1_B), |
| LC3_PRESET_B("48_2_1", LC3_CONFIG_48_2, LC3_QOS_48_2_1_B), |
| LC3_PRESET_B("48_3_1", LC3_CONFIG_48_3, LC3_QOS_48_3_1_B), |
| LC3_PRESET_B("48_4_1", LC3_CONFIG_48_4, LC3_QOS_48_4_1_B), |
| LC3_PRESET_B("48_5_1", LC3_CONFIG_48_5, LC3_QOS_48_5_1_B), |
| LC3_PRESET_B("48_6_1", LC3_CONFIG_48_6, LC3_QOS_48_6_1_B), |
| /* Broadcast Audio Stream configuration settings for high-reliability |
| * audio data. |
| */ |
| LC3_PRESET_B("8_1_2", LC3_CONFIG_8_1, LC3_QOS_8_1_1_B), |
| LC3_PRESET_B("8_2_2", LC3_CONFIG_8_2, LC3_QOS_8_2_2_B), |
| LC3_PRESET_B("16_1_2", LC3_CONFIG_16_1, LC3_QOS_16_1_2_B), |
| LC3_PRESET_B("16_2_2", LC3_CONFIG_16_2, LC3_QOS_16_2_2_B), |
| LC3_PRESET_B("24_1_2", LC3_CONFIG_24_1, LC3_QOS_24_1_2_B), |
| LC3_PRESET_B("24_2_2", LC3_CONFIG_24_2, LC3_QOS_24_2_2_B), |
| LC3_PRESET_B("32_1_2", LC3_CONFIG_32_1, LC3_QOS_32_1_2_B), |
| LC3_PRESET_B("32_2_2", LC3_CONFIG_32_2, LC3_QOS_32_2_2_B), |
| LC3_PRESET_B("44_1_2", LC3_CONFIG_44_1, LC3_QOS_44_1_2_B), |
| LC3_PRESET_B("44_2_2", LC3_CONFIG_44_2, LC3_QOS_44_2_2_B), |
| LC3_PRESET_B("48_1_2", LC3_CONFIG_48_1, LC3_QOS_48_1_2_B), |
| LC3_PRESET_B("48_2_2", LC3_CONFIG_48_2, LC3_QOS_48_2_2_B), |
| LC3_PRESET_B("48_3_2", LC3_CONFIG_48_3, LC3_QOS_48_3_2_B), |
| LC3_PRESET_B("48_4_2", LC3_CONFIG_48_4, LC3_QOS_48_4_2_B), |
| LC3_PRESET_B("48_5_2", LC3_CONFIG_48_5, LC3_QOS_48_5_2_B), |
| LC3_PRESET_B("48_6_2", LC3_CONFIG_48_6, LC3_QOS_48_6_2_B), |
| }; |
| |
| static void print_ltv(const char *str, void *user_data) |
| { |
| const char *label = user_data; |
| |
| bt_shell_printf("\t%s.%s\n", label, str); |
| } |
| |
| static void print_lc3_caps(uint8_t *data, int len) |
| { |
| const char *label = "Capabilities"; |
| |
| bt_bap_debug_caps(data, len, print_ltv, (void *)label); |
| } |
| |
| static void print_lc3_cfg(void *data, int len) |
| { |
| const char *label = "Configuration"; |
| |
| bt_bap_debug_config(data, len, print_ltv, (void *)label); |
| } |
| |
| static void print_lc3_meta(void *data, int len) |
| { |
| const char *label = "Metadata"; |
| |
| bt_bap_debug_metadata(data, len, print_ltv, (void *)label); |
| } |
| |
| #define PRESET(_uuid, _codec, _presets, _default_index) \ |
| { \ |
| .uuid = _uuid, \ |
| .codec = _codec, \ |
| .custom = { .name = "custom" }, \ |
| .default_preset = &_presets[_default_index], \ |
| .presets = _presets, \ |
| .num_presets = ARRAY_SIZE(_presets), \ |
| } |
| |
| static struct preset { |
| const char *uuid; |
| uint8_t codec; |
| uint16_t cid; |
| uint16_t vid; |
| struct codec_preset custom; |
| struct codec_preset *default_preset; |
| struct codec_preset *presets; |
| size_t num_presets; |
| } presets[] = { |
| PRESET(A2DP_SOURCE_UUID, A2DP_CODEC_SBC, sbc_presets, 6), |
| PRESET(A2DP_SINK_UUID, A2DP_CODEC_SBC, sbc_presets, 6), |
| PRESET(PAC_SINK_UUID, LC3_ID, lc3_ucast_presets, 3), |
| PRESET(PAC_SOURCE_UUID, LC3_ID, lc3_ucast_presets, 3), |
| PRESET(BCAA_SERVICE_UUID, LC3_ID, lc3_bcast_presets, 3), |
| PRESET(BAA_SERVICE_UUID, LC3_ID, lc3_bcast_presets, 3), |
| }; |
| |
| static void parse_vendor_codec(const char *codec, uint16_t *vid, uint16_t *cid) |
| { |
| char **list; |
| char *endptr = NULL; |
| |
| if (!codec) |
| return; |
| |
| list = g_strsplit(codec, ":", 2); |
| |
| if (vid) |
| *vid = strtol(list[0], &endptr, 0); |
| |
| if (cid) |
| *cid = strtol(list[1], &endptr, 0); |
| |
| g_strfreev(list); |
| } |
| |
| static struct preset *find_presets(const char *uuid, uint8_t codec, |
| uint16_t vid, uint16_t cid) |
| { |
| size_t i; |
| |
| if (codec == 0xff) { |
| GList *l; |
| |
| for (l = local_endpoints; l; l = g_list_next(l)) { |
| struct endpoint *ep = l->data; |
| |
| if (strcasecmp(ep->uuid, uuid) || ep->codec != codec) |
| continue; |
| |
| if (ep->codec == 0xff && (ep->vid != vid || |
| ep->cid != cid)) |
| continue; |
| |
| return ep->preset; |
| } |
| |
| return NULL; |
| } |
| |
| for (i = 0; i < ARRAY_SIZE(presets); i++) { |
| struct preset *preset = &presets[i]; |
| |
| if (preset->codec != codec) |
| continue; |
| |
| if (!strcasecmp(preset->uuid, uuid)) |
| return preset; |
| } |
| |
| return NULL; |
| } |
| |
| static struct preset *find_vendor_presets(const char *uuid, const char *codec) |
| { |
| uint16_t cid; |
| uint16_t vid; |
| |
| if (!uuid || !codec) |
| return NULL; |
| |
| parse_vendor_codec(codec, &vid, &cid); |
| |
| return find_presets(uuid, 0xff, vid, cid); |
| } |
| |
| static struct preset *find_presets_name(const char *uuid, const char *codec) |
| { |
| uint8_t id; |
| char *endptr = NULL; |
| |
| if (!uuid || !codec) |
| return NULL; |
| |
| if (strrchr(codec, ':')) |
| return find_vendor_presets(uuid, codec); |
| |
| id = strtol(codec, &endptr, 0); |
| |
| return find_presets(uuid, id, 0x0000, 0x0000); |
| } |
| |
| static struct codec_preset *preset_find_name(struct preset *preset, |
| const char *name) |
| { |
| size_t i; |
| |
| if (!preset) |
| return NULL; |
| |
| if (!name) |
| return preset->default_preset; |
| else if (!strcmp(name, "custom")) |
| return &preset->custom; |
| |
| for (i = 0; i < preset->num_presets; i++) { |
| struct codec_preset *p; |
| |
| p = &preset->presets[i]; |
| |
| if (!strcmp(p->name, name)) |
| return p; |
| } |
| |
| return NULL; |
| } |
| |
| static struct codec_preset *find_preset(const char *uuid, const char *codec, |
| const char *name) |
| { |
| struct preset *preset; |
| |
| preset = find_presets_name(uuid, codec); |
| if (!preset) |
| return NULL; |
| |
| return preset_find_name(preset, name); |
| } |
| |
| static DBusMessage *endpoint_select_config_reply(DBusMessage *msg, |
| uint8_t *data, size_t len) |
| { |
| DBusMessage *reply; |
| DBusMessageIter args, array; |
| |
| reply = dbus_message_new_method_return(msg); |
| if (!reply) |
| return NULL; |
| |
| dbus_message_iter_init_append(reply, &args); |
| |
| dbus_message_iter_open_container(&args, DBUS_TYPE_ARRAY, |
| DBUS_TYPE_BYTE_AS_STRING, |
| &array); |
| |
| dbus_message_iter_append_fixed_array(&array, DBUS_TYPE_BYTE, &data, |
| len); |
| |
| dbus_message_iter_close_container(&args, &array); |
| |
| return reply; |
| } |
| |
| static uint8_t *str2bytearray(char *arg, size_t *val_len) |
| { |
| uint8_t value[UINT8_MAX]; |
| char *entry; |
| unsigned int i; |
| |
| for (i = 0; (entry = strsep(&arg, " \t")) != NULL; i++) { |
| long val; |
| char *endptr = NULL; |
| |
| if (*entry == '\0') |
| continue; |
| |
| if (i >= G_N_ELEMENTS(value)) { |
| bt_shell_printf("Too much data\n"); |
| return NULL; |
| } |
| |
| val = strtol(entry, &endptr, 0); |
| if (!endptr || *endptr != '\0' || val > UINT8_MAX) { |
| bt_shell_printf("Invalid value at index %d\n", i); |
| return NULL; |
| } |
| |
| value[i] = val; |
| } |
| |
| *val_len = i; |
| |
| return util_memdup(value, i); |
| } |
| |
| static void select_config_response(const char *input, void *user_data) |
| { |
| struct endpoint *ep = user_data; |
| struct codec_preset *p; |
| DBusMessage *reply; |
| uint8_t *data; |
| size_t len; |
| |
| p = preset_find_name(ep->preset, input); |
| if (p) { |
| data = p->data.iov_base; |
| len = p->data.iov_len; |
| goto done; |
| } |
| |
| data = str2bytearray((void *) input, &len); |
| if (!data) { |
| g_dbus_send_error(dbus_conn, ep->msg, |
| "org.bluez.Error.Rejected", NULL); |
| ep->msg = NULL; |
| return; |
| } |
| |
| done: |
| reply = endpoint_select_config_reply(ep->msg, data, len); |
| if (!reply) |
| return; |
| |
| if (!p) |
| free(data); |
| |
| g_dbus_send_message(dbus_conn, reply); |
| dbus_message_unref(ep->msg); |
| ep->msg = NULL; |
| } |
| |
| static DBusMessage *endpoint_select_configuration(DBusConnection *conn, |
| DBusMessage *msg, void *user_data) |
| { |
| struct endpoint *ep = user_data; |
| struct codec_preset *p; |
| DBusMessageIter args; |
| DBusMessage *reply; |
| |
| dbus_message_iter_init(msg, &args); |
| |
| bt_shell_printf("Endpoint: SelectConfiguration\n"); |
| print_iter("\t", "Capabilities", &args); |
| |
| if (!ep->max_transports) { |
| bt_shell_printf("Maximum transports reached: rejecting\n"); |
| return g_dbus_create_error(msg, |
| "org.bluez.Error.Rejected", |
| "Maximum transports reached"); |
| } |
| |
| if (!ep->auto_accept) { |
| ep->msg = dbus_message_ref(msg); |
| bt_shell_prompt_input("Endpoint", "Enter preset/configuration:", |
| select_config_response, ep); |
| return NULL; |
| } |
| |
| p = preset_find_name(ep->preset, NULL); |
| if (!p) { |
| reply = g_dbus_create_error(msg, "org.bluez.Error.Rejected", |
| NULL); |
| return reply; |
| } |
| |
| reply = endpoint_select_config_reply(msg, p->data.iov_base, |
| p->data.iov_len); |
| if (!reply) { |
| reply = g_dbus_create_error(msg, "org.bluez.Error.Rejected", |
| NULL); |
| return reply; |
| } |
| |
| bt_shell_printf("Auto Accepting using %s...\n", p->name); |
| |
| return reply; |
| } |
| |
| struct endpoint_config { |
| GDBusProxy *proxy; |
| struct endpoint *ep; |
| struct iovec *caps; /* Codec Specific Configuration LTVs */ |
| struct iovec *meta; /* Metadata LTVs*/ |
| uint8_t target_latency; |
| struct bt_bap_qos qos; /* BAP QOS configuration parameters */ |
| }; |
| |
| static void append_io_qos(DBusMessageIter *iter, struct bt_bap_io_qos *qos) |
| { |
| bt_shell_printf("Interval %u\n", qos->interval); |
| |
| g_dbus_dict_append_entry(iter, "Interval", DBUS_TYPE_UINT32, |
| &qos->interval); |
| |
| bt_shell_printf("PHY 0x%02x\n", qos->phy); |
| |
| g_dbus_dict_append_entry(iter, "PHY", DBUS_TYPE_BYTE, &qos->phy); |
| |
| bt_shell_printf("SDU %u\n", qos->sdu); |
| |
| g_dbus_dict_append_entry(iter, "SDU", DBUS_TYPE_UINT16, &qos->sdu); |
| |
| bt_shell_printf("Retransmissions %u\n", qos->rtn); |
| |
| g_dbus_dict_append_entry(iter, "Retransmissions", DBUS_TYPE_BYTE, |
| &qos->rtn); |
| |
| bt_shell_printf("Latency %u\n", qos->latency); |
| |
| g_dbus_dict_append_entry(iter, "Latency", DBUS_TYPE_UINT16, |
| &qos->latency); |
| } |
| |
| static void append_ucast_qos(DBusMessageIter *iter, struct endpoint_config *cfg) |
| { |
| struct bt_bap_ucast_qos *qos = &cfg->qos.ucast; |
| |
| if (cfg->ep->iso_group != BT_ISO_QOS_GROUP_UNSET) { |
| bt_shell_printf("CIG 0x%2.2x\n", cfg->ep->iso_group); |
| g_dbus_dict_append_entry(iter, "CIG", DBUS_TYPE_BYTE, |
| &cfg->ep->iso_group); |
| } |
| |
| if (cfg->ep->iso_stream != BT_ISO_QOS_STREAM_UNSET) { |
| bt_shell_printf("CIS 0x%2.2x\n", cfg->ep->iso_stream); |
| g_dbus_dict_append_entry(iter, "CIS", DBUS_TYPE_BYTE, |
| &cfg->ep->iso_stream); |
| } |
| |
| bt_shell_printf("Framing 0x%02x\n", qos->framing); |
| |
| g_dbus_dict_append_entry(iter, "Framing", DBUS_TYPE_BYTE, |
| &qos->framing); |
| |
| bt_shell_printf("PresentationDelay %u\n", qos->delay); |
| |
| g_dbus_dict_append_entry(iter, "PresentationDelay", |
| DBUS_TYPE_UINT32, &qos->delay); |
| |
| if (cfg->target_latency) { |
| bt_shell_printf("TargetLatency 0x%02x\n", qos->target_latency); |
| g_dbus_dict_append_entry(iter, "TargetLatency", DBUS_TYPE_BYTE, |
| &qos->target_latency); |
| } |
| |
| append_io_qos(iter, &qos->io_qos); |
| } |
| |
| static void append_bcast_qos(DBusMessageIter *iter, struct endpoint_config *cfg) |
| { |
| struct bt_bap_bcast_qos *qos = &cfg->qos.bcast; |
| |
| if (cfg->ep->iso_group != BT_ISO_QOS_BIG_UNSET) { |
| bt_shell_printf("BIG 0x%2.2x\n", cfg->ep->iso_group); |
| g_dbus_dict_append_entry(iter, "BIG", DBUS_TYPE_BYTE, |
| &cfg->ep->iso_group); |
| } |
| |
| if (cfg->ep->iso_stream != BT_ISO_QOS_BIS_UNSET) { |
| bt_shell_printf("BIS 0x%2.2x\n", cfg->ep->iso_stream); |
| g_dbus_dict_append_entry(iter, "BIS", DBUS_TYPE_BYTE, |
| &cfg->ep->iso_stream); |
| } |
| |
| if (qos->sync_factor) { |
| bt_shell_printf("SyncFactor %u\n", qos->sync_factor); |
| g_dbus_dict_append_entry(iter, "SyncFactor", DBUS_TYPE_BYTE, |
| &qos->sync_factor); |
| } |
| |
| if (qos->options) { |
| bt_shell_printf("Options %u\n", qos->options); |
| g_dbus_dict_append_entry(iter, "Options", DBUS_TYPE_BYTE, |
| &qos->options); |
| } |
| |
| if (qos->skip) { |
| bt_shell_printf("Skip %u\n", qos->skip); |
| g_dbus_dict_append_entry(iter, "Skip", DBUS_TYPE_UINT16, |
| &qos->skip); |
| } |
| |
| if (qos->sync_timeout) { |
| bt_shell_printf("SyncTimeout %u\n", qos->sync_timeout); |
| g_dbus_dict_append_entry(iter, "SyncTimeout", DBUS_TYPE_UINT16, |
| &qos->sync_timeout); |
| } |
| |
| if (qos->sync_cte_type) { |
| bt_shell_printf("SyncCteType %u\n", qos->sync_cte_type); |
| g_dbus_dict_append_entry(iter, "SyncCteType", DBUS_TYPE_BYTE, |
| &qos->sync_cte_type); |
| } |
| |
| if (qos->mse) { |
| bt_shell_printf("MSE %u\n", qos->mse); |
| g_dbus_dict_append_entry(iter, "MSE", DBUS_TYPE_BYTE, |
| &qos->mse); |
| } |
| |
| if (qos->timeout) { |
| bt_shell_printf("Timeout %u\n", qos->timeout); |
| g_dbus_dict_append_entry(iter, "Timeout", DBUS_TYPE_UINT16, |
| &qos->timeout); |
| } |
| |
| if (cfg->ep->bcode->iov_len != 0) { |
| const char *key = "BCode"; |
| |
| bt_shell_printf("BCode:\n"); |
| bt_shell_hexdump(cfg->ep->bcode->iov_base, |
| cfg->ep->bcode->iov_len); |
| |
| g_dbus_dict_append_basic_array(iter, DBUS_TYPE_STRING, |
| &key, DBUS_TYPE_BYTE, |
| &cfg->ep->bcode->iov_base, |
| cfg->ep->bcode->iov_len); |
| } |
| |
| bt_shell_printf("Framing 0x%02x\n", qos->framing); |
| |
| g_dbus_dict_append_entry(iter, "Framing", DBUS_TYPE_BYTE, |
| &qos->framing); |
| |
| bt_shell_printf("PresentationDelay %u\n", qos->delay); |
| |
| g_dbus_dict_append_entry(iter, "PresentationDelay", |
| DBUS_TYPE_UINT32, &qos->delay); |
| |
| /* Add BAP codec QOS configuration */ |
| append_io_qos(iter, &qos->io_qos); |
| } |
| |
| static void append_qos(DBusMessageIter *iter, struct endpoint_config *cfg) |
| { |
| DBusMessageIter entry, var, dict; |
| const char *key = "QoS"; |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_DICT_ENTRY, |
| NULL, &entry); |
| |
| dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key); |
| |
| dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, |
| "a{sv}", &var); |
| |
| dbus_message_iter_open_container(&var, DBUS_TYPE_ARRAY, "{sv}", |
| &dict); |
| |
| if (cfg->ep->broadcast) |
| append_bcast_qos(&dict, cfg); |
| else |
| append_ucast_qos(&dict, cfg); |
| |
| dbus_message_iter_close_container(&var, &dict); |
| dbus_message_iter_close_container(&entry, &var); |
| dbus_message_iter_close_container(iter, &entry); |
| } |
| |
| static void append_properties(DBusMessageIter *iter, |
| struct endpoint_config *cfg) |
| { |
| DBusMessageIter dict; |
| const char *key = "Capabilities"; |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "{sv}", &dict); |
| |
| if (cfg->ep->codec == LC3_ID) { |
| print_lc3_cfg(cfg->caps->iov_base, cfg->caps->iov_len); |
| } else { |
| bt_shell_printf("Capabilities: "); |
| bt_shell_hexdump(cfg->caps->iov_base, cfg->caps->iov_len); |
| } |
| |
| g_dbus_dict_append_basic_array(&dict, DBUS_TYPE_STRING, &key, |
| DBUS_TYPE_BYTE, &cfg->caps->iov_base, |
| cfg->caps->iov_len); |
| |
| if (cfg->meta && cfg->meta->iov_len) { |
| const char *meta = "Metadata"; |
| |
| g_dbus_dict_append_basic_array(&dict, DBUS_TYPE_STRING, &meta, |
| DBUS_TYPE_BYTE, &cfg->meta->iov_base, |
| cfg->meta->iov_len); |
| |
| if (cfg->ep->codec == LC3_ID) { |
| print_lc3_meta(cfg->meta->iov_base, cfg->meta->iov_len); |
| } else { |
| bt_shell_printf("Metadata:\n"); |
| bt_shell_hexdump(cfg->meta->iov_base, |
| cfg->meta->iov_len); |
| } |
| } |
| |
| append_qos(&dict, cfg); |
| |
| dbus_message_iter_close_container(iter, &dict); |
| } |
| |
| static int parse_chan_alloc(DBusMessageIter *iter, uint32_t *location, |
| uint8_t *channels) |
| { |
| while (dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_DICT_ENTRY) { |
| const char *key; |
| DBusMessageIter value, entry; |
| int var; |
| |
| dbus_message_iter_recurse(iter, &entry); |
| dbus_message_iter_get_basic(&entry, &key); |
| |
| dbus_message_iter_next(&entry); |
| dbus_message_iter_recurse(&entry, &value); |
| |
| var = dbus_message_iter_get_arg_type(&value); |
| |
| if (!strcasecmp(key, "ChannelAllocation")) { |
| if (var != DBUS_TYPE_UINT32) |
| return -EINVAL; |
| dbus_message_iter_get_basic(&value, location); |
| if (*channels) |
| *channels = __builtin_popcount(*location); |
| return 0; |
| } |
| |
| dbus_message_iter_next(iter); |
| } |
| |
| return -EINVAL; |
| } |
| |
| static DBusMessage *endpoint_select_properties_reply(struct endpoint *ep, |
| DBusMessage *msg, |
| struct codec_preset *preset) |
| { |
| DBusMessage *reply; |
| DBusMessageIter iter, props; |
| struct endpoint_config *cfg; |
| struct bt_bap_io_qos *qos; |
| uint32_t location = 0; |
| uint8_t channels = 1; |
| |
| if (!preset) |
| return NULL; |
| |
| reply = dbus_message_new_method_return(msg); |
| if (!reply) |
| return NULL; |
| |
| cfg = new0(struct endpoint_config, 1); |
| cfg->ep = ep; |
| |
| /* Copy capabilities */ |
| cfg->caps = util_iov_dup(&preset->data, 1); |
| cfg->target_latency = preset->target_latency; |
| |
| dbus_message_iter_init(msg, &iter); |
| dbus_message_iter_recurse(&iter, &props); |
| |
| if (!parse_chan_alloc(&props, &location, &channels)) { |
| uint8_t chan_alloc_ltv[] = { |
| 0x05, LC3_CONFIG_CHAN_ALLOC, location & 0xff, |
| location >> 8, location >> 16, location >> 24 |
| }; |
| |
| util_iov_append(cfg->caps, &chan_alloc_ltv, |
| sizeof(chan_alloc_ltv)); |
| } |
| |
| /* Copy metadata */ |
| cfg->meta = util_iov_dup(ep->meta, 1); |
| |
| if (ep->broadcast) |
| qos = &preset->qos.bcast.io_qos; |
| else |
| qos = &preset->qos.ucast.io_qos; |
| |
| if (qos->phy) { |
| /* Set QoS parameters */ |
| cfg->qos = preset->qos; |
| /* Adjust the SDU size based on the number of |
| * locations/channels that is being requested. |
| */ |
| if (channels > 1) |
| qos->sdu *= channels; |
| } |
| |
| dbus_message_iter_init_append(reply, &iter); |
| |
| append_properties(&iter, cfg); |
| |
| free(cfg); |
| |
| return reply; |
| } |
| |
| static void select_properties_response(const char *input, void *user_data) |
| { |
| struct endpoint *ep = user_data; |
| struct codec_preset *p; |
| DBusMessage *reply; |
| |
| p = preset_find_name(ep->preset, input); |
| if (p) { |
| reply = endpoint_select_properties_reply(ep, ep->msg, p); |
| goto done; |
| } |
| |
| bt_shell_printf("Preset %s not found\n", input); |
| reply = g_dbus_create_error(ep->msg, "org.bluez.Error.Rejected", NULL); |
| |
| done: |
| g_dbus_send_message(dbus_conn, reply); |
| dbus_message_unref(ep->msg); |
| ep->msg = NULL; |
| } |
| |
| static DBusMessage *endpoint_select_properties(DBusConnection *conn, |
| DBusMessage *msg, void *user_data) |
| { |
| struct endpoint *ep = user_data; |
| struct codec_preset *p; |
| DBusMessageIter args; |
| DBusMessage *reply; |
| |
| dbus_message_iter_init(msg, &args); |
| |
| bt_shell_printf("Endpoint: SelectProperties\n"); |
| print_iter("\t", "Properties", &args); |
| |
| if (!ep->max_transports) { |
| bt_shell_printf("Maximum transports reached: rejecting\n"); |
| return g_dbus_create_error(msg, |
| "org.bluez.Error.Rejected", |
| "Maximum transports reached"); |
| } |
| |
| if (!ep->auto_accept) { |
| ep->msg = dbus_message_ref(msg); |
| bt_shell_prompt_input("Endpoint", "Enter preset/configuration:", |
| select_properties_response, ep); |
| return NULL; |
| } |
| |
| p = preset_find_name(ep->preset, NULL); |
| if (!p) |
| return NULL; |
| |
| reply = endpoint_select_properties_reply(ep, msg, p); |
| if (!reply) |
| return NULL; |
| |
| bt_shell_printf("Auto Accepting using %s...\n", p->name); |
| |
| return reply; |
| } |
| |
| static bool match_str(const void *data, const void *user_data) |
| { |
| return !strcmp(data, user_data); |
| } |
| |
| static DBusMessage *endpoint_clear_configuration(DBusConnection *conn, |
| DBusMessage *msg, void *user_data) |
| { |
| struct endpoint *ep = user_data; |
| DBusMessageIter args; |
| const char *path; |
| |
| dbus_message_iter_init(msg, &args); |
| |
| dbus_message_iter_get_basic(&args, &path); |
| |
| if (ep->max_transports != UINT8_MAX) |
| ep->max_transports++; |
| |
| queue_remove_if(ep->transports, match_str, (void *)path); |
| |
| return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); |
| } |
| |
| static struct endpoint *endpoint_find(const char *pattern) |
| { |
| GList *l; |
| |
| for (l = local_endpoints; l; l = g_list_next(l)) { |
| struct endpoint *ep = l->data; |
| |
| /* match object path */ |
| if (!strcmp(ep->path, pattern)) |
| return ep; |
| |
| /* match UUID */ |
| if (!strcmp(ep->uuid, pattern)) |
| return ep; |
| } |
| |
| return NULL; |
| } |
| |
| static void print_aptx_common(a2dp_aptx_t *aptx) |
| { |
| bt_shell_printf("\n\t\tFrequencies: "); |
| if (aptx->frequency & APTX_SAMPLING_FREQ_16000) |
| bt_shell_printf("16kHz "); |
| if (aptx->frequency & APTX_SAMPLING_FREQ_32000) |
| bt_shell_printf("32kHz "); |
| if (aptx->frequency & APTX_SAMPLING_FREQ_44100) |
| bt_shell_printf("44.1kHz "); |
| if (aptx->frequency & APTX_SAMPLING_FREQ_48000) |
| bt_shell_printf("48kHz "); |
| |
| bt_shell_printf("\n\t\tChannel modes: "); |
| if (aptx->channel_mode & APTX_CHANNEL_MODE_MONO) |
| bt_shell_printf("Mono "); |
| if (aptx->channel_mode & APTX_CHANNEL_MODE_STEREO) |
| bt_shell_printf("Stereo "); |
| } |
| |
| static void print_aptx(a2dp_aptx_t *aptx, uint8_t size) |
| { |
| bt_shell_printf("\t\tVendor Specific Value (aptX)"); |
| |
| if (size < sizeof(*aptx)) { |
| bt_shell_printf(" (broken)\n"); |
| return; |
| } |
| |
| print_aptx_common(aptx); |
| |
| bt_shell_printf("\n"); |
| } |
| |
| static void print_faststream(a2dp_faststream_t *faststream, uint8_t size) |
| { |
| bt_shell_printf("\t\tVendor Specific Value (FastStream)"); |
| |
| if (size < sizeof(*faststream)) { |
| bt_shell_printf(" (broken)\n"); |
| return; |
| } |
| |
| bt_shell_printf("\n\t\tDirections: "); |
| if (faststream->direction & FASTSTREAM_DIRECTION_SINK) |
| bt_shell_printf("sink "); |
| if (faststream->direction & FASTSTREAM_DIRECTION_SOURCE) |
| bt_shell_printf("source "); |
| |
| if (faststream->direction & FASTSTREAM_DIRECTION_SINK) { |
| bt_shell_printf("\n\t\tSink Frequencies: "); |
| if (faststream->sink_frequency & |
| FASTSTREAM_SINK_SAMPLING_FREQ_44100) |
| bt_shell_printf("44.1kHz "); |
| if (faststream->sink_frequency & |
| FASTSTREAM_SINK_SAMPLING_FREQ_48000) |
| bt_shell_printf("48kHz "); |
| } |
| |
| if (faststream->direction & FASTSTREAM_DIRECTION_SOURCE) { |
| bt_shell_printf("\n\t\tSource Frequencies: "); |
| if (faststream->source_frequency & |
| FASTSTREAM_SOURCE_SAMPLING_FREQ_16000) |
| bt_shell_printf("16kHz "); |
| } |
| |
| bt_shell_printf("\n"); |
| } |
| |
| static void print_aptx_ll(a2dp_aptx_ll_t *aptx_ll, uint8_t size) |
| { |
| a2dp_aptx_ll_new_caps_t *aptx_ll_new; |
| |
| bt_shell_printf("\t\tVendor Specific Value (aptX Low Latency)"); |
| |
| if (size < sizeof(*aptx_ll)) { |
| bt_shell_printf(" (broken)\n"); |
| return; |
| } |
| |
| print_aptx_common(&aptx_ll->aptx); |
| |
| bt_shell_printf("\n\tBidirectional link: %s", |
| aptx_ll->bidirect_link ? "Yes" : "No"); |
| |
| aptx_ll_new = &aptx_ll->new_caps[0]; |
| if (aptx_ll->has_new_caps && |
| size >= sizeof(*aptx_ll) + sizeof(*aptx_ll_new)) { |
| bt_shell_printf("\n\tTarget codec buffer level: %u", |
| (unsigned int)aptx_ll_new->target_level2 | |
| ((unsigned int)(aptx_ll_new->target_level1) << 8)); |
| bt_shell_printf("\n\tInitial codec buffer level: %u", |
| (unsigned int)aptx_ll_new->initial_level2 | |
| ((unsigned int)(aptx_ll_new->initial_level1) << 8)); |
| bt_shell_printf("\n\tSRA max rate: %g", |
| aptx_ll_new->sra_max_rate / 10000.0); |
| bt_shell_printf("\n\tSRA averaging time: %us", |
| (unsigned int)aptx_ll_new->sra_avg_time); |
| bt_shell_printf("\n\tGood working codec buffer level: %u", |
| (unsigned int)aptx_ll_new->good_working_level2 | |
| ((unsigned int)(aptx_ll_new->good_working_level1) << 8) |
| ); |
| } |
| |
| bt_shell_printf("\n"); |
| } |
| |
| static void print_aptx_hd(a2dp_aptx_hd_t *aptx_hd, uint8_t size) |
| { |
| bt_shell_printf("\t\tVendor Specific Value (aptX HD)"); |
| |
| if (size < sizeof(*aptx_hd)) { |
| bt_shell_printf(" (broken)\n"); |
| return; |
| } |
| |
| print_aptx_common(&aptx_hd->aptx); |
| |
| bt_shell_printf("\n"); |
| } |
| |
| static void print_ldac(a2dp_ldac_t *ldac, uint8_t size) |
| { |
| bt_shell_printf("\t\tVendor Specific Value (LDAC)"); |
| |
| if (size < sizeof(*ldac)) { |
| bt_shell_printf(" (broken)\n"); |
| return; |
| } |
| |
| bt_shell_printf("\n\t\tFrequencies: "); |
| if (ldac->frequency & LDAC_SAMPLING_FREQ_44100) |
| bt_shell_printf("44.1kHz "); |
| if (ldac->frequency & LDAC_SAMPLING_FREQ_48000) |
| bt_shell_printf("48kHz "); |
| if (ldac->frequency & LDAC_SAMPLING_FREQ_88200) |
| bt_shell_printf("88.2kHz "); |
| if (ldac->frequency & LDAC_SAMPLING_FREQ_96000) |
| bt_shell_printf("96kHz "); |
| if (ldac->frequency & LDAC_SAMPLING_FREQ_176400) |
| bt_shell_printf("176.4kHz "); |
| if (ldac->frequency & LDAC_SAMPLING_FREQ_192000) |
| bt_shell_printf("192kHz "); |
| |
| bt_shell_printf("\n\t\tChannel modes: "); |
| if (ldac->channel_mode & LDAC_CHANNEL_MODE_MONO) |
| bt_shell_printf("Mono "); |
| if (ldac->channel_mode & LDAC_CHANNEL_MODE_DUAL) |
| bt_shell_printf("Dual "); |
| if (ldac->channel_mode & LDAC_CHANNEL_MODE_STEREO) |
| bt_shell_printf("Stereo "); |
| |
| bt_shell_printf("\n"); |
| } |
| |
| static void print_opus_g(a2dp_opus_g_t *opus, uint8_t size) |
| { |
| bt_shell_printf("\t\tVendor Specific Value (Opus [Google])"); |
| |
| if (size < sizeof(*opus)) { |
| bt_shell_printf(" (broken)\n"); |
| return; |
| } |
| |
| bt_shell_printf("\n\t\tFrequencies: "); |
| if (opus->data & OPUS_G_FREQUENCY_48000) |
| bt_shell_printf("48kHz "); |
| |
| bt_shell_printf("\n\t\tChannel modes: "); |
| if (opus->data & OPUS_G_CHANNELS_MONO) |
| bt_shell_printf("Mono "); |
| if (opus->data & OPUS_G_CHANNELS_STEREO) |
| bt_shell_printf("Stereo "); |
| if (opus->data & OPUS_G_CHANNELS_DUAL) |
| bt_shell_printf("Dual Mono "); |
| |
| bt_shell_printf("\n\t\tFrame durations: "); |
| if (opus->data & OPUS_G_DURATION_100) |
| bt_shell_printf("10 ms "); |
| if (opus->data & OPUS_G_DURATION_200) |
| bt_shell_printf("20 ms "); |
| |
| bt_shell_printf("\n"); |
| } |
| |
| static void print_vendor(a2dp_vendor_codec_t *vendor, uint8_t size) |
| { |
| uint32_t vendor_id; |
| uint16_t codec_id; |
| int i; |
| |
| if (size < sizeof(*vendor)) { |
| bt_shell_printf("\tMedia Codec: Vendor Specific A2DP Codec " |
| "(broken)"); |
| return; |
| } |
| |
| vendor_id = A2DP_GET_VENDOR_ID(*vendor); |
| codec_id = A2DP_GET_CODEC_ID(*vendor); |
| |
| bt_shell_printf("\tMedia Codec: Vendor Specific A2DP Codec"); |
| |
| bt_shell_printf("\n\tVendor ID 0x%08x", vendor_id); |
| |
| bt_shell_printf("\n\tVendor Specific Codec ID 0x%04x", codec_id); |
| |
| bt_shell_printf("\n\tVendor Specific Data:"); |
| for (i = 6; i < size; ++i) |
| bt_shell_printf(" 0x%.02x", ((unsigned char *)vendor)[i]); |
| bt_shell_printf("\n"); |
| |
| if (vendor_id == APTX_VENDOR_ID && codec_id == APTX_CODEC_ID) |
| print_aptx((void *) vendor, size); |
| else if (vendor_id == FASTSTREAM_VENDOR_ID && |
| codec_id == FASTSTREAM_CODEC_ID) |
| print_faststream((void *) vendor, size); |
| else if (vendor_id == APTX_LL_VENDOR_ID && codec_id == APTX_LL_CODEC_ID) |
| print_aptx_ll((void *) vendor, size); |
| else if (vendor_id == APTX_HD_VENDOR_ID && codec_id == APTX_HD_CODEC_ID) |
| print_aptx_hd((void *) vendor, size); |
| else if (vendor_id == LDAC_VENDOR_ID && codec_id == LDAC_CODEC_ID) |
| print_ldac((void *) vendor, size); |
| else if (vendor_id == OPUS_G_VENDOR_ID && codec_id == OPUS_G_CODEC_ID) |
| print_opus_g((void *) vendor, size); |
| } |
| |
| static void print_mpeg24(a2dp_aac_t *aac, uint8_t size) |
| { |
| unsigned int freq, bitrate; |
| |
| if (size < sizeof(*aac)) { |
| bt_shell_printf("\tMedia Codec: MPEG24 (broken)\n"); |
| return; |
| } |
| |
| freq = AAC_GET_FREQUENCY(*aac); |
| bitrate = AAC_GET_BITRATE(*aac); |
| |
| bt_shell_printf("\tMedia Codec: MPEG24\n\tObject Types: "); |
| |
| if (aac->object_type & AAC_OBJECT_TYPE_MPEG2_AAC_LC) |
| bt_shell_printf("MPEG-2 AAC LC "); |
| if (aac->object_type & AAC_OBJECT_TYPE_MPEG4_AAC_LC) |
| bt_shell_printf("MPEG-4 AAC LC "); |
| if (aac->object_type & AAC_OBJECT_TYPE_MPEG4_AAC_LTP) |
| bt_shell_printf("MPEG-4 AAC LTP "); |
| if (aac->object_type & AAC_OBJECT_TYPE_MPEG4_AAC_SCA) |
| bt_shell_printf("MPEG-4 AAC scalable "); |
| |
| bt_shell_printf("\n\tFrequencies: "); |
| if (freq & AAC_SAMPLING_FREQ_8000) |
| bt_shell_printf("8kHz "); |
| if (freq & AAC_SAMPLING_FREQ_11025) |
| bt_shell_printf("11.025kHz "); |
| if (freq & AAC_SAMPLING_FREQ_12000) |
| bt_shell_printf("12kHz "); |
| if (freq & AAC_SAMPLING_FREQ_16000) |
| bt_shell_printf("16kHz "); |
| if (freq & AAC_SAMPLING_FREQ_22050) |
| bt_shell_printf("22.05kHz "); |
| if (freq & AAC_SAMPLING_FREQ_24000) |
| bt_shell_printf("24kHz "); |
| if (freq & AAC_SAMPLING_FREQ_32000) |
| bt_shell_printf("32kHz "); |
| if (freq & AAC_SAMPLING_FREQ_44100) |
| bt_shell_printf("44.1kHz "); |
| if (freq & AAC_SAMPLING_FREQ_48000) |
| bt_shell_printf("48kHz "); |
| if (freq & AAC_SAMPLING_FREQ_64000) |
| bt_shell_printf("64kHz "); |
| if (freq & AAC_SAMPLING_FREQ_88200) |
| bt_shell_printf("88.2kHz "); |
| if (freq & AAC_SAMPLING_FREQ_96000) |
| bt_shell_printf("96kHz "); |
| |
| bt_shell_printf("\n\tChannels: "); |
| if (aac->channels & AAC_CHANNELS_1) |
| bt_shell_printf("1 "); |
| if (aac->channels & AAC_CHANNELS_2) |
| bt_shell_printf("2 "); |
| |
| bt_shell_printf("\n\tBitrate: %u", bitrate); |
| |
| bt_shell_printf("\n\tVBR: %s", aac->vbr ? "Yes\n" : "No\n"); |
| } |
| |
| static void print_mpeg12(a2dp_mpeg_t *mpeg, uint8_t size) |
| { |
| uint16_t bitrate; |
| |
| if (size < sizeof(*mpeg)) { |
| bt_shell_printf("\tMedia Codec: MPEG12 (broken)\n"); |
| return; |
| } |
| |
| bitrate = MPEG_GET_BITRATE(*mpeg); |
| |
| bt_shell_printf("\tMedia Codec: MPEG12\n\tChannel Modes: "); |
| |
| if (mpeg->channel_mode & MPEG_CHANNEL_MODE_MONO) |
| bt_shell_printf("Mono "); |
| if (mpeg->channel_mode & MPEG_CHANNEL_MODE_DUAL_CHANNEL) |
| bt_shell_printf("DualChannel "); |
| if (mpeg->channel_mode & MPEG_CHANNEL_MODE_STEREO) |
| bt_shell_printf("Stereo "); |
| if (mpeg->channel_mode & MPEG_CHANNEL_MODE_JOINT_STEREO) |
| bt_shell_printf("JointStereo"); |
| |
| bt_shell_printf("\n\tFrequencies: "); |
| if (mpeg->frequency & MPEG_SAMPLING_FREQ_16000) |
| bt_shell_printf("16Khz "); |
| if (mpeg->frequency & MPEG_SAMPLING_FREQ_22050) |
| bt_shell_printf("22.05Khz "); |
| if (mpeg->frequency & MPEG_SAMPLING_FREQ_24000) |
| bt_shell_printf("24Khz "); |
| if (mpeg->frequency & MPEG_SAMPLING_FREQ_32000) |
| bt_shell_printf("32Khz "); |
| if (mpeg->frequency & MPEG_SAMPLING_FREQ_44100) |
| bt_shell_printf("44.1Khz "); |
| if (mpeg->frequency & MPEG_SAMPLING_FREQ_48000) |
| bt_shell_printf("48Khz "); |
| |
| bt_shell_printf("\n\tCRC: %s", mpeg->crc ? "Yes" : "No"); |
| |
| bt_shell_printf("\n\tLayer: "); |
| if (mpeg->layer & MPEG_LAYER_MP1) |
| bt_shell_printf("1 "); |
| if (mpeg->layer & MPEG_LAYER_MP2) |
| bt_shell_printf("2 "); |
| if (mpeg->layer & MPEG_LAYER_MP3) |
| bt_shell_printf("3 "); |
| |
| if (bitrate & MPEG_BIT_RATE_FREE) { |
| bt_shell_printf("\n\tBit Rate: Free format"); |
| } else { |
| if (mpeg->layer & MPEG_LAYER_MP1) { |
| bt_shell_printf("\n\tLayer 1 Bit Rate: "); |
| if (bitrate & MPEG_MP1_BIT_RATE_32000) |
| bt_shell_printf("32kbps "); |
| if (bitrate & MPEG_MP1_BIT_RATE_64000) |
| bt_shell_printf("64kbps "); |
| if (bitrate & MPEG_MP1_BIT_RATE_96000) |
| bt_shell_printf("96kbps "); |
| if (bitrate & MPEG_MP1_BIT_RATE_128000) |
| bt_shell_printf("128kbps "); |
| if (bitrate & MPEG_MP1_BIT_RATE_160000) |
| bt_shell_printf("160kbps "); |
| if (bitrate & MPEG_MP1_BIT_RATE_192000) |
| bt_shell_printf("192kbps "); |
| if (bitrate & MPEG_MP1_BIT_RATE_224000) |
| bt_shell_printf("224kbps "); |
| if (bitrate & MPEG_MP1_BIT_RATE_256000) |
| bt_shell_printf("256kbps "); |
| if (bitrate & MPEG_MP1_BIT_RATE_320000) |
| bt_shell_printf("320kbps "); |
| if (bitrate & MPEG_MP1_BIT_RATE_352000) |
| bt_shell_printf("352kbps "); |
| if (bitrate & MPEG_MP1_BIT_RATE_384000) |
| bt_shell_printf("384kbps "); |
| if (bitrate & MPEG_MP1_BIT_RATE_416000) |
| bt_shell_printf("416kbps "); |
| if (bitrate & MPEG_MP1_BIT_RATE_448000) |
| bt_shell_printf("448kbps "); |
| } |
| |
| if (mpeg->layer & MPEG_LAYER_MP2) { |
| bt_shell_printf("\n\tLayer 2 Bit Rate: "); |
| if (bitrate & MPEG_MP2_BIT_RATE_32000) |
| bt_shell_printf("32kbps "); |
| if (bitrate & MPEG_MP2_BIT_RATE_48000) |
| bt_shell_printf("48kbps "); |
| if (bitrate & MPEG_MP2_BIT_RATE_56000) |
| bt_shell_printf("56kbps "); |
| if (bitrate & MPEG_MP2_BIT_RATE_64000) |
| bt_shell_printf("64kbps "); |
| if (bitrate & MPEG_MP2_BIT_RATE_80000) |
| bt_shell_printf("80kbps "); |
| if (bitrate & MPEG_MP2_BIT_RATE_96000) |
| bt_shell_printf("96kbps "); |
| if (bitrate & MPEG_MP2_BIT_RATE_112000) |
| bt_shell_printf("112kbps "); |
| if (bitrate & MPEG_MP2_BIT_RATE_128000) |
| bt_shell_printf("128kbps "); |
| if (bitrate & MPEG_MP2_BIT_RATE_160000) |
| bt_shell_printf("160kbps "); |
| if (bitrate & MPEG_MP2_BIT_RATE_192000) |
| bt_shell_printf("192kbps "); |
| if (bitrate & MPEG_MP2_BIT_RATE_224000) |
| bt_shell_printf("224kbps "); |
| if (bitrate & MPEG_MP2_BIT_RATE_256000) |
| bt_shell_printf("256kbps "); |
| if (bitrate & MPEG_MP2_BIT_RATE_320000) |
| bt_shell_printf("320kbps "); |
| if (bitrate & MPEG_MP2_BIT_RATE_384000) |
| bt_shell_printf("384kbps "); |
| } |
| |
| if (mpeg->layer & MPEG_LAYER_MP3) { |
| bt_shell_printf("\n\tLayer 3 Bit Rate: "); |
| if (bitrate & MPEG_MP3_BIT_RATE_32000) |
| bt_shell_printf("32kbps "); |
| if (bitrate & MPEG_MP3_BIT_RATE_40000) |
| bt_shell_printf("40kbps "); |
| if (bitrate & MPEG_MP3_BIT_RATE_48000) |
| bt_shell_printf("48kbps "); |
| if (bitrate & MPEG_MP3_BIT_RATE_56000) |
| bt_shell_printf("56kbps "); |
| if (bitrate & MPEG_MP3_BIT_RATE_64000) |
| bt_shell_printf("64kbps "); |
| if (bitrate & MPEG_MP3_BIT_RATE_80000) |
| bt_shell_printf("80kbps "); |
| if (bitrate & MPEG_MP3_BIT_RATE_96000) |
| bt_shell_printf("96kbps "); |
| if (bitrate & MPEG_MP3_BIT_RATE_112000) |
| bt_shell_printf("112kbps "); |
| if (bitrate & MPEG_MP3_BIT_RATE_128000) |
| bt_shell_printf("128kbps "); |
| if (bitrate & MPEG_MP3_BIT_RATE_160000) |
| bt_shell_printf("160kbps "); |
| if (bitrate & MPEG_MP3_BIT_RATE_192000) |
| bt_shell_printf("192kbps "); |
| if (bitrate & MPEG_MP3_BIT_RATE_224000) |
| bt_shell_printf("224kbps "); |
| if (bitrate & MPEG_MP3_BIT_RATE_256000) |
| bt_shell_printf("256kbps "); |
| if (bitrate & MPEG_MP3_BIT_RATE_320000) |
| bt_shell_printf("320kbps "); |
| } |
| } |
| |
| bt_shell_printf("\n\tVBR: %s", mpeg->vbr ? "Yes" : "No"); |
| |
| bt_shell_printf("\n\tPayload Format: "); |
| if (mpeg->mpf) |
| bt_shell_printf("RFC-2250 RFC-3119\n"); |
| else |
| bt_shell_printf("RFC-2250\n"); |
| } |
| |
| static void print_sbc(a2dp_sbc_t *sbc, uint8_t size) |
| { |
| if (size < sizeof(*sbc)) { |
| bt_shell_printf("\tMedia Codec: SBC (broken)\n"); |
| return; |
| } |
| |
| bt_shell_printf("\tMedia Codec: SBC\n\tChannel Modes: "); |
| |
| if (sbc->channel_mode & SBC_CHANNEL_MODE_MONO) |
| bt_shell_printf("Mono "); |
| if (sbc->channel_mode & SBC_CHANNEL_MODE_DUAL_CHANNEL) |
| bt_shell_printf("DualChannel "); |
| if (sbc->channel_mode & SBC_CHANNEL_MODE_STEREO) |
| bt_shell_printf("Stereo "); |
| if (sbc->channel_mode & SBC_CHANNEL_MODE_JOINT_STEREO) |
| bt_shell_printf("JointStereo"); |
| |
| bt_shell_printf("\n\tFrequencies: "); |
| if (sbc->frequency & SBC_SAMPLING_FREQ_16000) |
| bt_shell_printf("16Khz "); |
| if (sbc->frequency & SBC_SAMPLING_FREQ_32000) |
| bt_shell_printf("32Khz "); |
| if (sbc->frequency & SBC_SAMPLING_FREQ_44100) |
| bt_shell_printf("44.1Khz "); |
| if (sbc->frequency & SBC_SAMPLING_FREQ_48000) |
| bt_shell_printf("48Khz "); |
| |
| bt_shell_printf("\n\tSubbands: "); |
| if (sbc->allocation_method & SBC_SUBBANDS_4) |
| bt_shell_printf("4 "); |
| if (sbc->allocation_method & SBC_SUBBANDS_8) |
| bt_shell_printf("8"); |
| |
| bt_shell_printf("\n\tBlocks: "); |
| if (sbc->block_length & SBC_BLOCK_LENGTH_4) |
| bt_shell_printf("4 "); |
| if (sbc->block_length & SBC_BLOCK_LENGTH_8) |
| bt_shell_printf("8 "); |
| if (sbc->block_length & SBC_BLOCK_LENGTH_12) |
| bt_shell_printf("12 "); |
| if (sbc->block_length & SBC_BLOCK_LENGTH_16) |
| bt_shell_printf("16 "); |
| |
| bt_shell_printf("\n\tBitpool Range: %d-%d\n", |
| sbc->min_bitpool, sbc->max_bitpool); |
| } |
| |
| static int print_a2dp_codec(uint8_t codec, void *data, uint8_t size) |
| { |
| int i; |
| |
| switch (codec) { |
| case A2DP_CODEC_SBC: |
| print_sbc(data, size); |
| break; |
| case A2DP_CODEC_MPEG12: |
| print_mpeg12(data, size); |
| break; |
| case A2DP_CODEC_MPEG24: |
| print_mpeg24(data, size); |
| break; |
| case A2DP_CODEC_VENDOR: |
| print_vendor(data, size); |
| break; |
| default: |
| bt_shell_printf("\tMedia Codec: Unknown\n"); |
| bt_shell_printf("\t\tCodec Data:"); |
| for (i = 0; i < size - 2; ++i) |
| bt_shell_printf(" 0x%.02x", ((unsigned char *)data)[i]); |
| bt_shell_printf("\n"); |
| } |
| |
| return 0; |
| } |
| |
| static void print_hexdump(const char *label, struct iovec *iov) |
| { |
| if (!iov) |
| return; |
| |
| bt_shell_printf("%s:\n", label); |
| bt_shell_hexdump(iov->iov_base, iov->iov_len); |
| } |
| |
| static void print_codec(const char *uuid, uint8_t codec, struct iovec *caps, |
| struct iovec *meta) |
| { |
| if (!strcasecmp(uuid, A2DP_SINK_UUID) || |
| !strcasecmp(uuid, A2DP_SOURCE_UUID)) { |
| print_a2dp_codec(codec, caps->iov_base, caps->iov_len); |
| return; |
| } |
| |
| if (codec != LC3_ID) { |
| print_hexdump("Capabilities", caps); |
| print_hexdump("Metadata", meta); |
| return; |
| } |
| |
| print_lc3_caps(caps->iov_base, caps->iov_len); |
| |
| if (!meta) |
| return; |
| |
| print_lc3_meta(meta->iov_base, meta->iov_len); |
| } |
| |
| static void print_capabilities(GDBusProxy *proxy) |
| { |
| DBusMessageIter iter, subiter; |
| const char *uuid; |
| uint8_t codec; |
| struct iovec caps, meta; |
| |
| if (!g_dbus_proxy_get_property(proxy, "UUID", &iter)) |
| return; |
| |
| dbus_message_iter_get_basic(&iter, &uuid); |
| |
| if (!g_dbus_proxy_get_property(proxy, "Codec", &iter)) |
| return; |
| |
| dbus_message_iter_get_basic(&iter, &codec); |
| |
| if (!g_dbus_proxy_get_property(proxy, "Capabilities", &iter)) |
| return; |
| |
| dbus_message_iter_recurse(&iter, &subiter); |
| |
| dbus_message_iter_get_fixed_array(&subiter, &caps.iov_base, |
| (int *)&caps.iov_len); |
| |
| if (g_dbus_proxy_get_property(proxy, "Metadata", &iter)) { |
| dbus_message_iter_recurse(&iter, &subiter); |
| dbus_message_iter_get_fixed_array(&subiter, &meta.iov_base, |
| (int *)&meta.iov_len); |
| } else { |
| meta.iov_base = NULL; |
| meta.iov_len = 0; |
| } |
| |
| print_codec(uuid, codec, &caps, &meta); |
| } |
| |
| static void print_local_endpoint(struct endpoint *ep) |
| { |
| bt_shell_printf("Endpoint %s\n", ep->path); |
| bt_shell_printf("\tUUID %s\n", ep->uuid); |
| bt_shell_printf("\tCodec 0x%02x (%u)\n", ep->codec, ep->codec); |
| if (ep->caps) |
| print_codec(ep->uuid, ep->codec, ep->caps, ep->meta); |
| if (ep->locations) |
| bt_shell_printf("\tLocations 0x%08x (%u)\n", ep->locations, |
| ep->locations); |
| if (ep->supported_context) |
| bt_shell_printf("\tSupportedContext 0x%08x (%u)\n", |
| ep->supported_context, ep->supported_context); |
| if (ep->context) |
| bt_shell_printf("\tContext 0x%08x (%u)\n", ep->context, |
| ep->context); |
| } |
| |
| static void cmd_show_endpoint(int argc, char *argv[]) |
| { |
| GDBusProxy *proxy; |
| |
| proxy = g_dbus_proxy_lookup(endpoints, NULL, argv[1], |
| BLUEZ_MEDIA_ENDPOINT_INTERFACE); |
| if (!proxy) { |
| struct endpoint *ep; |
| |
| ep = endpoint_find(argv[1]); |
| if (ep) |
| return print_local_endpoint(ep); |
| |
| bt_shell_printf("Endpoint %s not found\n", argv[1]); |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| bt_shell_printf("Endpoint %s\n", g_dbus_proxy_get_path(proxy)); |
| |
| print_property(proxy, "UUID"); |
| print_property(proxy, "Codec"); |
| print_capabilities(proxy); |
| print_property(proxy, "Device"); |
| print_property(proxy, "DelayReporting"); |
| print_property(proxy, "Locations"); |
| print_property(proxy, "SupportedContext"); |
| print_property(proxy, "Context"); |
| print_property(proxy, "QoS"); |
| |
| return bt_shell_noninteractive_quit(EXIT_SUCCESS); |
| } |
| |
| static const GDBusMethodTable endpoint_methods[] = { |
| { GDBUS_ASYNC_METHOD("SetConfiguration", |
| GDBUS_ARGS({ "endpoint", "o" }, |
| { "properties", "a{sv}" } ), |
| NULL, endpoint_set_configuration) }, |
| { GDBUS_ASYNC_METHOD("SelectConfiguration", |
| GDBUS_ARGS({ "caps", "ay" } ), |
| GDBUS_ARGS({ "cfg", "ay" } ), |
| endpoint_select_configuration) }, |
| { GDBUS_ASYNC_METHOD("SelectProperties", |
| GDBUS_ARGS({ "properties", "a{sv}" } ), |
| GDBUS_ARGS({ "properties", "a{sv}" } ), |
| endpoint_select_properties) }, |
| { GDBUS_ASYNC_METHOD("ClearConfiguration", |
| GDBUS_ARGS({ "transport", "o" } ), |
| NULL, endpoint_clear_configuration) }, |
| { }, |
| }; |
| |
| static void endpoint_free(void *data) |
| { |
| struct endpoint *ep = data; |
| |
| util_iov_free(ep->caps, 1); |
| util_iov_free(ep->meta, 1); |
| |
| if (ep->msg) |
| dbus_message_unref(ep->msg); |
| |
| if (ep->codec == 0xff) { |
| free(ep->preset->custom.name); |
| free(ep->preset); |
| } |
| |
| queue_destroy(ep->acquiring, NULL); |
| queue_destroy(ep->transports, free); |
| |
| g_free(ep->path); |
| g_free(ep->uuid); |
| g_free(ep); |
| } |
| |
| static gboolean endpoint_get_uuid(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct endpoint *ep = data; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &ep->uuid); |
| |
| return TRUE; |
| } |
| |
| static gboolean endpoint_get_codec(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct endpoint *ep = data; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_BYTE, &ep->codec); |
| |
| return TRUE; |
| } |
| |
| static gboolean endpoint_get_capabilities(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct endpoint *ep = data; |
| DBusMessageIter array; |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, |
| DBUS_TYPE_BYTE_AS_STRING, &array); |
| |
| dbus_message_iter_append_fixed_array(&array, DBUS_TYPE_BYTE, |
| &ep->caps->iov_base, |
| ep->caps->iov_len); |
| |
| dbus_message_iter_close_container(iter, &array); |
| |
| return TRUE; |
| } |
| |
| struct vendor { |
| uint16_t cid; |
| uint16_t vid; |
| } __packed; |
| |
| static gboolean endpoint_get_vendor(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct endpoint *ep = data; |
| struct vendor vendor = { ep->cid, ep->vid }; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32, &vendor); |
| |
| return TRUE; |
| } |
| |
| static gboolean endpoint_vendor_exists(const GDBusPropertyTable *property, |
| void *data) |
| { |
| struct endpoint *ep = data; |
| |
| return ep->cid && ep->vid; |
| } |
| |
| static gboolean endpoint_get_metadata(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct endpoint *ep = data; |
| DBusMessageIter array; |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, |
| DBUS_TYPE_BYTE_AS_STRING, &array); |
| |
| dbus_message_iter_append_fixed_array(&array, DBUS_TYPE_BYTE, |
| &ep->meta->iov_base, |
| ep->meta->iov_len); |
| |
| dbus_message_iter_close_container(iter, &array); |
| |
| return TRUE; |
| } |
| |
| static gboolean endpoint_metadata_exists(const GDBusPropertyTable *property, |
| void *data) |
| { |
| struct endpoint *ep = data; |
| |
| return ep->meta ? TRUE : FALSE; |
| } |
| |
| static gboolean endpoint_get_locations(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct endpoint *ep = data; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32, &ep->locations); |
| |
| return TRUE; |
| } |
| |
| static gboolean endpoint_locations_exists(const GDBusPropertyTable *property, |
| void *data) |
| { |
| struct endpoint *ep = data; |
| |
| return ep->supported_context ? TRUE : FALSE; |
| } |
| |
| static gboolean |
| endpoint_get_supported_context(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct endpoint *ep = data; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT16, |
| &ep->supported_context); |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| endpoint_supported_context_exists(const GDBusPropertyTable *property, |
| void *data) |
| { |
| struct endpoint *ep = data; |
| |
| return ep->supported_context ? TRUE : FALSE; |
| } |
| |
| static gboolean endpoint_get_context(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct endpoint *ep = data; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT16, &ep->context); |
| |
| return TRUE; |
| } |
| |
| static gboolean endpoint_context_exists(const GDBusPropertyTable *property, |
| void *data) |
| { |
| struct endpoint *ep = data; |
| |
| return ep->context ? TRUE : FALSE; |
| } |
| |
| static const GDBusPropertyTable endpoint_properties[] = { |
| { "UUID", "s", endpoint_get_uuid, NULL, NULL }, |
| { "Codec", "y", endpoint_get_codec, NULL, NULL }, |
| { "Capabilities", "ay", endpoint_get_capabilities, NULL, NULL }, |
| { "Metadata", "ay", endpoint_get_metadata, NULL, |
| endpoint_metadata_exists }, |
| { "Vendor", "u", endpoint_get_vendor, NULL, endpoint_vendor_exists }, |
| { "Locations", "u", endpoint_get_locations, NULL, |
| endpoint_locations_exists }, |
| { "SupportedContext", "q", endpoint_get_supported_context, NULL, |
| endpoint_supported_context_exists }, |
| { "Context", "q", endpoint_get_context, NULL, endpoint_context_exists }, |
| { } |
| }; |
| |
| static void register_endpoint_setup(DBusMessageIter *iter, void *user_data) |
| { |
| struct endpoint *ep = user_data; |
| DBusMessageIter dict; |
| const char *key = "Capabilities"; |
| const char *meta = "Metadata"; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, &ep->path); |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "{sv}", &dict); |
| |
| g_dbus_dict_append_entry(&dict, "UUID", DBUS_TYPE_STRING, &ep->uuid); |
| |
| g_dbus_dict_append_entry(&dict, "Codec", DBUS_TYPE_BYTE, &ep->codec); |
| |
| if (ep->cid && ep->vid) { |
| struct vendor vendor = { ep->cid, ep->vid }; |
| |
| g_dbus_dict_append_entry(&dict, "Vendor", DBUS_TYPE_UINT32, |
| &vendor); |
| } |
| |
| if (ep->caps) { |
| g_dbus_dict_append_basic_array(&dict, DBUS_TYPE_STRING, &key, |
| DBUS_TYPE_BYTE, &ep->caps->iov_base, |
| ep->caps->iov_len); |
| |
| bt_shell_printf("Capabilities:\n"); |
| bt_shell_hexdump(ep->caps->iov_base, ep->caps->iov_len); |
| } |
| |
| if (ep->meta) { |
| |