From e5f0faa6c0899dc1d2fa4ec3df9845344fcb0684 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Sun, 3 Sep 2023 19:09:20 +0200 Subject: [PATCH] Register MIDI application GATT server in BlueZ --- .github/spellcheck-wordlist.txt | 2 + src/Makefile.am | 5 + src/ba-transport-pcm.c | 8 +- src/ba-transport-pcm.h | 4 +- src/ba-transport.c | 68 +++++- src/ba-transport.h | 22 +- src/bluealsa-config.h | 1 + src/bluealsa-dbus.c | 7 + src/bluealsa-iface.h | 5 +- src/bluez-iface.h | 34 ++- src/bluez-iface.xml | 27 +++ src/bluez-midi.c | 375 ++++++++++++++++++++++++++++++++ src/bluez-midi.h | 21 ++ src/bluez.c | 57 ++++- src/main.c | 23 +- src/ofono-iface.h | 2 - src/ofono.c | 3 +- src/shared/bluetooth.h | 4 + 18 files changed, 622 insertions(+), 46 deletions(-) create mode 100644 src/bluez-midi.c create mode 100644 src/bluez-midi.h diff --git a/.github/spellcheck-wordlist.txt b/.github/spellcheck-wordlist.txt index b208dfaa1..d3ece7bef 100644 --- a/.github/spellcheck-wordlist.txt +++ b/.github/spellcheck-wordlist.txt @@ -147,6 +147,7 @@ dpsnk dpsrc DYN EAGAIN +EDE EINVAL ENODEV EP @@ -172,6 +173,7 @@ ioplug IPHONEACCEV iter keyp +LEAdvertisement LF LFE LHDC diff --git a/src/Makefile.am b/src/Makefile.am index eaba2a43c..4f841bc0a 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -83,6 +83,11 @@ bluealsa_SOURCES += \ a2dp-ldac.c endif +if ENABLE_MIDI +bluealsa_SOURCES += \ + bluez-midi.c +endif + if ENABLE_MPEG bluealsa_SOURCES += \ a2dp-mpeg.c diff --git a/src/ba-transport-pcm.c b/src/ba-transport-pcm.c index 5e8e579f9..e7eaa568a 100644 --- a/src/ba-transport-pcm.c +++ b/src/ba-transport-pcm.c @@ -64,8 +64,8 @@ static const char *transport_get_dbus_path_type( int transport_pcm_init( struct ba_transport_pcm *pcm, - struct ba_transport_thread *th, - enum ba_transport_pcm_mode mode) { + enum ba_transport_pcm_mode mode, + struct ba_transport_thread *th) { struct ba_transport *t = th->t; @@ -110,9 +110,7 @@ void transport_pcm_free( pthread_cond_destroy(&pcm->cond); g_hash_table_unref(pcm->delay_adjustments); - - if (pcm->ba_dbus_path != NULL) - g_free(pcm->ba_dbus_path); + g_free(pcm->ba_dbus_path); } diff --git a/src/ba-transport-pcm.h b/src/ba-transport-pcm.h index cb88ceeba..023e5911e 100644 --- a/src/ba-transport-pcm.h +++ b/src/ba-transport-pcm.h @@ -117,8 +117,8 @@ struct ba_transport_pcm { int transport_pcm_init( struct ba_transport_pcm *pcm, - struct ba_transport_thread *th, - enum ba_transport_pcm_mode mode); + enum ba_transport_pcm_mode mode, + struct ba_transport_thread *th); void transport_pcm_free( struct ba_transport_pcm *pcm); diff --git a/src/ba-transport.c b/src/ba-transport.c index ed87d62d8..3560df68b 100644 --- a/src/ba-transport.c +++ b/src/ba-transport.c @@ -693,13 +693,13 @@ struct ba_transport *ba_transport_new_a2dp( t->a2dp.state = BLUEZ_A2DP_TRANSPORT_STATE_IDLE; transport_pcm_init(&t->a2dp.pcm, - is_sink ? &t->thread_dec : &t->thread_enc, - is_sink ? BA_TRANSPORT_PCM_MODE_SOURCE : BA_TRANSPORT_PCM_MODE_SINK); + is_sink ? BA_TRANSPORT_PCM_MODE_SOURCE : BA_TRANSPORT_PCM_MODE_SINK, + is_sink ? &t->thread_dec : &t->thread_enc); t->a2dp.pcm.soft_volume = !config.a2dp.volume; transport_pcm_init(&t->a2dp.pcm_bc, - is_sink ? &t->thread_enc : &t->thread_dec, - is_sink ? BA_TRANSPORT_PCM_MODE_SINK : BA_TRANSPORT_PCM_MODE_SOURCE); + is_sink ? BA_TRANSPORT_PCM_MODE_SINK : BA_TRANSPORT_PCM_MODE_SOURCE, + is_sink ? &t->thread_enc : &t->thread_dec); t->a2dp.pcm_bc.soft_volume = !config.a2dp.volume; t->acquire = transport_acquire_bt_a2dp; @@ -808,12 +808,12 @@ struct ba_transport *ba_transport_new_sco( t->profile = profile; transport_pcm_init(&t->sco.pcm_spk, - is_ag ? &t->thread_enc : &t->thread_dec, - is_ag ? BA_TRANSPORT_PCM_MODE_SINK : BA_TRANSPORT_PCM_MODE_SOURCE); + is_ag ? BA_TRANSPORT_PCM_MODE_SINK : BA_TRANSPORT_PCM_MODE_SOURCE, + is_ag ? &t->thread_enc : &t->thread_dec); transport_pcm_init(&t->sco.pcm_mic, - is_ag ? &t->thread_dec : &t->thread_enc, - is_ag ? BA_TRANSPORT_PCM_MODE_SOURCE : BA_TRANSPORT_PCM_MODE_SINK); + is_ag ? BA_TRANSPORT_PCM_MODE_SOURCE : BA_TRANSPORT_PCM_MODE_SINK, + is_ag ? &t->thread_dec : &t->thread_enc); t->acquire = transport_acquire_bt_sco; t->release = transport_release_bt_sco; @@ -857,6 +857,54 @@ struct ba_transport *ba_transport_new_sco( return NULL; } +#if ENABLE_MIDI + +static int transport_acquire_bt_midi(struct ba_transport *t) { + + int fds[2]; + if (socketpair(AF_LOCAL, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, fds) == -1) + return -1; + + debug("New BLE-MIDI link: %d", fds[0]); + + t->bt_fd = fds[0]; + t->midi.fd = fds[1]; + + return 0; +} + +static int transport_release_bt_midi(struct ba_transport *t) { + + debug("Releasing BLE-MIDI link: %d", t->bt_fd); + + close(t->bt_fd); + t->bt_fd = -1; + close(t->midi.fd); + t->midi.fd = -1; + + return 0; +} + +struct ba_transport *ba_transport_new_midi( + struct ba_device *device, + enum ba_transport_profile profile, + const char *dbus_owner, + const char *dbus_path) { + + struct ba_transport *t; + if ((t = transport_new(device, dbus_owner, dbus_path)) == NULL) + return NULL; + + t->profile = profile; + + t->acquire = transport_acquire_bt_midi; + t->release = transport_release_bt_midi; + + return t; +} + +#endif + #if DEBUG /** * Get BlueALSA transport type debug name. @@ -958,6 +1006,10 @@ const char *ba_transport_debug_name( return "HSP Headset"; case BA_TRANSPORT_PROFILE_HSP_AG: return "HSP Audio Gateway"; +#if ENABLE_MIDI + case BA_TRANSPORT_PROFILE_MIDI: + return "MIDI"; +#endif } debug("Unknown transport: profile:%#x codec:%#x", profile, codec_id); return "N/A"; diff --git a/src/ba-transport.h b/src/ba-transport.h index fb630ad2c..901b72d26 100644 --- a/src/ba-transport.h +++ b/src/ba-transport.h @@ -130,6 +130,9 @@ enum ba_transport_profile { BA_TRANSPORT_PROFILE_HFP_AG = (2 << 2), BA_TRANSPORT_PROFILE_HSP_HS = (1 << 4), BA_TRANSPORT_PROFILE_HSP_AG = (2 << 4), +#if ENABLE_MIDI + BA_TRANSPORT_PROFILE_MIDI = (1 << 6), +#endif }; #define BA_TRANSPORT_PROFILE_MASK_A2DP \ @@ -178,7 +181,7 @@ struct ba_transport { /* This field stores a file descriptor (socket) associated with the BlueZ * side of the transport. The role of this socket depends on the transport - * type - it can be either A2DP or SCO link. */ + * type - it can be either A2DP, MIDI or SCO link. */ int bt_fd; /* max transfer unit values for bt_fd */ @@ -258,6 +261,16 @@ struct ba_transport { } sco; +#if ENABLE_MIDI + struct { + + /* The rare end of the BLE-MIDI link. This FD is passed to the BlueZ + * GATT subsystem which is responsible for BT data transport. */ + int fd; + + } midi; +#endif + }; /* callback functions for self-management */ @@ -282,6 +295,13 @@ struct ba_transport *ba_transport_new_sco( const char *dbus_owner, const char *dbus_path, int rfcomm_fd); +#if ENABLE_MIDI +struct ba_transport *ba_transport_new_midi( + struct ba_device *device, + enum ba_transport_profile profile, + const char *dbus_owner, + const char *dbus_path); +#endif #if DEBUG const char *ba_transport_debug_name( diff --git a/src/bluealsa-config.h b/src/bluealsa-config.h index 60b569b76..ab54de5a5 100644 --- a/src/bluealsa-config.h +++ b/src/bluealsa-config.h @@ -38,6 +38,7 @@ struct ba_config { bool hfp_ag; bool hsp_hs; bool hsp_ag; + bool midi; } profile; /* established D-Bus connection */ diff --git a/src/bluealsa-dbus.c b/src/bluealsa-dbus.c index 1a0b6c408..54be0506e 100644 --- a/src/bluealsa-dbus.c +++ b/src/bluealsa-dbus.c @@ -87,6 +87,9 @@ static GVariant *ba_variant_new_bluealsa_profiles(void) { { BLUEALSA_TRANSPORT_TYPE_HFP_HF, config.profile.hfp_hf }, { BLUEALSA_TRANSPORT_TYPE_HSP_AG, config.profile.hsp_ag }, { BLUEALSA_TRANSPORT_TYPE_HSP_HS, config.profile.hsp_hs }, +#if ENABLE_MIDI + { BLUEALSA_TRANSPORT_TYPE_MIDI, config.profile.midi }, +#endif }; const char *strv[ARRAYSIZE(profiles)]; @@ -195,6 +198,10 @@ static GVariant *ba_variant_new_transport_type(const struct ba_transport *t) { return g_variant_new_string(BLUEALSA_TRANSPORT_TYPE_HSP_AG); case BA_TRANSPORT_PROFILE_HSP_HS: return g_variant_new_string(BLUEALSA_TRANSPORT_TYPE_HSP_HS); +#if ENABLE_MIDI + case BA_TRANSPORT_PROFILE_MIDI: + return g_variant_new_string(BLUEALSA_TRANSPORT_TYPE_MIDI); +#endif case BA_TRANSPORT_PROFILE_NONE: break; } diff --git a/src/bluealsa-iface.h b/src/bluealsa-iface.h index a926e3e38..f0754724c 100644 --- a/src/bluealsa-iface.h +++ b/src/bluealsa-iface.h @@ -33,6 +33,7 @@ #define BLUEALSA_TRANSPORT_TYPE_HSP "HSP" #define BLUEALSA_TRANSPORT_TYPE_HSP_AG BLUEALSA_TRANSPORT_TYPE_HSP "-AG" #define BLUEALSA_TRANSPORT_TYPE_HSP_HS BLUEALSA_TRANSPORT_TYPE_HSP "-HS" +#define BLUEALSA_TRANSPORT_TYPE_MIDI "MIDI" #define BLUEALSA_PCM_CTRL_DRAIN "Drain" #define BLUEALSA_PCM_CTRL_DROP "Drop" @@ -66,8 +67,4 @@ OrgBluealsaRfcomm1Skeleton *org_bluealsa_rfcomm1_skeleton_new( const GDBusInterfaceSkeletonVTable *vtable, void *userdata, GDestroyNotify userdata_free_func); -extern const GDBusInterfaceInfo org_bluealsa_manager1_interface; -extern const GDBusInterfaceInfo org_bluealsa_pcm1_interface; -extern const GDBusInterfaceInfo org_bluealsa_rfcomm1_interface; - #endif diff --git a/src/bluez-iface.h b/src/bluez-iface.h index 9f07d62e1..850ce5fcf 100644 --- a/src/bluez-iface.h +++ b/src/bluez-iface.h @@ -12,6 +12,10 @@ #ifndef BLUEALSA_BLUEZIFACE_H_ #define BLUEALSA_BLUEZIFACE_H_ +#if HAVE_CONFIG_H +# include +#endif + #include #include @@ -23,6 +27,12 @@ #define BLUEZ_IFACE_BATTERY_PROVIDER BLUEZ_SERVICE ".BatteryProvider1" #define BLUEZ_IFACE_BATTERY_PROVIDER_MANAGER BLUEZ_SERVICE ".BatteryProviderManager1" #define BLUEZ_IFACE_DEVICE BLUEZ_SERVICE ".Device1" +#define BLUEZ_IFACE_GATT_CHARACTERISTIC BLUEZ_SERVICE ".GattCharacteristic1" +#define BLUEZ_IFACE_GATT_MANAGER BLUEZ_SERVICE ".GattManager1" +#define BLUEZ_IFACE_GATT_PROFILE BLUEZ_SERVICE ".GattProfile1" +#define BLUEZ_IFACE_GATT_SERVICE BLUEZ_SERVICE ".GattService1" +#define BLUEZ_IFACE_LE_ADVERTISEMENT BLUEZ_SERVICE ".LEAdvertisement1" +#define BLUEZ_IFACE_LE_ADVERTISING_MANAGER BLUEZ_SERVICE ".LEAdvertisingManager1" #define BLUEZ_IFACE_MEDIA BLUEZ_SERVICE ".Media1" #define BLUEZ_IFACE_MEDIA_ENDPOINT BLUEZ_SERVICE ".MediaEndpoint1" #define BLUEZ_IFACE_MEDIA_TRANSPORT BLUEZ_SERVICE ".MediaTransport1" @@ -41,6 +51,26 @@ OrgBluezBatteryProvider1Skeleton *org_bluez_battery_provider1_skeleton_new( const GDBusInterfaceSkeletonVTable *vtable, void *userdata, GDestroyNotify userdata_free_func); +#if ENABLE_MIDI + +typedef struct { + GDBusInterfaceSkeletonEx parent; +} OrgBluezGattCharacteristic1Skeleton; + +OrgBluezGattCharacteristic1Skeleton *org_bluez_gatt_characteristic1_skeleton_new( + const GDBusInterfaceSkeletonVTable *vtable, void *userdata, + GDestroyNotify userdata_free_func); + +typedef struct { + GDBusInterfaceSkeletonEx parent; +} OrgBluezGattService1Skeleton; + +OrgBluezGattService1Skeleton *org_bluez_gatt_service1_skeleton_new( + const GDBusInterfaceSkeletonVTable *vtable, void *userdata, + GDestroyNotify userdata_free_func); + +#endif + typedef struct { GDBusInterfaceSkeletonEx parent; } OrgBluezMediaEndpoint1Skeleton; @@ -57,8 +87,4 @@ OrgBluezProfile1Skeleton *org_bluez_profile1_skeleton_new( const GDBusInterfaceSkeletonVTable *vtable, void *userdata, GDestroyNotify userdata_free_func); -extern const GDBusInterfaceInfo org_bluez_battery_provider1_interface; -extern const GDBusInterfaceInfo org_bluez_media_endpoint1_interface; -extern const GDBusInterfaceInfo org_bluez_profile1_interface; - #endif diff --git a/src/bluez-iface.xml b/src/bluez-iface.xml index a76bae22e..932abc95b 100644 --- a/src/bluez-iface.xml +++ b/src/bluez-iface.xml @@ -11,6 +11,33 @@ the first interface must have at least one method. --> + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/bluez-midi.c b/src/bluez-midi.c new file mode 100644 index 000000000..d0d2abac3 --- /dev/null +++ b/src/bluez-midi.c @@ -0,0 +1,375 @@ +/* + * BlueALSA - bluez-midi.c + * Copyright (c) 2016-2023 Arkadiusz Bokowy + * + * This file is a part of bluez-alsa. + * + * This project is licensed under the terms of the MIT license. + * + */ + +#include "bluez-midi.h" + +#if HAVE_CONFIG_H +# include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "ba-adapter.h" +#include "ba-device.h" +#include "ba-transport.h" +#include "bluealsa-config.h" +#include "bluez-iface.h" +#include "dbus.h" +#include "shared/bluetooth.h" +#include "shared/defs.h" +#include "shared/log.h" + +/** + * BlueALSA MIDI GATT application. */ +struct bluez_midi_app { + /* D-Bus object registration paths */ + char path[64]; + char path_service[64 + 8]; + char path_char[64 + 16]; + /* associated adapter */ + int hci_dev_id; + /* associated transport */ + struct ba_transport *t; + /* GATT write/notify acquisition */ + bool acquired_write; + bool acquired_notify; + /* memory self-management */ + atomic_int ref_count; +}; + +static GVariant *variant_new_midi_service_uuid(void) { + return g_variant_new_string(BT_UUID_MIDI); +} + +static GVariant *variant_new_midi_characteristic_uuid(void) { + return g_variant_new_string(BT_UUID_MIDI_CHAR); +} + +static void bluez_midi_app_unref(struct bluez_midi_app *app) { + if (atomic_fetch_sub_explicit(&app->ref_count, 1, memory_order_relaxed) > 1) + return; + if (app->t != NULL) + ba_transport_unref(app->t); + free(app); +} + +/** + * Create new local MIDI transport. + * + * Unfortunately, BlueZ doesn't provide any meaningful information about the + * remote device which wants to acquire the write/notify access. There is a + * "device" option, but the acquire-write and acquire-notify methods are called + * only for the first device, and the application (us) is not notified when + * some other device wants to acquire the access. Therefore, from our point of + * view, we can tell only that there will be an incoming connection from given + * adapter. */ +static struct ba_transport *bluez_midi_transport_new( + struct bluez_midi_app *app) { + + struct ba_adapter *a = NULL; + struct ba_device *d = NULL; + struct ba_transport *t = NULL; + + if ((a = ba_adapter_lookup(app->hci_dev_id)) == NULL) { + error("Couldn't lookup adapter: hci%d: %s", app->hci_dev_id, strerror(errno)); + goto fail; + } + + bdaddr_t addr = *BDADDR_LOCAL; + if ((d = ba_device_lookup(a, &addr)) == NULL && + (d = ba_device_new(a, &addr)) == NULL) { + error("Couldn't create new device: %s", strerror(errno)); + goto fail; + } + + if ((t = ba_transport_lookup(d, app->path)) == NULL && + (t = ba_transport_new_midi(d, BA_TRANSPORT_PROFILE_MIDI, ":0", app->path)) == NULL) { + error("Couldn't create new transport: %s", strerror(errno)); + goto fail; + } + +fail: + if (a != NULL) + ba_adapter_unref(a); + if (d != NULL) + ba_device_unref(d); + return t; +} + +/** + * Lookup for existing local MIDI transport. */ +static struct ba_transport *bluez_midi_transport_lookup( + struct bluez_midi_app *app) { + + struct ba_adapter *a = NULL; + struct ba_device *d = NULL; + struct ba_transport *t = NULL; + + if ((a = ba_adapter_lookup(app->hci_dev_id)) == NULL) { + error("Couldn't lookup adapter: hci%d: %s", app->hci_dev_id, strerror(errno)); + goto fail; + } + + bdaddr_t addr = *BDADDR_LOCAL; + if ((d = ba_device_lookup(a, &addr)) == NULL) { + error("Couldn't lookup local device: %s", strerror(errno)); + goto fail; + } + + if ((t = ba_transport_lookup(d, app->path)) == NULL) { + error("Couldn't lookup local device MIDI transport: %s", strerror(errno)); + goto fail; + } + +fail: + if (a != NULL) + ba_adapter_unref(a); + if (d != NULL) + ba_device_unref(d); + return t; +} + +static GVariant *bluez_midi_service_iface_get_property( + const char *property, G_GNUC_UNUSED GError **error, + G_GNUC_UNUSED void *userdata) { + + if (strcmp(property, "UUID") == 0) + return variant_new_midi_service_uuid(); + if (strcmp(property, "Primary") == 0) + return g_variant_new_boolean(TRUE); + + g_assert_not_reached(); + return NULL; +} + +static GDBusObjectSkeleton *bluez_midi_service_skeleton_new( + struct bluez_midi_app *app) { + + static const GDBusInterfaceSkeletonVTable vtable = { + .get_property = bluez_midi_service_iface_get_property, + }; + + OrgBluezGattService1Skeleton *ifs_gatt_service; + if ((ifs_gatt_service = org_bluez_gatt_service1_skeleton_new(&vtable, + app, (GDestroyNotify)bluez_midi_app_unref)) == NULL) + return NULL; + + GDBusInterfaceSkeleton *ifs = G_DBUS_INTERFACE_SKELETON(ifs_gatt_service); + GDBusObjectSkeleton *skeleton = g_dbus_object_skeleton_new(app->path_service); + g_dbus_object_skeleton_add_interface(skeleton, ifs); + g_object_unref(ifs_gatt_service); + + atomic_fetch_add_explicit(&app->ref_count, 1, memory_order_relaxed); + return skeleton; +} + +static void bluez_midi_characteristic_read_value( + GDBusMethodInvocation *inv, G_GNUC_UNUSED void *userdata) { + GVariant *rv[] = { /* respond with no payload */ + g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, NULL, 0, sizeof(uint8_t)) }; + g_dbus_method_invocation_return_value(inv, g_variant_new_tuple(rv, 1)); +} + +static void bluez_midi_characteristic_acquire_write( + GDBusMethodInvocation *inv, void *userdata) { + + GVariant *params = g_dbus_method_invocation_get_parameters(inv); + struct bluez_midi_app *app = userdata; + + uint16_t mtu = 0; + if (!g_variant_lookup(params, "h", "mtu", &mtu)) { + error("Couldn't acquire MIDI char write: %s", "Invalid options"); + goto fail; + } + + struct ba_transport *t = NULL; + if ((t = bluez_midi_transport_lookup(app)) == NULL) + goto fail; + + app->acquired_write = true; + + GUnixFDList *fd_list = g_unix_fd_list_new(); + g_unix_fd_list_append(fd_list, t->midi.fd, NULL); + g_dbus_method_invocation_return_value_with_unix_fd_list(inv, + g_variant_new("(hq)", 0, mtu), fd_list); + g_object_unref(fd_list); + + return; + +fail: + g_dbus_method_invocation_return_error(inv, G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, "Unable to acquire write access"); +} + +static void bluez_midi_characteristic_acquire_notify( + GDBusMethodInvocation *inv, void *userdata) { + + GVariant *params = g_dbus_method_invocation_get_parameters(inv); + struct bluez_midi_app *app = userdata; + + uint16_t mtu = 0; + if (!g_variant_lookup(params, "h", "mtu", &mtu)) { + error("Couldn't acquire MIDI char notify: %s", "Invalid options"); + goto fail; + } + + struct ba_transport *t = NULL; + if ((t = bluez_midi_transport_lookup(app)) == NULL) + goto fail; + + app->acquired_notify = true; + + GUnixFDList *fd_list = g_unix_fd_list_new(); + g_unix_fd_list_append(fd_list, t->midi.fd, NULL); + g_dbus_method_invocation_return_value_with_unix_fd_list(inv, + g_variant_new("(hq)", 0, mtu), fd_list); + g_object_unref(fd_list); + + return; + +fail: + g_dbus_method_invocation_return_error(inv, G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, "Unable to acquire notification"); +} + +static GVariant *bluez_midi_characteristic_iface_get_property( + const char *property, G_GNUC_UNUSED GError **error, void *userdata) { + + struct bluez_midi_app *app = userdata; + + if (strcmp(property, "UUID") == 0) + return variant_new_midi_characteristic_uuid(); + if (strcmp(property, "Service") == 0) + return g_variant_new_object_path(app->path_service); + if (strcmp(property, "WriteAcquired") == 0) + return g_variant_new_boolean(app->acquired_write); + if (strcmp(property, "NotifyAcquired") == 0) + return g_variant_new_boolean(app->acquired_notify); + if (strcmp(property, "Flags") == 0) { + const char *values[] = { + "read", "write", "write-without-response", "notify" }; + return g_variant_new_strv(values, ARRAYSIZE(values)); + } + + g_assert_not_reached(); + return NULL; +} + +static GDBusObjectSkeleton *bluez_midi_characteristic_skeleton_new( + struct bluez_midi_app *app) { + + static const GDBusMethodCallDispatcher dispatchers[] = { + { .method = "ReadValue", + .handler = bluez_midi_characteristic_read_value }, + { .method = "AcquireWrite", + .handler = bluez_midi_characteristic_acquire_write }, + { .method = "AcquireNotify", + .handler = bluez_midi_characteristic_acquire_notify }, + { 0 }, + }; + + static const GDBusInterfaceSkeletonVTable vtable = { + .dispatchers = dispatchers, + .get_property = bluez_midi_characteristic_iface_get_property, + }; + + OrgBluezGattCharacteristic1Skeleton *ifs_gatt_char; + if ((ifs_gatt_char = org_bluez_gatt_characteristic1_skeleton_new(&vtable, + app, (GDestroyNotify)bluez_midi_app_unref)) == NULL) + return NULL; + + GDBusInterfaceSkeleton *ifs = G_DBUS_INTERFACE_SKELETON(ifs_gatt_char); + GDBusObjectSkeleton *skeleton = g_dbus_object_skeleton_new(app->path_char); + g_dbus_object_skeleton_add_interface(skeleton, ifs); + g_object_unref(ifs_gatt_char); + + atomic_fetch_add_explicit(&app->ref_count, 1, memory_order_relaxed); + return skeleton; +} + +static void bluez_midi_app_register_finish( + GObject *source, GAsyncResult *result, G_GNUC_UNUSED void *userdata) { + + GError *err = NULL; + GDBusMessage *rep = g_dbus_connection_send_message_with_reply_finish( + G_DBUS_CONNECTION(source), result, &err); + if (rep != NULL && + g_dbus_message_get_message_type(rep) == G_DBUS_MESSAGE_TYPE_ERROR) + g_dbus_message_to_gerror(rep, &err); + + if (rep != NULL) + g_object_unref(rep); + if (err != NULL) { + error("Couldn't register MIDI GATT application: %s", err->message); + g_error_free(err); + } + +} + +GDBusObjectManagerServer *bluez_midi_app_new( + struct ba_adapter *adapter, const char *path) { + + struct bluez_midi_app *app; + if ((app = calloc(1, sizeof(*app))) == NULL) + return NULL; + + snprintf(app->path, sizeof(app->path), "%s", path); + snprintf(app->path_service, sizeof(app->path_service), "%s/service", path); + snprintf(app->path_char, sizeof(app->path_char), "%s/char", app->path_service); + app->hci_dev_id = adapter->hci.dev_id; + + GDBusObjectManagerServer *manager = g_dbus_object_manager_server_new(path); + GDBusObjectSkeleton *skeleton; + + skeleton = bluez_midi_service_skeleton_new(app); + g_dbus_object_manager_server_export(manager, skeleton); + g_object_unref(skeleton); + + skeleton = bluez_midi_characteristic_skeleton_new(app); + g_dbus_object_manager_server_export(manager, skeleton); + g_object_unref(skeleton); + + g_dbus_object_manager_server_set_connection(manager, config.dbus); + + GDBusMessage *msg; + msg = g_dbus_message_new_method_call(BLUEZ_SERVICE, adapter->bluez_dbus_path, + BLUEZ_IFACE_GATT_MANAGER, "RegisterApplication"); + + g_dbus_message_set_body(msg, g_variant_new("(oa{sv})", path, NULL)); + + debug("Registering MIDI GATT application: %s", app->path); + g_dbus_connection_send_message_with_reply(config.dbus, msg, + G_DBUS_SEND_MESSAGE_FLAGS_NONE, -1, NULL, NULL, + bluez_midi_app_register_finish, NULL); + + struct ba_transport *t; + /* Setup local MIDI transport associated with our GATT server. */ + if ((t = bluez_midi_transport_new(app)) == NULL) + error("Couldn't create local MIDI transport: %s", strerror(errno)); + else if (ba_transport_acquire(t) == -1) + error("Couldn't acquire local MIDI transport: %s", strerror(errno)); + app->t = t; + + g_object_unref(msg); + return manager; +} diff --git a/src/bluez-midi.h b/src/bluez-midi.h new file mode 100644 index 000000000..fde471b98 --- /dev/null +++ b/src/bluez-midi.h @@ -0,0 +1,21 @@ +/* + * BlueALSA - bluez-midi.h + * Copyright (c) 2016-2023 Arkadiusz Bokowy + * + * This file is a part of bluez-alsa. + * + * This project is licensed under the terms of the MIT license. + * + */ + +#pragma once +#ifndef BLUEALSA_BLUEZMIDI_H_ +#define BLUEALSA_BLUEZMIDI_H_ + +#include + +#include "ba-adapter.h" + +GDBusObjectManagerServer *bluez_midi_app_new(struct ba_adapter *adapter, const char *path); + +#endif diff --git a/src/bluez.c b/src/bluez.c index b962b552b..e15d56bd0 100644 --- a/src/bluez.c +++ b/src/bluez.c @@ -41,6 +41,9 @@ #include "bluealsa-config.h" #include "bluealsa-dbus.h" #include "bluez-iface.h" +#if ENABLE_MIDI +# include "bluez-midi.h" +#endif #include "dbus.h" #include "hci.h" #include "sco.h" @@ -84,6 +87,10 @@ struct bluez_adapter { GDBusObjectManagerServer *manager_media_application; /* manager for battery provider objects */ GDBusObjectManagerServer *manager_battery_provider; +#if ENABLE_MIDI + /* manager for MIDI GATT objects */ + GDBusObjectManagerServer *manager_midi_application; +#endif /* array of end-points for connected devices */ GHashTable *device_sep_map; }; @@ -197,6 +204,21 @@ static void bluez_register_battery_provider(struct bluez_adapter *b_adapter) { } +#if ENABLE_MIDI +/** + * Register BLE MIDI application in BlueZ. */ +static void bluez_register_midi_application(struct bluez_adapter *b_adapter) { + + char path[64]; + struct ba_adapter *a = b_adapter->adapter; + snprintf(path, sizeof(path), "/org/bluez/%s/MIDI", a->hci.name); + + GDBusObjectManagerServer *manager = bluez_midi_app_new(a, path); + b_adapter->manager_midi_application = manager; + +} +#endif + static struct bluez_adapter *bluez_adapter_new(struct ba_adapter *a) { struct bluez_adapter *ba = &bluez_adapters[a->hci.dev_id]; @@ -214,6 +236,11 @@ static struct bluez_adapter *bluez_adapter_new(struct ba_adapter *a) { bluez_register_a2dp_all(a); } +#if ENABLE_MIDI + if (config.profile.midi) + bluez_register_midi_application(ba); +#endif + return ba; } @@ -230,6 +257,12 @@ static void bluez_adapter_free(struct bluez_adapter *b_adapter) { g_object_unref(b_adapter->manager_battery_provider); b_adapter->manager_battery_provider = NULL; } +#if ENABLE_MIDI + if (b_adapter->manager_midi_application != NULL) { + g_object_unref(b_adapter->manager_midi_application); + b_adapter->manager_midi_application = NULL; + } +#endif ba_adapter_destroy(b_adapter->adapter); b_adapter->adapter = NULL; } @@ -281,14 +314,10 @@ static bool bluez_match_dbus_adapter( return false; } -/** - * Get BlueZ D-Bus object path for given transport profile. */ -static const char *bluez_transport_profile_to_bluez_object_path( +static const char *bluez_get_media_endpoint_object_path( enum ba_transport_profile profile, uint16_t codec_id) { switch (profile) { - case BA_TRANSPORT_PROFILE_NONE: - return "/"; case BA_TRANSPORT_PROFILE_A2DP_SOURCE: switch (codec_id) { case A2DP_CODEC_SBC: @@ -361,6 +390,15 @@ static const char *bluez_transport_profile_to_bluez_object_path( error("Unsupported A2DP codec: %#x", codec_id); g_assert_not_reached(); } + default: + g_assert_not_reached(); + return "/"; + } +} + +static const char *bluez_get_profile_object_path( + enum ba_transport_profile profile) { + switch (profile) { case BA_TRANSPORT_PROFILE_HFP_HF: return "/HFP/HandsFree"; case BA_TRANSPORT_PROFILE_HFP_AG: @@ -369,9 +407,10 @@ static const char *bluez_transport_profile_to_bluez_object_path( return "/HSP/Headset"; case BA_TRANSPORT_PROFILE_HSP_AG: return "/HSP/AudioGateway"; + default: + g_assert_not_reached(); + return "/"; } - g_assert_not_reached(); - return "/"; } /** @@ -682,7 +721,7 @@ static void bluez_export_a2dp( char path[sizeof(dbus_obj->path)]; snprintf(path, sizeof(path), "/org/bluez/%s%s/%u", adapter->hci.name, - bluez_transport_profile_to_bluez_object_path(profile, codec->codec_id), + bluez_get_media_endpoint_object_path(profile, codec->codec_id), ++index); if ((dbus_obj = g_hash_table_lookup(dbus_object_data_map, path)) == NULL) { @@ -1055,7 +1094,7 @@ static void bluez_register_hfp( char path[sizeof(dbus_obj->path)]; snprintf(path, sizeof(path), "/org/bluez%s", - bluez_transport_profile_to_bluez_object_path(profile, -1)); + bluez_get_profile_object_path(profile)); if ((dbus_obj = g_hash_table_lookup(dbus_object_data_map, path)) == NULL) { diff --git a/src/main.c b/src/main.c index ac145ba40..a486e66ee 100644 --- a/src/main.c +++ b/src/main.c @@ -233,15 +233,18 @@ int main(int argc, char **argv) { #endif " --xapl-resp-name=NAME\t\tset product name used by XAPL\n" "\nAvailable BT profiles:\n" - " - a2dp-source\tAdvanced Audio Source (%s)\n" - " - a2dp-sink\tAdvanced Audio Sink (%s)\n" + " - a2dp-source\tAdvanced Audio Source (v1.3)\n" + " - a2dp-sink\tAdvanced Audio Sink (v1.3)\n" #if ENABLE_OFONO " - hfp-ofono\tHands-Free AG/HF handled by oFono\n" #endif - " - hfp-ag\tHands-Free Audio Gateway (%s)\n" - " - hfp-hf\tHands-Free (%s)\n" - " - hsp-ag\tHeadset Audio Gateway (%s)\n" - " - hsp-hs\tHeadset (%s)\n" + " - hfp-ag\tHands-Free Audio Gateway (v1.7)\n" + " - hfp-hf\tHands-Free (v1.7)\n" + " - hsp-ag\tHeadset Audio Gateway (v1.2)\n" + " - hsp-hs\tHeadset (v1.2)\n" +#if ENABLE_MIDI + " - midi\tBluetooth LE MIDI (v1.0)\n" +#endif "\n" "Available BT audio codecs:\n" " a2dp-source:\t%s\n" @@ -249,9 +252,6 @@ int main(int argc, char **argv) { " hfp-*:\t%s\n" "", argv[0], - "v1.3", "v1.3", - "v1.7", "v1.7", - "v1.2", "v1.2", get_a2dp_codecs(A2DP_SOURCE), get_a2dp_codecs(A2DP_SINK), get_hfp_codecs()); @@ -309,6 +309,9 @@ int main(int argc, char **argv) { { "hfp-ag", &config.profile.hfp_ag }, { "hsp-hs", &config.profile.hsp_hs }, { "hsp-ag", &config.profile.hsp_ag }, +#if ENABLE_MIDI + { "midi", &config.profile.midi }, +#endif }; bool matched = false; @@ -543,7 +546,7 @@ int main(int argc, char **argv) { if (!(config.profile.a2dp_source || config.profile.a2dp_sink || config.profile.hfp_hf || config.profile.hfp_ag || config.profile.hsp_hs || config.profile.hsp_ag || - config.profile.hfp_ofono)) { + config.profile.hfp_ofono || config.profile.midi)) { error("It is required to enabled at least one BT profile"); fprintf(stderr, "Try '%s --help' for more information.\n", argv[0]); return EXIT_FAILURE; diff --git a/src/ofono-iface.h b/src/ofono-iface.h index b0dfbeb12..ea93f4a35 100644 --- a/src/ofono-iface.h +++ b/src/ofono-iface.h @@ -43,6 +43,4 @@ OrgOfonoHandsfreeAudioAgentSkeleton *org_ofono_handsfree_audio_agent_skeleton_ne const GDBusInterfaceSkeletonVTable *vtable, void *userdata, GDestroyNotify userdata_free_func); -extern const GDBusInterfaceInfo org_ofono_handsfree_audio_agent_interface; - #endif diff --git a/src/ofono.c b/src/ofono.c index 8b6bb8741..1224856bc 100644 --- a/src/ofono.c +++ b/src/ofono.c @@ -215,7 +215,8 @@ static struct ba_transport *ofono_transport_new( /** * Lookup a transport associated with oFono card data. */ -static struct ba_transport *ofono_transport_lookup(struct ofono_card_data *ocd) { +static struct ba_transport *ofono_transport_lookup( + const struct ofono_card_data *ocd) { struct ba_adapter *a = NULL; struct ba_device *d = NULL; diff --git a/src/shared/bluetooth.h b/src/shared/bluetooth.h index d74d1295f..b4e933d49 100644 --- a/src/shared/bluetooth.h +++ b/src/shared/bluetooth.h @@ -39,4 +39,8 @@ #define BT_UUID_HFP_HF "0000111E-0000-1000-8000-00805F9B34FB" #define BT_UUID_HFP_AG "0000111F-0000-1000-8000-00805F9B34FB" +#define BT_UUID_MIDI "03B80E5A-EDE8-4B33-A751-6CE34EC4C700" +#define BT_UUID_MIDI_CHAR "7772E5DB-3868-4112-A1A9-F2669D106BF3" +#define BT_UUID_MIDI_DESC "00002901-0000-1000-8000-00805F9B34FB" + #endif