diff --git a/spa/plugins/bluez5/midi-enum.c b/spa/plugins/bluez5/midi-enum.c index 4ab353473..7396bb3ac 100644 --- a/spa/plugins/bluez5/midi-enum.c +++ b/spa/plugins/bluez5/midi-enum.c @@ -111,18 +111,29 @@ struct chr { struct spa_dbus_object object; char *service_path; + char *description; uint32_t id; DBusPendingCall *read_call; + DBusPendingCall *dsc_call; unsigned int node_emitted:1; unsigned int valid_uuid:1; unsigned int read_probed:1; unsigned int read_done:1; + unsigned int dsc_probed:1; + unsigned int dsc_done:1; +}; + +struct dsc +{ + struct spa_dbus_object object; + char *chr_path; + unsigned int valid_uuid:1; }; static void emit_chr_node(struct impl *impl, struct chr *chr, struct device *device) { struct spa_device_object_info info; - char class[16]; + char nick[512], class[16]; struct spa_dict_item items[23]; uint32_t n_items = 0; @@ -140,6 +151,10 @@ static void emit_chr_node(struct impl *impl, struct chr *chr, struct device *dev items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Midi/Bridge"); items[n_items++] = SPA_DICT_ITEM_INIT("node.description", device->alias ? device->alias : device->name); + if (chr->description && chr->description[0] != '\0') { + spa_scnprintf(nick, sizeof(nick), "%s (%s)", device->alias, chr->description); + items[n_items++] = SPA_DICT_ITEM_INIT("node.nick", nick); + } items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ICON, device->icon); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_PATH, chr->object.path); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ADDRESS, device->address); @@ -214,11 +229,109 @@ static int read_probe(struct impl *impl, struct chr *chr) read_probe_reply); } +struct dsc *find_dsc(struct impl *impl, struct chr *chr) +{ + struct dsc *dsc; + const struct spa_list *dscs = spa_dbus_monitor_object_list( + impl->dbus_monitor, BLUEZ_GATT_DSC_INTERFACE); + + spa_assert(dscs); + spa_list_for_each(dsc, dscs, object.link) { + if (dsc->valid_uuid && spa_streq(dsc->chr_path, chr->object.path)) + return dsc; + } + return NULL; +} + +static void read_dsc_reply(DBusPendingCall **call_ptr, DBusMessage *r) +{ + struct chr *chr = SPA_CONTAINER_OF(call_ptr, struct chr, dsc_call); + struct impl *impl = chr->object.user_data; + DBusError err; + DBusMessageIter args, arr; + void *value = NULL; + int n = 0; + + chr->dsc_done = true; + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, r)) { + spa_log_error(impl->log, "%s.ReadValue() failed: %s (%s)", + BLUEZ_GATT_DSC_INTERFACE, + err.name ? err.name : "unknown", + err.message ? err.message : ""); + dbus_error_free(&err); + return; + } + dbus_error_free(&err); + if (!dbus_message_has_signature(r, "ay")) { + spa_log_warn(impl->log, "invalid ReadValue() signature"); + return; + } + + dbus_message_iter_init(r, &args); + dbus_message_iter_recurse(&args, &arr); + dbus_message_iter_get_fixed_array(&arr, &value, &n); + + free(chr->description); + if (n > 0) + chr->description = strndup(value, n); + else + chr->description = NULL; + + spa_log_debug(impl->log, "MIDI GATT user descriptor value: '%s'", + chr->description); + + check_chr_node(impl, chr); +} + +static int read_dsc(struct impl *impl, struct chr *chr) +{ + DBusMessageIter i, d; + DBusMessage *m; + struct dsc *dsc; + + if (chr->dsc_probed) + return 0; + if (chr->dsc_call) + return -EBUSY; + + chr->dsc_probed = true; + + dsc = find_dsc(impl, chr); + if (dsc == NULL) { + chr->dsc_done = true; + return -ENOENT; + } + + spa_log_debug(impl->log, "MIDI GATT user descriptor read, path=%s", + dsc->object.path); + + m = dbus_message_new_method_call(BLUEZ_SERVICE, + dsc->object.path, + BLUEZ_GATT_DSC_INTERFACE, + "ReadValue"); + if (m == NULL) { + chr->dsc_done = true; + return -ENOMEM; + } + + dbus_message_iter_init_append(m, &i); + dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY, "{sv}", &d); + dbus_message_iter_close_container(&i, &d); + + return spa_dbus_async_call(impl->conn, m, &chr->dsc_call, + read_dsc_reply); +} + static int read_probe_reset(struct impl *impl, struct chr *chr) { spa_dbus_async_call_cancel(&chr->read_call); + spa_dbus_async_call_cancel(&chr->dsc_call); chr->read_probed = false; chr->read_done = false; + chr->dsc_probed = false; + chr->dsc_done = false; return 0; } @@ -255,6 +368,11 @@ static void check_chr_node(struct impl *impl, struct chr *chr) available = false; } + if (available && !chr->dsc_done) { + read_dsc(impl, chr); + available = chr->dsc_done; + } + if (chr->node_emitted && !available) { remove_chr_node(impl, chr); chr->node_emitted = false; @@ -477,6 +595,7 @@ static void chr_remove(struct spa_dbus_object *object) remove_chr_node(impl, chr); free(chr->service_path); + free(chr->description); } static void chr_property(struct spa_dbus_object *object, const char *key, DBusMessageIter *value) @@ -489,6 +608,35 @@ static void chr_property(struct spa_dbus_object *object, const char *key, DBusMe dup_dbus_string(value, &chr->service_path); } +static void dsc_update(struct spa_dbus_object *object) +{ + struct impl *impl = object->user_data; + struct dsc *dsc = SPA_CONTAINER_OF(object, struct dsc, object); + + if (!dsc->valid_uuid) { + spa_dbus_monitor_ignore_object(impl->dbus_monitor, object); + return; + } +} + +static void dsc_property(struct spa_dbus_object *object, const char *key, DBusMessageIter *value) +{ + struct dsc *dsc = SPA_CONTAINER_OF(object, struct dsc, object); + + if (spa_streq(key, "UUID")) + dsc->valid_uuid = spa_streq(get_dbus_string(value), + BT_GATT_CHARACTERISTIC_USER_DESCRIPTION_UUID); + else if (spa_streq(key, "Characteristic")) + dup_dbus_string(value, &dsc->chr_path); +} + +static void dsc_remove(struct spa_dbus_object *object) +{ + struct dsc *dsc = SPA_CONTAINER_OF(object, struct dsc, object); + + free(dsc->chr_path); +} + static const struct spa_dbus_interface monitor_interfaces[] = { { .name = BLUEZ_ADAPTER_INTERFACE, @@ -517,6 +665,13 @@ static const struct spa_dbus_interface monitor_interfaces[] = { .property = chr_property, .object_size = sizeof(struct chr), }, + { + .name = BLUEZ_GATT_DSC_INTERFACE, + .update = dsc_update, + .remove = dsc_remove, + .property = dsc_property, + .object_size = sizeof(struct dsc), + }, {NULL} }; diff --git a/spa/plugins/bluez5/midi-node.c b/spa/plugins/bluez5/midi-node.c index d708033f4..98d3cb9f7 100644 --- a/spa/plugins/bluez5/midi-node.c +++ b/spa/plugins/bluez5/midi-node.c @@ -81,6 +81,7 @@ enum node_role { struct props { char clock_name[64]; + char device_name[512]; int64_t latency_offset; }; @@ -311,6 +312,7 @@ static void reset_props(struct props *props) { props->latency_offset = DEFAULT_LATENCY_OFFSET; strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name)); + props->device_name[0] = '\0'; } static bool is_following(struct impl *this) @@ -1053,6 +1055,12 @@ static int server_release(void *user_data) return 0; } +static const char *server_description(void *user_data) +{ + struct impl *this = user_data; + return this->props.device_name; +} + static int do_remove_port_source(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) { @@ -1254,6 +1262,8 @@ next: switch (id) { case SPA_PARAM_PropInfo: { + struct props *p = &this->props; + switch (result.index) { case 0: param = spa_pod_builder_add_object(&b, @@ -1262,6 +1272,13 @@ next: SPA_PROP_INFO_description, SPA_POD_String("Latency offset (ns)"), SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(0LL, INT64_MIN, INT64_MAX)); break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_deviceName), + SPA_PROP_INFO_description, SPA_POD_String("Device name"), + SPA_PROP_INFO_type, SPA_POD_String(p->device_name)); + break; default: return 0; } @@ -1275,7 +1292,8 @@ next: case 0: param = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_Props, id, - SPA_PROP_latencyOffsetNsec, SPA_POD_Long(p->latency_offset)); + SPA_PROP_latencyOffsetNsec, SPA_POD_Long(p->latency_offset), + SPA_PROP_deviceName, SPA_POD_String(p->device_name)); break; default: return 0; @@ -1322,7 +1340,9 @@ static int apply_props(struct impl *this, const struct spa_pod *param) } else { spa_pod_parse_object(param, SPA_TYPE_OBJECT_Props, NULL, - SPA_PROP_latencyOffsetNsec, SPA_POD_OPT_Long(&new_props.latency_offset)); + SPA_PROP_latencyOffsetNsec, SPA_POD_OPT_Long(&new_props.latency_offset), + SPA_PROP_deviceName, SPA_POD_OPT_Stringn(new_props.device_name, + sizeof(new_props.device_name))); } changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0); @@ -1814,6 +1834,7 @@ static const struct spa_bt_midi_server_cb impl_server = { .acquire_write = server_acquire_write, .acquire_notify = server_acquire_notify, .release = server_release, + .get_description = server_description, }; static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) @@ -1870,6 +1891,7 @@ impl_init(const struct spa_handle_factory *factory, uint32_t n_support) { struct impl *this; + const char *device_name = ""; int res = 0; size_t i; @@ -1917,6 +1939,11 @@ impl_init(const struct spa_handle_factory *factory, if (spa_streq(str, "server")) this->role = NODE_SERVER; } + + if ((str = spa_dict_lookup(info, "node.nick")) != NULL) + device_name = str; + else if ((str = spa_dict_lookup(info, "node.description")) != NULL) + device_name = str; } if (this->role == NODE_CLIENT && this->chr_path == NULL) { @@ -1953,6 +1980,9 @@ impl_init(const struct spa_handle_factory *factory, reset_props(&this->props); + spa_scnprintf(this->props.device_name, sizeof(this->props.device_name), + "%s", device_name); + /* set the node info */ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | SPA_NODE_CHANGE_MASK_PROPS | diff --git a/spa/plugins/bluez5/midi-server.c b/spa/plugins/bluez5/midi-server.c index d80ca68c8..b30b239b7 100644 --- a/spa/plugins/bluez5/midi-server.c +++ b/spa/plugins/bluez5/midi-server.c @@ -42,6 +42,7 @@ #define MIDI_SERVER_PATH "/midiserver" #define MIDI_SERVICE_PATH "/midiserver/service" #define MIDI_CHR_PATH "/midiserver/service/chr" +#define MIDI_DSC_PATH "/midiserver/service/chr/dsc" #define BLE_DEFAULT_MTU 23 @@ -56,6 +57,7 @@ struct impl struct spa_dbus_local_object *service; struct spa_dbus_local_object *chr; + struct spa_dbus_local_object *dsc; struct spa_dbus_monitor *dbus_monitor; struct spa_dbus_object_manager *objects; @@ -76,6 +78,118 @@ struct chr { unsigned int notify_acquired:1; }; +struct dsc { + struct spa_dbus_local_object object; +}; + + +/* + * Characteristic user descriptor: not in BLE MIDI standard, but we + * put a device name here in case we have multiple MIDI endpoints. + */ + +static DBusMessage *parse_options(struct impl *impl, DBusMessage *m, const char *out_key, uint16_t *out_value); + +static DBusMessage *dsc_read_value(struct spa_dbus_local_object *object, DBusMessage *m) +{ + struct dsc *dsc = SPA_CONTAINER_OF(object, struct dsc, object); + struct impl *impl = dsc->object.user_data; + DBusMessage *r; + DBusMessageIter i, a; + const char *description = NULL; + const uint8_t *ptr; + uint16_t offset = 0; + int len; + + r = parse_options(impl, m, "offset", &offset); + if (r) + return r; + + if (impl->cb->get_description) + description = impl->cb->get_description(impl->user_data); + if (!description) + description = ""; + + len = strlen(description); + if (offset > len) + return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, + "Invalid arguments"); + + r = dbus_message_new_method_return(m); + if (r == NULL) + return NULL; + + ptr = SPA_PTROFF(description, offset, const uint8_t); + len -= offset; + + dbus_message_iter_init_append(r, &i); + dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY, "y", &a); + dbus_message_iter_append_fixed_array(&a, DBUS_TYPE_BYTE, &ptr, len); + dbus_message_iter_close_container(&i, &a); + return r; +} + +static int dsc_prop_uuid_get(struct spa_dbus_local_object *object, DBusMessageIter *value) +{ + const char *uuid = BT_GATT_CHARACTERISTIC_USER_DESCRIPTION_UUID; + dbus_message_iter_append_basic(value, DBUS_TYPE_STRING, &uuid); + return 0; +} + +static int dsc_prop_characteristic_get(struct spa_dbus_local_object *object, DBusMessageIter *value) +{ + struct dsc *dsc = SPA_CONTAINER_OF(object, struct dsc, object); + struct impl *impl = dsc->object.user_data; + dbus_message_iter_append_basic(value, DBUS_TYPE_OBJECT_PATH, &impl->chr->path); + return 0; +} + +static int dsc_prop_flags_get(struct spa_dbus_local_object *object, DBusMessageIter *value) +{ + DBusMessageIter a; + const char *flags[] = {"encrypt-read", NULL }; + const char **p; + + dbus_message_iter_open_container(value, DBUS_TYPE_ARRAY, "s", &a); + for (p = &flags[0]; *p; ++p) + dbus_message_iter_append_basic(&a, DBUS_TYPE_STRING, p); + dbus_message_iter_close_container(value, &a); + + return 0; +} + +static const struct spa_dbus_local_interface midi_dsc_interfaces[] = { + { + .name = BLUEZ_GATT_DSC_INTERFACE, + .methods = (struct spa_dbus_method[]) { + { + .name = "ReadValue", + .call = dsc_read_value, + }, + {NULL} + }, + .properties = (struct spa_dbus_property[]) { + { + .name = "UUID", + .signature = "s", + .get = dsc_prop_uuid_get, + }, + { + .name = "Characteristic", + .signature = "o", + .get = dsc_prop_characteristic_get, + }, + { + .name = "Flags", + .signature = "as", + .get = dsc_prop_flags_get, + }, + {NULL} + }, + }, + {NULL} +}; + /* * MIDI characteristic @@ -122,7 +236,7 @@ static void chr_change_acquired(struct impl *impl, struct chr *chr, bool write, spa_dbus_object_manager_properties_changed(impl->objects, impl->chr, iface, changed); } -static DBusMessage *parse_options(struct impl *impl, DBusMessage *m, uint16_t *mtu) +static DBusMessage *parse_options(struct impl *impl, DBusMessage *m, const char *out_key, uint16_t *out_value) { DBusMessageIter args, options; @@ -146,8 +260,8 @@ static DBusMessage *parse_options(struct impl *impl, DBusMessage *m, uint16_t *m dbus_message_iter_recurse(&entry, &value); type = dbus_message_iter_get_arg_type(&value); - if (spa_streq(key, "mtu") && type == DBUS_TYPE_UINT16) - dbus_message_iter_get_basic(&value, mtu); + if (spa_streq(key, out_key) && type == DBUS_TYPE_UINT16) + dbus_message_iter_get_basic(&value, out_value); dbus_message_iter_next(&options); } @@ -184,7 +298,7 @@ static DBusMessage *chr_acquire(struct spa_dbus_local_object *object, DBusMessag goto fail; } - r = parse_options(impl, m, &mtu); + r = parse_options(impl, m, "mtu", &mtu); if (r) return r; @@ -270,7 +384,8 @@ static int chr_prop_write_acquired_get(struct spa_dbus_local_object *object, DBu static int chr_prop_flags_get(struct spa_dbus_local_object *object, DBusMessageIter *value) { DBusMessageIter a; - const char *flags[] = {"read", "write-without-response", "notify", NULL }; + const char *flags[] = {"encrypt-read", "write-without-response", + "encrypt-write", "encrypt-notify", NULL }; const char **p; dbus_message_iter_open_container(value, DBUS_TYPE_ARRAY, "s", &a); @@ -481,6 +596,14 @@ static int register_objects(struct impl *impl) if (impl->chr == NULL) goto fail; + impl->dsc = spa_dbus_object_manager_register(impl->objects, + MIDI_DSC_PATH, + midi_dsc_interfaces, + sizeof(struct dsc), + impl); + if (impl->dsc == NULL) + goto fail; + impl->dbus_monitor = spa_dbus_monitor_new(impl->conn, BLUEZ_SERVICE, "/", monitor_interfaces, impl->log, impl); diff --git a/spa/plugins/bluez5/midi.h b/spa/plugins/bluez5/midi.h index 562342931..e5955f688 100644 --- a/spa/plugins/bluez5/midi.h +++ b/spa/plugins/bluez5/midi.h @@ -40,9 +40,11 @@ #define BLUEZ_GATT_PROFILE_INTERFACE BLUEZ_SERVICE ".GattProfile1" #define BLUEZ_GATT_SERVICE_INTERFACE BLUEZ_SERVICE ".GattService1" #define BLUEZ_GATT_CHR_INTERFACE BLUEZ_SERVICE ".GattCharacteristic1" +#define BLUEZ_GATT_DSC_INTERFACE BLUEZ_SERVICE ".GattDescriptor1" #define BT_MIDI_SERVICE_UUID "03b80e5a-ede8-4b33-a751-6ce34ec4c700" #define BT_MIDI_CHR_UUID "7772e5db-3868-4112-a1a9-f2669d106bf3" +#define BT_GATT_CHARACTERISTIC_USER_DESCRIPTION_UUID "00002901-0000-1000-8000-00805f9b34fb" #define MIDI_BUF_SIZE 8192 #define MIDI_MAX_MTU 8192 @@ -76,6 +78,7 @@ struct spa_bt_midi_server_cb int (*acquire_notify)(void *user_data, int fd, uint16_t mtu); int (*acquire_write)(void *user_data, int fd, uint16_t mtu); int (*release)(void *user_data); + const char *(*get_description)(void *user_data); }; static inline void spa_bt_midi_parser_init(struct spa_bt_midi_parser *parser)