mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2025-10-29 05:40:27 -04:00
config.h needs to be consistently included before any standard headers if we ever want to set feature test macros (like _GNU_SOURCE or whatever) inside. It can lead to hard-to-debug issues without that. It can also be problematic just for our own HAVE_* that it may define if it's not consistently made available before our own headers. Just always include it first, before everything. We already did this in many files, just not consistently.
870 lines
23 KiB
C
870 lines
23 KiB
C
/* Spa midi dbus */
|
|
/* SPDX-FileCopyrightText: Copyright © 2022 Pauli Virtanen */
|
|
/* SPDX-License-Identifier: MIT */
|
|
|
|
#include "config.h"
|
|
|
|
#include <errno.h>
|
|
#include <stddef.h>
|
|
|
|
#include <spa/support/log.h>
|
|
#include <spa/support/loop.h>
|
|
#include <spa/support/plugin.h>
|
|
#include <spa/monitor/device.h>
|
|
#include <spa/monitor/utils.h>
|
|
#include <spa/utils/hook.h>
|
|
#include <spa/utils/type.h>
|
|
#include <spa/utils/keys.h>
|
|
#include <spa/utils/names.h>
|
|
#include <spa/utils/result.h>
|
|
#include <spa/utils/string.h>
|
|
#include <spa/utils/json.h>
|
|
#include <spa/node/node.h>
|
|
#include <spa/node/keys.h>
|
|
|
|
#include "midi.h"
|
|
|
|
#include "bluez5-interface-gen.h"
|
|
#include "dbus-monitor.h"
|
|
|
|
#define MIDI_OBJECT_PATH "/midi"
|
|
#define MIDI_PROFILE_PATH MIDI_OBJECT_PATH "/profile"
|
|
|
|
SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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;
|
|
|
|
GDBusConnection *conn;
|
|
struct dbus_monitor monitor;
|
|
GDBusObjectManagerServer *manager;
|
|
|
|
struct spa_hook_list hooks;
|
|
|
|
uint32_t id;
|
|
};
|
|
|
|
struct _MidiEnumCharacteristicProxy
|
|
{
|
|
Bluez5GattCharacteristic1Proxy parent_instance;
|
|
|
|
struct impl *impl;
|
|
|
|
gchar *description;
|
|
uint32_t id;
|
|
GCancellable *read_call;
|
|
GCancellable *dsc_call;
|
|
unsigned int node_emitted:1;
|
|
unsigned int read_probed:1;
|
|
unsigned int read_done:1;
|
|
unsigned int dsc_probed:1;
|
|
unsigned int dsc_done:1;
|
|
};
|
|
|
|
G_DECLARE_FINAL_TYPE(MidiEnumCharacteristicProxy, midi_enum_characteristic_proxy, MIDI_ENUM,
|
|
CHARACTERISTIC_PROXY, Bluez5GattCharacteristic1Proxy)
|
|
G_DEFINE_TYPE(MidiEnumCharacteristicProxy, midi_enum_characteristic_proxy, BLUEZ5_TYPE_GATT_CHARACTERISTIC1_PROXY)
|
|
#define MIDI_ENUM_TYPE_CHARACTERISTIC_PROXY (midi_enum_characteristic_proxy_get_type())
|
|
|
|
struct _MidiEnumManagerProxy
|
|
{
|
|
Bluez5GattManager1Proxy parent_instance;
|
|
|
|
GCancellable *register_call;
|
|
unsigned int registered:1;
|
|
};
|
|
|
|
G_DECLARE_FINAL_TYPE(MidiEnumManagerProxy, midi_enum_manager_proxy, MIDI_ENUM,
|
|
MANAGER_PROXY, Bluez5GattManager1Proxy)
|
|
G_DEFINE_TYPE(MidiEnumManagerProxy, midi_enum_manager_proxy, BLUEZ5_TYPE_GATT_MANAGER1_PROXY)
|
|
#define MIDI_ENUM_TYPE_MANAGER_PROXY (midi_enum_manager_proxy_get_type())
|
|
|
|
|
|
static void emit_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr, Bluez5Device1 *device)
|
|
{
|
|
struct spa_device_object_info info;
|
|
char nick[512], class[16];
|
|
struct spa_dict_item items[23];
|
|
uint32_t n_items = 0;
|
|
const char *path = g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr));
|
|
const char *alias = bluez5_device1_get_alias(device);
|
|
|
|
spa_log_debug(impl->log, "emit node for path=%s", 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",
|
|
alias ? alias : bluez5_device1_get_name(device));
|
|
if (chr->description && chr->description[0] != '\0') {
|
|
spa_scnprintf(nick, sizeof(nick), "%s (%s)", 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, bluez5_device1_get_icon(device));
|
|
items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_PATH, path);
|
|
items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ADDRESS, bluez5_device1_get_address(device));
|
|
snprintf(class, sizeof(class), "0x%06x", bluez5_device1_get_class(device));
|
|
items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_CLASS, class);
|
|
items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ROLE, "client");
|
|
|
|
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, MidiEnumCharacteristicProxy *chr)
|
|
{
|
|
spa_log_debug(impl->log, "remove node for path=%s", g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)));
|
|
|
|
spa_device_emit_object_info(&impl->hooks, chr->id, NULL);
|
|
}
|
|
|
|
static void check_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr);
|
|
|
|
static void read_probe_reply(GObject *source_object, GAsyncResult *res, gpointer user_data)
|
|
{
|
|
MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(source_object);
|
|
struct impl *impl = user_data;
|
|
gchar *value = NULL;
|
|
GError *err = NULL;
|
|
|
|
bluez5_gatt_characteristic1_call_read_value_finish(
|
|
BLUEZ5_GATT_CHARACTERISTIC1(source_object), &value, res, &err);
|
|
|
|
if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
|
|
/* Operation canceled: user_data may be invalid by now */
|
|
g_error_free(err);
|
|
goto done;
|
|
}
|
|
if (err) {
|
|
spa_log_error(impl->log, "%s.ReadValue() failed: %s",
|
|
BLUEZ_GATT_CHR_INTERFACE,
|
|
err->message);
|
|
g_error_free(err);
|
|
goto done;
|
|
}
|
|
|
|
g_free(value);
|
|
|
|
spa_log_debug(impl->log, "MIDI GATT read probe done for path=%s",
|
|
g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)));
|
|
|
|
chr->read_done = true;
|
|
|
|
check_chr_node(impl, chr);
|
|
|
|
done:
|
|
g_clear_object(&chr->read_call);
|
|
}
|
|
|
|
static int read_probe(struct impl *impl, MidiEnumCharacteristicProxy *chr)
|
|
{
|
|
GVariantBuilder builder;
|
|
GVariant *options;
|
|
|
|
/*
|
|
* 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",
|
|
g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)));
|
|
|
|
chr->read_call = g_cancellable_new();
|
|
|
|
g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
|
|
options = g_variant_builder_end(&builder);
|
|
|
|
bluez5_gatt_characteristic1_call_read_value(BLUEZ5_GATT_CHARACTERISTIC1(chr),
|
|
options,
|
|
chr->read_call,
|
|
read_probe_reply,
|
|
impl);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static Bluez5GattDescriptor1 *find_dsc(struct impl *impl, MidiEnumCharacteristicProxy *chr)
|
|
{
|
|
const char *path = g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr));
|
|
Bluez5GattDescriptor1 *found = NULL;;
|
|
GList *objects;
|
|
|
|
objects = g_dbus_object_manager_get_objects(dbus_monitor_manager(&impl->monitor));
|
|
|
|
for (GList *llo = g_list_first(objects); llo; llo = llo->next) {
|
|
GList *interfaces = g_dbus_object_get_interfaces(G_DBUS_OBJECT(llo->data));
|
|
|
|
for (GList *lli = g_list_first(interfaces); lli; lli = lli->next) {
|
|
Bluez5GattDescriptor1 *dsc;
|
|
|
|
if (!BLUEZ5_IS_GATT_DESCRIPTOR1(lli->data))
|
|
continue;
|
|
|
|
dsc = BLUEZ5_GATT_DESCRIPTOR1(lli->data);
|
|
|
|
if (!spa_streq(bluez5_gatt_descriptor1_get_uuid(dsc),
|
|
BT_GATT_CHARACTERISTIC_USER_DESCRIPTION_UUID))
|
|
continue;
|
|
|
|
if (spa_streq(bluez5_gatt_descriptor1_get_characteristic(dsc), path)) {
|
|
found = dsc;
|
|
break;
|
|
}
|
|
}
|
|
g_list_free_full(interfaces, g_object_unref);
|
|
|
|
if (found)
|
|
break;
|
|
}
|
|
g_list_free_full(objects, g_object_unref);
|
|
|
|
return found;
|
|
}
|
|
|
|
static void read_dsc_reply(GObject *source_object, GAsyncResult *res, gpointer user_data)
|
|
{
|
|
MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(user_data);
|
|
struct impl *impl = chr->impl;
|
|
gchar *value = NULL;
|
|
GError *err = NULL;
|
|
|
|
chr->dsc_done = true;
|
|
|
|
bluez5_gatt_descriptor1_call_read_value_finish(
|
|
BLUEZ5_GATT_DESCRIPTOR1(source_object), &value, res, &err);
|
|
|
|
if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
|
|
/* Operation canceled: user_data may be invalid by now */
|
|
g_error_free(err);
|
|
goto done;
|
|
}
|
|
if (err) {
|
|
spa_log_error(impl->log, "%s.ReadValue() failed: %s",
|
|
BLUEZ_GATT_DSC_INTERFACE,
|
|
err->message);
|
|
g_error_free(err);
|
|
goto done;
|
|
}
|
|
|
|
spa_log_debug(impl->log, "MIDI GATT read probe done for path=%s",
|
|
g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)));
|
|
|
|
g_free(chr->description);
|
|
chr->description = value;
|
|
|
|
spa_log_debug(impl->log, "MIDI GATT user descriptor value: '%s'",
|
|
chr->description);
|
|
|
|
check_chr_node(impl, chr);
|
|
|
|
done:
|
|
g_clear_object(&chr->dsc_call);
|
|
}
|
|
|
|
static int read_dsc(struct impl *impl, MidiEnumCharacteristicProxy *chr)
|
|
{
|
|
Bluez5GattDescriptor1 *dsc;
|
|
GVariant *options;
|
|
GVariantBuilder builder;
|
|
|
|
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",
|
|
g_dbus_proxy_get_object_path(G_DBUS_PROXY(dsc)));
|
|
|
|
chr->dsc_call = g_cancellable_new();
|
|
|
|
g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
|
|
options = g_variant_builder_end(&builder);
|
|
|
|
bluez5_gatt_descriptor1_call_read_value(BLUEZ5_GATT_DESCRIPTOR1(dsc),
|
|
options,
|
|
chr->dsc_call,
|
|
read_dsc_reply,
|
|
chr);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int read_probe_reset(struct impl *impl, MidiEnumCharacteristicProxy *chr)
|
|
{
|
|
g_cancellable_cancel(chr->read_call);
|
|
g_clear_object(&chr->read_call);
|
|
|
|
g_cancellable_cancel(chr->dsc_call);
|
|
g_clear_object(&chr->dsc_call);
|
|
|
|
chr->read_probed = false;
|
|
chr->read_done = false;
|
|
chr->dsc_probed = false;
|
|
chr->dsc_done = false;
|
|
return 0;
|
|
}
|
|
|
|
static void lookup_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr,
|
|
Bluez5GattService1 **service, Bluez5Device1 **device)
|
|
{
|
|
GDBusObject *object;
|
|
const char *service_path;
|
|
const char *device_path;
|
|
|
|
*service = NULL;
|
|
*device = NULL;
|
|
|
|
service_path = bluez5_gatt_characteristic1_get_service(BLUEZ5_GATT_CHARACTERISTIC1(chr));
|
|
if (!service_path)
|
|
return;
|
|
|
|
object = g_dbus_object_manager_get_object(dbus_monitor_manager(&impl->monitor), service_path);
|
|
if (object) {
|
|
GDBusInterface *iface = g_dbus_object_get_interface(object, BLUEZ_GATT_SERVICE_INTERFACE);
|
|
*service = BLUEZ5_GATT_SERVICE1(iface);
|
|
}
|
|
|
|
if (!*service)
|
|
return;
|
|
|
|
device_path = bluez5_gatt_service1_get_device(*service);
|
|
if (!device_path)
|
|
return;
|
|
|
|
object = g_dbus_object_manager_get_object(dbus_monitor_manager(&impl->monitor), device_path);
|
|
if (object) {
|
|
GDBusInterface *iface = g_dbus_object_get_interface(object, BLUEZ_DEVICE_INTERFACE);
|
|
*device = BLUEZ5_DEVICE1(iface);
|
|
}
|
|
}
|
|
|
|
static void check_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr)
|
|
{
|
|
Bluez5GattService1 *service;
|
|
Bluez5Device1 *device;
|
|
bool available;
|
|
|
|
lookup_chr_node(impl, chr, &service, &device);
|
|
|
|
if (!device || !bluez5_device1_get_connected(device)) {
|
|
/* Retry read probe on each connection */
|
|
read_probe_reset(impl, chr);
|
|
}
|
|
|
|
spa_log_debug(impl->log,
|
|
"At %s, connected:%d resolved:%d",
|
|
g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)),
|
|
bluez5_device1_get_connected(device),
|
|
bluez5_device1_get_services_resolved(device));
|
|
|
|
available = service && device &&
|
|
bluez5_device1_get_connected(device) &&
|
|
bluez5_device1_get_services_resolved(device) &&
|
|
spa_streq(bluez5_gatt_service1_get_uuid(service), BT_MIDI_SERVICE_UUID) &&
|
|
spa_streq(bluez5_gatt_characteristic1_get_uuid(BLUEZ5_GATT_CHARACTERISTIC1(chr)),
|
|
BT_MIDI_CHR_UUID);
|
|
|
|
if (available && !chr->read_done) {
|
|
read_probe(impl, 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;
|
|
} else if (!chr->node_emitted && available) {
|
|
emit_chr_node(impl, chr, device);
|
|
chr->node_emitted = true;
|
|
}
|
|
}
|
|
|
|
static GList *get_all_valid_chr(struct impl *impl)
|
|
{
|
|
GList *lst = NULL;
|
|
GList *objects;
|
|
|
|
if (!dbus_monitor_manager(&impl->monitor)) {
|
|
/* Still initializing (or it failed) */
|
|
return NULL;
|
|
}
|
|
|
|
objects = g_dbus_object_manager_get_objects(dbus_monitor_manager(&impl->monitor));
|
|
for (GList *p = g_list_first(objects); p; p = p->next) {
|
|
GList *interfaces = g_dbus_object_get_interfaces(G_DBUS_OBJECT(p->data));
|
|
|
|
for (GList *p2 = g_list_first(interfaces); p2; p2 = p2->next) {
|
|
MidiEnumCharacteristicProxy *chr;
|
|
|
|
if (!MIDI_ENUM_IS_CHARACTERISTIC_PROXY(p2->data))
|
|
continue;
|
|
|
|
chr = MIDI_ENUM_CHARACTERISTIC_PROXY(p2->data);
|
|
if (chr->impl == NULL)
|
|
continue;
|
|
|
|
lst = g_list_append(lst, g_object_ref(chr));
|
|
}
|
|
g_list_free_full(interfaces, g_object_unref);
|
|
}
|
|
g_list_free_full(objects, g_object_unref);
|
|
|
|
return lst;
|
|
}
|
|
|
|
static void check_all_nodes(struct impl *impl)
|
|
{
|
|
/*
|
|
* Check if the nodes we have emitted are in sync with connected devices.
|
|
*/
|
|
|
|
GList *chrs = get_all_valid_chr(impl);
|
|
|
|
for (GList *p = chrs; p; p = p->next)
|
|
check_chr_node(impl, MIDI_ENUM_CHARACTERISTIC_PROXY(p->data));
|
|
|
|
g_list_free_full(chrs, g_object_unref);
|
|
}
|
|
|
|
static void manager_register_application_reply(GObject *source_object, GAsyncResult *res, gpointer user_data)
|
|
{
|
|
MidiEnumManagerProxy *manager = MIDI_ENUM_MANAGER_PROXY(source_object);
|
|
struct impl *impl = user_data;
|
|
GError *err = NULL;
|
|
|
|
bluez5_gatt_manager1_call_register_application_finish(
|
|
BLUEZ5_GATT_MANAGER1(source_object), res, &err);
|
|
|
|
if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
|
|
/* Operation canceled: user_data may be invalid by now */
|
|
g_error_free(err);
|
|
goto done;
|
|
}
|
|
if (err) {
|
|
spa_log_error(impl->log, "%s.RegisterApplication() failed: %s",
|
|
BLUEZ_GATT_MANAGER_INTERFACE,
|
|
err->message);
|
|
g_error_free(err);
|
|
goto done;
|
|
}
|
|
|
|
manager->registered = true;
|
|
|
|
done:
|
|
g_clear_object(&manager->register_call);
|
|
}
|
|
|
|
static int manager_register_application(struct impl *impl, MidiEnumManagerProxy *manager)
|
|
{
|
|
GVariantBuilder builder;
|
|
GVariant *options;
|
|
|
|
if (manager->registered)
|
|
return 0;
|
|
if (manager->register_call)
|
|
return -EBUSY;
|
|
|
|
spa_log_debug(impl->log, "%s.RegisterApplication(%s) on %s",
|
|
BLUEZ_GATT_MANAGER_INTERFACE,
|
|
g_dbus_object_manager_get_object_path(G_DBUS_OBJECT_MANAGER(impl->manager)),
|
|
g_dbus_proxy_get_object_path(G_DBUS_PROXY(manager)));
|
|
|
|
manager->register_call = g_cancellable_new();
|
|
|
|
g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
|
|
options = g_variant_builder_end(&builder);
|
|
|
|
bluez5_gatt_manager1_call_register_application(BLUEZ5_GATT_MANAGER1(manager),
|
|
g_dbus_object_manager_get_object_path(G_DBUS_OBJECT_MANAGER(impl->manager)),
|
|
options,
|
|
manager->register_call,
|
|
manager_register_application_reply,
|
|
impl);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* DBus monitoring (Glib)
|
|
*/
|
|
|
|
static void midi_enum_characteristic_proxy_init(MidiEnumCharacteristicProxy *chr)
|
|
{
|
|
}
|
|
|
|
static void midi_enum_characteristic_proxy_finalize(GObject *object)
|
|
{
|
|
MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(object);
|
|
|
|
g_cancellable_cancel(chr->read_call);
|
|
g_clear_object(&chr->read_call);
|
|
|
|
g_cancellable_cancel(chr->dsc_call);
|
|
g_clear_object(&chr->dsc_call);
|
|
|
|
if (chr->impl && chr->node_emitted)
|
|
remove_chr_node(chr->impl, chr);
|
|
|
|
chr->impl = NULL;
|
|
|
|
g_free(chr->description);
|
|
chr->description = NULL;
|
|
}
|
|
|
|
static void midi_enum_characteristic_proxy_class_init(MidiEnumCharacteristicProxyClass *klass)
|
|
{
|
|
GObjectClass *object_class = (GObjectClass *) klass;
|
|
|
|
object_class->finalize = midi_enum_characteristic_proxy_finalize;
|
|
}
|
|
|
|
static void midi_enum_manager_proxy_init(MidiEnumManagerProxy *manager)
|
|
{
|
|
}
|
|
|
|
static void midi_enum_manager_proxy_finalize(GObject *object)
|
|
{
|
|
MidiEnumManagerProxy *manager = MIDI_ENUM_MANAGER_PROXY(object);
|
|
|
|
g_cancellable_cancel(manager->register_call);
|
|
g_clear_object(&manager->register_call);
|
|
}
|
|
|
|
static void midi_enum_manager_proxy_class_init(MidiEnumManagerProxyClass *klass)
|
|
{
|
|
GObjectClass *object_class = (GObjectClass *) klass;
|
|
|
|
object_class->finalize = midi_enum_manager_proxy_finalize;
|
|
}
|
|
|
|
static void manager_update(struct dbus_monitor *monitor, GDBusInterface *iface)
|
|
{
|
|
struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);
|
|
|
|
manager_register_application(impl, MIDI_ENUM_MANAGER_PROXY(iface));
|
|
}
|
|
|
|
static void manager_clear(struct dbus_monitor *monitor, GDBusInterface *iface)
|
|
{
|
|
midi_enum_manager_proxy_finalize(G_OBJECT(iface));
|
|
}
|
|
|
|
static void device_update(struct dbus_monitor *monitor, GDBusInterface *iface)
|
|
{
|
|
struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);
|
|
|
|
check_all_nodes(impl);
|
|
}
|
|
|
|
static void service_update(struct dbus_monitor *monitor, GDBusInterface *iface)
|
|
{
|
|
struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);
|
|
Bluez5GattService1 *service = BLUEZ5_GATT_SERVICE1(iface);
|
|
|
|
if (!spa_streq(bluez5_gatt_service1_get_uuid(service), BT_MIDI_SERVICE_UUID))
|
|
return;
|
|
|
|
check_all_nodes(impl);
|
|
}
|
|
|
|
static void chr_update(struct dbus_monitor *monitor, GDBusInterface *iface)
|
|
{
|
|
struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);
|
|
MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(iface);
|
|
|
|
if (!spa_streq(bluez5_gatt_characteristic1_get_uuid(BLUEZ5_GATT_CHARACTERISTIC1(chr)),
|
|
BT_MIDI_CHR_UUID))
|
|
return;
|
|
|
|
if (chr->impl == NULL) {
|
|
chr->impl = impl;
|
|
chr->id = ++impl->id;
|
|
}
|
|
|
|
check_chr_node(impl, chr);
|
|
}
|
|
|
|
static void chr_clear(struct dbus_monitor *monitor, GDBusInterface *iface)
|
|
{
|
|
midi_enum_characteristic_proxy_finalize(G_OBJECT(iface));
|
|
}
|
|
|
|
static void monitor_start(struct impl *impl)
|
|
{
|
|
struct dbus_monitor_proxy_type proxy_types[] = {
|
|
{ BLUEZ_DEVICE_INTERFACE, BLUEZ5_TYPE_DEVICE1_PROXY, device_update, NULL },
|
|
{ BLUEZ_GATT_MANAGER_INTERFACE, MIDI_ENUM_TYPE_MANAGER_PROXY, manager_update, manager_clear },
|
|
{ BLUEZ_GATT_SERVICE_INTERFACE, BLUEZ5_TYPE_GATT_SERVICE1_PROXY, service_update, NULL },
|
|
{ BLUEZ_GATT_CHR_INTERFACE, MIDI_ENUM_TYPE_CHARACTERISTIC_PROXY, chr_update, chr_clear },
|
|
{ BLUEZ_GATT_DSC_INTERFACE, BLUEZ5_TYPE_GATT_DESCRIPTOR1_PROXY, NULL, NULL },
|
|
{ NULL, BLUEZ5_TYPE_OBJECT_PROXY, NULL, NULL },
|
|
{ NULL, G_TYPE_INVALID, NULL, NULL }
|
|
};
|
|
|
|
SPA_STATIC_ASSERT(SPA_N_ELEMENTS(proxy_types) <= DBUS_MONITOR_MAX_TYPES);
|
|
|
|
dbus_monitor_init(&impl->monitor, BLUEZ5_TYPE_OBJECT_MANAGER_CLIENT,
|
|
impl->log, impl->conn, BLUEZ_SERVICE, "/", proxy_types, NULL);
|
|
}
|
|
|
|
/*
|
|
* DBus GATT profile, to enable BlueZ autoconnect
|
|
*/
|
|
|
|
static gboolean profile_handle_release(Bluez5GattProfile1 *iface, GDBusMethodInvocation *invocation)
|
|
{
|
|
bluez5_gatt_profile1_complete_release(iface, invocation);
|
|
return TRUE;
|
|
}
|
|
|
|
static int export_profile(struct impl *impl)
|
|
{
|
|
static const char *uuids[] = { BT_MIDI_SERVICE_UUID, NULL };
|
|
GDBusObjectSkeleton *skeleton = NULL;
|
|
Bluez5GattProfile1 *iface = NULL;
|
|
int res = -ENOMEM;
|
|
|
|
iface = bluez5_gatt_profile1_skeleton_new();
|
|
if (!iface)
|
|
goto done;
|
|
|
|
skeleton = g_dbus_object_skeleton_new(MIDI_PROFILE_PATH);
|
|
if (!skeleton)
|
|
goto done;
|
|
g_dbus_object_skeleton_add_interface(skeleton, G_DBUS_INTERFACE_SKELETON(iface));
|
|
|
|
bluez5_gatt_profile1_set_uuids(iface, uuids);
|
|
g_signal_connect(iface, "handle-release", G_CALLBACK(profile_handle_release), NULL);
|
|
|
|
g_dbus_object_manager_server_export(impl->manager, skeleton);
|
|
|
|
spa_log_debug(impl->log, "MIDI GATT Profile exported, path=%s",
|
|
g_dbus_object_get_object_path(G_DBUS_OBJECT(skeleton)));
|
|
|
|
res = 0;
|
|
|
|
done:
|
|
g_clear_object(&iface);
|
|
g_clear_object(&skeleton);
|
|
return res;
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
GList *chrs;
|
|
|
|
spa_return_val_if_fail(this != NULL, -EINVAL);
|
|
spa_return_val_if_fail(events != NULL, -EINVAL);
|
|
|
|
chrs = get_all_valid_chr(this);
|
|
|
|
spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
|
|
|
|
for (GList *p = g_list_first(chrs); p; p = p->next) {
|
|
MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(p->data);
|
|
Bluez5Device1 *device;
|
|
Bluez5GattService1 *service;
|
|
|
|
if (!chr->node_emitted)
|
|
continue;
|
|
|
|
lookup_chr_node(this, chr, &service, &device);
|
|
if (device)
|
|
emit_chr_node(this, chr, device);
|
|
}
|
|
g_list_free_full(chrs, g_object_unref);
|
|
|
|
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;
|
|
|
|
dbus_monitor_clear(&this->monitor);
|
|
g_clear_object(&this->manager);
|
|
g_clear_object(&this->conn);
|
|
|
|
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;
|
|
GError *error = NULL;
|
|
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);
|
|
|
|
if (this->log == NULL)
|
|
return -EINVAL;
|
|
|
|
spa_log_topic_init(this->log, &log_topic);
|
|
|
|
if (!(info && spa_atob(spa_dict_lookup(info, SPA_KEY_API_GLIB_MAINLOOP)))) {
|
|
spa_log_error(this->log, "Glib mainloop is not usable: %s not set",
|
|
SPA_KEY_API_GLIB_MAINLOOP);
|
|
return -EINVAL;
|
|
}
|
|
|
|
spa_hook_list_init(&this->hooks);
|
|
|
|
this->device.iface = SPA_INTERFACE_INIT(
|
|
SPA_TYPE_INTERFACE_Device,
|
|
SPA_VERSION_DEVICE,
|
|
&impl_device, this);
|
|
|
|
this->conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error);
|
|
if (!this->conn) {
|
|
spa_log_error(this->log, "Creating GDBus connection failed: %s",
|
|
error->message);
|
|
g_error_free(error);
|
|
goto fail;
|
|
}
|
|
|
|
g_dbus_connection_set_exit_on_close(this->conn, FALSE);
|
|
|
|
this->manager = g_dbus_object_manager_server_new(MIDI_OBJECT_PATH);
|
|
if (!this->manager){
|
|
spa_log_error(this->log, "Creating GDBus object manager failed");
|
|
goto fail;
|
|
}
|
|
|
|
if ((res = export_profile(this)) < 0)
|
|
goto fail;
|
|
|
|
g_dbus_object_manager_server_set_connection(this->manager, this->conn);
|
|
|
|
monitor_start(this);
|
|
|
|
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 <pav@iki.fi>" },
|
|
{ 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,
|
|
};
|