blob: ed0c8cdd6c7acf935d7585ab60551f3ed80692d0 [file]
// SPDX-License-Identifier: GPL-2.0-or-later
/*
*
* BlueZ - Bluetooth protocol stack for Linux
*
* Copyright 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 <glib.h>
#include "gdbus/gdbus.h"
#include "bluetooth/bluetooth.h"
#include "bluetooth/uuid.h"
#include "src/shared/util.h"
#include "src/shared/shell.h"
#include "src/shared/io.h"
#include "src/shared/queue.h"
#include "print.h"
#include "assistant.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 MEDIA_ASSISTANT_INTERFACE "org.bluez.MediaAssistant1"
#define BCODE_LEN 16
struct assistant_config {
GDBusProxy *proxy; /* DBus object reference */
char *state; /* Assistant state */
char *device; /* Device address */
struct iovec *meta; /* Stream metadata LTVs */
struct bt_iso_qos qos; /* Stream QoS parameters */
};
static DBusConnection *dbus_conn;
static GList *assistants;
static void assistant_menu_pre_run(const struct bt_shell_menu *menu);
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_assistant(GDBusProxy *proxy, const char *description)
{
char *str;
str = proxy_description(proxy, "Assistant", description);
bt_shell_printf("%s\n", str);
g_free(str);
}
static void assistant_added(GDBusProxy *proxy)
{
assistants = g_list_append(assistants, proxy);
print_assistant(proxy, COLORED_NEW);
}
static void proxy_added(GDBusProxy *proxy, void *user_data)
{
const char *interface;
interface = g_dbus_proxy_get_interface(proxy);
if (!strcmp(interface, MEDIA_ASSISTANT_INTERFACE))
assistant_added(proxy);
}
static void assistant_removed(GDBusProxy *proxy)
{
assistants = g_list_remove(assistants, proxy);
print_assistant(proxy, COLORED_DEL);
}
static void proxy_removed(GDBusProxy *proxy, void *user_data)
{
const char *interface;
interface = g_dbus_proxy_get_interface(proxy);
if (!strcmp(interface, MEDIA_ASSISTANT_INTERFACE))
assistant_removed(proxy);
}
static void assistant_property_changed(GDBusProxy *proxy, const char *name,
DBusMessageIter *iter)
{
char *str;
str = proxy_description(proxy, "Assistant", COLORED_CHG);
print_iter(str, name, iter);
g_free(str);
}
static void property_changed(GDBusProxy *proxy, const char *name,
DBusMessageIter *iter, void *user_data)
{
const char *interface;
interface = g_dbus_proxy_get_interface(proxy);
if (!strcmp(interface, MEDIA_ASSISTANT_INTERFACE))
assistant_property_changed(proxy, name, iter);
}
static void assistant_unregister(void *data)
{
GDBusProxy *proxy = data;
bt_shell_printf("Assistant %s unregistered\n",
g_dbus_proxy_get_path(proxy));
}
static void disconnect_handler(DBusConnection *connection, void *user_data)
{
g_list_free_full(assistants, assistant_unregister);
assistants = NULL;
}
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 append_qos(DBusMessageIter *iter, struct assistant_config *cfg)
{
DBusMessageIter entry, var, dict;
const char *key = "QoS";
const char *bcode_key = "BCode";
uint8_t *bcode = cfg->qos.bcast.bcode;
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);
g_dbus_dict_append_basic_array(&dict, DBUS_TYPE_STRING,
&bcode_key, DBUS_TYPE_BYTE,
&bcode, BCODE_LEN);
dbus_message_iter_close_container(&var, &dict);
dbus_message_iter_close_container(&entry, &var);
dbus_message_iter_close_container(iter, &entry);
}
static void push_setup(DBusMessageIter *iter, void *user_data)
{
struct assistant_config *cfg = user_data;
DBusMessageIter dict;
const char *meta = "Metadata";
dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "{sv}", &dict);
if (cfg->meta)
g_dbus_dict_append_basic_array(&dict, DBUS_TYPE_STRING, &meta,
DBUS_TYPE_BYTE, &cfg->meta->iov_base,
cfg->meta->iov_len);
if (cfg->device)
g_dbus_dict_append_entry(&dict, "Device", DBUS_TYPE_OBJECT_PATH,
&cfg->device);
if (cfg->qos.bcast.encryption)
append_qos(&dict, cfg);
dbus_message_iter_close_container(iter, &dict);
}
static void push_reply(DBusMessage *message, void *user_data)
{
struct assistant_config *cfg = user_data;
DBusError error;
dbus_error_init(&error);
if (dbus_set_error_from_message(&error, message)) {
bt_shell_printf("Failed to push assistant: %s\n",
error.name);
dbus_error_free(&error);
return bt_shell_noninteractive_quit(EXIT_FAILURE);
}
bt_shell_printf("Assistant %s pushed\n",
g_dbus_proxy_get_path(cfg->proxy));
free(cfg->meta);
g_free(cfg);
return bt_shell_noninteractive_quit(EXIT_SUCCESS);
}
static void assistant_set_bcode_cfg(const char *input, void *user_data)
{
struct assistant_config *cfg = user_data;
if (!strcasecmp(input, "a") || !strcasecmp(input, "auto")) {
memset(cfg->qos.bcast.bcode, 0, BCODE_LEN);
} else {
if (strnlen(input, BCODE_LEN + 1) > BCODE_LEN) {
bt_shell_printf("Input string too long %s\n", input);
goto fail;
}
memcpy(cfg->qos.bcast.bcode, input, strlen(input));
}
if (!g_dbus_proxy_method_call(cfg->proxy, "Push",
push_setup, push_reply,
cfg, NULL)) {
bt_shell_printf("Failed to push assistant\n");
goto fail;
}
return;
fail:
free(cfg->meta);
g_free(cfg);
return bt_shell_noninteractive_quit(EXIT_FAILURE);
}
static bool assistant_get_qos(struct assistant_config *cfg)
{
DBusMessageIter iter, dict;
const char *key;
/* Get QoS property to check if the stream is encrypted */
if (!g_dbus_proxy_get_property(cfg->proxy, "QoS", &iter))
return false;
if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY)
return false;
dbus_message_iter_recurse(&iter, &dict);
while (dbus_message_iter_get_arg_type(&dict) != DBUS_TYPE_DICT_ENTRY) {
DBusMessageIter entry, value;
int var;
dbus_message_iter_recurse(&dict, &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, "Encryption")) {
if (var != DBUS_TYPE_BYTE)
return false;
dbus_message_iter_get_basic(&value,
&cfg->qos.bcast.encryption);
} else if (!strcasecmp(key, "BCode")) {
DBusMessageIter array;
struct iovec iov = {0};
if (var != DBUS_TYPE_ARRAY)
return false;
dbus_message_iter_recurse(&value, &array);
dbus_message_iter_get_fixed_array(&array,
&iov.iov_base,
(int *)&iov.iov_len);
if (iov.iov_len != 16) {
bt_shell_printf("Invalid size for BCode: "
"%zu != 16\n", iov.iov_len);
return false;
}
memcpy(cfg->qos.bcast.bcode, iov.iov_base, iov.iov_len);
}
}
return true;
}
static void assistant_set_metadata_cfg(const char *input, void *user_data)
{
struct assistant_config *cfg = user_data;
uint8_t no_bcode[16] = {};
if (!strcasecmp(input, "a") || !strcasecmp(input, "auto"))
goto done;
if (!cfg->meta)
cfg->meta = g_new0(struct iovec, 1);
cfg->meta->iov_base = str2bytearray((char *) input,
&cfg->meta->iov_len);
if (!cfg->meta->iov_base) {
free(cfg->meta);
cfg->meta = NULL;
}
done:
if (!assistant_get_qos(cfg))
goto fail;
if (cfg->qos.bcast.encryption &&
!memcmp(cfg->qos.bcast.bcode, no_bcode, 16))
/* Prompt user to enter the Broadcast Code to decrypt
* the stream
*/
bt_shell_prompt_input("Assistant",
"Enter Broadcast Code (auto/value):",
assistant_set_bcode_cfg, cfg);
else
if (!g_dbus_proxy_method_call(cfg->proxy, "Push",
push_setup, push_reply,
cfg, NULL)) {
bt_shell_printf("Failed to push assistant\n");
goto fail;
}
return;
fail:
free(cfg->meta);
g_free(cfg);
return bt_shell_noninteractive_quit(EXIT_FAILURE);
}
static void assistant_set_device_cfg(const char *input, void *user_data)
{
struct assistant_config *cfg = user_data;
uint8_t no_bcode[16] = {};
cfg->device = strdup(input);
if (!assistant_get_qos(cfg))
goto fail;
if (cfg->qos.bcast.encryption &&
!memcmp(cfg->qos.bcast.bcode, no_bcode, 16)) {
/* Prompt user to enter the Broadcast Code to decrypt
* the stream
*/
bt_shell_prompt_input("Assistant",
"Enter Broadcast Code (auto/value):",
assistant_set_bcode_cfg, cfg);
} else {
if (!g_dbus_proxy_method_call(cfg->proxy, "Push",
push_setup, push_reply,
cfg, NULL)) {
bt_shell_printf("Failed to push assistant\n");
goto fail;
}
}
return;
fail:
free(cfg->device);
free(cfg->meta);
g_free(cfg);
return bt_shell_noninteractive_quit(EXIT_FAILURE);
}
static void cmd_push_assistant(int argc, char *argv[])
{
struct assistant_config *cfg;
DBusMessageIter iter;
cfg = new0(struct assistant_config, 1);
if (!cfg)
goto fail;
/* Search for DBus object */
cfg->proxy = g_dbus_proxy_lookup(assistants, NULL, argv[1],
MEDIA_ASSISTANT_INTERFACE);
if (!cfg->proxy) {
bt_shell_printf("Assistant %s not found\n", argv[1]);
goto fail;
}
if (g_dbus_proxy_get_property(cfg->proxy, "State", &iter)) {
dbus_message_iter_get_basic(&iter, &cfg->state);
if (!strcmp(cfg->state, "local")) {
/* Prompt user to enter metadata */
bt_shell_prompt_input("Assistant",
"Enter Device (path):",
assistant_set_device_cfg, cfg);
return;
}
}
/* Prompt user to enter metadata */
bt_shell_prompt_input("Assistant",
"Enter Metadata (auto/value):",
assistant_set_metadata_cfg, cfg);
return;
fail:
g_free(cfg);
return bt_shell_noninteractive_quit(EXIT_FAILURE);
}
static void cmd_list_assistant(int argc, char *argv[])
{
GList *l;
for (l = assistants; l; l = g_list_next(l)) {
GDBusProxy *proxy = l->data;
print_assistant(proxy, NULL);
}
return bt_shell_noninteractive_quit(EXIT_SUCCESS);
}
static void print_assistant_properties(GDBusProxy *proxy)
{
bt_shell_printf("Transport %s\n", g_dbus_proxy_get_path(proxy));
print_property(proxy, "State");
print_property(proxy, "Metadata");
print_property(proxy, "QoS");
}
static void print_assistants(void *data, void *user_data)
{
print_assistant_properties(data);
}
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 *assistant_generator(const char *text, int state)
{
return generic_generator(text, state, assistants);
}
static void cmd_show_assistant(int argc, char *argv[])
{
GDBusProxy *proxy;
/* Show all transports if no argument is given */
if (argc != 2) {
g_list_foreach(assistants, print_assistants, NULL);
return bt_shell_noninteractive_quit(EXIT_SUCCESS);
}
proxy = g_dbus_proxy_lookup(assistants, NULL, argv[1],
MEDIA_ASSISTANT_INTERFACE);
if (!proxy) {
bt_shell_printf("Assistant %s not found\n", argv[1]);
return bt_shell_noninteractive_quit(EXIT_FAILURE);
}
print_assistant_properties(proxy);
return bt_shell_noninteractive_quit(EXIT_SUCCESS);
}
static const struct bt_shell_menu assistant_menu = {
.name = "assistant",
.desc = "Media Assistant Submenu",
.pre_run = assistant_menu_pre_run,
.entries = {
{ "list", NULL, cmd_list_assistant, "List available assistants" },
{ "show", "[assistant]", cmd_show_assistant,
"Assistant information",
assistant_generator },
{ "push", "<assistant>", cmd_push_assistant,
"Send stream information to peer",
assistant_generator },
{} },
};
static GDBusClient * client;
void assistant_add_submenu(void)
{
bt_shell_add_submenu(&assistant_menu);
}
static void assistant_menu_pre_run(const struct bt_shell_menu *menu)
{
dbus_conn = bt_shell_get_env("DBUS_CONNECTION");
if (!dbus_conn || client)
return;
client = g_dbus_client_new(dbus_conn, "org.bluez", "/org/bluez");
g_dbus_client_set_proxy_handlers(client, proxy_added, proxy_removed,
property_changed, NULL);
g_dbus_client_set_disconnect_watch(client, disconnect_handler, NULL);
}
void assistant_remove_submenu(void)
{
g_dbus_client_unref(client);
client = NULL;
}