diff --git a/spa/include/spa/utils/names.h b/spa/include/spa/utils/names.h index f8104b742..cd92c5c9e 100644 --- a/spa/include/spa/utils/names.h +++ b/spa/include/spa/utils/names.h @@ -120,6 +120,8 @@ extern "C" { #define SPA_NAME_API_BLUEZ5_A2DP_SOURCE "api.bluez5.a2dp.source" /**< alias for media.source */ #define SPA_NAME_API_BLUEZ5_SCO_SINK "api.bluez5.sco.sink" /**< a playback Node interface for HSP/HFP profiles */ #define SPA_NAME_API_BLUEZ5_SCO_SOURCE "api.bluez5.sco.source" /**< a capture Node interface for HSP/HFP profiles */ +#define SPA_NAME_API_BLUEZ5_MIDI_ENUM "api.bluez5.midi.enum" /**< a dbus midi Device interface */ +#define SPA_NAME_API_BLUEZ5_MIDI_NODE "api.bluez5.midi.node" /**< a midi Node interface */ /** keys for codec factory names */ #define SPA_NAME_API_CODEC_BLUEZ5_MEDIA "api.codec.bluez5.media" /**< Bluez5 Media codec plugin */ diff --git a/spa/plugins/bluez5/meson.build b/spa/plugins/bluez5/meson.build index 1b3955274..902462e24 100644 --- a/spa/plugins/bluez5/meson.build +++ b/spa/plugins/bluez5/meson.build @@ -31,6 +31,7 @@ bluez5_sources = [ 'hci.c', 'dbus-manager.c', 'dbus-monitor.c', + 'midi-enum.c', ] bluez5_data = ['bluez-hardware.conf'] diff --git a/spa/plugins/bluez5/midi-enum.c b/spa/plugins/bluez5/midi-enum.c new file mode 100644 index 000000000..cfc16d95d --- /dev/null +++ b/spa/plugins/bluez5/midi-enum.c @@ -0,0 +1,775 @@ +/* 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, +}; diff --git a/spa/plugins/bluez5/midi.h b/spa/plugins/bluez5/midi.h new file mode 100644 index 000000000..a8ce39401 --- /dev/null +++ b/spa/plugins/bluez5/midi.h @@ -0,0 +1,45 @@ +/* Spa V4l2 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. + */ +#ifndef SPA_BT_MIDI_H_ +#define SPA_BT_MIDI_H_ + +#include +#include +#include + +#include +#include + +#define BLUEZ_SERVICE "org.bluez" +#define BLUEZ_ADAPTER_INTERFACE BLUEZ_SERVICE ".Adapter1" +#define BLUEZ_DEVICE_INTERFACE BLUEZ_SERVICE ".Device1" +#define BLUEZ_GATT_MANAGER_INTERFACE BLUEZ_SERVICE ".GattManager1" +#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 BT_MIDI_SERVICE_UUID "03b80e5a-ede8-4b33-a751-6ce34ec4c700" +#define BT_MIDI_CHR_UUID "7772e5db-3868-4112-a1a9-f2669d106bf3" + +#endif diff --git a/spa/plugins/bluez5/plugin.c b/spa/plugins/bluez5/plugin.c index 4a06f81d3..dbbce9bf1 100644 --- a/spa/plugins/bluez5/plugin.c +++ b/spa/plugins/bluez5/plugin.c @@ -35,6 +35,7 @@ extern const struct spa_handle_factory spa_sco_sink_factory; extern const struct spa_handle_factory spa_sco_source_factory; extern const struct spa_handle_factory spa_a2dp_sink_factory; extern const struct spa_handle_factory spa_a2dp_source_factory; +extern const struct spa_handle_factory spa_bluez5_midi_enum_factory; SPA_EXPORT int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) @@ -67,6 +68,9 @@ int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t case 7: *factory = &spa_a2dp_source_factory; break; + case 8: + *factory = &spa_bluez5_midi_enum_factory; + break; default: return 0; }