// Copyright 2019-2020, Collabora, Ltd. // SPDX-License-Identifier: BSL-1.0 /*! * @file * @brief BLE implementation based on Linux Bluez/dbus. * @author Pete Black * @author Jakob Bornecrantz * @ingroup aux_os */ #include "os_ble.h" #include "util/u_misc.h" #include "util/u_logging.h" #include #include #include #include #include #include #include #include /* * * Defines. * */ #define I(bch, ...) U_LOG_I(__VA_ARGS__) #define E(bch, ...) U_LOG_E(__VA_ARGS__) /* * * Structs. * */ /*! * Small helper that keeps track of a connection and a error. */ struct ble_conn_helper { DBusConnection *conn; DBusError err; }; /*! * An implementation of @ref os_ble_device using a DBus connection to BlueZ. * @implements os_ble_device */ struct ble_notify { struct os_ble_device base; struct ble_conn_helper bch; int fd; }; /* * * Send helpers. * */ static void add_single_byte_array(DBusMessage *msg, uint8_t value) { // Create an array of bytes. const char *container_signature = "y"; // dbus type signature string DBusMessageIter iter; DBusMessageIter array; // attach it to our dbus message dbus_message_iter_init_append(msg, &iter); dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, container_signature, &array); dbus_message_iter_append_basic(&array, DBUS_TYPE_BYTE, &value); dbus_message_iter_close_container(&iter, &array); } static void add_empty_dict_sv(DBusMessage *msg) { // Create an empty array of string variant dicts. const char *container_signature = "{sv}"; // dbus type signature string DBusMessageIter iter; DBusMessageIter options; // attach it to our dbus message dbus_message_iter_init_append(msg, &iter); dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, container_signature, &options); dbus_message_iter_close_container(&iter, &options); } static int send_message(DBusConnection *conn, DBusError *err, DBusMessage **msg_ptr) { DBusPendingCall *pending; // Take the message and null it. DBusMessage *msg = *msg_ptr; *msg_ptr = NULL; if (msg == NULL) { U_LOG_E("Message Null after construction\n"); return -1; } // send message and get a handle for a reply if (!dbus_connection_send_with_reply(conn, msg, &pending, -1)) { // -1 is default timeout U_LOG_E("Out Of Memory!\n"); return -1; } if (pending == NULL) { U_LOG_E("Pending Call Null\n"); return -1; } dbus_connection_flush(conn); // Unref the message. dbus_message_unref(msg); msg = NULL; // block until we receive a reply dbus_pending_call_block(pending); // get the reply message msg = dbus_pending_call_steal_reply(pending); // free the pending message handle dbus_pending_call_unref(pending); pending = NULL; if (msg == NULL) { U_LOG_E("Reply Null\n"); return -1; } *msg_ptr = msg; return 0; } /* * * Dump functions * */ static void dump_recurse(DBusMessageIter *parent, DBusMessageIter *sub, int level); static int dump_one_element(DBusMessageIter *element, int level) { int type = dbus_message_iter_get_arg_type(element); char *str; switch (type) { case DBUS_TYPE_INVALID: { U_LOG_E("%*s<>", level, ""); return -1; } case DBUS_TYPE_BOOLEAN: { int val; dbus_message_iter_get_basic(element, &val); U_LOG_D("%*sBOOLEAN: %s", level, "", val == 0 ? "false" : "true"); return 0; } case DBUS_TYPE_BYTE: { int8_t val; dbus_message_iter_get_basic(element, &val); U_LOG_D("%*sBYTE: %02x", level, "", val); return 0; } case DBUS_TYPE_INT32: { int32_t val; dbus_message_iter_get_basic(element, &val); U_LOG_D("%*sINT32: %" PRIi32, level, "", val); return 0; } case DBUS_TYPE_UINT32: { uint32_t val; dbus_message_iter_get_basic(element, &val); U_LOG_D("%*sUINT32: %" PRIu32, level, "", val); return 0; } case DBUS_TYPE_INT64: { int64_t val; dbus_message_iter_get_basic(element, &val); U_LOG_D("%*sINT64: %" PRIi64, level, "", val); return 0; } case DBUS_TYPE_UINT64: { uint64_t val; dbus_message_iter_get_basic(element, &val); U_LOG_D("%*sUINT32: %" PRIu64, level, "", val); return 0; } case DBUS_TYPE_STRING: { dbus_message_iter_get_basic(element, &str); U_LOG_D("%*sSTRING: %s", level, "", str); return 0; } case DBUS_TYPE_OBJECT_PATH: { dbus_message_iter_get_basic(element, &str); U_LOG_D("%*sOBJECT_PATH: %s", level, "", str); return 0; } case DBUS_TYPE_ARRAY: { int elm_type = dbus_message_iter_get_element_type(element); int elm_count = dbus_message_iter_get_element_count(element); U_LOG_D("%*sARRAY: %c:%i", level, "", elm_type, elm_count); DBusMessageIter sub; dbus_message_iter_recurse(element, &sub); dump_recurse(element, &sub, level + 2); return 0; } case DBUS_TYPE_VARIANT: { DBusMessageIter var; dbus_message_iter_recurse(element, &var); int var_type = dbus_message_iter_get_arg_type(&var); U_LOG_D("%*sVARIANT: %c", level, "", var_type); dump_one_element(&var, level + 2); return 0; } case DBUS_TYPE_DICT_ENTRY: { U_LOG_D("%*sDICT", level, ""); DBusMessageIter sub; dbus_message_iter_recurse(element, &sub); dump_recurse(element, &sub, level + 2); return 0; } default: U_LOG_D("%*sGot! %c", level, "", type); // line break return 0; } } static void dump_recurse(DBusMessageIter *parent, DBusMessageIter *sub, int level) { while (true) { if (dump_one_element(sub, level) < 0) { return; } dbus_message_iter_next(sub); } } /* * * DBus iterator helper functions. * */ /*! * Checks if a string starts with, has extra slash and room for more. */ static bool starts_with_and_has_slash(const char *str, const char *beginning) { size_t str_len = strlen(str); size_t beginning_len = strlen(beginning); if (str_len <= beginning_len + 1) { return false; } size_t i = 0; for (; i < beginning_len; i++) { if (str[i] != beginning[i]) { return false; } } if (str[i] != '/') { return false; } return true; } static int dict_get_string_and_varient_child(DBusMessageIter *dict, const char **out_str, DBusMessageIter *out_child) { DBusMessageIter child; int type = dbus_message_iter_get_arg_type(dict); if (type != DBUS_TYPE_DICT_ENTRY) { U_LOG_E("Expected dict got '%c'!\n", type); return -1; } dbus_message_iter_recurse(dict, &child); type = dbus_message_iter_get_arg_type(&child); if (type != DBUS_TYPE_STRING && type != DBUS_TYPE_OBJECT_PATH) { U_LOG_E( "Expected dict first thing to be string or object path, " "got '%c'\n", type); return -1; } dbus_message_iter_get_basic(&child, out_str); dbus_message_iter_next(&child); type = dbus_message_iter_get_arg_type(&child); if (type != DBUS_TYPE_VARIANT) { U_LOG_E("Expected variant got '%c'\n", type); return -1; } dbus_message_iter_recurse(&child, out_child); return 0; } static int dict_get_string_and_array_elm(const DBusMessageIter *in_dict, const char **out_str, DBusMessageIter *out_array_elm) { DBusMessageIter dict = *in_dict; DBusMessageIter child; int type = dbus_message_iter_get_arg_type(&dict); if (type != DBUS_TYPE_DICT_ENTRY) { U_LOG_E("Expected dict got '%c'!\n", type); return -1; } dbus_message_iter_recurse(&dict, &child); type = dbus_message_iter_get_arg_type(&child); if (type != DBUS_TYPE_STRING && type != DBUS_TYPE_OBJECT_PATH) { U_LOG_E( "Expected dict first thing to be string or object path, " "got '%c'\n", type); return -1; } dbus_message_iter_get_basic(&child, out_str); dbus_message_iter_next(&child); type = dbus_message_iter_get_arg_type(&child); if (type != DBUS_TYPE_ARRAY) { U_LOG_E("Expected array got '%c'\n", type); return -1; } dbus_message_iter_recurse(&child, out_array_elm); return 0; } #define for_each(i, first) \ for (DBusMessageIter i = first; dbus_message_iter_get_arg_type(&i) != DBUS_TYPE_INVALID; \ dbus_message_iter_next(&i)) /*! * Ensures that the @p parent is a array and has a element type the given type, * outputs the first element of the array on success. */ static int array_get_first_elem_of_type(const DBusMessageIter *in_parent, int of_type, DBusMessageIter *out_elm) { DBusMessageIter parent = *in_parent; int type = dbus_message_iter_get_arg_type(&parent); if (type != DBUS_TYPE_ARRAY) { U_LOG_E("Expected array got '%c'!\n", type); return -1; } DBusMessageIter elm; dbus_message_iter_recurse(&parent, &elm); type = dbus_message_iter_get_arg_type(&elm); if (type != of_type) { U_LOG_E("Expected elem type of '%c' got '%c'!\n", of_type, type); return -1; } *out_elm = elm; return 1; } /*! * Given a the first element in a array of dict, loop over them and check if * the key matches its string value. Returns positive if a match is found, * zero if not found and negative on failure. The argument @p out_value holds * the value of the dict pair. */ static int array_find_variant_value(const DBusMessageIter *first_elm, const char *key, DBusMessageIter *out_value) { const char *str; for_each(elm, *first_elm) { dict_get_string_and_varient_child(&elm, &str, out_value); if (strcmp(key, str) == 0) { return 1; } } return 0; } /*! * Given a array which elements are of type string, loop over them and check if * any of them matches the given @p key. Returns positive if a match is found, * zero if not found and negative on failure. */ static int array_match_string_element(const DBusMessageIter *in_array, const char *key) { DBusMessageIter array = *in_array; int type = dbus_message_iter_get_arg_type(&array); if (type != DBUS_TYPE_ARRAY) { U_LOG_E("Expected array type ('%c')\n", type); return -1; } int elm_type = dbus_message_iter_get_element_type(&array); if (elm_type != DBUS_TYPE_STRING) { U_LOG_E("Expected string element type ('%c')\n", type); return -1; } DBusMessageIter first_elm; dbus_message_iter_recurse(&array, &first_elm); for_each(elm, first_elm) { const char *str = NULL; dbus_message_iter_get_basic(&elm, &str); if (strcmp(key, str) == 0) { return 1; } } return 0; } /* * * D-BUS helpers. * */ static int dbus_has_name(DBusConnection *conn, const char *name) { DBusMessage *msg; DBusError err; msg = dbus_message_new_method_call("org.freedesktop.DBus", // target for the method call "/org/freedesktop/DBus", // object to call on "org.freedesktop.DBus", // interface to call on "ListNames"); // method name if (send_message(conn, &err, &msg) != 0) { return -1; } DBusMessageIter args; dbus_message_iter_init(msg, &args); // Check if this is a error message. int type = dbus_message_iter_get_arg_type(&args); if (type == DBUS_TYPE_STRING) { char *response = NULL; dbus_message_iter_get_basic(&args, &response); U_LOG_E("Error getting calling ListNames:\n%s\n", response); response = NULL; // free reply dbus_message_unref(msg); return -1; } DBusMessageIter first_elm; int ret = array_get_first_elem_of_type(&args, DBUS_TYPE_STRING, &first_elm); if (ret < 0) { // free reply dbus_message_unref(msg); return -1; } for_each(elm, first_elm) { char *response = NULL; dbus_message_iter_get_basic(&elm, &response); if (strcmp(response, name) == 0) { // free reply dbus_message_unref(msg); return 0; } } // free reply dbus_message_unref(msg); return -1; } /* * * Bluez helpers iterator helpers. * */ /*! * Returns true if the object implements the `org.bluez.Device1` interface, * and one of its `UUIDs` matches the given @p uuid. */ static int device_has_uuid(const DBusMessageIter *dict, const char *uuid, const char **out_path_str) { DBusMessageIter iface_elm; DBusMessageIter first_elm; const char *iface_str; const char *path_str; int ret = dict_get_string_and_array_elm(dict, &path_str, &first_elm); if (ret < 0) { return ret; } for_each(elm, first_elm) { dict_get_string_and_array_elm(&elm, &iface_str, &iface_elm); if (strcmp(iface_str, "org.bluez.Device1") != 0) { continue; } DBusMessageIter value; int ret = array_find_variant_value(&iface_elm, "UUIDs", &value); if (ret <= 0) { continue; } ret = array_match_string_element(&value, uuid); if (ret <= 0) { continue; } *out_path_str = path_str; return 1; } return 0; } /*! * On a gatt interface object get its Flags property and check if notify is * set, returns positive if it found that Flags property, zero on not finding it * and negative on error. */ static int gatt_iface_get_flag_notifiable(const DBusMessageIter *iface_elm, bool *out_bool) { DBusMessageIter value; int ret = array_find_variant_value(iface_elm, "Flags", &value); if (ret <= 0) { return ret; } ret = array_match_string_element(&value, "notify"); if (ret < 0) { // Error return ret; } if (ret > 0) { // Found the notify field! *out_bool = true; } // We found the Flags field. return 1; } /*! * On a gatt interface object get its UUID string property, returns positive * if found, zero on not finding it and negative on error. */ static int gatt_iface_get_uuid(const DBusMessageIter *iface_elm, const char **out_str) { DBusMessageIter value; int ret = array_find_variant_value(iface_elm, "UUID", &value); if (ret <= 0) { return ret; } int type = dbus_message_iter_get_arg_type(&value); if (type != DBUS_TYPE_STRING) { U_LOG_E("Invalid UUID value type ('%c')\n", type); return -1; } dbus_message_iter_get_basic(&value, out_str); return 1; } /*! * Returns positive value if the object implements the * `org.bluez.GattCharacteristic1` interface, its `UUID` matches the given @p * uuid. */ static int gatt_char_has_uuid(const DBusMessageIter *dict, const char *uuid, const char **out_path_str, bool *out_notifiable) { DBusMessageIter first_elm; DBusMessageIter iface_elm; const char *iface_str; const char *path_str; const char *uuid_str; int ret = dict_get_string_and_array_elm(dict, &path_str, &first_elm); if (ret < 0) { return ret; } for_each(elm, first_elm) { dict_get_string_and_array_elm(&elm, &iface_str, &iface_elm); if (strcmp(iface_str, "org.bluez.GattCharacteristic1") != 0) { continue; } if (gatt_iface_get_uuid(&iface_elm, &uuid_str) <= 0) { continue; } if (strcmp(uuid_str, uuid) != 0) { continue; } bool notifiable = false; ret = gatt_iface_get_flag_notifiable(&iface_elm, ¬ifiable); if (ret <= 0) { continue; } *out_path_str = path_str; *out_notifiable = notifiable; return 1; } return 0; } /* * * Bluez helpers.. * */ static void ble_close(struct ble_conn_helper *bch) { if (bch->conn == NULL) { return; } dbus_error_free(&bch->err); dbus_connection_unref(bch->conn); bch->conn = NULL; } static int ble_init(struct ble_conn_helper *bch) { dbus_error_init(&bch->err); bch->conn = dbus_bus_get(DBUS_BUS_SYSTEM, &bch->err); if (dbus_error_is_set(&bch->err)) { E(bch, "DBUS Connection Error: %s\n", bch->err.message); dbus_error_free(&bch->err); } if (bch->conn == NULL) { return -1; } // Check if org.bluez is running. int ret = dbus_has_name(bch->conn, "org.bluez"); if (ret != 0) { ble_close(bch); return -1; } return 0; } static int ble_dbus_send(struct ble_conn_helper *bch, DBusMessage **out_msg) { return send_message(bch->conn, &bch->err, out_msg); } static int ble_get_managed_objects(struct ble_conn_helper *bch, DBusMessage **out_msg) { DBusMessageIter args; DBusMessage *msg; int type; msg = dbus_message_new_method_call("org.bluez", // target for the method call "/", // object to call on "org.freedesktop.DBus.ObjectManager", // interface to call on "GetManagedObjects"); // method name if (msg == NULL) { E(bch, "Could not create new message!"); return -1; } int ret = ble_dbus_send(bch, &msg); if (ret != 0) { E(bch, "Could send message '%i'!", ret); dbus_message_unref(msg); return ret; } dbus_message_iter_init(msg, &args); // Check if this is a error message. type = dbus_message_iter_get_arg_type(&args); if (type == DBUS_TYPE_STRING) { char *response = NULL; dbus_message_iter_get_basic(&args, &response); E(bch, "Error getting objects:\n%s\n", response); response = NULL; // free reply dbus_message_unref(msg); return -1; } // Message is verified. *out_msg = msg; return 0; } static int ble_connect(struct ble_conn_helper *bch, const char *dbus_address) { DBusMessage *msg = NULL; DBusMessageIter args; char *response = NULL; int ret; int type; I(bch, "Connecting '%s'", dbus_address); msg = dbus_message_new_method_call("org.bluez", // target for the method call dbus_address, // object to call on "org.bluez.Device1", // interface to call on "Connect"); // method name if (msg == NULL) { E(bch, "Message NULL after construction\n"); return -1; } // Send the message, consumes our message and returns what we received. ret = ble_dbus_send(bch, &msg); if (ret != 0) { E(bch, "Failed to send message '%i'\n", ret); return -1; } // Function returns nothing on success, check for error message. dbus_message_iter_init(msg, &args); type = dbus_message_iter_get_arg_type(&args); if (type == DBUS_TYPE_STRING) { dbus_message_iter_get_basic(&args, &response); E(bch, "DBus call returned message: %s\n", response); // Free reply. dbus_message_unref(msg); return -1; } // Free reply dbus_message_unref(msg); return 0; } static int ble_connect_all_devices_with_service_uuid(struct ble_conn_helper *bch, const char *service_uuid) { DBusMessageIter args; DBusMessageIter first_elm; DBusMessage *msg = NULL; int ret = ble_get_managed_objects(bch, &msg); if (ret != 0) { return ret; } dbus_message_iter_init(msg, &args); ret = array_get_first_elem_of_type(&args, DBUS_TYPE_DICT_ENTRY, &first_elm); if (ret < 0) { // free reply dbus_message_unref(msg); return -1; } for_each(elm, first_elm) { const char *dev_path_str; ret = device_has_uuid(&elm, service_uuid, &dev_path_str); if (ret <= 0) { continue; } ble_connect(bch, dev_path_str); } // free reply dbus_message_unref(msg); return 0; } static int ble_write_value(struct ble_conn_helper *bch, const char *dbus_address, uint8_t value) { DBusMessage *msg = NULL; DBusMessageIter args; char *response = NULL; int ret; int type; msg = dbus_message_new_method_call("org.bluez", // target for the method call dbus_address, // object to call on "org.bluez.GattCharacteristic1", // interface to call on "WriteValue"); // method name if (msg == NULL) { E(bch, "Message NULL after construction\n"); return -1; } // Write has a argument of Array of Bytes, Array of Dicts. add_single_byte_array(msg, value); add_empty_dict_sv(msg); // Send the message, consumes our message and returns what we received. ret = ble_dbus_send(bch, &msg); if (ret != 0) { E(bch, "Failed to send message '%i'\n", ret); return -1; } // Function returns nothing on success, check for error message. dbus_message_iter_init(msg, &args); type = dbus_message_iter_get_arg_type(&args); if (type == DBUS_TYPE_STRING) { dbus_message_iter_get_basic(&args, &response); E(bch, "DBus call returned message: %s\n", response); // Free reply. dbus_message_unref(msg); return -1; } // Free reply dbus_message_unref(msg); return 0; } static ssize_t get_path_to_notify_char( struct ble_conn_helper *bch, const char *dev_uuid, const char *char_uuid, char *output, size_t output_len) { DBusMessageIter args; DBusMessageIter first_elm; DBusMessage *msg; int ret = ble_get_managed_objects(bch, &msg); if (ret != 0) { return ret; } dbus_message_iter_init(msg, &args); ret = array_get_first_elem_of_type(&args, DBUS_TYPE_DICT_ENTRY, &first_elm); if (ret < 0) { // free reply dbus_message_unref(msg); return -1; } for_each(elm, first_elm) { const char *dev_path_str; const char *char_path_str; bool notifiable; ret = device_has_uuid(&elm, dev_uuid, &dev_path_str); if (ret <= 0) { continue; } for_each(c, first_elm) { ret = gatt_char_has_uuid(&c, char_uuid, &char_path_str, ¬ifiable); if (ret <= 0 || !notifiable) { continue; } if (!starts_with_and_has_slash(char_path_str, dev_path_str)) { continue; } ssize_t written = snprintf(output, output_len, "%s", char_path_str); // free reply dbus_message_unref(msg); return written; } } // free reply dbus_message_unref(msg); return 0; } static int init_ble_notify(const char *dev_uuid, const char *char_uuid, struct ble_notify *bledev) { DBusMessage *msg; int ret; ret = ble_init(&bledev->bch); if (ret != 0) { return ret; } char dbus_address[256]; // should be long enough XRT_MAYBE_UNUSED ssize_t written = get_path_to_notify_char(&bledev->bch, dev_uuid, char_uuid, dbus_address, sizeof(dbus_address)); if (written == 0) { return -1; } if (written < 0) { return -1; } msg = dbus_message_new_method_call("org.bluez", // target for the method call dbus_address, // object to call on "org.bluez.GattCharacteristic1", // interface to call on "AcquireNotify"); // method name if (msg == NULL) { E(&bledev->bch, "Message Null after construction\n"); return -1; } // AcquireNotify has a argument of Array of Dicts. add_empty_dict_sv(msg); // Send the message, consumes our message and returns what we received. if (ble_dbus_send(&bledev->bch, &msg) != 0) { return -1; } DBusMessageIter args; char *response = NULL; dbus_message_iter_init(msg, &args); while (true) { int type = dbus_message_iter_get_arg_type(&args); if (type == DBUS_TYPE_INVALID) { break; } if (type == DBUS_TYPE_STRING) { dbus_message_iter_get_basic(&args, &response); E(&bledev->bch, "DBus call returned message: %s\n", response); } else if (type == DBUS_TYPE_UNIX_FD) { dbus_message_iter_get_basic(&args, &bledev->fd); } dbus_message_iter_next(&args); } // free reply dbus_message_unref(msg); // We didn't get a fd. if (bledev->fd == -1) { return -1; } return 0; } /* * * BLE notify object implementation. * */ static int os_ble_notify_read(struct os_ble_device *bdev, uint8_t *data, size_t length, int milliseconds) { struct ble_notify *dev = (struct ble_notify *)bdev; struct pollfd fds; int ret; if (milliseconds >= 0) { fds.fd = dev->fd; fds.events = POLLIN; fds.revents = 0; ret = poll(&fds, 1, milliseconds); if (ret == -1 || ret == 0) { // Error or timeout. return ret; } if (fds.revents & (POLLERR | POLLHUP | POLLNVAL)) { // Device disconnect? return -1; } } ret = read(dev->fd, data, length); if (ret < 0 && (errno == EAGAIN || errno == EINPROGRESS)) { // Process most likely received a signal. ret = 0; } return ret; } static void os_ble_notify_destroy(struct os_ble_device *bdev) { struct ble_notify *dev = (struct ble_notify *)bdev; if (dev->fd != -1) { close(dev->fd); dev->fd = -1; } ble_close(&dev->bch); free(dev); } /* * * 'Exported' functions. * */ int os_ble_notify_open(const char *dev_uuid, const char *char_uuid, struct os_ble_device **out_ble) { struct ble_notify *bledev = U_TYPED_CALLOC(struct ble_notify); bledev->base.read = os_ble_notify_read; bledev->base.destroy = os_ble_notify_destroy; bledev->fd = -1; int ret = init_ble_notify(dev_uuid, char_uuid, bledev); if (ret != 0) { os_ble_notify_destroy(&bledev->base); return -1; } *out_ble = &bledev->base; return 1; } int os_ble_broadcast_write_value(const char *service_uuid, const char *char_uuid, uint8_t value) { struct ble_conn_helper bch = {0}; DBusMessageIter args; DBusMessageIter first_elm; DBusMessage *msg = NULL; int ret = 0; /* * Init dbus */ ret = ble_init(&bch); if (ret != 0) { return ret; } /* * Connect devices */ // Connect all of the devices so we can write to them. ble_connect_all_devices_with_service_uuid(&bch, service_uuid); /* * Write to all connected devices. */ /* * We get the objects again, because their services and characteristics * might not have been created before. */ ret = ble_get_managed_objects(&bch, &msg); if (ret != 0) { ble_close(&bch); return ret; } dbus_message_iter_init(msg, &args); ret = array_get_first_elem_of_type(&args, DBUS_TYPE_DICT_ENTRY, &first_elm); if (ret < 0) { // free reply dbus_message_unref(msg); ble_close(&bch); return -1; } for_each(elm, first_elm) { const char *dev_path_str; const char *char_path_str; bool notifiable; ret = device_has_uuid(&elm, service_uuid, &dev_path_str); if (ret <= 0) { continue; } for_each(c, first_elm) { ret = gatt_char_has_uuid(&c, char_uuid, &char_path_str, ¬ifiable); if (ret <= 0) { continue; } if (!starts_with_and_has_slash(char_path_str, dev_path_str)) { continue; } ble_write_value(&bch, char_path_str, value); } } // free reply dbus_message_unref(msg); ble_close(&bch); return 0; }