This adds a simple abstraction layer for Bluetooth Low Energy Generic Attribute profile type devices. It provides a simple interface to scan for specific devices bases on the UUIDs of the device and desired attributes.
This only adds a Linux specific BlueZ backend for the generic frontend. However, since this backend simply uses glib's DBus interface, it should be available without any new dependencies to sigrok itself. It should simply fail gracefully (i.e. not detect anything) if BlueZ is not available or there is no BLE interface on it. --- Makefile.am | 4 + configure.ac | 8 + include/libsigrok/libsigrok.h | 2 + src/backend.c | 13 + src/blegatt_bluez.c | 1847 +++++++++++++++++++++++++++++++++ src/device.c | 12 + src/libsigrok-internal.h | 37 + src/std.c | 4 + 8 files changed, 1927 insertions(+) create mode 100644 src/blegatt_bluez.c diff --git a/Makefile.am b/Makefile.am index 500d6620..4b1ccf44 100644 --- a/Makefile.am +++ b/Makefile.am @@ -135,6 +135,10 @@ if NEED_GPIB libsigrok_la_SOURCES += \ src/scpi/scpi_libgpib.c endif +if NEED_BLEGATT_BLUEZDBUS +libsigrok_la_SOURCES += \ + src/blegatt_bluez.c +endif # Modbus support libsigrok_la_SOURCES += \ diff --git a/configure.ac b/configure.ac index ea3e574b..5e18378d 100644 --- a/configure.ac +++ b/configure.ac @@ -130,6 +130,14 @@ SR_ARG_OPT_CHECK([libieee1284], [LIBIEEE1284],, [ AS_IF([test "x$sr_have_libieee1284" = xyes], [SR_PREPEND([SR_EXTRA_LIBS], [-lieee1284])]) +# Could check for BlueZ >= 5.42, but we should fail gracefully if the +# API isn't exposed regardless and it was available as experimental before that +AS_CASE([$host_os], + [linux*], [SR_ARG_OPT_PKG([bluezdbus], + [BLUEZ_DBUS],, [gio-2.0 >= 2.26.0])] +) +AM_CONDITIONAL([NEED_BLEGATT_BLUEZDBUS], [test "x$sr_have_bluezdbus" = xyes]) + ###################### ## Feature checks ## ###################### diff --git a/include/libsigrok/libsigrok.h b/include/libsigrok/libsigrok.h index a8b118c3..8d510398 100644 --- a/include/libsigrok/libsigrok.h +++ b/include/libsigrok/libsigrok.h @@ -1092,6 +1092,8 @@ enum sr_dev_inst_type { SR_INST_USER, /** Device instance type for Modbus devices. */ SR_INST_MODBUS, + /** Device instance type for Bluetooth Low Energy GATT devices. */ + SR_INST_BLEGATT, }; /** Device instance status, struct sr_dev_inst.status */ diff --git a/src/backend.c b/src/backend.c index 1fbba5c1..51b90ef3 100644 --- a/src/backend.c +++ b/src/backend.c @@ -592,6 +592,15 @@ SR_API int sr_init(struct sr_context **ctx) goto done; } #endif + +#ifdef HAVE_BLUEZ_DBUS + ret = sr_blegatt_init(&context->bluezdbus_ctx); + if (ret != SR_OK) { + sr_err("Error initializing BlueZ D-Bus."); + goto done; + } +#endif + sr_resource_set_hooks(context, NULL, NULL, NULL, NULL); *ctx = context; @@ -630,6 +639,10 @@ SR_API int sr_exit(struct sr_context *ctx) libusb_exit(ctx->libusb_ctx); #endif +#ifdef HAVE_BLUEZ_DBUS + sr_blegatt_shutdown(ctx->bluezdbus_ctx); +#endif + g_free(sr_driver_list(ctx)); g_free(ctx); diff --git a/src/blegatt_bluez.c b/src/blegatt_bluez.c new file mode 100644 index 00000000..bb2a1afd --- /dev/null +++ b/src/blegatt_bluez.c @@ -0,0 +1,1847 @@ +/* + * This file is part of the libsigrok project. + * + * Copyright (C) 2019 Derek Hageman <hageman@inthat.cloud> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <config.h> +#include <glib.h> +#include <glib-unix.h> +#include <gmodule.h> +#include <gio/gio.h> +#include <libsigrok/libsigrok.h> +#include <sys/eventfd.h> +#include "libsigrok-internal.h" + +/** @cond PRIVATE */ + +#define LOG_PREFIX "blegatt-bluez" + +#define BUS_BLUEZ "org.bluez" + +#define PATH_OBJECT_MANAGER "/" +#define INTERFACE_OBJECT_MANAGER "org.freedesktop.DBus.ObjectManager" + +#define PATH_HCI_ROOT "/org/bluez" +#define INTERFACE_DEVICE "org.bluez.Device1" +#define INTERFACE_CHARACTERISTIC "org.bluez.GattCharacteristic1" +#define INTERFACE_ADAPTER "org.bluez.Adapter1" + + +struct blegatt_global_ctx { + GMutex mutex; + GCond cond; + GThread *thd; + + GMainContext *main_context; + GMainLoop *main_loop; + + GHashTable *notify_active; + int notify_any_event; +}; + +struct blegatt_notify_ctx { + struct blegatt_global_ctx *gctx; + + GDBusProxy *proxy; + + int event; + GSList *pending; + + gulong read_connection; + void (*read)(const void *buf, int len, void *param); + void *read_param; +}; + +struct blegatt_stream_while_ctx { + gboolean (*condition)(void *param); + void *param; + + GMainLoop *loop; + int result; +}; + +struct blegatt_dev_ctx { + struct blegatt_global_ctx *gctx; + + GDBusConnection *bus; + GDBusProxy *device; + + struct sr_blegatt_characteristic *characteristics; + int count_characteristics; + + gboolean is_open; +}; + +struct blegatt_char_ctx { + struct blegatt_dev_ctx *dctx; + GDBusProxy *proxy; + GSource *notify_source; +}; + +struct blegatt_scan_ctx { + struct blegatt_global_ctx *gctx; + + GDBusConnection *bus; + GDBusProxy *om; + + GMainContext *main_context; + GMainLoop *main_loop; + GSList *discovering_adapters; + + GSList *devices; + const char *device_uuid; + const char **characteristics_uuids; + int count_characteristics; + char *required_address; + char *required_hci; + int scan_timeout; +}; + +struct blegatt_new_proxy_ctx { + struct blegatt_global_ctx *gctx; + gboolean done; + GDBusProxy *result; + + GDBusConnection *connection; + const gchar *name; + const gchar *object_path; + const gchar *interface_name; + GError **error; +}; + +/** @endcond */ + + +/* + * An unfortunate aspect of of GDBusConnection (inherited by GDBusProxy, etc) + * is that it synchronizes with the default GMainContext at the time of + * any call (e.x. GDBusProxy construction signal connection) with no way of + * altering that context. Additionally since we want to provide a way + * of not dropping notify signals, we can't just re-create the proxies + * or signal connections. That leaves us with the only real option of + * creating a thread specifically to create proxies (undoing most of the + * work GDBusConnection does...). Thankfully the G-DBus objects are explicitly + * thread safe, so we can use the proxies, etc from any thread, we just + * use the "worker" thread to handle signal emission. + * + * So we create a thread with a GMainContext, and set that context + * before creating any proxies. Then we basically repeat the work that + * GDBusConnection does, having a mapping of signal->context to actually + * deliver the notify, with the addition/option of having "no context" that + * queues the delivery until there is one. + */ + + +static gpointer worker_thread_run(gpointer data) +{ + struct blegatt_global_ctx *gctx = data; + g_main_context_push_thread_default(gctx->main_context); + g_main_loop_run(gctx->main_loop); + return NULL; +} + +static int call_simple_method(GDBusProxy *proxy, const gchar *method, + int timeout) +{ + GError *err = NULL; + GVariant *result; + + if (!(result = g_dbus_proxy_call_sync(proxy, + method, + NULL, + G_DBUS_CALL_FLAGS_NONE, + timeout, + NULL, + &err))) { + sr_dbg("Error calling method %s: %s.", method, err->message); + g_error_free(err); + return SR_ERR; + } + + g_variant_unref(result); + return SR_OK; +} + +static void worker_thread_async(struct blegatt_global_ctx *gctx, + gboolean (*call)(void *param), void *param) +{ + GSource *idle_source; + idle_source = g_idle_source_new(); + g_source_set_priority(idle_source, G_PRIORITY_DEFAULT); + g_source_set_callback(idle_source, + (GSourceFunc)call, + param, + NULL); + g_source_set_name(idle_source, + "blegatt_worker_thread_async"); + g_source_attach(idle_source, gctx->main_context); + g_source_unref(idle_source); +} + +static gboolean worker_thread_new_proxy(void *param) +{ + struct blegatt_new_proxy_ctx *ctx = param; + + ctx->result = g_dbus_proxy_new_sync(ctx->connection, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + ctx->name, + ctx->object_path, + ctx->interface_name, + NULL, + ctx->error); + + g_mutex_lock(&ctx->gctx->mutex); + ctx->done = TRUE; + g_cond_broadcast(&ctx->gctx->cond); + g_mutex_unlock(&ctx->gctx->mutex); + + return FALSE; +} + +static GDBusProxy *new_proxy(struct blegatt_global_ctx *gctx, + GDBusConnection *connection, + const gchar *name, + const gchar *object_path, + const gchar *interface_name, + GError **error) +{ + struct blegatt_new_proxy_ctx ctx; + ctx.gctx = gctx; + ctx.done = FALSE; + ctx.connection = connection; + ctx.name = name; + ctx.object_path = object_path; + ctx.interface_name = interface_name; + ctx.error = error; + + worker_thread_async(gctx, worker_thread_new_proxy, &ctx); + + g_mutex_lock(&gctx->mutex); + while (!ctx.done) { + g_cond_wait(&gctx->cond, &gctx->mutex); + } + g_mutex_unlock(&gctx->mutex); + + return ctx.result; +} + +static void characteristic_properties_changed(GDBusProxy *proxy, + GVariant *changed_properties, + GStrv invalidated_properties, + gpointer user_data) +{ + struct blegatt_notify_ctx *nctx = user_data; + GVariant *value; + GVariant *byte; + gsize length; + GByteArray *ba; + + (void)invalidated_properties; + + if (proxy != nctx->proxy) + return; + + if (!(value = g_variant_lookup_value(changed_properties, "Value", + G_VARIANT_TYPE_BYTESTRING))) { + return; + } + + length = g_variant_n_children(value); + if (!length) { + g_variant_unref(value); + return; + } + + ba = g_byte_array_new(); + g_byte_array_set_size(ba, length); + for (gsize i = 0; i < length; i++) { + byte = g_variant_get_child_value(value, i); + ba->data[i] = g_variant_get_byte(byte); + g_variant_unref(byte); + } + g_variant_unref(value); + + g_mutex_lock(&nctx->gctx->mutex); + nctx->pending = g_slist_append(nctx->pending, ba); + g_mutex_unlock(&nctx->gctx->mutex); + + /* Safe outside the lock, since destroy starts with disconnection, + * so it won't return until we do */ + if (nctx->event >= 0) + eventfd_write(nctx->event, 1); +} + +static void notify_value_destroy(gpointer data) +{ + struct blegatt_notify_ctx *nctx = data; + GByteArray *ba; + + if (nctx->read_connection) { + g_signal_handler_disconnect(nctx->proxy, nctx->read_connection); + } + + for (GSList *p = nctx->pending; p; p = p->next) { + ba = p->data; + g_byte_array_free(ba, TRUE); + } + g_slist_free(nctx->pending); + + if (nctx->event >= 0) + close(nctx->event); + + g_object_unref(nctx->proxy); + + g_free(nctx); +} + +static void set_characteristic_notify_stream(struct blegatt_char_ctx *cctx, + void (*handler)(const void *buf, int len, void *param), + void *param) +{ + struct blegatt_global_ctx *gctx = cctx->dctx->gctx; + struct blegatt_notify_ctx *nctx; + gboolean start_notify = FALSE; + gboolean destroy_nctx = FALSE; + + /* Assume that manipulation of this is done only when no + * event loop is active, so it's safe to close the + * eventfd. This also covers the case of how we would notify + * the existing event loop (we'd need to generate a new source + * for it). */ + + g_mutex_lock(&gctx->mutex); + + if (!handler) { + if (!(nctx = g_hash_table_lookup(gctx->notify_active, cctx))) + goto done; + g_hash_table_steal(gctx->notify_active, cctx); + destroy_nctx = TRUE; + } else { + nctx = g_hash_table_lookup(gctx->notify_active, cctx); + if (nctx) { + nctx->read = handler; + nctx->read_param = param; + goto done; + } + + start_notify = TRUE; + + nctx = g_new0(struct blegatt_notify_ctx, 1); + g_hash_table_insert(gctx->notify_active, cctx, nctx); + + nctx->gctx = gctx; + nctx->proxy = g_object_ref(cctx->proxy); + nctx->read = handler; + nctx->read_param = param; + + nctx->event = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (nctx->event < 0) { + sr_err("Error creating eventfd: %s.\n", + g_strerror(errno)); + } + + nctx->read_connection = g_signal_connect(nctx->proxy, + "g-properties-changed", + G_CALLBACK(characteristic_properties_changed), + nctx); + } + +done: + + g_mutex_unlock(&gctx->mutex); + + if (start_notify) + call_simple_method(cctx->proxy, "StartNotify", -1); + + /* Can't destroy in the lock, because the signal handler disconnection + * must return, and it takes the lock before it does */ + if (destroy_nctx) { + call_simple_method(cctx->proxy, "StopNotify", -1); + + notify_value_destroy(nctx); + } +} + +static gboolean characteristic_notify_source_ready(gint fd, + GIOCondition condition, + gpointer user_data) +{ + struct blegatt_notify_ctx *nctx = user_data; + struct blegatt_global_ctx *gctx = nctx->gctx; + eventfd_t event_value; + GSList *pending_values; + void (*pending_call)(const void *buf, int len, void *param); + void *pending_param; + GByteArray *ba; + + (void)condition; + + eventfd_read(fd, &event_value); + + g_mutex_lock(&gctx->mutex); + if (!nctx->pending || !nctx->read) { + g_mutex_unlock(&gctx->mutex); + return TRUE; + } + + pending_values = nctx->pending; + nctx->pending = NULL; + pending_call = nctx->read; + pending_param = nctx->read_param; + + g_mutex_unlock(&gctx->mutex); + + for (GSList *p = pending_values; p; p = p->next) { + ba = p->data; + (*pending_call)(ba->data, ba->len, pending_param); + g_byte_array_free(ba, TRUE); + } + g_slist_free(pending_values); + + if (gctx->notify_any_event >= 0) + eventfd_write(gctx->notify_any_event, 1); + + return TRUE; +} + +static GSource *create_characteristic_notify_source( + struct blegatt_char_ctx *cctx) +{ + struct blegatt_global_ctx *gctx = cctx->dctx->gctx; + struct blegatt_notify_ctx *nctx; + GSource *result = NULL; + + g_mutex_lock(&gctx->mutex); + nctx = g_hash_table_lookup(gctx->notify_active, cctx); + if (!nctx) + goto done; + + result = g_unix_fd_source_new(nctx->event, G_IO_IN); + g_source_set_callback(result, + (GSourceFunc)characteristic_notify_source_ready, + nctx, NULL); + +done: + g_mutex_unlock(&gctx->mutex); + return result; +} + +SR_PRIV int sr_blegatt_init(void **priv) +{ + struct blegatt_global_ctx *gctx; + + gctx = g_new0(struct blegatt_global_ctx, 1); + *priv = gctx; + + gctx->main_context = g_main_context_new(); + gctx->main_loop = g_main_loop_new(gctx->main_context, TRUE); + + gctx->notify_active = g_hash_table_new_full(NULL, NULL, NULL, + notify_value_destroy); + + g_mutex_init(&gctx->mutex); + g_cond_init(&gctx->cond); + + gctx->notify_any_event = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (gctx->notify_any_event < 0) { + sr_err("Error creating eventfd: %s.\n", g_strerror(errno)); + return SR_ERR; + } + + gctx->thd = g_thread_new("sr_blegatt_worker", + (GThreadFunc)worker_thread_run, + gctx); + + return SR_OK; +} + +SR_PRIV int sr_blegatt_shutdown(void *priv) +{ + struct blegatt_global_ctx *gctx = priv; + + if (!gctx) + return SR_OK; + + g_main_loop_quit(gctx->main_loop); + g_thread_join(gctx->thd); + + g_hash_table_unref(gctx->notify_active); + + g_main_loop_unref(gctx->main_loop); + g_main_context_unref(gctx->main_context); + + if (gctx->notify_any_event >= 0) + close(gctx->notify_any_event); + + g_cond_clear(&gctx->cond); + g_mutex_clear(&gctx->mutex); + + g_free(gctx); + return SR_OK; +} + + +static gboolean is_device_connected(GDBusProxy *device) +{ + GVariant *value; + gboolean connected; + + if (!(value = g_dbus_proxy_get_cached_property(device, "Connected"))) { + sr_dbg("Connected state missing in device properties."); + return FALSE; + } + + connected = g_variant_get_boolean(value); + + g_variant_unref(value); + return connected; +} + +static int characteristic_read(void *priv, void *buf, int maxlen) +{ + struct blegatt_char_ctx *cctx = priv; + GError *err = NULL; + GVariant *result; + GVariant *args; + GVariant *byte; + GVariantBuilder options; + gsize length; + uint8_t *wrb; + + g_variant_builder_init(&options, G_VARIANT_TYPE("a{sv}")); + + args = g_variant_new("(a{sv})", options); + if (!(result = g_dbus_proxy_call_sync(cctx->proxy, + "ReadValue", + args, + G_DBUS_CALL_FLAGS_NONE, + 2000, + NULL, + &err))) { + sr_dbg("Error reading from device: %s.", err->message); + g_error_free(err); + return SR_ERR; + } + + if (maxlen <= 0) { + g_variant_unref(result); + return 0; + } + + length = g_variant_n_children(result); + length = MIN(length, (gsize)maxlen); + if (!length) { + g_variant_unref(result); + return 0; + } + + wrb = buf; + for (gsize i = 0; i < length; i++, wrb++) { + byte = g_variant_get_child_value(result, i); + *wrb = g_variant_get_byte(byte); + g_variant_unref(byte); + } + + g_variant_unref(result); + return (int)length; +} + +static void characteristic_write_complete(GDBusProxy *proxy, + GAsyncResult *res, + gpointer user_data) +{ + GError *err = NULL; + GVariant *result; + + (void)user_data; + + if (!(result = g_dbus_proxy_call_finish(proxy, res, &err))) { + sr_dbg("Write failure to %s: %s.", + g_dbus_proxy_get_object_path(proxy), + err->message); + g_error_free(err); + return; + } + + g_variant_unref(result); +} + +static int characteristic_write(void *priv, const void *buf, int len) +{ + struct blegatt_char_ctx *cctx = priv; + GVariant *args; + GVariant *contents; + GVariantBuilder options; + + contents = g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, + buf, (gsize)len, 1); + + g_variant_builder_init(&options, G_VARIANT_TYPE("a{sv}")); + + args = g_variant_new("(@aya{sv})", contents, &options); + g_dbus_proxy_call(cctx->proxy, + "WriteValue", + args, + G_DBUS_CALL_FLAGS_NONE, + 2000, + NULL, + (GAsyncReadyCallback)characteristic_write_complete, + NULL); + + return SR_OK; +} + +static int characteristic_read_stream(void *priv, + void (*handler)(const void *buf, int len, void *param), + void *param) +{ + struct blegatt_char_ctx *cctx = priv; + if (!cctx->dctx->is_open && handler) { + sr_err("Read stream set is only valid on open devices."); + return SR_ERR_BUG; + } + set_characteristic_notify_stream(cctx, handler, param); + return SR_OK; +} + +static int device_open(void *priv) +{ + struct blegatt_dev_ctx *dctx = priv; + int ret; + + if (!is_device_connected(dctx->device)) { + ret = call_simple_method(dctx->device, "Connect", 5000); + if (ret != SR_OK) + goto err_disconnect; + } + + dctx->is_open = TRUE; + + return SR_OK; + +err_disconnect: + call_simple_method(dctx->device, "Disconnect", -1); + + return ret; +} + +static int device_close(void *priv) +{ + struct blegatt_dev_ctx *dctx = priv; + + dctx->is_open = FALSE; + + return call_simple_method(dctx->device, "Disconnect", -1); +} + + +static gboolean check_stop_stream(gint fd, + GIOCondition condition, + gpointer user_data) +{ + struct blegatt_stream_while_ctx *ctx = user_data; + eventfd_t eventfd_value; + + (void)condition; + + eventfd_read(fd, &eventfd_value); + + if ((*ctx->condition)(ctx->param)) + return TRUE; + + g_main_loop_quit(ctx->loop); + + sr_spew("Stream loop ending."); + + return FALSE; +} + +static gboolean source_stream_timeout(gpointer user_data) +{ + struct blegatt_stream_while_ctx *ctx = user_data; + + g_main_loop_quit(ctx->loop); + ctx->result = SR_ERR_TIMEOUT; + + sr_dbg("Stream loop timeout."); + + return FALSE; +} + +static int device_stream_while(struct sr_blegatt_dev_inst *dev, int timeout, + gboolean (*condition)(void *param), void *param) +{ + struct blegatt_dev_ctx *dctx = dev->priv; + struct blegatt_stream_while_ctx ctx; + struct blegatt_char_ctx *cctx; + GMainContext *context = NULL; + GSource *end_check_source = NULL; + GSource *timeout_source = NULL; + GSource *source; + GSList *notify_sources = NULL; + + ctx.condition = condition; + ctx.param = param; + ctx.result = SR_OK; + + context = g_main_context_new(); + g_main_context_push_thread_default(context); + ctx.loop = g_main_loop_new(context, TRUE); + + end_check_source = g_unix_fd_source_new(dctx->gctx->notify_any_event, + G_IO_IN); + g_source_set_callback(end_check_source, + (GSourceFunc)check_stop_stream, + &ctx, NULL); + g_source_set_name(end_check_source, "blegatt_stream_while_complete"); + g_source_attach(end_check_source, context); + + for (int i = 0; i < dctx->count_characteristics; i++) { + cctx = dctx->characteristics[i].priv; + source = create_characteristic_notify_source(cctx); + if (!source) + continue; + g_source_attach(source, context); + notify_sources = g_slist_prepend(notify_sources, source); + } + + if (timeout > 0) { + timeout_source = g_timeout_source_new((guint)timeout); + g_source_set_callback(timeout_source, + (GSourceFunc)source_stream_timeout, + &ctx, + NULL); + g_source_set_name(timeout_source, + "blegatt_stream_while_timeout"); + g_source_attach(timeout_source, context); + } + + if ((*ctx.condition)(param)) { + sr_spew("Starting stream loop."); + g_main_loop_run(ctx.loop); + } else { + sr_spew("Stream loop completed without iteration."); + } + + g_main_loop_unref(ctx.loop); + if (end_check_source) { + g_source_destroy(end_check_source); + g_source_unref(end_check_source); + } + if (timeout_source) { + g_source_destroy(timeout_source); + g_source_unref(timeout_source); + } + for (GSList *s = notify_sources; s; s = s->next) { + source = s->data; + g_source_destroy(source); + g_source_unref(source); + } + g_slist_free(notify_sources); + g_main_context_pop_thread_default(context); + g_main_context_unref(context); + + return ctx.result; +} + +static int device_enable_stream(void *priv, struct sr_session *session) +{ + struct blegatt_dev_ctx *dctx = priv; + struct blegatt_char_ctx *cctx; + + for (int i = 0; i < dctx->count_characteristics; i++) { + cctx = dctx->characteristics[i].priv; + if (cctx->notify_source != NULL) { + sr_err("Duplicate call to BLE stream enable."); + continue; + } + cctx->notify_source = create_characteristic_notify_source(cctx); + if (!cctx->notify_source) + continue; + sr_session_source_add_internal(session, cctx, + cctx->notify_source); + } + + return SR_OK; +} + +static int device_disable_stream(void *priv, struct sr_session *session) +{ + struct blegatt_dev_ctx *dctx = priv; + struct blegatt_char_ctx *cctx; + + for (int i = 0; i < dctx->count_characteristics; i++) { + cctx = dctx->characteristics[i].priv; + if (!cctx->notify_source) + continue; + sr_session_source_destroyed(session, cctx, cctx->notify_source); + g_source_destroy(cctx->notify_source); + g_source_unref(cctx->notify_source); + cctx->notify_source = NULL; + } + + return SR_OK; +} + + +static void free_device_context(struct blegatt_dev_ctx *dctx) +{ + if (dctx->device) { + g_object_unref(dctx->device); + dctx->device = NULL; + } + + if (dctx->bus) { + g_object_unref(dctx->bus); + dctx->bus = NULL; + } +} + +static void free_char_context(struct blegatt_char_ctx *cctx) +{ + if (cctx->proxy) { + set_characteristic_notify_stream(cctx, NULL, NULL); + g_object_unref(cctx->proxy); + cctx->proxy = NULL; + } +} + +static int connect_to_bus(struct blegatt_scan_ctx *sctx) +{ + GError *err = NULL; + if (!(sctx->bus = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &err))) { + sr_dbg("Error connecting to system bus: %s.", err->message); + g_error_free(err); + return SR_ERR; + } + + /* Create the ObjectManager proxy on the current (local) context, + * since we don't need it after the scan is done (i.e. we don't + * need its signals to persist. */ + if (!(sctx->om = g_dbus_proxy_new_sync(sctx->bus, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + BUS_BLUEZ, + PATH_OBJECT_MANAGER, + INTERFACE_OBJECT_MANAGER, + NULL, + &err))) { + sr_dbg("Error attaching to BlueZ: %s.", err->message); + g_error_free(err); + goto err_free_bus; + } + + return SR_OK; + +err_free_bus: + g_object_unref(sctx->bus); + sctx->bus = NULL; + return SR_ERR; +} + +static struct sr_blegatt_dev_inst *allocate_device( + struct blegatt_scan_ctx *sctx) +{ + struct sr_blegatt_dev_inst *dev; + struct sr_blegatt_characteristic *ch; + struct blegatt_dev_ctx *dctx; + struct blegatt_char_ctx *cctx; + + dev = g_new0(struct sr_blegatt_dev_inst, 1); + sctx->devices = g_slist_prepend(sctx->devices, dev); + + dctx = g_new0(struct blegatt_dev_ctx, 1); + dev->priv = dctx; + dctx->gctx = sctx->gctx; + dctx->bus = g_object_ref(sctx->bus); + + dev->open = device_open; + dev->close = device_close; + dev->stream_while = device_stream_while; + dev->enable_stream = device_enable_stream; + dev->disable_stream = device_disable_stream; + + dev->characteristics = g_new0(struct sr_blegatt_characteristic, + sctx->count_characteristics); + dev->count_characteristics = sctx->count_characteristics; + dctx->characteristics = dev->characteristics; + dctx->count_characteristics = dev->count_characteristics; + for (int i = 0; i < dev->count_characteristics; i++) { + ch = &dev->characteristics[i]; + cctx = g_new0(struct blegatt_char_ctx, 1); + ch->priv = cctx; + cctx->dctx = dctx; + + ch->read = characteristic_read; + ch->write = characteristic_write; + ch->read_stream = characteristic_read_stream; + } + + return dev; +} + +static int for_bus_interfaces(struct blegatt_scan_ctx *sctx, + void (*call)(const gchar *path, const gchar *interface, + GVariant *properties, void *param), + void *param) +{ + GError *err = NULL; + GVariant *result; + GVariant *objects; + GVariantIter iter_path; + gchar *bus_path = NULL; + GVariantIter *iter_interface = NULL; + gchar *interface_name = NULL; + GVariant *properties = NULL; + + if (!(result = g_dbus_proxy_call_sync(sctx->om, + "GetManagedObjects", + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + &err))) { + sr_dbg("Error fetching BlueZ objects: %s.", err->message); + g_error_free(err); + return SR_ERR; + } + + objects = g_variant_get_child_value(result, 0); + + g_variant_iter_init(&iter_path, objects); + while (g_variant_iter_loop(&iter_path, "{&oa{sa{sv}}}", + &bus_path, &iter_interface)) { + while (g_variant_iter_loop(iter_interface, "{&s*}", + &interface_name, &properties)) { + call(bus_path, interface_name, properties, param); + } + } + + g_variant_unref(objects); + g_variant_unref(result); + return SR_OK; +} + +static gboolean device_matches(struct blegatt_scan_ctx *sctx, + GVariant *properties) +{ + GVariant *uuids; + GVariantIter iter; + gchar *uuid = NULL; + + if (!(uuids = g_variant_lookup_value(properties, "UUIDs", + G_VARIANT_TYPE_STRING_ARRAY))) { + return FALSE; + } + g_variant_iter_init(&iter, uuids); + while (g_variant_iter_loop(&iter, "&s", &uuid)) { + if (!g_ascii_strcasecmp(uuid, sctx->device_uuid)) { + g_variant_unref(uuids); + return TRUE; + } + } + g_variant_unref(uuids); + return FALSE; +} + +static int characteristic_matches(struct blegatt_scan_ctx *sctx, + GVariant *properties) +{ + GVariant *uuid; + const char *check; + if (!(uuid = g_variant_lookup_value(properties, "UUID", + G_VARIANT_TYPE_STRING))) { + return 0; + } + + check = g_variant_get_string(uuid, NULL); + for (int i = 0; i < sctx->count_characteristics; i++) { + if (!g_ascii_strcasecmp(check, + sctx->characteristics_uuids[i])) { + g_variant_unref(uuid); + return i; + } + } + g_variant_unref(uuid); + return -1; +} + +static size_t characteristic_device_path_length(const char *path) +{ + const char *device_path_end; + + device_path_end = path; + for (int delim = 0; delim < 5; delim++) { + device_path_end = strchr(device_path_end, '/'); + if (!device_path_end) { + sr_dbg("Invalid characteristic path structure: %s.", + path); + return 0; + } + ++device_path_end; + } + return device_path_end - path - 1; +} + +static GDBusProxy *characteristic_parent_device(struct blegatt_scan_ctx *sctx, + const char *path, size_t device_path_length) +{ + GError *err = NULL; + GDBusProxy *proxy; + gchar *device_path = g_strndup(path, device_path_length); + if (!(proxy = new_proxy(sctx->gctx, + sctx->bus, + BUS_BLUEZ, + device_path, + INTERFACE_DEVICE, + &err))) { + sr_dbg("Error attaching to parent device: %s.", err->message); + g_error_free(err); + goto err_free_device_path; + } + g_free(device_path); + + return proxy; + +err_free_device_path: + g_free(device_path); + return NULL; +} + +static struct sr_blegatt_dev_inst *handle_scanned_characteristic( + struct blegatt_scan_ctx *sctx, + const char *path, GVariant *properties) +{ + + int index; + GError *err = NULL; + GDBusProxy *proxy; + GDBusProxy *parent_device; + struct sr_blegatt_dev_inst *dev; + struct blegatt_dev_ctx *dctx; + struct blegatt_char_ctx *cctx; + size_t device_path_length; + + if ((index = characteristic_matches(sctx, properties)) == -1) + return NULL; + + if (!(device_path_length = characteristic_device_path_length(path))) + return NULL; + + if (!(proxy = new_proxy(sctx->gctx, + sctx->bus, + BUS_BLUEZ, + path, + INTERFACE_CHARACTERISTIC, + &err))) { + sr_dbg("Error attaching to characteristic: %s.", err->message); + g_error_free(err); + return NULL; + } + + /* Check to see if we already know about it */ + for (GSList *existing = sctx->devices; existing; + existing = existing->next) { + dev = existing->data; + dctx = dev->priv; + if (strncmp(g_dbus_proxy_get_object_path(dctx->device), + path, device_path_length)) + continue; + + cctx = dev->characteristics[index].priv; + if (cctx->proxy) + g_object_unref(cctx->proxy); + cctx->proxy = proxy; + + sr_spew("Attached characteristic %s to existing device %s.", + g_dbus_proxy_get_object_path(cctx->proxy), + g_dbus_proxy_get_object_path(dctx->device)); + return dev; + } + + if (!(parent_device = characteristic_parent_device(sctx, path, + device_path_length))) { + goto err_free_device_path; + } + + dev = allocate_device(sctx); + + dctx = dev->priv; + dctx->device = parent_device; + + cctx = dev->characteristics[index].priv; + cctx->proxy = proxy; + + sr_spew("Attached characteristic %s to device %s.", + g_dbus_proxy_get_object_path(cctx->proxy), + g_dbus_proxy_get_object_path(dctx->device)); + return dev; + +err_free_device_path: + g_object_unref(proxy); + return NULL; +} + +static struct sr_blegatt_dev_inst *handle_scanned_device( + struct blegatt_scan_ctx *sctx, + const gchar *path, GVariant *properties) +{ + + GError *err = NULL; + GDBusProxy *proxy; + struct sr_blegatt_dev_inst *dev; + struct blegatt_dev_ctx *dctx; + + if (!device_matches(sctx, properties)) + return NULL; + + if (!(proxy = new_proxy(sctx->gctx, + sctx->bus, + BUS_BLUEZ, + path, + INTERFACE_DEVICE, + &err))) { + sr_dbg("Error attaching to device: %s.", err->message); + g_error_free(err); + return NULL; + } + + /* Check to see if we already know about it */ + for (GSList *existing = sctx->devices; existing; + existing = existing->next) { + dev = existing->data; + dctx = dev->priv; + if (strcmp(g_dbus_proxy_get_object_path(dctx->device), path)) + continue; + + g_object_unref(dctx->device); + + dctx->device = proxy; + + sr_spew("Re-detected device %s.", + g_dbus_proxy_get_object_path(dctx->device)); + return dev; + } + + dev = allocate_device(sctx); + + dctx = dev->priv; + dctx->device = proxy; + + sr_spew("Detected device %s.", + g_dbus_proxy_get_object_path(dctx->device)); + + return dev; +} + +/* Paths look like: + * /org/bluez/hci0/dev_FF_FF_FF_FF_FF_FF/service000c/char000d */ +static gboolean accept_device_path(const gchar *path, + const gchar *hci, const gchar *address) +{ + const gchar *next; + size_t remaining; + size_t len; + + if (!g_str_has_prefix(path, PATH_HCI_ROOT)) + return FALSE; + path += strlen(PATH_HCI_ROOT); + if (path[0] != '/') + return FALSE; + ++path; + + if (hci && hci[0]) { + if (!g_str_has_prefix(path, hci)) + return FALSE; + path += strlen(hci); + if (path[0] != '/') + return FALSE; + path++; + } else { + path = strchr(path, '/'); + if (!path) + return FALSE; + path++; + } + + if (strncmp(path, "dev_", 4)) + return FALSE; + if (!address || !address[0]) + return TRUE; + path += 4; + + remaining = strlen(path); + next = strchr(path, '/'); + if (next) + remaining = next - path; + + for (;;) { + next = strchr(address, ':'); + if (!next) { + return g_ascii_strncasecmp(path, address, + remaining) == 0; + } + + len = (size_t)(next - address); + len = MIN(remaining, len); + if (g_ascii_strncasecmp(path, address, len) != 0) + return FALSE; + + remaining -= (next - address) + 1; + address = next + 1; + } +} + +static gboolean accept_discovery_hci(const gchar *path, + const gchar *hci) +{ + if (!g_str_has_prefix(path, PATH_HCI_ROOT)) + return FALSE; + path += strlen(PATH_HCI_ROOT); + if (path[0] != '/') + return FALSE; + ++path; + + if (hci && hci[0]) { + if (!g_str_has_prefix(path, hci)) + return FALSE; + path += strlen(hci); + if (path[0]) + return FALSE; + } else { + path = strchr(path, '/'); + if (path) + return FALSE; + } + + return TRUE; +} + +static void inspect_known(const gchar *path, const gchar *interface, + GVariant *properties, void *param) +{ + struct blegatt_scan_ctx *sctx = param; + + if (!accept_device_path(path, sctx->required_hci, + sctx->required_address)) { + return; + } + + if (!strcmp(interface, INTERFACE_DEVICE)) { + handle_scanned_device(sctx, path, properties); + return; + } + + if (!strcmp(interface, INTERFACE_CHARACTERISTIC)) { + handle_scanned_characteristic(sctx, path, properties); + return; + } +} + +static gboolean is_selectable_device(struct sr_blegatt_dev_inst *dev) +{ + struct blegatt_dev_ctx *dctx = dev->priv; + struct blegatt_char_ctx *cctx; + if (!dctx->device) + return FALSE; + for (int i = 0; i < dev->count_characteristics; i++) { + cctx = dev->characteristics[i].priv; + if (!cctx->proxy) + return FALSE; + } + return TRUE; +} + +static void set_discovery_filter(struct blegatt_scan_ctx *sctx, + GDBusProxy *adapter) +{ + GError *err = NULL; + GVariant *args; + GVariant *uuids; + GVariantBuilder builder; + GVariant *result; + + /* Request a scan for only BLE devices ("le" transport) and the + * UUIDs we care about. Note that the actual discovery may + * find more than this, so this is entirely optional. */ + + g_variant_builder_init(&builder, G_VARIANT_TYPE("as")); + g_variant_builder_add_value(&builder, + g_variant_new_string(sctx->device_uuid)); + for (const char **uuid = sctx->characteristics_uuids; *uuid; ++uuid) { + g_variant_builder_add_value(&builder, + g_variant_new_string(*uuid)); + } + uuids = g_variant_builder_end(&builder); + + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&builder, "{sv}", + "Transport", g_variant_new_string("le")); + g_variant_builder_add(&builder, "{sv}", + "UUIDs", uuids); + + args = g_variant_new("(a{sv})", &builder); + if (!(result = g_dbus_proxy_call_sync(adapter, + "SetDiscoveryFilter", + args, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + &err))) { + sr_dbg("Error setting discovery filter: %s.", err->message); + g_error_free(err); + /* Ignore failure, since we don't really need the filter */ + } else { + g_variant_unref(result); + } +} + +static GDBusProxy *start_discovery_from_path(struct blegatt_scan_ctx *sctx, + const gchar *path) +{ + GError *err = NULL; + GDBusProxy *adapter; + + /* Same as the ObjectManager, we only need this for the + * scanning, so don't create it on the persistent thread */ + if (!(adapter = g_dbus_proxy_new_sync(sctx->bus, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + BUS_BLUEZ, + path, + INTERFACE_ADAPTER, + NULL, + &err))) { + sr_dbg("Error attaching to adapter: %s.", err->message); + g_error_free(err); + return NULL; + } + + set_discovery_filter(sctx, adapter); + + if (call_simple_method(adapter, "StartDiscovery", -1)) + goto err_free_disc; + + return adapter; + +err_free_disc: + g_object_unref(adapter); + return NULL; +} + +static void start_discovery_on_adapater(const gchar *path, + const gchar *interface, GVariant *properties, void *param) +{ + struct blegatt_scan_ctx *sctx = param; + GDBusProxy *adapter; + + (void)properties; + + if (strcmp(interface, INTERFACE_ADAPTER)) + return; + + if (!accept_discovery_hci(path, sctx->required_hci)) + return; + + if (!(adapter = start_discovery_from_path(sctx, path))) + return; + + sctx->discovering_adapters = g_slist_prepend(sctx->discovering_adapters, + adapter); +} + +static gboolean is_scan_complete(struct blegatt_scan_ctx *sctx) +{ + struct sr_blegatt_dev_inst *dev; + + if (sctx->scan_timeout < 0) + return FALSE; + if (sctx->scan_timeout == 0) + return TRUE; + + for (GSList *check = sctx->devices; check; check = check->next) { + dev = check->data; + if (!is_selectable_device(dev)) + continue; + + return TRUE; + } + return FALSE; +} + +static void device_connection_complete(GDBusProxy *proxy, + GAsyncResult *res, + gpointer user_data) +{ + GError *err = NULL; + GVariant *result; + + (void)user_data; + + if (!(result = g_dbus_proxy_call_finish(proxy, res, &err))) { + sr_spew("Connection failure to %s: %s.", + g_dbus_proxy_get_object_path(proxy), + err->message); + g_error_free(err); + return; + } + + g_variant_unref(result); + sr_spew("Connected to %s.", g_dbus_proxy_get_object_path(proxy)); +} + +static void start_connection_if_needed(struct sr_blegatt_dev_inst *dev) +{ + struct blegatt_dev_ctx *dctx = dev->priv; + struct blegatt_char_ctx *cctx; + gboolean have_all_characteristics = TRUE; + + /* Only attempt a connection in discovery if it's missing + * characteristics, since they might not enumerate until the + * first initial connection */ + for (int i = 0; i < dev->count_characteristics; i++) { + cctx = dev->characteristics[i].priv; + if (cctx->proxy) + continue; + have_all_characteristics = FALSE; + break; + } + if (have_all_characteristics) + return; + + if (is_device_connected(dctx->device)) + return; + + sr_spew("Starting connection to device %s.", + g_dbus_proxy_get_object_path(dctx->device)); + g_dbus_proxy_call(dctx->device, + "Connect", + NULL, + G_DBUS_CALL_FLAGS_NONE, + 5000, + NULL, + (GAsyncReadyCallback)device_connection_complete, + NULL); +} + +static void start_connection_to_all(GSList *devices) +{ + for (; devices; devices = devices->next) { + start_connection_if_needed(devices->data); + } +} + +static void discovery_added(struct blegatt_scan_ctx *sctx, + GVariant *parameters) +{ + GVariant *path_object; + const gchar *path; + GVariant *interfaces; + GVariantIter iter_interface; + gchar *interface_name; + GVariant *properties; + struct sr_blegatt_dev_inst *dev; + GSList *added_devices = NULL; + + path_object = g_variant_get_child_value(parameters, 0); + path = g_variant_get_string(path_object, NULL); + + interfaces = g_variant_get_child_value(parameters, 1); + g_variant_iter_init(&iter_interface, interfaces); + while (g_variant_iter_loop(&iter_interface, "{&s*}", + &interface_name, &properties)) { + if (!accept_device_path(path, sctx->required_hci, + sctx->required_address)) { + continue; + } + + if (!strcmp(interface_name, INTERFACE_DEVICE)) { + dev = handle_scanned_device(sctx, path, properties); + if (dev) { + if (sctx->scan_timeout >= 0 && + is_selectable_device(dev)) { + g_main_loop_quit(sctx->main_loop); + continue; + } + added_devices = g_slist_prepend(added_devices, + dev); + } + } else if (!strcmp(interface_name, INTERFACE_CHARACTERISTIC)) { + dev = handle_scanned_characteristic(sctx, + path, properties); + if (dev) { + if (sctx->scan_timeout >= 0 && + is_selectable_device(dev)) { + g_main_loop_quit(sctx->main_loop); + continue; + } + } + } + } + + g_variant_unref(path_object); + g_variant_unref(interfaces); + + start_connection_to_all(added_devices); + g_slist_free(added_devices); +} + +static void handle_remove_device(struct blegatt_scan_ctx *sctx, + const gchar *path) +{ + struct sr_blegatt_dev_inst *dev; + struct blegatt_dev_ctx *dctx; + + for (GSList *existing = sctx->devices, *prior = NULL; existing;) { + dev = existing->data; + dctx = dev->priv; + + if (strcmp(g_dbus_proxy_get_object_path(dctx->device), path)) { + prior = existing; + existing = existing->next; + continue; + } + + if (!prior) { + prior = existing; + existing = existing->next; + sctx->devices = existing; + + sr_blegatt_free(prior->data); + g_slist_free_1(prior); + + prior = NULL; + } else { + prior->next = existing->next; + sr_blegatt_free(existing->data); + g_slist_free_1(existing); + } + } +} + +static void handle_remove_characteristic(struct blegatt_scan_ctx *sctx, + const gchar *path) +{ + struct sr_blegatt_dev_inst *dev; + struct blegatt_char_ctx *cctx; + + for (GSList *existing = sctx->devices; existing; + existing = existing->next) { + dev = existing->data; + + for (int i = 0; i < dev->count_characteristics; i++) { + cctx = dev->characteristics[i].priv; + + if (cctx->proxy) { + if (!strcmp(g_dbus_proxy_get_object_path( + cctx->proxy), + path)) { + g_object_unref(cctx->proxy); + cctx->proxy = NULL; + continue; + } + } + } + } +} + +static void discovery_removed(struct blegatt_scan_ctx *sctx, + GVariant *parameters) +{ + GVariant *path_object; + const gchar *path; + GVariant *interfaces; + GVariantIter iter_interface; + gchar *interface_name; + + path_object = g_variant_get_child_value(parameters, 0); + path = g_variant_get_string(path_object, NULL); + + interfaces = g_variant_get_child_value(parameters, 1); + g_variant_iter_init(&iter_interface, interfaces); + while (g_variant_iter_loop(&iter_interface, "&s", + &interface_name)) { + if (!strcmp(interface_name, INTERFACE_DEVICE)) { + handle_remove_device(sctx, path); + continue; + } + + if (!strcmp(interface_name, INTERFACE_CHARACTERISTIC)) { + handle_remove_characteristic(sctx, path); + continue; + } + } + + g_variant_unref(path_object); + g_variant_unref(interfaces); +} + +static void discovery_signal(GDBusProxy *proxy, + const gchar *sender_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + struct blegatt_scan_ctx *sctx = user_data; + + (void)sender_name; + (void)proxy; + + if (!strcmp(signal_name, "InterfacesAdded")) { + discovery_added(sctx, parameters); + return; + } else if (!strcmp(signal_name, "InterfacesRemoved")) { + discovery_removed(sctx, parameters); + return; + } +} + +static gboolean source_scan_timeout(gpointer user_data) +{ + struct blegatt_scan_ctx *sctx = user_data; + + g_main_loop_quit(sctx->main_loop); + sr_spew("Detection ending after timeout."); + + return FALSE; +} + +static gboolean source_scan_check_complete(gpointer user_data) +{ + struct blegatt_scan_ctx *sctx = user_data; + + if (is_scan_complete(sctx)) { + g_main_loop_quit(sctx->main_loop); + sr_spew("Detection ending on first check."); + } + + return FALSE; +} + +static void execute_scan(struct blegatt_scan_ctx *sctx) +{ + GSource *timeout_source = NULL; + GSource *scan_completed_source = NULL; + GDBusProxy *adapter; + gulong om_updated_connection = 0; + + if (!(om_updated_connection = g_signal_connect(sctx->om, + "g-signal", + G_CALLBACK(discovery_signal), + sctx))) { + sr_dbg("Error connecting receive handler."); + goto scan_complete; + } + + for_bus_interfaces(sctx, inspect_known, sctx); + if (is_scan_complete(sctx)) { + sr_spew("Detection completed on initial iteration."); + goto scan_complete; + } + + for_bus_interfaces(sctx, start_discovery_on_adapater, sctx); + start_connection_to_all(sctx->devices); + + timeout_source = g_timeout_source_new((guint)abs(sctx->scan_timeout)); + g_source_set_callback(timeout_source, + (GSourceFunc)source_scan_timeout, + sctx, + NULL); + g_source_set_name(timeout_source, "blegatt_scan_timeout"); + g_source_attach(timeout_source, sctx->main_context); + + if (sctx->scan_timeout >= 0) { + scan_completed_source = g_idle_source_new(); + g_source_set_callback(scan_completed_source, + (GSourceFunc)source_scan_check_complete, + sctx, + NULL); + g_source_set_name(scan_completed_source, + "blegatt_scan_initial_complete_check"); + g_source_attach(scan_completed_source, sctx->main_context); + } + + g_main_loop_run(sctx->main_loop); + +scan_complete: + if (om_updated_connection) + g_signal_handler_disconnect(sctx->om, om_updated_connection); + if (timeout_source) { + g_source_destroy(timeout_source); + g_source_unref(timeout_source); + } + if (scan_completed_source) { + g_source_destroy(scan_completed_source); + g_source_unref(scan_completed_source); + } + for (GSList *da = sctx->discovering_adapters; da; da = da->next) { + adapter = da->data; + call_simple_method(adapter, "StopDiscovery", -1); + g_object_unref(adapter); + } + g_slist_free(sctx->discovering_adapters); +} + +static void process_address(struct blegatt_scan_ctx *sctx, char *address) +{ + if (!address[0]) + return; + + sctx->required_address = g_strdup(address); +} + +static void process_hci(struct blegatt_scan_ctx *sctx, char *hci) +{ + if (!hci[0]) + return; + sctx->required_hci = g_strdup(hci); +} + +static void breakdown_connection(struct blegatt_scan_ctx *sctx, + const char *conn) +{ + char **parts; + + if (!conn || !conn[0]) + return; + + parts = g_strsplit_set(conn, "/;", 3); + + if (parts[0]) { + process_address(sctx, parts[0]); + if (parts[1]) { + sr_atoi(parts[1], &sctx->scan_timeout); + + if (parts[2]) { + process_hci(sctx, parts[2]); + } + } + } + + g_strfreev(parts); +} + +/** + * Scan for matching BLE GATT devices. This finds devices with a UUID + * specified by device_uuid, having all characteristics with the UUIDs + * specified in characteristics_uuids. The connection string option is + * interpreted like: <MAC>/<SCAN_TIMEOUT>/<HCI>. The MAC address filters + * the Bluetooth MAC of the device, the HCI specifies the controller to + * scan on (instead of any), for example "hci0". The timeout is in ms, + * and if negative forces the whole time to elapse (i.e. complete the + * discovery scan even if a matching device is found). + * + * @param drvc The driver context doing the scan. + * @param options The scan options to find devices. + * @param device_uuid The UUID to match the device with. + * @param characteristics_uuids The NULL terminated list of characteristics to require + * @return A list of the devices found or NULL if no devices were found. + */ +SR_PRIV GSList *sr_blegatt_scan(struct drv_context *drvc, GSList *options, + const char *device_uuid, const char **characteristics_uuids) +{ + struct blegatt_scan_ctx sctx; + struct sr_blegatt_dev_inst *dev; + struct blegatt_dev_ctx *dctx; + GSList *result = NULL; + + memset(&sctx, 0, sizeof(sctx)); + sctx.gctx = drvc->sr_ctx->bluezdbus_ctx; + + sctx.main_context = g_main_context_new(); + g_main_context_push_thread_default(sctx.main_context); + sctx.main_loop = g_main_loop_new(sctx.main_context, TRUE); + + if (connect_to_bus(&sctx) != SR_OK) + goto done; + + sctx.device_uuid = device_uuid; + sctx.characteristics_uuids = characteristics_uuids; + sctx.scan_timeout = 10000; + + for (const char **c = characteristics_uuids; *c; c++) { + sctx.count_characteristics++; + } + + for (GSList *o = options; o; o = o->next) { + struct sr_config *src = o->data; + switch (src->key) { + case SR_CONF_CONN: + breakdown_connection(&sctx, + g_variant_get_string(src->data, + NULL)); + break; + default: + break; + } + } + + execute_scan(&sctx); + + for (GSList *check = sctx.devices; check; check = check->next) { + dev = check->data; + dctx = dev->priv; + if (!is_selectable_device(dev)) { + sr_blegatt_free(dev); + continue; + } + + dev->identifier = g_strdup( + g_dbus_proxy_get_object_path(dctx->device)); + + sr_spew("Accepting device %s.", dev->identifier); + + result = g_slist_prepend(result, dev); + check->data = NULL; + } + + g_slist_free(sctx.devices); + if (sctx.required_address) + g_free(sctx.required_address); + if (sctx.required_hci) + g_free(sctx.required_hci); + g_object_unref(sctx.om); + g_object_unref(sctx.bus); + +done: + g_main_loop_unref(sctx.main_loop); + g_main_context_pop_thread_default(sctx.main_context); + g_main_context_unref(sctx.main_context); + return result; + +} + +/** + * Free a BLE GATT device. + * + * @param gatt A previously initialized device. + */ +SR_PRIV void sr_blegatt_free(struct sr_blegatt_dev_inst *dev) +{ + g_free(dev->identifier); + + for (int i = 0; i < dev->count_characteristics; i++) { + free_char_context(dev->characteristics[i].priv); + g_free(dev->characteristics[i].priv); + } + g_free(dev->characteristics); + + free_device_context(dev->priv); + g_free(dev->priv); + g_free(dev); +} \ No newline at end of file diff --git a/src/device.c b/src/device.c index 34d80a9d..75e4829f 100644 --- a/src/device.c +++ b/src/device.c @@ -858,6 +858,18 @@ SR_API const char *sr_dev_inst_connid_get(const struct sr_dev_inst *sdi) } #endif +#ifdef HAVE_BLUEZ_DBUS + struct sr_blegatt_dev_inst *blegatt; + + if ((!sdi->connection_id) && (sdi->inst_type == SR_INST_BLEGATT)) { + /* connection_id isn't populated, let's do that here. */ + + blegatt = sdi->conn; + ((struct sr_dev_inst *)sdi)->connection_id = + g_strdup(blegatt->identifier); + } +#endif + return sdi->connection_id; } diff --git a/src/libsigrok-internal.h b/src/libsigrok-internal.h index b50cd57b..b7904489 100644 --- a/src/libsigrok-internal.h +++ b/src/libsigrok-internal.h @@ -340,6 +340,9 @@ struct sr_context { struct sr_dev_driver **driver_list; #ifdef HAVE_LIBUSB_1_0 libusb_context *libusb_ctx; +#endif +#ifdef HAVE_BLUEZ_DBUS + void *bluezdbus_ctx; #endif sr_resource_open_callback resource_open_cb; sr_resource_close_callback resource_close_cb; @@ -1174,6 +1177,40 @@ SR_PRIV int sr_modbus_write_multiple_registers(struct sr_modbus_dev_inst*modbus, SR_PRIV int sr_modbus_close(struct sr_modbus_dev_inst *modbus); SR_PRIV void sr_modbus_free(struct sr_modbus_dev_inst *modbus); +/*--- blegatt_bluez.c -------------------------------------------------------*/ + +#ifdef HAVE_BLUEZ_DBUS +struct sr_blegatt_characteristic { + int (*read)(void *priv, void *buf, int maxlen); + int (*write)(void *priv, const void *buf, int len); + int (*read_stream)(void *priv, void (*handler)(const void *buf, int len, void *param), void *param); + void *priv; +}; +struct sr_blegatt_dev_inst { + char *identifier; + + struct sr_blegatt_characteristic *characteristics; + int count_characteristics; + + int (*open)(void *priv); + int (*close)(void *priv); + int (*stream_while)(struct sr_blegatt_dev_inst *gatt, int timeout, + gboolean (*condition)(void *param), void *param); + + int (*enable_stream)(void *priv, struct sr_session *session); + int (*disable_stream)(void *priv, struct sr_session *session); + + void *priv; +}; + +SR_PRIV int sr_blegatt_init(void **priv); +SR_PRIV int sr_blegatt_shutdown(void *priv); + +SR_PRIV GSList *sr_blegatt_scan(struct drv_context *drvc, GSList *options, + const char *device_uuid, const char **characteristics_uuids); +SR_PRIV void sr_blegatt_free(struct sr_blegatt_dev_inst *gatt); +#endif + /*--- hardware/dmm/es519xx.c ------------------------------------------------*/ /** diff --git a/src/std.c b/src/std.c index 6a6c80ad..f9b1afd4 100644 --- a/src/std.c +++ b/src/std.c @@ -466,6 +466,10 @@ SR_PRIV int std_dev_clear_with_callback(const struct sr_dev_driver *driver, sr_scpi_free(sdi->conn); if (sdi->inst_type == SR_INST_MODBUS) sr_modbus_free(sdi->conn); +#ifdef HAVE_BLUEZ_DBUS + if (sdi->inst_type == SR_INST_BLEGATT) + sr_blegatt_free(sdi->conn); +#endif } /* Clear driver-specific stuff, if any. */ -- 2.20.1 _______________________________________________ sigrok-devel mailing list sigrok-devel@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/sigrok-devel