From b5a785e4b686b4334ba2ac33dd2cf4e086ba4858 Mon Sep 17 00:00:00 2001 From: Olivier Martin Date: Fri, 13 May 2022 16:10:55 +0200 Subject: [PATCH] dbus: Refactore gattlib_adapter_scan_enable_with_filter() to also introduce the non-blocking version --- dbus/gattlib_adapter.c | 161 ++++++++++++++++++++------------- dbus/gattlib_internal.h | 27 +++++- gattlib-py/gattlib/__init__.py | 9 +- gattlib-py/gattlib/adapter.py | 38 ++++++-- include/gattlib.h | 22 +++++ 5 files changed, 184 insertions(+), 73 deletions(-) diff --git a/dbus/gattlib_adapter.c b/dbus/gattlib_adapter.c index ee67ef2..5453cfc 100644 --- a/dbus/gattlib_adapter.c +++ b/dbus/gattlib_adapter.c @@ -1,7 +1,7 @@ /* * SPDX-License-Identifier: BSD-3-Clause * - * Copyright (c) 2016-2021, Olivier Martin + * Copyright (c) 2016-2022, Olivier Martin */ #include "gattlib_internal.h" @@ -98,18 +98,7 @@ GDBusObjectManager *get_device_manager_from_adapter(struct gattlib_adapter *gatt return gattlib_adapter->device_manager; } -/* - * Internal structure to pass to Device Manager signal handlers - */ -struct discovered_device_arg { - void *adapter; - uint32_t enabled_filters; - gattlib_discovered_device_t callback; - void *user_data; - GSList** discovered_devices_ptr; -}; - -static void device_manager_on_device1_signal(const char* device1_path, struct discovered_device_arg *arg) +static void device_manager_on_device1_signal(const char* device1_path, struct gattlib_adapter* gattlib_adapter) { GError *error = NULL; OrgBluezDevice1* device1 = org_bluez_device1_proxy_new_for_bus_sync( @@ -135,20 +124,31 @@ static void device_manager_on_device1_signal(const char* device1_path, struct di } // Check if the device is already part of the list - GSList *item = g_slist_find_custom(*arg->discovered_devices_ptr, address, (GCompareFunc)g_ascii_strcasecmp); + GSList *item = g_slist_find_custom(gattlib_adapter->ble_scan.discovered_devices, address, (GCompareFunc)g_ascii_strcasecmp); // First time this device is in the list if (item == NULL) { // Add the device to the list - *arg->discovered_devices_ptr = g_slist_append(*arg->discovered_devices_ptr, g_strdup(address)); + gattlib_adapter->ble_scan.discovered_devices = g_slist_append(gattlib_adapter->ble_scan.discovered_devices, g_strdup(address)); } - if ((item == NULL) || (arg->enabled_filters & GATTLIB_DISCOVER_FILTER_NOTIFY_CHANGE)) { - arg->callback( - arg->adapter, + if ((item == NULL) || (gattlib_adapter->ble_scan.enabled_filters & GATTLIB_DISCOVER_FILTER_NOTIFY_CHANGE)) { +#if defined(WITH_PYTHON) + // In case of Python support, we ensure we acquire the GIL (Global Intepreter Lock) to have + // a thread-safe Python execution. + PyGILState_STATE d_gstate; + d_gstate = PyGILState_Ensure(); +#endif + + gattlib_adapter->ble_scan.discovered_device_callback( + gattlib_adapter, org_bluez_device1_get_address(device1), org_bluez_device1_get_name(device1), - arg->user_data); + gattlib_adapter->ble_scan.discovered_device_user_data); + +#if defined(WITH_PYTHON) + PyGILState_Release(d_gstate); +#endif } g_object_unref(device1); } @@ -196,15 +196,41 @@ on_interface_proxy_properties_changed (GDBusObjectManagerClient *device_manager, device_manager_on_device1_signal(g_dbus_proxy_get_object_path(interface_proxy), user_data); } -int gattlib_adapter_scan_enable_with_filter(void *adapter, uuid_t **uuid_list, int16_t rssi_threshold, uint32_t enabled_filters, +static void* _ble_scan_loop(void* args) { + struct gattlib_adapter *gattlib_adapter = args; + + // Run Glib loop for 'timeout' seconds + gattlib_adapter->ble_scan.scan_loop = g_main_loop_new(NULL, 0); + if (gattlib_adapter->ble_scan.ble_scan_timeout > 0) { + gattlib_adapter->ble_scan.ble_scan_timeout_id = g_timeout_add_seconds(gattlib_adapter->ble_scan.ble_scan_timeout, + stop_scan_func, gattlib_adapter->ble_scan.scan_loop); + } + + // And start the loop... + g_main_loop_run(gattlib_adapter->ble_scan.scan_loop); + // At this point, either the timeout expired (and automatically was removed) or scan_disable was called, removing the timer. + gattlib_adapter->ble_scan.ble_scan_timeout_id = 0; + + // Note: The function only resumes when loop timeout as expired or g_main_loop_quit has been called. + + g_signal_handler_disconnect(G_DBUS_OBJECT_MANAGER(gattlib_adapter->device_manager), gattlib_adapter->ble_scan.added_signal_id); + g_signal_handler_disconnect(G_DBUS_OBJECT_MANAGER(gattlib_adapter->device_manager), gattlib_adapter->ble_scan.changed_signal_id); + + // Ensure BLE device discovery is stopped + gattlib_adapter_scan_disable(gattlib_adapter); + + // Free discovered device list + g_slist_foreach(gattlib_adapter->ble_scan.discovered_devices, (GFunc)g_free, NULL); + g_slist_free(gattlib_adapter->ble_scan.discovered_devices); + return 0; +} + +static int _gattlib_adapter_scan_enable_with_filter(void *adapter, uuid_t **uuid_list, int16_t rssi_threshold, uint32_t enabled_filters, gattlib_discovered_device_t discovered_device_cb, size_t timeout, void *user_data) { struct gattlib_adapter *gattlib_adapter = adapter; GDBusObjectManager *device_manager; GError *error = NULL; - int ret = GATTLIB_SUCCESS; - int added_signal_id, changed_signal_id; - GSList *discovered_devices = NULL; GVariantBuilder arg_properties_builder; GVariant *rssi_variant = NULL; @@ -250,28 +276,26 @@ int gattlib_adapter_scan_enable_with_filter(void *adapter, uuid_t **uuid_list, i // device_manager = get_device_manager_from_adapter(gattlib_adapter); if (device_manager == NULL) { - goto DISABLE_SCAN; + return GATTLIB_ERROR_DBUS; } - // Pass the user callback and the discovered device list pointer to the signal handlers - struct discovered_device_arg discovered_device_arg = { - .adapter = adapter, - .enabled_filters = enabled_filters, - .callback = discovered_device_cb, - .user_data = user_data, - .discovered_devices_ptr = &discovered_devices, - }; + // Clear BLE scan structure + memset(&gattlib_adapter->ble_scan, 0, sizeof(gattlib_adapter->ble_scan)); + gattlib_adapter->ble_scan.enabled_filters = enabled_filters; + gattlib_adapter->ble_scan.ble_scan_timeout = timeout; + gattlib_adapter->ble_scan.discovered_device_callback = discovered_device_cb; + gattlib_adapter->ble_scan.discovered_device_user_data = user_data; - added_signal_id = g_signal_connect(G_DBUS_OBJECT_MANAGER(device_manager), + gattlib_adapter->ble_scan.added_signal_id = g_signal_connect(G_DBUS_OBJECT_MANAGER(device_manager), "object-added", G_CALLBACK (on_dbus_object_added), - &discovered_device_arg); + gattlib_adapter); // List for object changes to see if there are still devices around - changed_signal_id = g_signal_connect(G_DBUS_OBJECT_MANAGER(device_manager), + gattlib_adapter->ble_scan.changed_signal_id = g_signal_connect(G_DBUS_OBJECT_MANAGER(device_manager), "interface-proxy-properties-changed", G_CALLBACK(on_interface_proxy_properties_changed), - &discovered_device_arg); + gattlib_adapter); // Now, start BLE discovery org_bluez_adapter1_call_start_discovery_sync(gattlib_adapter->adapter_proxy, NULL, &error); @@ -281,28 +305,43 @@ int gattlib_adapter_scan_enable_with_filter(void *adapter, uuid_t **uuid_list, i return GATTLIB_ERROR_DBUS; } - // Run Glib loop for 'timeout' seconds - gattlib_adapter->scan_loop = g_main_loop_new(NULL, 0); - if (timeout > 0) { - gattlib_adapter->timeout_id = g_timeout_add_seconds(timeout, stop_scan_func, gattlib_adapter->scan_loop); + return GATTLIB_SUCCESS; +} + +int gattlib_adapter_scan_enable_with_filter(void *adapter, uuid_t **uuid_list, int16_t rssi_threshold, uint32_t enabled_filters, + gattlib_discovered_device_t discovered_device_cb, size_t timeout, void *user_data) +{ + int ret; + + ret = _gattlib_adapter_scan_enable_with_filter(adapter, uuid_list, rssi_threshold, enabled_filters, + discovered_device_cb, timeout, user_data); + if (ret != GATTLIB_SUCCESS) { + return ret; } - g_main_loop_run(gattlib_adapter->scan_loop); - // At this point, either the timeout expired (and automatically was removed) or scan_disable was called, removing the timer. - gattlib_adapter->timeout_id = 0; - // Note: The function only resumes when loop timeout as expired or g_main_loop_quit has been called. + _ble_scan_loop(adapter); + return 0; +} - g_signal_handler_disconnect(G_DBUS_OBJECT_MANAGER(device_manager), added_signal_id); - g_signal_handler_disconnect(G_DBUS_OBJECT_MANAGER(device_manager), changed_signal_id); +int gattlib_adapter_scan_enable_with_filter_non_blocking(void *adapter, uuid_t **uuid_list, int16_t rssi_threshold, uint32_t enabled_filters, + gattlib_discovered_device_t discovered_device_cb, size_t timeout, void *user_data) +{ + struct gattlib_adapter *gattlib_adapter = adapter; + int ret; -DISABLE_SCAN: - // Stop BLE device discovery - gattlib_adapter_scan_disable(adapter); + ret = _gattlib_adapter_scan_enable_with_filter(adapter, uuid_list, rssi_threshold, enabled_filters, + discovered_device_cb, timeout, user_data); + if (ret != GATTLIB_SUCCESS) { + return ret; + } - // Free discovered device list - g_slist_foreach(discovered_devices, (GFunc)g_free, NULL); - g_slist_free(discovered_devices); - return ret; + ret = pthread_create(&gattlib_adapter->ble_scan.thread, NULL, _ble_scan_loop, gattlib_adapter); + if (ret != 0) { + GATTLIB_LOG(GATTLIB_ERROR, "Failt to create BLE scan thread."); + return ret; + } + + return 0; } int gattlib_adapter_scan_enable(void* adapter, gattlib_discovered_device_t discovered_device_cb, size_t timeout, void *user_data) @@ -316,24 +355,24 @@ int gattlib_adapter_scan_enable(void* adapter, gattlib_discovered_device_t disco int gattlib_adapter_scan_disable(void* adapter) { struct gattlib_adapter *gattlib_adapter = adapter; - if (gattlib_adapter->scan_loop) { + if (gattlib_adapter->ble_scan.scan_loop) { GError *error = NULL; org_bluez_adapter1_call_stop_discovery_sync(gattlib_adapter->adapter_proxy, NULL, &error); // Ignore the error // Remove timeout - if (gattlib_adapter->timeout_id) { - g_source_remove(gattlib_adapter->timeout_id); - gattlib_adapter->timeout_id = 0; + if (gattlib_adapter->ble_scan.ble_scan_timeout_id) { + g_source_remove(gattlib_adapter->ble_scan.ble_scan_timeout_id); + gattlib_adapter->ble_scan.ble_scan_timeout_id = 0; } // Ensure the scan loop is quit - if (g_main_loop_is_running(gattlib_adapter->scan_loop)) { - g_main_loop_quit(gattlib_adapter->scan_loop); + if (g_main_loop_is_running(gattlib_adapter->ble_scan.scan_loop)) { + g_main_loop_quit(gattlib_adapter->ble_scan.scan_loop); } - g_main_loop_unref(gattlib_adapter->scan_loop); - gattlib_adapter->scan_loop = NULL; + g_main_loop_unref(gattlib_adapter->ble_scan.scan_loop); + gattlib_adapter->ble_scan.scan_loop = NULL; } return GATTLIB_SUCCESS; diff --git a/dbus/gattlib_internal.h b/dbus/gattlib_internal.h index 61b87dd..162ba5b 100644 --- a/dbus/gattlib_internal.h +++ b/dbus/gattlib_internal.h @@ -1,7 +1,7 @@ /* * SPDX-License-Identifier: BSD-3-Clause * - * Copyright (c) 2016-2021, Olivier Martin + * Copyright (c) 2016-2022, Olivier Martin */ #ifndef __GATTLIB_INTERNAL_H__ @@ -19,6 +19,10 @@ #include "org-bluez-gattdescriptor1.h" #include "org-bluez-gattservice1.h" +#if defined(WITH_PYTHON) + #include +#endif + #include "bluez5/lib/uuid.h" #define BLUEZ_VERSIONS(major, minor) (((major) << 8) | (minor)) @@ -55,8 +59,25 @@ struct gattlib_adapter { OrgBluezAdapter1 *adapter_proxy; char* adapter_name; - GMainLoop *scan_loop; - guint timeout_id; + // Internal attributes only needed during BLE scanning + struct { + // This list is used to stored discovered devices during BLE scan. + // The list is freed when the BLE scanning is completed. + GSList *discovered_devices; + + int added_signal_id; + int changed_signal_id; + + size_t ble_scan_timeout; + guint ble_scan_timeout_id; + + pthread_t thread; // Thread used to run the scan_loop + GMainLoop *scan_loop; + + uint32_t enabled_filters; + gattlib_discovered_device_t discovered_device_callback; + void *discovered_device_user_data; + } ble_scan; }; struct dbus_characteristic { diff --git a/gattlib-py/gattlib/__init__.py b/gattlib-py/gattlib/__init__.py index 2b973db..812694a 100644 --- a/gattlib-py/gattlib/__init__.py +++ b/gattlib-py/gattlib/__init__.py @@ -5,6 +5,9 @@ # from ctypes import * +import logging + +logger = logging.getLogger(__name__) gattlib = CDLL("libgattlib.so") @@ -82,10 +85,10 @@ gattlib_discovered_device_with_data_type = CFUNCTYPE(None, c_void_p, c_char_p, c POINTER(GattlibAdvertisementData), c_size_t, c_uint16, c_void_p, c_size_t, py_object) -# int gattlib_adapter_scan_enable_with_filter(void *adapter, uuid_t **uuid_list, int16_t rssi_threshold, uint32_t enabled_filters, +# int gattlib_adapter_scan_enable_with_filter_non_blocking(void *adapter, uuid_t **uuid_list, int16_t rssi_threshold, uint32_t enabled_filters, # gattlib_discovered_device_t discovered_device_cb, size_t timeout, void *user_data) -gattlib_adapter_scan_enable_with_filter = gattlib.gattlib_adapter_scan_enable_with_filter -gattlib_adapter_scan_enable_with_filter.argtypes = [c_void_p, POINTER(POINTER(GattlibUuid)), c_int16, c_uint32, gattlib_discovered_device_type, c_size_t, py_object] +gattlib_adapter_scan_enable_with_filter_non_blocking = gattlib.gattlib_adapter_scan_enable_with_filter_non_blocking +gattlib_adapter_scan_enable_with_filter_non_blocking.argtypes = [c_void_p, POINTER(POINTER(GattlibUuid)), c_int16, c_uint32, gattlib_discovered_device_type, c_size_t, py_object] # int gattlib_adapter_scan_eddystone(void *adapter, int16_t rssi_threshold, uint32_t eddsytone_types, # gattlib_discovered_device_with_data_t discovered_device_cb, size_t timeout, void *user_data) diff --git a/gattlib-py/gattlib/adapter.py b/gattlib-py/gattlib/adapter.py index 6414513..b1883c9 100644 --- a/gattlib-py/gattlib/adapter.py +++ b/gattlib-py/gattlib/adapter.py @@ -61,13 +61,35 @@ class Adapter: self._is_opened = False return ret - def on_discovered_device(self, adapter, addr, name, user_data): - device = Device(self, addr, name) - self.on_discovered_device_callback(device, user_data) + # Use a closure to return a method that can be called by the C-library (see: https://stackoverflow.com/a/7261524/6267288) + def get_on_discovered_device_callback(self): + def on_discovered_device(adapter, addr, name, user_data): + try: + device = Device(self, addr, name) + self.on_discovered_device_user_callback(device, user_data) + except Exception as e: + logger.exception(e) + return gattlib_discovered_device_type(on_discovered_device) def scan_enable(self, on_discovered_device_callback, timeout, notify_change=False, uuids=None, rssi_threshold=None, user_data=None): + """ + Scan for BLE devices + + @param adapter: is the context of the newly opened adapter + @param uuid_list: is a NULL-terminated list of UUIDs to filter. The rule only applies to advertised UUID. + Returned devices would match any of the UUIDs of the list. + @param rssi_threshold: is the imposed RSSI threshold for the returned devices. + @param enabled_filters: defines the parameters to use for filtering. There are selected by using the macros + GATTLIB_DISCOVER_FILTER_USE_UUID and GATTLIB_DISCOVER_FILTER_USE_RSSI. + @param discovered_device_cb: is the function callback called for each new Bluetooth device discovered + @param timeout: defines the duration of the Bluetooth scanning. When timeout=None or 0, we scan indefinitely. + @param user_data: is the data passed to the callback `discovered_device_cb()` + """ assert on_discovered_device_callback != None - self.on_discovered_device_callback = on_discovered_device_callback + self.on_discovered_device_user_callback = on_discovered_device_callback + # Save callback to prevent it to be cleaned by garbage collector see + # comment: https://stackoverflow.com/questions/7259794/how-can-i-get-methods-to-work-as-callbacks-with-python-ctypes#comment38658391_7261524 + self.on_discovered_device_callback = self.get_on_discovered_device_callback() if not self._is_opened: raise AdapterNotOpened() @@ -99,9 +121,13 @@ class Adapter: if notify_change: enabled_filters |= GATTLIB_DISCOVER_FILTER_NOTIFY_CHANGE - ret = gattlib_adapter_scan_enable_with_filter(self._adapter, + # gattlib_adapter_scan_enable_with_filter_non_blocking() assumes a 0-timeout means scanning indefintely + if timeout is None: + timeout = 0 + + ret = gattlib_adapter_scan_enable_with_filter_non_blocking(self._adapter, uuid_list, rssi, enabled_filters, - gattlib_discovered_device_type(self.on_discovered_device), + self.on_discovered_device_callback, timeout, user_data) handle_return(ret) diff --git a/include/gattlib.h b/include/gattlib.h index 4477288..c1ec8eb 100644 --- a/include/gattlib.h +++ b/include/gattlib.h @@ -230,6 +230,8 @@ int gattlib_adapter_scan_enable(void* adapter, gattlib_discovered_device_t disco /** * @brief Enable Bluetooth scanning on a given adapter * + * This function will block until either the timeout has expired or gattlib_adapter_scan_disable() has been called. + * * @param adapter is the context of the newly opened adapter * @param uuid_list is a NULL-terminated list of UUIDs to filter. The rule only applies to advertised UUID. * Returned devices would match any of the UUIDs of the list. @@ -245,6 +247,26 @@ int gattlib_adapter_scan_enable(void* adapter, gattlib_discovered_device_t disco int gattlib_adapter_scan_enable_with_filter(void *adapter, uuid_t **uuid_list, int16_t rssi_threshold, uint32_t enabled_filters, gattlib_discovered_device_t discovered_device_cb, size_t timeout, void *user_data); +/** + * @brief Enable Bluetooth scanning on a given adapter (non-blocking) + * + * This function will return as soon as the BLE scan has been started. + * + * @param adapter is the context of the newly opened adapter + * @param uuid_list is a NULL-terminated list of UUIDs to filter. The rule only applies to advertised UUID. + * Returned devices would match any of the UUIDs of the list. + * @param rssi_threshold is the imposed RSSI threshold for the returned devices. + * @param enabled_filters defines the parameters to use for filtering. There are selected by using the macros + * GATTLIB_DISCOVER_FILTER_USE_UUID and GATTLIB_DISCOVER_FILTER_USE_RSSI. + * @param discovered_device_cb is the function callback called for each new Bluetooth device discovered + * @param timeout defines the duration of the Bluetooth scanning. When timeout=0, we scan indefinitely. + * @param user_data is the data passed to the callback `discovered_device_cb()` + * + * @return GATTLIB_SUCCESS on success or GATTLIB_* error code + */ +int gattlib_adapter_scan_enable_with_filter_non_blocking(void *adapter, uuid_t **uuid_list, int16_t rssi_threshold, uint32_t enabled_filters, + gattlib_discovered_device_t discovered_device_cb, size_t timeout, void *user_data); + /** * @brief Enable Eddystone Bluetooth Device scanning on a given adapter *