| /* |
| * |
| * BlueZ - Bluetooth protocol stack for Linux |
| * |
| * Copyright (C) 2014 Google Inc. |
| * |
| * |
| * 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 2 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. |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <stdbool.h> |
| #include <stdint.h> |
| #include <errno.h> |
| #include <unistd.h> |
| #include <fcntl.h> |
| |
| #include <dbus/dbus.h> |
| |
| #include "lib/bluetooth.h" |
| #include "lib/sdp.h" |
| #include "lib/uuid.h" |
| |
| #include "gdbus/gdbus.h" |
| |
| #include "log.h" |
| #include "error.h" |
| #include "adapter.h" |
| #include "device.h" |
| #include "src/shared/io.h" |
| #include "src/shared/queue.h" |
| #include "src/shared/att.h" |
| #include "src/shared/gatt-db.h" |
| #include "src/shared/gatt-client.h" |
| #include "src/shared/util.h" |
| #include "gatt-client.h" |
| #include "dbus-common.h" |
| |
| #ifndef NELEM |
| #define NELEM(x) (sizeof(x) / sizeof((x)[0])) |
| #endif |
| |
| #define GATT_SERVICE_IFACE "org.bluez.GattService1" |
| #define GATT_CHARACTERISTIC_IFACE "org.bluez.GattCharacteristic1" |
| #define GATT_DESCRIPTOR_IFACE "org.bluez.GattDescriptor1" |
| |
| struct btd_gatt_client { |
| struct btd_device *device; |
| bool ready; |
| char devaddr[18]; |
| struct gatt_db *db; |
| struct bt_gatt_client *gatt; |
| |
| struct queue *services; |
| struct queue *all_notify_clients; |
| struct queue *ios; |
| }; |
| |
| struct service { |
| struct btd_gatt_client *client; |
| bool primary; |
| uint16_t start_handle; |
| uint16_t end_handle; |
| bt_uuid_t uuid; |
| char *path; |
| struct queue *chrcs; |
| struct queue *incl_services; |
| }; |
| |
| typedef bool (*async_dbus_op_complete_t)(void *data); |
| |
| struct async_dbus_op { |
| int ref_count; |
| unsigned int id; |
| struct queue *msgs; |
| void *data; |
| uint16_t offset; |
| async_dbus_op_complete_t complete; |
| }; |
| |
| struct pipe_io { |
| DBusMessage *msg; |
| struct io *io; |
| void (*destroy)(void *data); |
| void *data; |
| }; |
| |
| struct characteristic { |
| struct service *service; |
| struct gatt_db_attribute *attr; |
| uint16_t handle; |
| uint16_t value_handle; |
| uint8_t props; |
| uint16_t ext_props; |
| uint16_t ext_props_handle; |
| bt_uuid_t uuid; |
| char *path; |
| |
| unsigned int ready_id; |
| struct pipe_io *write_io; |
| struct pipe_io *notify_io; |
| |
| struct async_dbus_op *read_op; |
| struct async_dbus_op *write_op; |
| |
| struct queue *descs; |
| |
| bool notifying; |
| struct queue *notify_clients; |
| }; |
| |
| struct descriptor { |
| struct characteristic *chrc; |
| struct gatt_db_attribute *attr; |
| uint16_t handle; |
| bt_uuid_t uuid; |
| char *path; |
| |
| struct async_dbus_op *read_op; |
| struct async_dbus_op *write_op; |
| }; |
| |
| static bool uuid_cmp(const bt_uuid_t *uuid, uint16_t u16) |
| { |
| bt_uuid_t uuid16; |
| |
| bt_uuid16_create(&uuid16, u16); |
| |
| return bt_uuid_cmp(uuid, &uuid16) == 0; |
| } |
| |
| static gboolean descriptor_get_uuid(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| char uuid[MAX_LEN_UUID_STR + 1]; |
| const char *ptr = uuid; |
| struct descriptor *desc = data; |
| |
| bt_uuid_to_string(&desc->uuid, uuid, sizeof(uuid)); |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &ptr); |
| |
| return TRUE; |
| } |
| |
| static gboolean descriptor_get_characteristic( |
| const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct descriptor *desc = data; |
| const char *str = desc->chrc->path; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, &str); |
| |
| return TRUE; |
| } |
| |
| static void read_cb(struct gatt_db_attribute *attrib, int err, |
| const uint8_t *value, size_t length, |
| void *user_data) |
| { |
| DBusMessageIter *array = user_data; |
| |
| if (err) |
| return; |
| |
| dbus_message_iter_append_fixed_array(array, DBUS_TYPE_BYTE, &value, |
| length); |
| } |
| |
| static gboolean descriptor_get_value(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct descriptor *desc = data; |
| DBusMessageIter array; |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "y", &array); |
| |
| gatt_db_attribute_read(desc->attr, 0, 0, NULL, read_cb, &array); |
| |
| dbus_message_iter_close_container(iter, &array); |
| |
| return TRUE; |
| } |
| |
| static void read_check_cb(struct gatt_db_attribute *attrib, int err, |
| const uint8_t *value, size_t length, |
| void *user_data) |
| { |
| gboolean *ret = user_data; |
| |
| if (err) { |
| *ret = FALSE; |
| return; |
| } |
| |
| *ret = TRUE; |
| } |
| |
| static gboolean descriptor_value_exists(const GDBusPropertyTable *property, |
| void *data) |
| { |
| struct descriptor *desc = data; |
| gboolean ret; |
| |
| gatt_db_attribute_read(desc->attr, 0, 0, NULL, read_check_cb, &ret); |
| |
| return ret; |
| } |
| |
| static int parse_value_arg(DBusMessageIter *iter, uint8_t **value, int *len) |
| { |
| DBusMessageIter array; |
| |
| if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_ARRAY) |
| return -EINVAL; |
| |
| dbus_message_iter_recurse(iter, &array); |
| dbus_message_iter_get_fixed_array(&array, value, len); |
| |
| return 0; |
| } |
| |
| static void async_dbus_op_free(void *data) |
| { |
| struct async_dbus_op *op = data; |
| |
| queue_destroy(op->msgs, (void *)dbus_message_unref); |
| |
| free(op); |
| } |
| |
| static struct async_dbus_op *async_dbus_op_ref(struct async_dbus_op *op) |
| { |
| __sync_fetch_and_add(&op->ref_count, 1); |
| |
| return op; |
| } |
| |
| static void async_dbus_op_unref(void *data) |
| { |
| struct async_dbus_op *op = data; |
| |
| if (__sync_sub_and_fetch(&op->ref_count, 1)) |
| return; |
| |
| async_dbus_op_free(op); |
| } |
| |
| static void message_append_byte_array(DBusMessage *msg, const uint8_t *bytes, |
| size_t len) |
| { |
| DBusMessageIter iter, array; |
| |
| dbus_message_iter_init_append(msg, &iter); |
| dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "y", &array); |
| dbus_message_iter_append_fixed_array(&array, DBUS_TYPE_BYTE, &bytes, |
| len); |
| dbus_message_iter_close_container(&iter, &array); |
| } |
| |
| static DBusMessage *create_gatt_dbus_error(DBusMessage *msg, uint8_t att_ecode) |
| { |
| switch (att_ecode) { |
| case BT_ATT_ERROR_READ_NOT_PERMITTED: |
| return btd_error_not_permitted(msg, "Read not permitted"); |
| case BT_ATT_ERROR_WRITE_NOT_PERMITTED: |
| return btd_error_not_permitted(msg, "Write not permitted"); |
| case BT_ATT_ERROR_AUTHENTICATION: |
| case BT_ATT_ERROR_INSUFFICIENT_ENCRYPTION: |
| case BT_ATT_ERROR_INSUFFICIENT_ENCRYPTION_KEY_SIZE: |
| return btd_error_not_permitted(msg, "Not paired"); |
| case BT_ATT_ERROR_INVALID_OFFSET: |
| return btd_error_invalid_args_str(msg, "Invalid offset"); |
| case BT_ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LEN: |
| return btd_error_invalid_args_str(msg, "Invalid Length"); |
| case BT_ATT_ERROR_AUTHORIZATION: |
| return btd_error_not_authorized(msg); |
| case BT_ATT_ERROR_REQUEST_NOT_SUPPORTED: |
| return btd_error_not_supported(msg); |
| case 0: |
| return btd_error_failed(msg, "Operation failed"); |
| default: |
| return g_dbus_create_error(msg, ERROR_INTERFACE ".Failed", |
| "Operation failed with ATT error: 0x%02x", |
| att_ecode); |
| } |
| |
| return NULL; |
| } |
| |
| static void write_descriptor_cb(struct gatt_db_attribute *attr, int err, |
| void *user_data) |
| { |
| struct descriptor *desc = user_data; |
| |
| if (err) |
| return; |
| |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), desc->path, |
| GATT_DESCRIPTOR_IFACE, "Value"); |
| } |
| |
| static void async_dbus_op_reply(struct async_dbus_op *op, int err, |
| const uint8_t *value, ssize_t length) |
| { |
| const struct queue_entry *entry; |
| DBusMessage *reply; |
| |
| op->id = 0; |
| |
| for (entry = queue_get_entries(op->msgs); entry; entry = entry->next) { |
| DBusMessage *msg = entry->data; |
| |
| if (err) { |
| reply = err > 0 ? create_gatt_dbus_error(msg, err) : |
| btd_error_failed(msg, strerror(-err)); |
| goto send_reply; |
| } |
| |
| reply = g_dbus_create_reply(msg, DBUS_TYPE_INVALID); |
| if (!reply) { |
| error("Failed to allocate D-Bus message reply"); |
| return; |
| } |
| |
| if (length >= 0) |
| message_append_byte_array(reply, value, length); |
| |
| send_reply: |
| g_dbus_send_message(btd_get_dbus_connection(), reply); |
| } |
| } |
| |
| static void read_op_cb(struct gatt_db_attribute *attrib, int err, |
| const uint8_t *value, size_t length, |
| void *user_data) |
| { |
| struct async_dbus_op *op = user_data; |
| |
| async_dbus_op_reply(op, err, value, length); |
| } |
| |
| static void desc_read_cb(bool success, uint8_t att_ecode, |
| const uint8_t *value, uint16_t length, |
| void *user_data) |
| { |
| struct async_dbus_op *op = user_data; |
| struct descriptor *desc = op->data; |
| |
| if (!success) |
| goto fail; |
| |
| if (!op->offset) |
| gatt_db_attribute_reset(desc->attr); |
| |
| if (!gatt_db_attribute_write(desc->attr, op->offset, value, length, 0, |
| NULL, write_descriptor_cb, desc)) { |
| error("Failed to store attribute"); |
| att_ecode = BT_ATT_ERROR_UNLIKELY; |
| goto fail; |
| } |
| |
| /* Read the stored data from db */ |
| if (!gatt_db_attribute_read(desc->attr, 0, 0, NULL, read_op_cb, op)) { |
| error("Failed to read database"); |
| att_ecode = BT_ATT_ERROR_UNLIKELY; |
| goto fail; |
| } |
| |
| desc->read_op = NULL; |
| |
| return; |
| |
| fail: |
| async_dbus_op_reply(op, att_ecode, NULL, 0); |
| desc->read_op = NULL; |
| } |
| |
| static int parse_options(DBusMessageIter *iter, uint16_t *offset) |
| { |
| DBusMessageIter dict; |
| |
| if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_ARRAY) |
| return -EINVAL; |
| |
| dbus_message_iter_recurse(iter, &dict); |
| |
| while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) { |
| const char *key; |
| DBusMessageIter value, entry; |
| int var; |
| |
| dbus_message_iter_recurse(&dict, &entry); |
| dbus_message_iter_get_basic(&entry, &key); |
| |
| dbus_message_iter_next(&entry); |
| dbus_message_iter_recurse(&entry, &value); |
| |
| var = dbus_message_iter_get_arg_type(&value); |
| if (strcasecmp(key, "offset") == 0) { |
| if (var != DBUS_TYPE_UINT16) |
| return -EINVAL; |
| dbus_message_iter_get_basic(&value, offset); |
| } |
| |
| dbus_message_iter_next(&dict); |
| } |
| |
| return 0; |
| } |
| |
| static struct async_dbus_op *async_dbus_op_new(DBusMessage *msg, void *data) |
| { |
| struct async_dbus_op *op; |
| |
| op = new0(struct async_dbus_op, 1); |
| op->msgs = queue_new(); |
| queue_push_tail(op->msgs, dbus_message_ref(msg)); |
| op->data = data; |
| |
| return op; |
| } |
| |
| static struct async_dbus_op *read_value(struct bt_gatt_client *gatt, |
| DBusMessage *msg, uint16_t handle, |
| uint16_t offset, |
| bt_gatt_client_read_callback_t callback, |
| void *data) |
| { |
| struct async_dbus_op *op; |
| |
| op = async_dbus_op_new(msg, data); |
| op->offset = offset; |
| |
| op->id = bt_gatt_client_read_long_value(gatt, handle, offset, callback, |
| async_dbus_op_ref(op), |
| async_dbus_op_unref); |
| if (op->id) |
| return op; |
| |
| async_dbus_op_free(op); |
| |
| return NULL; |
| } |
| |
| static DBusMessage *descriptor_read_value(DBusConnection *conn, |
| DBusMessage *msg, void *user_data) |
| { |
| struct descriptor *desc = user_data; |
| struct bt_gatt_client *gatt = desc->chrc->service->client->gatt; |
| DBusMessageIter iter; |
| uint16_t offset = 0; |
| |
| if (!gatt) |
| return btd_error_failed(msg, "Not connected"); |
| |
| dbus_message_iter_init(msg, &iter); |
| |
| if (parse_options(&iter, &offset)) |
| return btd_error_invalid_args(msg); |
| |
| if (desc->read_op) { |
| if (desc->read_op->offset != offset) |
| return btd_error_in_progress(msg); |
| queue_push_tail(desc->read_op->msgs, dbus_message_ref(msg)); |
| return NULL; |
| } |
| |
| desc->read_op = read_value(gatt, msg, desc->handle, offset, |
| desc_read_cb, desc); |
| if (!desc->read_op) |
| return btd_error_failed(msg, "Failed to send read request"); |
| |
| return NULL; |
| } |
| |
| static void write_result_cb(bool success, bool reliable_error, |
| uint8_t att_ecode, void *user_data) |
| { |
| struct async_dbus_op *op = user_data; |
| int err = 0; |
| |
| if (op->complete && !op->complete(op->data)) { |
| err = -EFAULT; |
| goto done; |
| } |
| |
| if (!success) { |
| if (reliable_error) |
| err = -EFAULT; |
| else |
| err = att_ecode; |
| } |
| |
| done: |
| async_dbus_op_reply(op, err, NULL, -1); |
| } |
| |
| static void write_cb(bool success, uint8_t att_ecode, void *user_data) |
| { |
| write_result_cb(success, false, att_ecode, user_data); |
| } |
| |
| static struct async_dbus_op *start_long_write(DBusMessage *msg, uint16_t handle, |
| struct bt_gatt_client *gatt, |
| bool reliable, const uint8_t *value, |
| size_t value_len, uint16_t offset, |
| void *data, |
| async_dbus_op_complete_t complete) |
| { |
| struct async_dbus_op *op; |
| |
| op = async_dbus_op_new(msg, data); |
| op->complete = complete; |
| op->offset = offset; |
| |
| op->id = bt_gatt_client_write_long_value(gatt, reliable, handle, offset, |
| value, value_len, |
| write_result_cb, op, |
| async_dbus_op_free); |
| |
| if (!op->id) { |
| async_dbus_op_free(op); |
| return NULL; |
| } |
| |
| return op; |
| } |
| |
| static struct async_dbus_op *start_write_request(DBusMessage *msg, |
| uint16_t handle, |
| struct bt_gatt_client *gatt, |
| const uint8_t *value, size_t value_len, |
| void *data, |
| async_dbus_op_complete_t complete) |
| { |
| struct async_dbus_op *op; |
| |
| op = async_dbus_op_new(msg, data); |
| op->complete = complete; |
| |
| op->id = bt_gatt_client_write_value(gatt, handle, value, value_len, |
| write_cb, op, |
| async_dbus_op_free); |
| if (!op->id) { |
| async_dbus_op_free(op); |
| return NULL; |
| } |
| |
| return op; |
| } |
| |
| static bool desc_write_complete(void *data) |
| { |
| struct descriptor *desc = data; |
| |
| desc->write_op = NULL; |
| |
| /* |
| * The descriptor might have been unregistered during the read. Return |
| * failure. |
| */ |
| return !!desc->chrc; |
| } |
| |
| static DBusMessage *descriptor_write_value(DBusConnection *conn, |
| DBusMessage *msg, void *user_data) |
| { |
| struct descriptor *desc = user_data; |
| struct bt_gatt_client *gatt = desc->chrc->service->client->gatt; |
| DBusMessageIter iter; |
| uint8_t *value = NULL; |
| int value_len = 0; |
| uint16_t offset = 0; |
| |
| if (!gatt) |
| return btd_error_failed(msg, "Not connected"); |
| |
| if (desc->write_op) |
| return btd_error_in_progress(msg); |
| |
| dbus_message_iter_init(msg, &iter); |
| |
| if (parse_value_arg(&iter, &value, &value_len)) |
| return btd_error_invalid_args(msg); |
| |
| dbus_message_iter_next(&iter); |
| |
| if (parse_options(&iter, &offset)) |
| return btd_error_invalid_args(msg); |
| |
| /* |
| * Don't allow writing to Client Characteristic Configuration |
| * descriptors. We achieve this through the StartNotify and StopNotify |
| * methods on GattCharacteristic1. |
| */ |
| if (uuid_cmp(&desc->uuid, GATT_CLIENT_CHARAC_CFG_UUID)) |
| return btd_error_not_permitted(msg, "Write not permitted"); |
| |
| /* |
| * Based on the value length and the MTU, either use a write or a long |
| * write. |
| */ |
| if (value_len <= bt_gatt_client_get_mtu(gatt) - 3 && !offset) |
| desc->write_op = start_write_request(msg, desc->handle, |
| gatt, value, |
| value_len, desc, |
| desc_write_complete); |
| else |
| desc->write_op = start_long_write(msg, desc->handle, gatt, |
| false, value, |
| value_len, offset, desc, |
| desc_write_complete); |
| |
| if (!desc->write_op) |
| return btd_error_failed(msg, "Failed to initiate write"); |
| |
| return NULL; |
| } |
| |
| static const GDBusPropertyTable descriptor_properties[] = { |
| { "UUID", "s", descriptor_get_uuid }, |
| { "Characteristic", "o", descriptor_get_characteristic, }, |
| { "Value", "ay", descriptor_get_value, NULL, descriptor_value_exists }, |
| { } |
| }; |
| |
| static const GDBusMethodTable descriptor_methods[] = { |
| { GDBUS_ASYNC_METHOD("ReadValue", GDBUS_ARGS({ "options", "a{sv}" }), |
| GDBUS_ARGS({ "value", "ay" }), |
| descriptor_read_value) }, |
| { GDBUS_ASYNC_METHOD("WriteValue", GDBUS_ARGS({ "value", "ay" }, |
| { "options", "a{sv}" }), |
| NULL, |
| descriptor_write_value) }, |
| { } |
| }; |
| |
| static void descriptor_free(void *data) |
| { |
| struct descriptor *desc = data; |
| |
| g_free(desc->path); |
| free(desc); |
| } |
| |
| static struct descriptor *descriptor_create(struct gatt_db_attribute *attr, |
| struct characteristic *chrc) |
| { |
| struct descriptor *desc; |
| |
| desc = new0(struct descriptor, 1); |
| desc->chrc = chrc; |
| desc->attr = attr; |
| desc->handle = gatt_db_attribute_get_handle(attr); |
| |
| bt_uuid_to_uuid128(gatt_db_attribute_get_type(attr), &desc->uuid); |
| |
| desc->path = g_strdup_printf("%s/desc%04x", chrc->path, desc->handle); |
| |
| if (!g_dbus_register_interface(btd_get_dbus_connection(), desc->path, |
| GATT_DESCRIPTOR_IFACE, |
| descriptor_methods, NULL, |
| descriptor_properties, |
| desc, descriptor_free)) { |
| error("Unable to register GATT descriptor with handle 0x%04x", |
| desc->handle); |
| descriptor_free(desc); |
| |
| return NULL; |
| } |
| |
| DBG("Exported GATT characteristic descriptor: %s", desc->path); |
| |
| if (uuid_cmp(&desc->uuid, GATT_CHARAC_EXT_PROPER_UUID)) |
| chrc->ext_props_handle = desc->handle; |
| |
| return desc; |
| } |
| |
| static void unregister_descriptor(void *data) |
| { |
| struct descriptor *desc = data; |
| struct bt_gatt_client *gatt = desc->chrc->service->client->gatt; |
| |
| DBG("Removing GATT descriptor: %s", desc->path); |
| |
| if (desc->read_op) |
| bt_gatt_client_cancel(gatt, desc->read_op->id); |
| |
| if (desc->write_op) |
| bt_gatt_client_cancel(gatt, desc->write_op->id); |
| |
| desc->chrc = NULL; |
| |
| g_dbus_unregister_interface(btd_get_dbus_connection(), desc->path, |
| GATT_DESCRIPTOR_IFACE); |
| } |
| |
| static gboolean characteristic_get_uuid(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| char uuid[MAX_LEN_UUID_STR + 1]; |
| const char *ptr = uuid; |
| struct characteristic *chrc = data; |
| |
| bt_uuid_to_string(&chrc->uuid, uuid, sizeof(uuid)); |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &ptr); |
| |
| return TRUE; |
| } |
| |
| static gboolean characteristic_get_service(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct characteristic *chrc = data; |
| const char *str = chrc->service->path; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, &str); |
| |
| return TRUE; |
| } |
| |
| static gboolean characteristic_get_value(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct characteristic *chrc = data; |
| DBusMessageIter array; |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "y", &array); |
| |
| gatt_db_attribute_read(chrc->attr, 0, 0, NULL, read_cb, &array); |
| |
| dbus_message_iter_close_container(iter, &array); |
| |
| return TRUE; |
| } |
| |
| static gboolean characteristic_value_exists(const GDBusPropertyTable *property, |
| void *data) |
| { |
| struct characteristic *chrc = data; |
| gboolean ret; |
| |
| gatt_db_attribute_read(chrc->attr, 0, 0, NULL, read_check_cb, &ret); |
| |
| return ret; |
| } |
| |
| static gboolean characteristic_get_notifying(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct characteristic *chrc = data; |
| dbus_bool_t notifying = chrc->notifying ? TRUE : FALSE; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, ¬ifying); |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| characteristic_notifying_exists(const GDBusPropertyTable *property, void *data) |
| { |
| struct characteristic *chrc = data; |
| |
| return (chrc->props & BT_GATT_CHRC_PROP_NOTIFY || |
| chrc->props & BT_GATT_CHRC_PROP_INDICATE); |
| } |
| |
| struct chrc_prop_data { |
| uint8_t prop; |
| char *str; |
| }; |
| |
| static struct chrc_prop_data chrc_props[] = { |
| /* Default Properties */ |
| { BT_GATT_CHRC_PROP_BROADCAST, "broadcast" }, |
| { BT_GATT_CHRC_PROP_READ, "read" }, |
| { BT_GATT_CHRC_PROP_WRITE_WITHOUT_RESP, "write-without-response" }, |
| { BT_GATT_CHRC_PROP_WRITE, "write" }, |
| { BT_GATT_CHRC_PROP_NOTIFY, "notify" }, |
| { BT_GATT_CHRC_PROP_INDICATE, "indicate" }, |
| { BT_GATT_CHRC_PROP_AUTH, "authenticated-signed-writes" }, |
| { BT_GATT_CHRC_PROP_EXT_PROP, "extended-properties" } |
| }; |
| |
| static struct chrc_prop_data chrc_ext_props[] = { |
| /* Extended Properties */ |
| { BT_GATT_CHRC_EXT_PROP_RELIABLE_WRITE, "reliable-write" }, |
| { BT_GATT_CHRC_EXT_PROP_WRITABLE_AUX, "writable-auxiliaries" } |
| }; |
| |
| static gboolean characteristic_get_flags(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct characteristic *chrc = data; |
| DBusMessageIter array; |
| unsigned i; |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "s", &array); |
| |
| for (i = 0; i < NELEM(chrc_props); i++) { |
| if (chrc->props & chrc_props[i].prop) |
| dbus_message_iter_append_basic(&array, DBUS_TYPE_STRING, |
| &chrc_props[i].str); |
| } |
| |
| for (i = 0; i < NELEM(chrc_ext_props); i++) { |
| if (chrc->ext_props & chrc_ext_props[i].prop) |
| dbus_message_iter_append_basic(&array, DBUS_TYPE_STRING, |
| &chrc_ext_props[i].str); |
| } |
| |
| dbus_message_iter_close_container(iter, &array); |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| characteristic_get_write_acquired(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct characteristic *chrc = data; |
| dbus_bool_t locked = chrc->write_io ? TRUE : FALSE; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, &locked); |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| characteristic_write_acquired_exists(const GDBusPropertyTable *property, |
| void *data) |
| { |
| struct characteristic *chrc = data; |
| |
| return (chrc->props & BT_GATT_CHRC_PROP_WRITE_WITHOUT_RESP); |
| } |
| |
| static gboolean |
| characteristic_get_notify_acquired(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct characteristic *chrc = data; |
| dbus_bool_t locked = chrc->notify_io ? TRUE : FALSE; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, &locked); |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| characteristic_notify_acquired_exists(const GDBusPropertyTable *property, |
| void *data) |
| { |
| struct characteristic *chrc = data; |
| |
| return (chrc->props & BT_GATT_CHRC_PROP_NOTIFY); |
| } |
| |
| static void write_characteristic_cb(struct gatt_db_attribute *attr, int err, |
| void *user_data) |
| { |
| struct characteristic *chrc = user_data; |
| |
| if (err) |
| return; |
| |
| g_dbus_emit_property_changed_full(btd_get_dbus_connection(), |
| chrc->path, GATT_CHARACTERISTIC_IFACE, |
| "Value", G_DBUS_PROPERTY_CHANGED_FLAG_FLUSH); |
| |
| } |
| |
| static void chrc_read_cb(bool success, uint8_t att_ecode, const uint8_t *value, |
| uint16_t length, void *user_data) |
| { |
| struct async_dbus_op *op = user_data; |
| struct characteristic *chrc = op->data; |
| |
| if (!success) |
| goto fail; |
| |
| if (!op->offset) |
| gatt_db_attribute_reset(chrc->attr); |
| |
| if (!gatt_db_attribute_write(chrc->attr, op->offset, value, length, 0, |
| NULL, write_characteristic_cb, chrc)) { |
| error("Failed to store attribute"); |
| att_ecode = BT_ATT_ERROR_UNLIKELY; |
| goto fail; |
| } |
| |
| /* Read the stored data from db */ |
| if (!gatt_db_attribute_read(chrc->attr, 0, 0, NULL, read_op_cb, op)) { |
| error("Failed to read database"); |
| att_ecode = BT_ATT_ERROR_UNLIKELY; |
| goto fail; |
| } |
| |
| chrc->read_op = NULL; |
| |
| return; |
| |
| fail: |
| async_dbus_op_reply(op, att_ecode, NULL, 0); |
| chrc->read_op = NULL; |
| } |
| |
| static DBusMessage *characteristic_read_value(DBusConnection *conn, |
| DBusMessage *msg, void *user_data) |
| { |
| struct characteristic *chrc = user_data; |
| struct bt_gatt_client *gatt = chrc->service->client->gatt; |
| DBusMessageIter iter; |
| uint16_t offset = 0; |
| |
| if (!gatt) |
| return btd_error_failed(msg, "Not connected"); |
| |
| dbus_message_iter_init(msg, &iter); |
| |
| if (parse_options(&iter, &offset)) |
| return btd_error_invalid_args(msg); |
| |
| if (chrc->read_op) { |
| if (chrc->read_op->offset != offset) |
| return btd_error_in_progress(msg); |
| queue_push_tail(chrc->read_op->msgs, dbus_message_ref(msg)); |
| return NULL; |
| } |
| |
| chrc->read_op = read_value(gatt, msg, chrc->value_handle, offset, |
| chrc_read_cb, chrc); |
| if (!chrc->read_op) |
| return btd_error_failed(msg, "Failed to send read request"); |
| |
| return NULL; |
| } |
| |
| static bool chrc_write_complete(void *data) |
| { |
| struct characteristic *chrc = data; |
| |
| chrc->write_op = NULL; |
| |
| /* |
| * The characteristic might have been unregistered during the read. |
| * Return failure. |
| */ |
| return !!chrc->service; |
| } |
| |
| static DBusMessage *characteristic_write_value(DBusConnection *conn, |
| DBusMessage *msg, void *user_data) |
| { |
| struct characteristic *chrc = user_data; |
| struct bt_gatt_client *gatt = chrc->service->client->gatt; |
| DBusMessageIter iter; |
| uint8_t *value = NULL; |
| int value_len = 0; |
| bool supported = false; |
| uint16_t offset = 0; |
| |
| if (!gatt) |
| return btd_error_failed(msg, "Not connected"); |
| |
| if (chrc->write_io) |
| return btd_error_not_permitted(msg, "Write acquired"); |
| |
| if (chrc->write_op) |
| return btd_error_in_progress(msg); |
| |
| dbus_message_iter_init(msg, &iter); |
| |
| if (parse_value_arg(&iter, &value, &value_len)) |
| return btd_error_invalid_args(msg); |
| |
| dbus_message_iter_next(&iter); |
| |
| if (parse_options(&iter, &offset)) |
| return btd_error_invalid_args(msg); |
| |
| /* |
| * Decide which write to use based on characteristic properties. For now |
| * we don't perform signed writes since gatt-client doesn't support them |
| * and the user can always encrypt the through pairing. The procedure to |
| * use is determined based on the following priority: |
| * |
| * * "reliable-write" property set -> reliable long-write. |
| * * "write" property set -> write request. |
| * - If value is larger than MTU - 3: long-write |
| * * "write-without-response" property set -> write command. |
| */ |
| if ((chrc->ext_props & BT_GATT_CHRC_EXT_PROP_RELIABLE_WRITE)) { |
| supported = true; |
| chrc->write_op = start_long_write(msg, chrc->value_handle, gatt, |
| true, value, value_len, offset, |
| chrc, chrc_write_complete); |
| if (chrc->write_op) |
| return NULL; |
| } |
| |
| if (chrc->props & BT_GATT_CHRC_PROP_WRITE) { |
| uint16_t mtu; |
| |
| supported = true; |
| mtu = bt_gatt_client_get_mtu(gatt); |
| if (!mtu) |
| return btd_error_failed(msg, "No ATT transport"); |
| |
| if (value_len <= mtu - 3 && !offset) |
| chrc->write_op = start_write_request(msg, |
| chrc->value_handle, |
| gatt, value, value_len, |
| chrc, chrc_write_complete); |
| else |
| chrc->write_op = start_long_write(msg, |
| chrc->value_handle, gatt, |
| false, value, value_len, offset, |
| chrc, chrc_write_complete); |
| |
| if (chrc->write_op) |
| return NULL; |
| } |
| |
| if (!(chrc->props & BT_GATT_CHRC_PROP_WRITE_WITHOUT_RESP)) |
| goto fail; |
| |
| supported = true; |
| |
| if (bt_gatt_client_write_without_response(gatt, |
| chrc->value_handle, |
| chrc->props & BT_GATT_CHRC_PROP_AUTH, |
| value, value_len)) |
| return dbus_message_new_method_return(msg); |
| |
| fail: |
| if (supported) |
| return btd_error_failed(msg, "Failed to initiate write"); |
| |
| return btd_error_not_supported(msg); |
| } |
| |
| static bool chrc_pipe_read(struct io *io, void *user_data) |
| { |
| struct characteristic *chrc = user_data; |
| struct bt_gatt_client *gatt = chrc->service->client->gatt; |
| uint8_t buf[512]; |
| int fd = io_get_fd(io); |
| ssize_t bytes_read; |
| |
| bytes_read = read(fd, buf, sizeof(buf)); |
| if (bytes_read < 0) |
| return false; |
| |
| if (!gatt) |
| return false; |
| |
| bt_gatt_client_write_without_response(gatt, chrc->value_handle, |
| chrc->props & BT_GATT_CHRC_PROP_AUTH, |
| buf, bytes_read); |
| |
| return true; |
| } |
| |
| static void pipe_io_destroy(struct pipe_io *io) |
| { |
| if (io->destroy) |
| io->destroy(io->data); |
| |
| if (io->msg) |
| dbus_message_unref(io->msg); |
| |
| io_destroy(io->io); |
| free(io); |
| } |
| |
| static void characteristic_destroy_pipe(struct characteristic *chrc, |
| struct io *io) |
| { |
| queue_remove(chrc->service->client->ios, io); |
| |
| if (chrc->write_io && io == chrc->write_io->io) { |
| pipe_io_destroy(chrc->write_io); |
| chrc->write_io = NULL; |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), |
| chrc->path, |
| GATT_CHARACTERISTIC_IFACE, |
| "WriteAcquired"); |
| } else if (chrc->notify_io) { |
| pipe_io_destroy(chrc->notify_io); |
| chrc->notify_io = NULL; |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), |
| chrc->path, |
| GATT_CHARACTERISTIC_IFACE, |
| "NotifyAcquired"); |
| } |
| } |
| |
| static bool characteristic_pipe_hup(struct io *io, void *user_data) |
| { |
| struct characteristic *chrc = user_data; |
| |
| DBG("%s: io %p", chrc->path, io); |
| |
| characteristic_destroy_pipe(chrc, io); |
| |
| return false; |
| } |
| |
| static DBusMessage *characteristic_create_pipe(struct characteristic *chrc, |
| DBusMessage *msg) |
| { |
| struct bt_gatt_client *gatt = chrc->service->client->gatt; |
| int pipefd[2]; |
| struct io *io; |
| bool dir; |
| uint16_t mtu; |
| DBusMessage *reply; |
| |
| if (!gatt || !bt_gatt_client_is_ready(gatt)) |
| return btd_error_failed(msg, "Not connected"); |
| |
| if (pipe2(pipefd, O_DIRECT | O_NONBLOCK | O_CLOEXEC) < 0) |
| return btd_error_failed(msg, strerror(errno)); |
| |
| dir = dbus_message_has_member(msg, "AcquireWrite"); |
| |
| io = io_new(pipefd[!dir]); |
| if (!io) { |
| close(pipefd[0]); |
| close(pipefd[1]); |
| return btd_error_failed(msg, strerror(EIO)); |
| } |
| |
| io_set_close_on_destroy(io, true); |
| |
| if (!io_set_read_handler(io, chrc_pipe_read, chrc, NULL)) |
| goto fail; |
| |
| if (!io_set_disconnect_handler(io, characteristic_pipe_hup, chrc, NULL)) |
| goto fail; |
| |
| mtu = bt_gatt_client_get_mtu(gatt); |
| |
| reply = g_dbus_create_reply(msg, DBUS_TYPE_UNIX_FD, &pipefd[dir], |
| DBUS_TYPE_UINT16, &mtu, |
| DBUS_TYPE_INVALID); |
| |
| close(pipefd[dir]); |
| |
| if (dir) { |
| chrc->write_io->io = io; |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), |
| chrc->path, |
| GATT_CHARACTERISTIC_IFACE, |
| "WriteAcquired"); |
| } else { |
| chrc->notify_io->io = io; |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), |
| chrc->path, |
| GATT_CHARACTERISTIC_IFACE, |
| "NotifyAcquired"); |
| } |
| |
| queue_push_tail(chrc->service->client->ios, io); |
| |
| DBG("%s: sender %s io %p", dbus_message_get_member(msg), |
| dbus_message_get_sender(msg), io); |
| |
| return reply; |
| |
| fail: |
| io_destroy(io); |
| close(pipefd[dir]); |
| return btd_error_failed(msg, strerror(EIO)); |
| } |
| |
| static void characteristic_ready(bool success, uint8_t ecode, void *user_data) |
| { |
| struct characteristic *chrc = user_data; |
| DBusMessage *reply; |
| |
| chrc->ready_id = 0; |
| |
| if (chrc->write_io && chrc->write_io->msg) { |
| reply = characteristic_create_pipe(chrc, chrc->write_io->msg); |
| |
| g_dbus_send_message(btd_get_dbus_connection(), reply); |
| |
| dbus_message_unref(chrc->write_io->msg); |
| chrc->write_io->msg = NULL; |
| } |
| |
| if (chrc->notify_io && chrc->notify_io->msg) { |
| reply = characteristic_create_pipe(chrc, chrc->notify_io->msg); |
| |
| g_dbus_send_message(btd_get_dbus_connection(), reply); |
| |
| dbus_message_unref(chrc->notify_io->msg); |
| chrc->notify_io->msg = NULL; |
| } |
| } |
| |
| static DBusMessage *characteristic_acquire_write(DBusConnection *conn, |
| DBusMessage *msg, void *user_data) |
| { |
| struct characteristic *chrc = user_data; |
| struct bt_gatt_client *gatt = chrc->service->client->gatt; |
| |
| if (!gatt) |
| return btd_error_failed(msg, "Not connected"); |
| |
| if (chrc->write_io) |
| return btd_error_not_permitted(msg, "Write acquired"); |
| |
| if (!(chrc->props & BT_GATT_CHRC_PROP_WRITE_WITHOUT_RESP)) |
| return btd_error_not_supported(msg); |
| |
| chrc->write_io = new0(struct pipe_io, 1); |
| |
| if (!bt_gatt_client_is_ready(gatt)) { |
| /* GATT not ready, wait until it becomes ready */ |
| if (!chrc->ready_id) |
| chrc->ready_id = bt_gatt_client_ready_register(gatt, |
| characteristic_ready, |
| chrc, NULL); |
| chrc->write_io->msg = dbus_message_ref(msg); |
| return NULL; |
| } |
| |
| return characteristic_create_pipe(chrc, msg); |
| } |
| |
| struct notify_client { |
| struct characteristic *chrc; |
| int ref_count; |
| char *owner; |
| guint watch; |
| unsigned int notify_id; |
| }; |
| |
| static void notify_client_free(struct notify_client *client) |
| { |
| DBG("owner %s", client->owner); |
| |
| g_dbus_remove_watch(btd_get_dbus_connection(), client->watch); |
| bt_gatt_client_unregister_notify(client->chrc->service->client->gatt, |
| client->notify_id); |
| free(client->owner); |
| free(client); |
| } |
| |
| static void notify_client_unref(void *data) |
| { |
| struct notify_client *client = data; |
| |
| DBG("owner %s", client->owner); |
| |
| if (__sync_sub_and_fetch(&client->ref_count, 1)) |
| return; |
| |
| notify_client_free(client); |
| } |
| |
| static struct notify_client *notify_client_ref(struct notify_client *client) |
| { |
| DBG("owner %s", client->owner); |
| |
| __sync_fetch_and_add(&client->ref_count, 1); |
| |
| return client; |
| } |
| |
| static bool match_notifying(const void *a, const void *b) |
| { |
| const struct notify_client *client = a; |
| |
| return !!client->notify_id; |
| } |
| |
| static void update_notifying(struct characteristic *chrc) |
| { |
| if (!chrc->notifying) |
| return; |
| |
| if (queue_find(chrc->notify_clients, match_notifying, NULL)) |
| return; |
| |
| chrc->notifying = false; |
| |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), chrc->path, |
| GATT_CHARACTERISTIC_IFACE, |
| "Notifying"); |
| } |
| |
| static void notify_client_disconnect(DBusConnection *conn, void *user_data) |
| { |
| struct notify_client *client = user_data; |
| struct characteristic *chrc = client->chrc; |
| |
| DBG("owner %s", client->owner); |
| |
| queue_remove(chrc->notify_clients, client); |
| queue_remove(chrc->service->client->all_notify_clients, client); |
| |
| update_notifying(chrc); |
| |
| notify_client_unref(client); |
| } |
| |
| static struct notify_client *notify_client_create(struct characteristic *chrc, |
| const char *owner) |
| { |
| struct notify_client *client; |
| |
| client = new0(struct notify_client, 1); |
| client->chrc = chrc; |
| client->owner = strdup(owner); |
| if (!client->owner) { |
| free(client); |
| return NULL; |
| } |
| |
| client->watch = g_dbus_add_disconnect_watch(btd_get_dbus_connection(), |
| owner, notify_client_disconnect, |
| client, NULL); |
| if (!client->watch) { |
| free(client->owner); |
| free(client); |
| return NULL; |
| } |
| |
| return notify_client_ref(client); |
| } |
| |
| static bool match_notify_sender(const void *a, const void *b) |
| { |
| const struct notify_client *client = a; |
| const char *sender = b; |
| |
| return strcmp(client->owner, sender) == 0; |
| } |
| |
| static void notify_cb(uint16_t value_handle, const uint8_t *value, |
| uint16_t length, void *user_data) |
| { |
| struct async_dbus_op *op = user_data; |
| struct notify_client *client = op->data; |
| struct characteristic *chrc = client->chrc; |
| |
| /* |
| * Even if the value didn't change, we want to send a PropertiesChanged |
| * signal so that we propagate the notification/indication to |
| * applications. |
| */ |
| gatt_db_attribute_reset(chrc->attr); |
| gatt_db_attribute_write(chrc->attr, 0, value, length, 0, NULL, |
| write_characteristic_cb, chrc); |
| } |
| |
| static void create_notify_reply(struct async_dbus_op *op, bool success, |
| uint8_t att_ecode) |
| { |
| int err; |
| |
| if (success) |
| err = 0; |
| else |
| err = att_ecode ? att_ecode : -ENOENT; |
| |
| async_dbus_op_reply(op, err, NULL, -1); |
| } |
| |
| static void register_notify_cb(uint16_t att_ecode, void *user_data) |
| { |
| struct async_dbus_op *op = user_data; |
| struct notify_client *client = op->data; |
| struct characteristic *chrc = client->chrc; |
| |
| if (att_ecode) { |
| queue_remove(chrc->notify_clients, client); |
| queue_remove(chrc->service->client->all_notify_clients, client); |
| notify_client_free(client); |
| |
| create_notify_reply(op, false, att_ecode); |
| |
| return; |
| } |
| |
| if (!chrc->notifying) { |
| chrc->notifying = true; |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), |
| chrc->path, GATT_CHARACTERISTIC_IFACE, |
| "Notifying"); |
| } |
| |
| create_notify_reply(op, true, 0); |
| } |
| |
| static void notify_io_cb(uint16_t value_handle, const uint8_t *value, |
| uint16_t length, void *user_data) |
| { |
| struct iovec iov; |
| struct notify_client *client = user_data; |
| struct characteristic *chrc = client->chrc; |
| int err; |
| |
| /* Drop notification if the pipe is not ready */ |
| if (!chrc->notify_io->io) |
| return; |
| |
| iov.iov_base = (void *) value; |
| iov.iov_len = length; |
| |
| err = io_send(chrc->notify_io->io, &iov, 1); |
| if (err < 0) |
| error("io_send: %s", strerror(-err)); |
| } |
| |
| static void register_notify_io_cb(uint16_t att_ecode, void *user_data) |
| { |
| struct notify_client *client = user_data; |
| struct characteristic *chrc = client->chrc; |
| struct bt_gatt_client *gatt = chrc->service->client->gatt; |
| |
| if (att_ecode) { |
| queue_remove(chrc->notify_clients, client); |
| notify_client_free(client); |
| return; |
| } |
| |
| if (!bt_gatt_client_is_ready(gatt)) { |
| if (!chrc->ready_id) |
| chrc->ready_id = bt_gatt_client_ready_register(gatt, |
| characteristic_ready, |
| chrc, NULL); |
| return; |
| } |
| |
| characteristic_ready(true, 0, chrc); |
| } |
| |
| static void notify_io_destroy(void *data) |
| { |
| struct notify_client *client = data; |
| |
| if (queue_remove(client->chrc->notify_clients, client)) |
| notify_client_unref(client); |
| } |
| |
| static DBusMessage *characteristic_acquire_notify(DBusConnection *conn, |
| DBusMessage *msg, void *user_data) |
| { |
| struct characteristic *chrc = user_data; |
| struct bt_gatt_client *gatt = chrc->service->client->gatt; |
| const char *sender = dbus_message_get_sender(msg); |
| struct notify_client *client; |
| |
| if (!gatt) |
| return btd_error_failed(msg, "Not connected"); |
| |
| if (chrc->notify_io) |
| return btd_error_not_permitted(msg, "Notify acquired"); |
| |
| /* Each client can only have one active notify session. */ |
| if (!queue_isempty(chrc->notify_clients)) |
| return btd_error_in_progress(msg); |
| |
| if (!(chrc->props & BT_GATT_CHRC_PROP_NOTIFY)) |
| return btd_error_not_supported(msg); |
| |
| client = notify_client_create(chrc, sender); |
| if (!client) |
| return btd_error_failed(msg, "Failed allocate notify session"); |
| |
| client->notify_id = bt_gatt_client_register_notify(gatt, |
| chrc->value_handle, |
| register_notify_io_cb, |
| notify_io_cb, |
| client, NULL); |
| if (!client->notify_id) { |
| notify_client_unref(client); |
| return btd_error_failed(msg, "Failed to subscribe"); |
| } |
| |
| queue_push_tail(chrc->notify_clients, client); |
| |
| chrc->notify_io = new0(struct pipe_io, 1); |
| chrc->notify_io->data = client; |
| chrc->notify_io->msg = dbus_message_ref(msg); |
| chrc->notify_io->destroy = notify_io_destroy; |
| |
| return NULL; |
| } |
| |
| static DBusMessage *characteristic_start_notify(DBusConnection *conn, |
| DBusMessage *msg, void *user_data) |
| { |
| struct characteristic *chrc = user_data; |
| struct bt_gatt_client *gatt = chrc->service->client->gatt; |
| const char *sender = dbus_message_get_sender(msg); |
| struct async_dbus_op *op; |
| struct notify_client *client; |
| |
| if (chrc->notify_io) |
| return btd_error_not_permitted(msg, "Notify acquired"); |
| |
| if (!(chrc->props & BT_GATT_CHRC_PROP_NOTIFY || |
| chrc->props & BT_GATT_CHRC_PROP_INDICATE)) |
| return btd_error_not_supported(msg); |
| |
| /* Each client can only have one active notify session. */ |
| client = queue_find(chrc->notify_clients, match_notify_sender, sender); |
| if (client) |
| return client->notify_id ? |
| g_dbus_create_reply(msg, DBUS_TYPE_INVALID) : |
| btd_error_in_progress(msg); |
| |
| client = notify_client_create(chrc, sender); |
| if (!client) |
| return btd_error_failed(msg, "Failed allocate notify session"); |
| |
| queue_push_tail(chrc->notify_clients, client); |
| queue_push_tail(chrc->service->client->all_notify_clients, client); |
| |
| /* |
| * If the device is currently not connected, return success. We will |
| * automatically try and register all clients when a GATT client becomes |
| * ready. |
| */ |
| if (!gatt) { |
| DBusMessage *reply; |
| |
| reply = g_dbus_create_reply(msg, DBUS_TYPE_INVALID); |
| if (reply) |
| return reply; |
| |
| /* |
| * Clean up and respond with an error instead of timing out to |
| * avoid any ambiguities. |
| */ |
| error("Failed to construct D-Bus message reply"); |
| goto fail; |
| } |
| |
| op = async_dbus_op_new(msg, client); |
| |
| client->notify_id = bt_gatt_client_register_notify(gatt, |
| chrc->value_handle, |
| register_notify_cb, notify_cb, |
| op, async_dbus_op_free); |
| if (client->notify_id) |
| return NULL; |
| |
| async_dbus_op_free(op); |
| |
| fail: |
| queue_remove(chrc->notify_clients, client); |
| queue_remove(chrc->service->client->all_notify_clients, client); |
| |
| /* Directly free the client */ |
| notify_client_free(client); |
| |
| return btd_error_failed(msg, "Failed to register notify session"); |
| } |
| |
| static DBusMessage *characteristic_stop_notify(DBusConnection *conn, |
| DBusMessage *msg, void *user_data) |
| { |
| struct characteristic *chrc = user_data; |
| struct bt_gatt_client *gatt = chrc->service->client->gatt; |
| const char *sender = dbus_message_get_sender(msg); |
| struct notify_client *client; |
| |
| client = queue_remove_if(chrc->notify_clients, match_notify_sender, |
| (void *) sender); |
| if (!client) |
| return btd_error_failed(msg, "No notify session started"); |
| |
| if (chrc->notify_io) { |
| characteristic_destroy_pipe(chrc, chrc->notify_io->io); |
| return dbus_message_new_method_return(msg); |
| } |
| |
| queue_remove(chrc->service->client->all_notify_clients, client); |
| bt_gatt_client_unregister_notify(gatt, client->notify_id); |
| update_notifying(chrc); |
| |
| notify_client_unref(client); |
| |
| return dbus_message_new_method_return(msg); |
| } |
| |
| static const GDBusPropertyTable characteristic_properties[] = { |
| { "UUID", "s", characteristic_get_uuid, NULL, NULL }, |
| { "Service", "o", characteristic_get_service, NULL, NULL }, |
| { "Value", "ay", characteristic_get_value, NULL, |
| characteristic_value_exists }, |
| { "Notifying", "b", characteristic_get_notifying, NULL, |
| characteristic_notifying_exists }, |
| { "Flags", "as", characteristic_get_flags, NULL, NULL }, |
| { "WriteAcquired", "b", characteristic_get_write_acquired, NULL, |
| characteristic_write_acquired_exists }, |
| { "NotifyAcquired", "b", characteristic_get_notify_acquired, NULL, |
| characteristic_notify_acquired_exists }, |
| { } |
| }; |
| |
| static const GDBusMethodTable characteristic_methods[] = { |
| { GDBUS_ASYNC_METHOD("ReadValue", GDBUS_ARGS({ "options", "a{sv}" }), |
| GDBUS_ARGS({ "value", "ay" }), |
| characteristic_read_value) }, |
| { GDBUS_ASYNC_METHOD("WriteValue", GDBUS_ARGS({ "value", "ay" }, |
| { "options", "a{sv}" }), |
| NULL, |
| characteristic_write_value) }, |
| { GDBUS_ASYNC_METHOD("AcquireWrite", |
| GDBUS_ARGS({ "options", "a{sv}" }), |
| GDBUS_ARGS({ "fd", "h" }, |
| { "mtu", "q" }), |
| characteristic_acquire_write) }, |
| { GDBUS_ASYNC_METHOD("AcquireNotify", |
| GDBUS_ARGS({ "options", "a{sv}" }), |
| GDBUS_ARGS({ "fd", "h" }, |
| { "mtu", "q" }), |
| characteristic_acquire_notify) }, |
| { GDBUS_ASYNC_METHOD("StartNotify", NULL, NULL, |
| characteristic_start_notify) }, |
| { GDBUS_METHOD("StopNotify", NULL, NULL, |
| characteristic_stop_notify) }, |
| { } |
| }; |
| |
| static void characteristic_free(void *data) |
| { |
| struct characteristic *chrc = data; |
| |
| /* List should be empty here */ |
| queue_destroy(chrc->descs, NULL); |
| queue_destroy(chrc->notify_clients, NULL); |
| |
| if (chrc->write_io) { |
| queue_remove(chrc->service->client->ios, chrc->write_io->io); |
| pipe_io_destroy(chrc->write_io); |
| } |
| |
| if (chrc->notify_io) { |
| queue_remove(chrc->service->client->ios, chrc->notify_io->io); |
| pipe_io_destroy(chrc->notify_io); |
| } |
| |
| g_free(chrc->path); |
| free(chrc); |
| } |
| |
| static struct characteristic *characteristic_create( |
| struct gatt_db_attribute *attr, |
| struct service *service) |
| { |
| struct characteristic *chrc; |
| bt_uuid_t uuid; |
| |
| chrc = new0(struct characteristic, 1); |
| chrc->descs = queue_new(); |
| chrc->notify_clients = queue_new(); |
| chrc->service = service; |
| |
| gatt_db_attribute_get_char_data(attr, &chrc->handle, |
| &chrc->value_handle, |
| &chrc->props, |
| &chrc->ext_props, |
| &uuid); |
| |
| chrc->attr = gatt_db_get_attribute(service->client->db, |
| chrc->value_handle); |
| if (!chrc->attr) { |
| error("Attribute 0x%04x not found", chrc->value_handle); |
| characteristic_free(chrc); |
| return NULL; |
| } |
| |
| bt_uuid_to_uuid128(&uuid, &chrc->uuid); |
| |
| chrc->path = g_strdup_printf("%s/char%04x", service->path, |
| chrc->handle); |
| |
| if (!g_dbus_register_interface(btd_get_dbus_connection(), chrc->path, |
| GATT_CHARACTERISTIC_IFACE, |
| characteristic_methods, NULL, |
| characteristic_properties, |
| chrc, characteristic_free)) { |
| error("Unable to register GATT characteristic with handle " |
| "0x%04x", chrc->handle); |
| characteristic_free(chrc); |
| |
| return NULL; |
| } |
| |
| DBG("Exported GATT characteristic: %s", chrc->path); |
| |
| return chrc; |
| } |
| |
| static void remove_client(void *data) |
| { |
| struct notify_client *ntfy_client = data; |
| struct btd_gatt_client *client = ntfy_client->chrc->service->client; |
| |
| queue_remove(client->all_notify_clients, ntfy_client); |
| |
| notify_client_unref(ntfy_client); |
| } |
| |
| static void unregister_characteristic(void *data) |
| { |
| struct characteristic *chrc = data; |
| struct bt_gatt_client *gatt = chrc->service->client->gatt; |
| |
| DBG("Removing GATT characteristic: %s", chrc->path); |
| |
| if (chrc->read_op) |
| bt_gatt_client_cancel(gatt, chrc->read_op->id); |
| |
| if (chrc->write_op) |
| bt_gatt_client_cancel(gatt, chrc->write_op->id); |
| |
| queue_remove_all(chrc->notify_clients, NULL, NULL, remove_client); |
| queue_remove_all(chrc->descs, NULL, NULL, unregister_descriptor); |
| |
| g_dbus_unregister_interface(btd_get_dbus_connection(), chrc->path, |
| GATT_CHARACTERISTIC_IFACE); |
| } |
| |
| static gboolean service_get_uuid(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| char uuid[MAX_LEN_UUID_STR + 1]; |
| const char *ptr = uuid; |
| struct service *service = data; |
| |
| bt_uuid_to_string(&service->uuid, uuid, sizeof(uuid)); |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &ptr); |
| |
| return TRUE; |
| } |
| |
| static gboolean service_get_device(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct service *service = data; |
| const char *str = device_get_path(service->client->device); |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, &str); |
| |
| return TRUE; |
| } |
| |
| static gboolean service_get_primary(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct service *service = data; |
| dbus_bool_t primary; |
| |
| primary = service->primary ? TRUE : FALSE; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, &primary); |
| |
| return TRUE; |
| } |
| |
| static void append_incl_service_path(void *data, void *user_data) |
| { |
| struct service *incl_service = data; |
| DBusMessageIter *array = user_data; |
| |
| dbus_message_iter_append_basic(array, DBUS_TYPE_OBJECT_PATH, |
| &incl_service->path); |
| } |
| |
| static gboolean service_get_includes(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct service *service = data; |
| DBusMessageIter array; |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "{o}", &array); |
| |
| queue_foreach(service->incl_services, append_incl_service_path, &array); |
| |
| dbus_message_iter_close_container(iter, &array); |
| |
| return TRUE; |
| |
| } |
| |
| static const GDBusPropertyTable service_properties[] = { |
| { "UUID", "s", service_get_uuid }, |
| { "Device", "o", service_get_device }, |
| { "Primary", "b", service_get_primary }, |
| { "Includes", "ao", service_get_includes }, |
| { } |
| }; |
| |
| static void service_free(void *data) |
| { |
| struct service *service = data; |
| |
| queue_destroy(service->chrcs, NULL); /* List should be empty here */ |
| queue_destroy(service->incl_services, NULL); |
| g_free(service->path); |
| free(service); |
| } |
| |
| static struct service *service_create(struct gatt_db_attribute *attr, |
| struct btd_gatt_client *client) |
| { |
| struct service *service; |
| const char *device_path = device_get_path(client->device); |
| bt_uuid_t uuid; |
| |
| service = new0(struct service, 1); |
| service->chrcs = queue_new(); |
| service->incl_services = queue_new(); |
| service->client = client; |
| |
| gatt_db_attribute_get_service_data(attr, &service->start_handle, |
| &service->end_handle, |
| &service->primary, |
| &uuid); |
| bt_uuid_to_uuid128(&uuid, &service->uuid); |
| |
| service->path = g_strdup_printf("%s/service%04x", device_path, |
| service->start_handle); |
| |
| if (!g_dbus_register_interface(btd_get_dbus_connection(), service->path, |
| GATT_SERVICE_IFACE, |
| NULL, NULL, |
| service_properties, |
| service, service_free)) { |
| error("Unable to register GATT service with handle 0x%04x for " |
| "device %s", |
| service->start_handle, |
| client->devaddr); |
| service_free(service); |
| |
| return NULL; |
| } |
| |
| DBG("Exported GATT service: %s", service->path); |
| |
| /* Set service active so we can skip discovering next time */ |
| gatt_db_service_set_active(attr, true); |
| |
| /* Mark the service as claimed since it going to be exported */ |
| gatt_db_service_set_claimed(attr, true); |
| |
| return service; |
| } |
| |
| static void on_service_removed(void *data, void *user_data) |
| { |
| struct service *service = data; |
| struct service *removed_service = user_data; |
| |
| if (queue_remove(service->incl_services, removed_service)) |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), |
| service->path, |
| GATT_SERVICE_IFACE, |
| "Includes"); |
| } |
| |
| static void unregister_service(void *data) |
| { |
| struct service *service = data; |
| struct btd_gatt_client *client = service->client; |
| |
| DBG("Removing GATT service: %s", service->path); |
| |
| queue_remove_all(service->chrcs, NULL, NULL, unregister_characteristic); |
| queue_remove_all(service->incl_services, NULL, NULL, NULL); |
| |
| queue_foreach(client->services, on_service_removed, service); |
| |
| g_dbus_unregister_interface(btd_get_dbus_connection(), service->path, |
| GATT_SERVICE_IFACE); |
| } |
| |
| struct export_data { |
| void *root; |
| bool failed; |
| }; |
| |
| static void export_desc(struct gatt_db_attribute *attr, void *user_data) |
| { |
| struct descriptor *desc; |
| struct export_data *data = user_data; |
| struct characteristic *charac = data->root; |
| |
| if (data->failed) |
| return; |
| |
| desc = descriptor_create(attr, charac); |
| if (!desc) { |
| data->failed = true; |
| return; |
| } |
| |
| queue_push_tail(charac->descs, desc); |
| } |
| |
| static bool create_descriptors(struct gatt_db_attribute *attr, |
| struct characteristic *charac) |
| { |
| struct export_data data; |
| |
| data.root = charac; |
| data.failed = false; |
| |
| gatt_db_service_foreach_desc(attr, export_desc, &data); |
| |
| return !data.failed; |
| } |
| |
| static void export_char(struct gatt_db_attribute *attr, void *user_data) |
| { |
| struct characteristic *charac; |
| struct export_data *data = user_data; |
| struct service *service = data->root; |
| |
| if (data->failed) |
| return; |
| |
| charac = characteristic_create(attr, service); |
| if (!charac) |
| goto fail; |
| |
| if (!create_descriptors(attr, charac)) { |
| unregister_characteristic(charac); |
| goto fail; |
| } |
| |
| queue_push_tail(service->chrcs, charac); |
| |
| return; |
| |
| fail: |
| data->failed = true; |
| } |
| |
| static bool create_characteristics(struct gatt_db_attribute *attr, |
| struct service *service) |
| { |
| struct export_data data; |
| |
| data.root = service; |
| data.failed = false; |
| |
| gatt_db_service_foreach_char(attr, export_char, &data); |
| |
| return !data.failed; |
| } |
| |
| static void export_service(struct gatt_db_attribute *attr, void *user_data) |
| { |
| struct btd_gatt_client *client = user_data; |
| struct service *service; |
| |
| if (gatt_db_service_get_claimed(attr)) |
| return; |
| |
| service = service_create(attr, client); |
| if (!service) |
| return; |
| |
| if (!create_characteristics(attr, service)) { |
| error("Exporting characteristics failed"); |
| unregister_service(service); |
| return; |
| } |
| |
| queue_push_tail(client->services, service); |
| } |
| |
| static bool match_service_handle(const void *a, const void *b) |
| { |
| const struct service *service = a; |
| uint16_t start_handle = PTR_TO_UINT(b); |
| |
| return service->start_handle == start_handle; |
| } |
| |
| struct update_incl_data { |
| struct service *service; |
| bool changed; |
| }; |
| |
| static void update_included_service(struct gatt_db_attribute *attrib, |
| void *user_data) |
| { |
| struct update_incl_data *update_data = user_data; |
| struct btd_gatt_client *client = update_data->service->client; |
| struct service *service = update_data->service; |
| struct service *incl_service; |
| uint16_t start_handle; |
| |
| gatt_db_attribute_get_incl_data(attrib, NULL, &start_handle, NULL); |
| |
| incl_service = queue_find(client->services, match_service_handle, |
| UINT_TO_PTR(start_handle)); |
| |
| if (!incl_service) |
| return; |
| |
| /* Check if service is already on list */ |
| if (queue_find(service->incl_services, NULL, incl_service)) |
| return; |
| |
| queue_push_tail(service->incl_services, incl_service); |
| update_data->changed = true; |
| } |
| |
| static void update_included_services(void *data, void *user_data) |
| { |
| struct btd_gatt_client *client = user_data; |
| struct service *service = data; |
| struct gatt_db_attribute *attr; |
| struct update_incl_data inc_data = { |
| .changed = false, |
| .service = service, |
| }; |
| |
| attr = gatt_db_get_attribute(client->db, service->start_handle); |
| gatt_db_service_foreach_incl(attr, update_included_service, &inc_data); |
| |
| if (inc_data.changed) |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), |
| service->path, |
| GATT_SERVICE_IFACE, |
| "Includes"); |
| } |
| |
| static void create_services(struct btd_gatt_client *client) |
| { |
| DBG("Exporting objects for GATT services: %s", client->devaddr); |
| |
| gatt_db_foreach_service(client->db, NULL, export_service, client); |
| |
| queue_foreach(client->services, update_included_services, client); |
| } |
| |
| struct btd_gatt_client *btd_gatt_client_new(struct btd_device *device) |
| { |
| struct btd_gatt_client *client; |
| struct gatt_db *db; |
| |
| if (!device) |
| return NULL; |
| |
| db = btd_device_get_gatt_db(device); |
| if (!db) |
| return NULL; |
| |
| client = new0(struct btd_gatt_client, 1); |
| client->services = queue_new(); |
| client->all_notify_clients = queue_new(); |
| client->ios = queue_new(); |
| client->device = device; |
| ba2str(device_get_address(device), client->devaddr); |
| |
| client->db = gatt_db_ref(db); |
| |
| return client; |
| } |
| |
| void btd_gatt_client_destroy(struct btd_gatt_client *client) |
| { |
| if (!client) |
| return; |
| |
| queue_destroy(client->services, unregister_service); |
| queue_destroy(client->all_notify_clients, NULL); |
| queue_destroy(client->ios, NULL); |
| bt_gatt_client_unref(client->gatt); |
| gatt_db_unref(client->db); |
| free(client); |
| } |
| |
| static void register_notify(void *data, void *user_data) |
| { |
| struct notify_client *notify_client = data; |
| struct btd_gatt_client *client = user_data; |
| struct async_dbus_op *op; |
| |
| DBG("Re-register subscribed notification client"); |
| |
| op = new0(struct async_dbus_op, 1); |
| op->data = notify_client; |
| |
| notify_client->notify_id = bt_gatt_client_register_notify(client->gatt, |
| notify_client->chrc->value_handle, |
| register_notify_cb, notify_cb, |
| op, async_dbus_op_free); |
| if (notify_client->notify_id) |
| return; |
| |
| async_dbus_op_free(op); |
| |
| DBG("Failed to re-register notification client"); |
| |
| queue_remove(notify_client->chrc->notify_clients, notify_client); |
| queue_remove(client->all_notify_clients, notify_client); |
| |
| notify_client_free(notify_client); |
| } |
| |
| void btd_gatt_client_ready(struct btd_gatt_client *client) |
| { |
| if (!client) |
| return; |
| |
| if (!client->gatt) { |
| struct bt_gatt_client *gatt; |
| |
| gatt = btd_device_get_gatt_client(client->device); |
| client->gatt = bt_gatt_client_clone(gatt); |
| if (!client->gatt) { |
| error("GATT client not initialized"); |
| return; |
| } |
| } |
| |
| client->ready = true; |
| |
| DBG("GATT client ready"); |
| |
| create_services(client); |
| } |
| |
| void btd_gatt_client_connected(struct btd_gatt_client *client) |
| { |
| struct bt_gatt_client *gatt; |
| |
| gatt = btd_device_get_gatt_client(client->device); |
| if (!gatt) { |
| error("GATT client not initialized"); |
| return; |
| } |
| |
| DBG("Device connected."); |
| |
| bt_gatt_client_unref(client->gatt); |
| client->gatt = bt_gatt_client_clone(gatt); |
| |
| /* |
| * Services have already been created before. Re-enable notifications |
| * for any pre-registered notification sessions. |
| */ |
| queue_foreach(client->all_notify_clients, register_notify, client); |
| } |
| |
| void btd_gatt_client_service_added(struct btd_gatt_client *client, |
| struct gatt_db_attribute *attrib) |
| { |
| if (!client || !attrib || !client->ready) |
| return; |
| |
| export_service(attrib, client); |
| |
| queue_foreach(client->services, update_included_services, client); |
| } |
| |
| void btd_gatt_client_service_removed(struct btd_gatt_client *client, |
| struct gatt_db_attribute *attrib) |
| { |
| uint16_t start_handle, end_handle; |
| |
| if (!client || !attrib || !client->ready) |
| return; |
| |
| gatt_db_attribute_get_service_handles(attrib, &start_handle, |
| &end_handle); |
| |
| DBG("GATT Services Removed - start: 0x%04x, end: 0x%04x", start_handle, |
| end_handle); |
| queue_remove_all(client->services, match_service_handle, |
| UINT_TO_PTR(start_handle), |
| unregister_service); |
| } |
| |
| static void clear_notify_id(void *data, void *user_data) |
| { |
| struct notify_client *client = data; |
| |
| client->notify_id = 0; |
| } |
| |
| void btd_gatt_client_disconnected(struct btd_gatt_client *client) |
| { |
| if (!client || !client->gatt) |
| return; |
| |
| DBG("Device disconnected. Cleaning up."); |
| |
| queue_remove_all(client->ios, NULL, NULL, |
| (queue_destroy_func_t) io_shutdown); |
| |
| /* |
| * TODO: Once GATT over BR/EDR is properly supported, we should pass the |
| * correct bdaddr_type based on the transport over which GATT is being |
| * done. |
| */ |
| queue_foreach(client->all_notify_clients, clear_notify_id, NULL); |
| |
| bt_gatt_client_unref(client->gatt); |
| client->gatt = NULL; |
| } |
| |
| struct foreach_service_data { |
| btd_gatt_client_service_path_t func; |
| void *user_data; |
| }; |
| |
| static void client_service_foreach(void *data, void *user_data) |
| { |
| struct service *service = data; |
| struct foreach_service_data *foreach_data = user_data; |
| |
| foreach_data->func(service->path, foreach_data->user_data); |
| } |
| |
| void btd_gatt_client_foreach_service(struct btd_gatt_client *client, |
| btd_gatt_client_service_path_t func, |
| void *user_data) |
| { |
| struct foreach_service_data data; |
| |
| if (!client) |
| return; |
| |
| data.func = func; |
| data.user_data = user_data; |
| |
| queue_foreach(client->services, client_service_foreach, &data); |
| } |