/* Spa midi dbus * * Copyright © 2022 Pauli Virtanen * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "dbus-monitor.h" #include "dbus-manager.h" #include "midi.h" #include "config.h" #define MIDI_OBJECT_PATH "/midi" #define MIDI_PROFILE_PATH MIDI_OBJECT_PATH "/profile" static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.midi"); #undef SPA_LOG_TOPIC_DEFAULT #define SPA_LOG_TOPIC_DEFAULT &log_topic struct impl { struct spa_handle handle; struct spa_device device; struct spa_log *log; struct spa_dbus *dbus; struct spa_dbus_connection *dbus_connection; DBusConnection *conn; struct spa_dbus_monitor *dbus_monitor; struct spa_dbus_object_manager *object_manager; struct spa_dbus_local_object *profile; struct spa_hook_list hooks; uint32_t id; unsigned int object_manager_registered:1; unsigned int profile_registered:1; }; struct adapter { struct spa_dbus_object object; DBusPendingCall *register_call; unsigned int registered:1; }; struct device { struct spa_dbus_object object; char *adapter_path; char *name; char *alias; char *address; char *icon; uint32_t class; uint16_t appearance; unsigned int connected:1; unsigned int services_resolved:1; }; struct service { struct spa_dbus_object object; char *device_path; unsigned int valid_uuid:1; }; struct chr { struct spa_dbus_object object; char *service_path; uint32_t id; DBusPendingCall *read_call; unsigned int node_emitted:1; unsigned int valid_uuid:1; unsigned int read_probed:1; unsigned int read_done:1; }; static void emit_chr_node(struct impl *impl, struct chr *chr, struct device *device) { struct spa_device_object_info info; char class[16]; struct spa_dict_item items[23]; uint32_t n_items = 0; spa_log_debug(impl->log, "emit node for path=%s", chr->object.path); info = SPA_DEVICE_OBJECT_INFO_INIT(); info.type = SPA_TYPE_INTERFACE_Node; info.factory_name = SPA_NAME_API_BLUEZ5_MIDI_NODE; info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS | SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; info.flags = 0; items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "bluez5"); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS, "bluetooth"); 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); 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); snprintf(class, sizeof(class), "0x%06x", device->class); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_CLASS, class); info.props = &SPA_DICT_INIT(items, n_items); spa_device_emit_object_info(&impl->hooks, chr->id, &info); } static void remove_chr_node(struct impl *impl, struct chr *chr) { spa_log_debug(impl->log, "remove node for path=%s", chr->object.path); spa_device_emit_object_info(&impl->hooks, chr->id, NULL); } static void check_chr_node(struct impl *impl, struct chr *chr); static void read_probe_reply(DBusPendingCall **call_ptr, DBusMessage *r) { struct chr *chr = SPA_CONTAINER_OF(call_ptr, struct chr, read_call); struct impl *impl = chr->object.user_data; if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { spa_log_error(impl->log, "%s.ReadValue() failed: %s", BLUEZ_GATT_CHR_INTERFACE, dbus_message_get_error_name(r)); return; } spa_log_debug(impl->log, "MIDI GATT read probe done for path=%s", chr->object.path); chr->read_done = true; check_chr_node(impl, chr); } static int read_probe(struct impl *impl, struct chr *chr) { DBusMessageIter i, d; DBusMessage *m; /* * BLE MIDI-1.0 §5: The Central shall read the MIDI I/O characteristic * of the Peripheral after establishing a connection with the accessory. */ if (chr->read_probed) return 0; if (chr->read_call) return -EBUSY; chr->read_probed = true; spa_log_debug(impl->log, "MIDI GATT read probe for path=%s", chr->object.path); m = dbus_message_new_method_call(BLUEZ_SERVICE, chr->object.path, BLUEZ_GATT_CHR_INTERFACE, "ReadValue"); if (m == NULL) 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->read_call, read_probe_reply); } static int read_probe_reset(struct impl *impl, struct chr *chr) { spa_dbus_async_call_cancel(&chr->read_call); chr->read_probed = false; chr->read_done = false; return 0; } static void lookup_chr_node(struct impl *impl, struct chr *chr, struct service **service, struct device **device) { *service = (struct service *)spa_dbus_monitor_find(impl->dbus_monitor, chr->service_path, BLUEZ_GATT_SERVICE_INTERFACE); if (*service) *device = (struct device *)spa_dbus_monitor_find(impl->dbus_monitor, (*service)->device_path, BLUEZ_DEVICE_INTERFACE); else *device = NULL; } static void check_chr_node(struct impl *impl, struct chr *chr) { struct service *service; struct device *device; bool available; lookup_chr_node(impl, chr, &service, &device); if (!device || !device->connected) { /* Retry read probe on each connection */ read_probe_reset(impl, chr); } available = service && device && device->connected && device->services_resolved && service->valid_uuid && chr->valid_uuid; if (available && !chr->read_done) { read_probe(impl, chr); available = false; } if (chr->node_emitted && !available) { remove_chr_node(impl, chr); chr->node_emitted = false; } else if (!chr->node_emitted && available) { emit_chr_node(impl, chr, device); chr->node_emitted = true; } } static void check_all_nodes(struct impl *impl) { /* * Check if the nodes we have emitted are in sync with connected devices. */ struct spa_list *chrs = spa_dbus_monitor_object_list( impl->dbus_monitor, BLUEZ_GATT_CHR_INTERFACE); struct chr *chr; spa_assert(chrs); spa_list_for_each(chr, chrs, object.link) check_chr_node(impl, chr); } static void adapter_register_application_reply(DBusPendingCall **call_ptr, DBusMessage *r) { struct adapter *adapter = SPA_CONTAINER_OF(call_ptr, struct adapter, register_call); struct impl *impl = adapter->object.user_data; if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { spa_log_error(impl->log, "%s.RegisterApplication() failed: %s", BLUEZ_GATT_MANAGER_INTERFACE, dbus_message_get_error_name(r)); return; } adapter->registered = true; } static int adapter_register_application(struct adapter *adapter) { struct impl *impl = adapter->object.user_data; DBusMessageIter i, d; DBusMessage *m; m = dbus_message_new_method_call(BLUEZ_SERVICE, adapter->object.path, BLUEZ_GATT_MANAGER_INTERFACE, "RegisterApplication"); if (m == NULL) return -ENOMEM; dbus_message_iter_init_append(m, &i); dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &impl->object_manager->path); 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, &adapter->register_call, adapter_register_application_reply); } /* * DBus monitoring */ static const char *get_dbus_string(DBusMessageIter *value) { const char *v = NULL; int type = dbus_message_iter_get_arg_type(value); if (value && (type == DBUS_TYPE_STRING || type == DBUS_TYPE_OBJECT_PATH)) dbus_message_iter_get_basic(value, &v); return v; } static void dup_dbus_string(DBusMessageIter *value, char **v) { const char *str = get_dbus_string(value); free(*v); *v = str ? strdup(str) : NULL; } static bool get_dbus_bool(DBusMessageIter *value) { dbus_bool_t v = FALSE; if (value && dbus_message_iter_get_arg_type(value) == DBUS_TYPE_BOOLEAN) dbus_message_iter_get_basic(value, &v); return v ? true : false; } static uint32_t get_dbus_uint32(DBusMessageIter *value) { uint32_t v = 0; if (value && dbus_message_iter_get_arg_type(value) == DBUS_TYPE_UINT32) dbus_message_iter_get_basic(value, &v); return v; } static uint16_t get_dbus_uint16(DBusMessageIter *value) { uint16_t v = 0; if (value && dbus_message_iter_get_arg_type(value) == DBUS_TYPE_UINT16) dbus_message_iter_get_basic(value, &v); return v; } static void adapter_update(struct spa_dbus_object *object) { struct adapter *adapter = SPA_CONTAINER_OF(object, struct adapter, object); if (adapter->registered || adapter->register_call) return; adapter_register_application(adapter); } static void adapter_remove(struct spa_dbus_object *object) { struct adapter *adapter = SPA_CONTAINER_OF(object, struct adapter, object); spa_dbus_async_call_cancel(&adapter->register_call); } static void device_update(struct spa_dbus_object *object) { struct impl *impl = object->user_data; check_all_nodes(impl); } static void device_remove(struct spa_dbus_object *object) { struct device *device = SPA_CONTAINER_OF(object, struct device, object); free(device->adapter_path); free(device->name); free(device->alias); free(device->address); free(device->icon); } static void device_property(struct spa_dbus_object *object, const char *key, DBusMessageIter *value) { struct device *device = SPA_CONTAINER_OF(object, struct device, object); if (spa_streq(key, "Adapter")) dup_dbus_string(value, &device->adapter_path); else if (spa_streq(key, "Connected")) device->connected = get_dbus_bool(value); else if (spa_streq(key, "ServicesResolved")) device->services_resolved = get_dbus_bool(value); else if (spa_streq(key, "Name")) dup_dbus_string(value, &device->name); else if (spa_streq(key, "Alias")) dup_dbus_string(value, &device->alias); else if (spa_streq(key, "Address")) dup_dbus_string(value, &device->address); else if (spa_streq(key, "Icon")) dup_dbus_string(value, &device->icon); else if (spa_streq(key, "Class")) device->class = get_dbus_uint32(value); else if (spa_streq(key, "Appearance")) device->appearance = get_dbus_uint16(value); } static void service_update(struct spa_dbus_object *object) { struct service *service = SPA_CONTAINER_OF(object, struct service, object); struct impl *impl = object->user_data; if (!service->valid_uuid) { spa_dbus_monitor_ignore_object(impl->dbus_monitor, object); return; } check_all_nodes(impl); } static void service_remove(struct spa_dbus_object *object) { struct service *service = SPA_CONTAINER_OF(object, struct service, object); free(service->device_path); } static void service_property(struct spa_dbus_object *object, const char *key, DBusMessageIter *value) { struct service *service = SPA_CONTAINER_OF(object, struct service, object); if (spa_streq(key, "UUID")) service->valid_uuid = spa_streq(get_dbus_string(value), BT_MIDI_SERVICE_UUID); else if (spa_streq(key, "Device")) dup_dbus_string(value, &service->device_path); } static void chr_update(struct spa_dbus_object *object) { struct impl *impl = object->user_data; struct chr *chr = SPA_CONTAINER_OF(object, struct chr, object); if (!chr->valid_uuid) { spa_dbus_monitor_ignore_object(impl->dbus_monitor, object); return; } if (chr->id == 0) chr->id = ++impl->id; check_chr_node(impl, chr); } static void chr_remove(struct spa_dbus_object *object) { struct impl *impl = object->user_data; struct chr *chr = SPA_CONTAINER_OF(object, struct chr, object); read_probe_reset(impl, chr); if (chr->node_emitted) remove_chr_node(impl, chr); free(chr->service_path); } static void chr_property(struct spa_dbus_object *object, const char *key, DBusMessageIter *value) { struct chr *chr = SPA_CONTAINER_OF(object, struct chr, object); if (spa_streq(key, "UUID")) chr->valid_uuid = spa_streq(get_dbus_string(value), BT_MIDI_CHR_UUID); else if (spa_streq(key, "Service")) dup_dbus_string(value, &chr->service_path); } static const struct spa_dbus_interface monitor_interfaces[] = { { .name = BLUEZ_ADAPTER_INTERFACE, .update = adapter_update, .remove = adapter_remove, .object_size = sizeof(struct adapter), }, { .name = BLUEZ_DEVICE_INTERFACE, .update = device_update, .remove = device_remove, .property = device_property, .object_size = sizeof(struct device), }, { .name = BLUEZ_GATT_SERVICE_INTERFACE, .update = service_update, .remove = service_remove, .property = service_property, .object_size = sizeof(struct service), }, { .name = BLUEZ_GATT_CHR_INTERFACE, .update = chr_update, .remove = chr_remove, .property = chr_property, .object_size = sizeof(struct chr), }, {NULL} }; /* * DBus GATT profile, to enable BlueZ autoconnect */ static DBusMessage *profile_release(struct spa_dbus_local_object *object, DBusMessage *m) { /* noop */ return dbus_message_new_method_return(m); } static int profile_uuids_get(struct spa_dbus_local_object *object, DBusMessageIter *value) { DBusMessageIter a; const char *uuid = BT_MIDI_SERVICE_UUID; dbus_message_iter_open_container(value, DBUS_TYPE_ARRAY, "s", &a); dbus_message_iter_append_basic(&a, DBUS_TYPE_STRING, &uuid); dbus_message_iter_close_container(value, &a); return 0; } static const struct spa_dbus_local_interface profile_interfaces[] = { { .name = BLUEZ_GATT_PROFILE_INTERFACE, .methods = (struct spa_dbus_method[]) { { .name = "Release", .call = profile_release, }, {NULL} }, .properties = (struct spa_dbus_property[]) { { .name = "UUIDs", .signature = "as", .get = profile_uuids_get, }, {NULL}, }, }, {NULL} }; /* * Monitor impl */ static int impl_device_add_listener(void *object, struct spa_hook *listener, const struct spa_device_events *events, void *data) { struct impl *this = object; struct spa_hook_list save; struct spa_list *chrs; struct chr *chr; spa_return_val_if_fail(this != NULL, -EINVAL); spa_return_val_if_fail(events != NULL, -EINVAL); chrs = spa_dbus_monitor_object_list( this->dbus_monitor, BLUEZ_GATT_CHR_INTERFACE); spa_hook_list_isolate(&this->hooks, &save, listener, events, data); spa_list_for_each(chr, chrs, object.link) { struct service *service; struct device *device; if (!chr->node_emitted) continue; lookup_chr_node(this, chr, &service, &device); if (device) emit_chr_node(this, chr, device); } spa_hook_list_join(&this->hooks, &save); return 0; } static const struct spa_device_methods impl_device = { SPA_VERSION_DEVICE_METHODS, .add_listener = impl_device_add_listener, }; static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) { struct impl *this; spa_return_val_if_fail(handle != NULL, -EINVAL); spa_return_val_if_fail(interface != NULL, -EINVAL); this = (struct impl *) handle; if (spa_streq(type, SPA_TYPE_INTERFACE_Device)) *interface = &this->device; else return -ENOENT; return 0; } static int impl_clear(struct spa_handle *handle) { struct impl *this; this = (struct impl *) handle; if (this->object_manager) spa_dbus_object_manager_destroy(this->object_manager); if (this->dbus_monitor) spa_dbus_monitor_destroy(this->dbus_monitor); if (this->conn) dbus_connection_unref(this->conn); if (this->dbus_connection) spa_dbus_connection_destroy(this->dbus_connection); spa_zero(*this); return 0; } static size_t impl_get_size(const struct spa_handle_factory *factory, const struct spa_dict *params) { return sizeof(struct impl); } static int impl_init(const struct spa_handle_factory *factory, struct spa_handle *handle, const struct spa_dict *info, const struct spa_support *support, uint32_t n_support) { struct impl *this; int res = 0; spa_return_val_if_fail(factory != NULL, -EINVAL); spa_return_val_if_fail(handle != NULL, -EINVAL); handle->get_interface = impl_get_interface; handle->clear = impl_clear; this = (struct impl *) handle; this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); this->dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus); if (this->log == NULL) return -EINVAL; spa_log_topic_init(this->log, &log_topic); if (this->dbus == NULL) { spa_log_error(this->log, "a dbus is needed"); return -EINVAL; } this->conn = NULL; this->dbus_connection = NULL; this->dbus_connection = spa_dbus_get_connection(this->dbus, SPA_DBUS_TYPE_SYSTEM); if (this->dbus_connection == NULL) { spa_log_error(this->log, "no dbus connection"); res = -EIO; goto fail; } this->conn = spa_dbus_connection_get(this->dbus_connection); if (this->conn == NULL) { spa_log_error(this->log, "failed to get dbus connection"); res = -EIO; goto fail; } /* * XXX: We should handle spa_dbus reconnecting, but we don't, so ref * XXX: the handle so that we can keep it if spa_dbus unrefs it. */ dbus_connection_ref(this->conn); spa_hook_list_init(&this->hooks); this->device.iface = SPA_INTERFACE_INIT( SPA_TYPE_INTERFACE_Device, SPA_VERSION_DEVICE, &impl_device, this); this->dbus_monitor = spa_dbus_monitor_new(this->conn, BLUEZ_SERVICE, "/", monitor_interfaces, this->log, this); if (!this->dbus_monitor) goto fail; this->object_manager = spa_dbus_object_manager_new(this->conn, MIDI_OBJECT_PATH, this->log); if (!this->object_manager) goto fail; this->profile = spa_dbus_object_manager_register(this->object_manager, MIDI_PROFILE_PATH, profile_interfaces, sizeof(struct spa_dbus_local_object), this); if (!this->profile) goto fail; return 0; fail: res = (res < 0) ? res : ((errno > 0) ? -errno : -EIO); impl_clear(handle); return res; } static const struct spa_interface_info impl_interfaces[] = { {SPA_TYPE_INTERFACE_Device,}, }; static int impl_enum_interface_info(const struct spa_handle_factory *factory, const struct spa_interface_info **info, uint32_t *index) { spa_return_val_if_fail(factory != NULL, -EINVAL); spa_return_val_if_fail(info != NULL, -EINVAL); spa_return_val_if_fail(index != NULL, -EINVAL); if (*index >= SPA_N_ELEMENTS(impl_interfaces)) return 0; *info = &impl_interfaces[(*index)++]; return 1; } static const struct spa_dict_item info_items[] = { { SPA_KEY_FACTORY_AUTHOR, "Pauli Virtanen " }, { SPA_KEY_FACTORY_DESCRIPTION, "Bluez5 MIDI connection" }, }; static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items); const struct spa_handle_factory spa_bluez5_midi_enum_factory = { SPA_VERSION_HANDLE_FACTORY, SPA_NAME_API_BLUEZ5_MIDI_ENUM, &info, impl_get_size, impl_init, impl_enum_interface_info, };