From 7e25356deec3369773e3949fe7336d84c10834c0 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 1 Sep 2015 10:10:22 +0100 Subject: fish --- bluez/gatt-client.c | 3013 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3013 insertions(+) create mode 100644 bluez/gatt-client.c (limited to 'bluez/gatt-client.c') diff --git a/bluez/gatt-client.c b/bluez/gatt-client.c new file mode 100644 index 0000000..a569992 --- /dev/null +++ b/bluez/gatt-client.c @@ -0,0 +1,3013 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2014 Google Inc. + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "att.h" +#include +#include "uuid.h" +#include "gatt-helpers.h" +#include "util.h" +#include "queue.h" +#include "gatt-db.h" +#include "gatt-client.h" + +#include +#include +#include + +#ifndef MAX +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#endif + +#ifndef MIN +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#endif + +#define UUID_BYTES (BT_GATT_UUID_SIZE * sizeof(uint8_t)) + +#define GATT_SVC_UUID 0x1801 +#define SVC_CHNGD_UUID 0x2a05 + +struct bt_gatt_client { + struct bt_att *att; + int ref_count; + + bt_gatt_client_callback_t ready_callback; + bt_gatt_client_destroy_func_t ready_destroy; + void *ready_data; + + bt_gatt_client_service_changed_callback_t svc_chngd_callback; + bt_gatt_client_destroy_func_t svc_chngd_destroy; + void *svc_chngd_data; + + bt_gatt_client_debug_func_t debug_callback; + bt_gatt_client_destroy_func_t debug_destroy; + void *debug_data; + + struct gatt_db *db; + bool in_init; + bool ready; + + /* + * Queue of long write requests. An error during "prepare write" + * requests can result in a cancel through "execute write". To prevent + * cancelation of prepared writes to the wrong attribute and multiple + * requests to the same attribute that may result in a corrupted final + * value, we avoid interleaving prepared writes. + */ + struct queue *long_write_queue; + bool in_long_write; + + unsigned int reliable_write_session_id; + + /* List of registered disconnect/notification/indication callbacks */ + struct queue *notify_list; + struct queue *notify_chrcs; + int next_reg_id; + unsigned int disc_id, notify_id, ind_id; + + /* + * Handles of the GATT Service and the Service Changed characteristic + * value handle. These will have the value 0 if they are not present on + * the remote peripheral. + */ + unsigned int svc_chngd_ind_id; + bool svc_chngd_registered; + struct queue *svc_chngd_queue; /* Queued service changed events */ + bool in_svc_chngd; + + /* + * List of pending read/write operations. For operations that span + * across multiple PDUs, this list provides a mapping from an operation + * id to an ATT request id. + */ + struct queue *pending_requests; + unsigned int next_request_id; + + struct bt_gatt_request *discovery_req; + unsigned int mtu_req_id; +}; + +struct request { + struct bt_gatt_client *client; + bool long_write; + bool prep_write; + bool removed; + int ref_count; + unsigned int id; + unsigned int att_id; + void *data; + void (*destroy)(void *); +}; + +static struct request *request_ref(struct request *req) +{ + __sync_fetch_and_add(&req->ref_count, 1); + + return req; +} + +static struct request *request_create(struct bt_gatt_client *client) +{ + struct request *req; + + req = new0(struct request, 1); + if (!req) + return NULL; + + if (client->next_request_id < 1) + client->next_request_id = 1; + + queue_push_tail(client->pending_requests, req); + req->client = client; + req->id = client->next_request_id++; + + return request_ref(req); +} + +static void request_unref(void *data) +{ + struct request *req = data; + + if (__sync_sub_and_fetch(&req->ref_count, 1)) + return; + + if (req->destroy) + req->destroy(req->data); + + if (!req->removed) + queue_remove(req->client->pending_requests, req); + + free(req); +} + +struct notify_chrc { + uint16_t value_handle; + uint16_t ccc_handle; + uint16_t properties; + int notify_count; /* Reference count of registered notify callbacks */ + + /* Pending calls to register_notify are queued here so that they can be + * processed after a write that modifies the CCC descriptor. + */ + struct queue *reg_notify_queue; + unsigned int ccc_write_id; +}; + +struct notify_data { + struct bt_gatt_client *client; + unsigned int id; + unsigned int att_id; + int ref_count; + struct notify_chrc *chrc; + bt_gatt_client_register_callback_t callback; + bt_gatt_client_notify_callback_t notify; + void *user_data; + bt_gatt_client_destroy_func_t destroy; +}; + +static struct notify_data *notify_data_ref(struct notify_data *notify_data) +{ + __sync_fetch_and_add(¬ify_data->ref_count, 1); + + return notify_data; +} + +static void notify_data_unref(void *data) +{ + struct notify_data *notify_data = data; + + if (__sync_sub_and_fetch(¬ify_data->ref_count, 1)) + return; + + if (notify_data->destroy) + notify_data->destroy(notify_data->user_data); + + free(notify_data); +} + +static void find_ccc(struct gatt_db_attribute *attr, void *user_data) +{ + struct gatt_db_attribute **ccc_ptr = user_data; + bt_uuid_t uuid; + + if (*ccc_ptr) + return; + + bt_uuid16_create(&uuid, GATT_CLIENT_CHARAC_CFG_UUID); + + if (bt_uuid_cmp(&uuid, gatt_db_attribute_get_type(attr))) + return; + + *ccc_ptr = attr; +} + +static struct notify_chrc *notify_chrc_create(struct bt_gatt_client *client, + uint16_t value_handle) +{ + struct gatt_db_attribute *attr, *ccc; + struct notify_chrc *chrc; + bt_uuid_t uuid; + uint8_t properties; + + /* Check that chrc_value_handle belongs to a known characteristic */ + attr = gatt_db_get_attribute(client->db, value_handle - 1); + if (!attr) + return NULL; + + bt_uuid16_create(&uuid, GATT_CHARAC_UUID); + if (bt_uuid_cmp(&uuid, gatt_db_attribute_get_type(attr))) + return NULL; + + if (!gatt_db_attribute_get_char_data(attr, NULL, NULL, + &properties, NULL)) + return NULL; + + chrc = new0(struct notify_chrc, 1); + if (!chrc) + return NULL; + + chrc->reg_notify_queue = queue_new(); + if (!chrc->reg_notify_queue) { + free(chrc); + return NULL; + } + + /* + * Find the CCC characteristic. Some characteristics that allow + * notifications may not have a CCC descriptor. We treat these as + * automatically successful. + */ + ccc = NULL; + gatt_db_service_foreach_desc(attr, find_ccc, &ccc); + if (ccc) + chrc->ccc_handle = gatt_db_attribute_get_handle(ccc); + + chrc->value_handle = value_handle; + chrc->properties = properties; + + queue_push_tail(client->notify_chrcs, chrc); + + return chrc; +} + +static void notify_chrc_free(void *data) +{ + struct notify_chrc *chrc = data; + + queue_destroy(chrc->reg_notify_queue, notify_data_unref); + free(chrc); +} + +static bool match_notify_data_id(const void *a, const void *b) +{ + const struct notify_data *notify_data = a; + unsigned int id = PTR_TO_UINT(b); + + return notify_data->id == id; +} + +struct handle_range { + uint16_t start; + uint16_t end; +}; + +static bool match_notify_data_handle_range(const void *a, const void *b) +{ + const struct notify_data *notify_data = a; + struct notify_chrc *chrc = notify_data->chrc; + const struct handle_range *range = b; + + return chrc->value_handle >= range->start && + chrc->value_handle <= range->end; +} + +static bool match_notify_chrc_handle_range(const void *a, const void *b) +{ + const struct notify_chrc *chrc = a; + const struct handle_range *range = b; + + return chrc->value_handle >= range->start && + chrc->value_handle <= range->end; +} + +static void gatt_client_remove_all_notify_in_range( + struct bt_gatt_client *client, + uint16_t start_handle, uint16_t end_handle) +{ + struct handle_range range; + + range.start = start_handle; + range.end = end_handle; + + queue_remove_all(client->notify_list, match_notify_data_handle_range, + &range, notify_data_unref); +} + +static void gatt_client_remove_notify_chrcs_in_range( + struct bt_gatt_client *client, + uint16_t start_handle, uint16_t end_handle) +{ + struct handle_range range; + + range.start = start_handle; + range.end = end_handle; + + queue_remove_all(client->notify_chrcs, match_notify_chrc_handle_range, + &range, notify_chrc_free); +} + +struct discovery_op; + +typedef void (*discovery_op_complete_func_t)(struct discovery_op *op, + bool success, + uint8_t att_ecode); +typedef void (*discovery_op_fail_func_t)(struct discovery_op *op); + +struct discovery_op { + struct bt_gatt_client *client; + struct queue *pending_svcs; + struct queue *pending_chrcs; + struct queue *tmp_queue; + struct gatt_db_attribute *cur_svc; + bool success; + uint16_t start; + uint16_t end; + int ref_count; + discovery_op_complete_func_t complete_func; + discovery_op_fail_func_t failure_func; +}; + +static void discovery_op_free(struct discovery_op *op) +{ + queue_destroy(op->pending_svcs, NULL); + queue_destroy(op->pending_chrcs, free); + queue_destroy(op->tmp_queue, NULL); + free(op); +} + +static struct discovery_op *discovery_op_create(struct bt_gatt_client *client, + uint16_t start, uint16_t end, + discovery_op_complete_func_t complete_func, + discovery_op_fail_func_t failure_func) +{ + struct discovery_op *op; + + op = new0(struct discovery_op, 1); + if (!op) + return NULL; + + op->pending_svcs = queue_new(); + if (!op->pending_svcs) + goto fail; + + op->pending_chrcs = queue_new(); + if (!op->pending_chrcs) + goto fail; + + op->tmp_queue = queue_new(); + if (!op->tmp_queue) + goto fail; + + op->client = client; + op->complete_func = complete_func; + op->failure_func = failure_func; + op->start = start; + op->end = end; + + return op; + +fail: + discovery_op_free(op); + return NULL; +} + +static struct discovery_op *discovery_op_ref(struct discovery_op *op) +{ + __sync_fetch_and_add(&op->ref_count, 1); + + return op; +} + +static void discovery_op_unref(void *data) +{ + struct discovery_op *op = data; + + if (__sync_sub_and_fetch(&op->ref_count, 1)) + return; + + if (!op->success) + op->failure_func(op); + + discovery_op_free(op); +} + +static void discovery_req_clear(struct bt_gatt_client *client) +{ + if (!client->discovery_req) + return; + + bt_gatt_request_unref(client->discovery_req); + client->discovery_req = NULL; +} + +static void discover_chrcs_cb(bool success, uint8_t att_ecode, + struct bt_gatt_result *result, + void *user_data); + +static void discover_incl_cb(bool success, uint8_t att_ecode, + struct bt_gatt_result *result, void *user_data) +{ + struct discovery_op *op = user_data; + struct bt_gatt_client *client = op->client; + struct bt_gatt_iter iter; + struct gatt_db_attribute *attr, *tmp; + uint16_t handle, start, end; + uint128_t u128; + bt_uuid_t uuid; + char uuid_str[MAX_LEN_UUID_STR]; + unsigned int includes_count, i; + + discovery_req_clear(client); + + if (!success) { + if (att_ecode == BT_ATT_ERROR_ATTRIBUTE_NOT_FOUND) + goto next; + + goto failed; + } + + /* Get the currently processed service */ + attr = op->cur_svc; + if (!attr) + goto failed; + + if (!result || !bt_gatt_iter_init(&iter, result)) + goto failed; + + includes_count = bt_gatt_result_included_count(result); + if (includes_count == 0) + goto failed; + + util_debug(client->debug_callback, client->debug_data, + "Included services found: %u", + includes_count); + + for (i = 0; i < includes_count; i++) { + if (!bt_gatt_iter_next_included_service(&iter, &handle, &start, + &end, u128.data)) + break; + + bt_uuid128_create(&uuid, u128); + + /* Log debug message */ + bt_uuid_to_string(&uuid, uuid_str, sizeof(uuid_str)); + util_debug(client->debug_callback, client->debug_data, + "handle: 0x%04x, start: 0x%04x, end: 0x%04x," + "uuid: %s", handle, start, end, uuid_str); + + tmp = gatt_db_get_attribute(client->db, start); + if (!tmp) + goto failed; + + tmp = gatt_db_service_add_included(attr, tmp); + if (!tmp) + goto failed; + + /* + * GATT requires that all include definitions precede + * characteristic declarations. Based on the order we're adding + * these entries, the correct handle must be assigned to the new + * attribute. + */ + if (gatt_db_attribute_get_handle(tmp) != handle) + goto failed; + } + +next: + /* Move on to the next service */ + attr = queue_pop_head(op->pending_svcs); + if (!attr) { + struct queue *tmp_queue; + + tmp_queue = op->pending_svcs; + op->pending_svcs = op->tmp_queue; + op->tmp_queue = tmp_queue; + + /* + * We have processed all include definitions. Move on to + * characteristics. + */ + attr = queue_pop_head(op->pending_svcs); + if (!attr) + goto failed; + + if (!gatt_db_attribute_get_service_handles(attr, &start, &end)) + goto failed; + + op->cur_svc = attr; + + client->discovery_req = bt_gatt_discover_characteristics( + client->att, + start, end, + discover_chrcs_cb, + discovery_op_ref(op), + discovery_op_unref); + if (client->discovery_req) + return; + + util_debug(client->debug_callback, client->debug_data, + "Failed to start characteristic discovery"); + discovery_op_unref(op); + goto failed; + } + + queue_push_tail(op->tmp_queue, attr); + op->cur_svc = attr; + if (!gatt_db_attribute_get_service_handles(attr, &start, &end)) + goto failed; + + if (start == end) + goto next; + + client->discovery_req = bt_gatt_discover_included_services(client->att, + start, end, + discover_incl_cb, + discovery_op_ref(op), + discovery_op_unref); + if (client->discovery_req) + return; + + util_debug(client->debug_callback, client->debug_data, + "Failed to start included discovery"); + discovery_op_unref(op); + +failed: + op->success = false; + op->complete_func(op, false, att_ecode); +} + +struct chrc { + uint16_t start_handle; + uint16_t end_handle; + uint16_t value_handle; + uint8_t properties; + bt_uuid_t uuid; +}; + +static void discover_descs_cb(bool success, uint8_t att_ecode, + struct bt_gatt_result *result, + void *user_data); + +static bool discover_descs(struct discovery_op *op, bool *discovering) +{ + struct bt_gatt_client *client = op->client; + struct gatt_db_attribute *attr; + struct chrc *chrc_data; + uint16_t desc_start; + + *discovering = false; + + while ((chrc_data = queue_pop_head(op->pending_chrcs))) { + attr = gatt_db_service_add_characteristic(op->cur_svc, + &chrc_data->uuid, 0, + chrc_data->properties, + NULL, NULL, NULL); + + if (!attr) + goto failed; + + if (gatt_db_attribute_get_handle(attr) != + chrc_data->value_handle) + goto failed; + + desc_start = chrc_data->value_handle + 1; + + if (desc_start > chrc_data->end_handle) { + free(chrc_data); + continue; + } + + client->discovery_req = bt_gatt_discover_descriptors( + client->att, desc_start, + chrc_data->end_handle, + discover_descs_cb, + discovery_op_ref(op), + discovery_op_unref); + if (client->discovery_req) { + *discovering = true; + goto done; + } + + util_debug(client->debug_callback, client->debug_data, + "Failed to start descriptor discovery"); + discovery_op_unref(op); + + goto failed; + } + +done: + free(chrc_data); + return true; + +failed: + free(chrc_data); + return false; +} + +static void discover_descs_cb(bool success, uint8_t att_ecode, + struct bt_gatt_result *result, + void *user_data) +{ + struct discovery_op *op = user_data; + struct bt_gatt_client *client = op->client; + struct bt_gatt_iter iter; + struct gatt_db_attribute *attr; + uint16_t handle, start, end; + uint128_t u128; + bt_uuid_t uuid; + char uuid_str[MAX_LEN_UUID_STR]; + unsigned int desc_count; + bool discovering; + + discovery_req_clear(client); + + if (!success) { + if (att_ecode == BT_ATT_ERROR_ATTRIBUTE_NOT_FOUND) { + success = true; + goto next; + } + + goto done; + } + + if (!result || !bt_gatt_iter_init(&iter, result)) + goto failed; + + desc_count = bt_gatt_result_descriptor_count(result); + if (desc_count == 0) + goto failed; + + util_debug(client->debug_callback, client->debug_data, + "Descriptors found: %u", desc_count); + + while (bt_gatt_iter_next_descriptor(&iter, &handle, u128.data)) { + bt_uuid128_create(&uuid, u128); + + /* Log debug message */ + bt_uuid_to_string(&uuid, uuid_str, sizeof(uuid_str)); + util_debug(client->debug_callback, client->debug_data, + "handle: 0x%04x, uuid: %s", + handle, uuid_str); + + attr = gatt_db_service_add_descriptor(op->cur_svc, &uuid, 0, + NULL, NULL, NULL); + if (!attr) + goto failed; + + if (gatt_db_attribute_get_handle(attr) != handle) + goto failed; + } + + if (!discover_descs(op, &discovering)) + goto failed; + + if (discovering) + return; + +next: + /* Done with the current service */ + gatt_db_service_set_active(op->cur_svc, true); + + attr = queue_pop_head(op->pending_svcs); + if (!attr) + goto done; + + if (!gatt_db_attribute_get_service_handles(attr, &start, &end)) + goto failed; + + if (start == end) + goto next; + + /* Move on to the next service */ + op->cur_svc = attr; + + client->discovery_req = bt_gatt_discover_characteristics(client->att, + start, end, + discover_chrcs_cb, + discovery_op_ref(op), + discovery_op_unref); + if (client->discovery_req) + return; + + util_debug(client->debug_callback, client->debug_data, + "Failed to start characteristic discovery"); + discovery_op_unref(op); + +failed: + success = false; + +done: + op->success = success; + op->complete_func(op, success, att_ecode); +} + +static void discover_chrcs_cb(bool success, uint8_t att_ecode, + struct bt_gatt_result *result, + void *user_data) +{ + struct discovery_op *op = user_data; + struct bt_gatt_client *client = op->client; + struct bt_gatt_iter iter; + struct gatt_db_attribute *attr; + struct chrc *chrc_data; + uint16_t start, end, value; + uint8_t properties; + uint128_t u128; + bt_uuid_t uuid; + char uuid_str[MAX_LEN_UUID_STR]; + unsigned int chrc_count; + bool discovering; + + discovery_req_clear(client); + + if (!success) { + if (att_ecode == BT_ATT_ERROR_ATTRIBUTE_NOT_FOUND) { + success = true; + goto next; + } + + goto done; + } + + if (!op->cur_svc || !result || !bt_gatt_iter_init(&iter, result)) + goto failed; + + chrc_count = bt_gatt_result_characteristic_count(result); + util_debug(client->debug_callback, client->debug_data, + "Characteristics found: %u", chrc_count); + + if (chrc_count == 0) + goto failed; + + while (bt_gatt_iter_next_characteristic(&iter, &start, &end, &value, + &properties, u128.data)) { + bt_uuid128_create(&uuid, u128); + + /* Log debug message */ + bt_uuid_to_string(&uuid, uuid_str, sizeof(uuid_str)); + util_debug(client->debug_callback, client->debug_data, + "start: 0x%04x, end: 0x%04x, value: 0x%04x, " + "props: 0x%02x, uuid: %s", + start, end, value, properties, uuid_str); + + chrc_data = new0(struct chrc, 1); + if (!chrc_data) + goto failed; + + chrc_data->start_handle = start; + chrc_data->end_handle = end; + chrc_data->value_handle = value; + chrc_data->properties = properties; + chrc_data->uuid = uuid; + + queue_push_tail(op->pending_chrcs, chrc_data); + } + + /* + * Sequentially discover descriptors for each characteristic and insert + * the characteristics into the database as we proceed. + */ + if (!discover_descs(op, &discovering)) + goto failed; + + if (discovering) + return; + +next: + /* Done with the current service */ + gatt_db_service_set_active(op->cur_svc, true); + + attr = queue_pop_head(op->pending_svcs); + if (!attr) + goto done; + + if (!gatt_db_attribute_get_service_handles(attr, &start, &end)) + goto failed; + + if (start == end) + goto next; + + /* Move on to the next service */ + op->cur_svc = attr; + + client->discovery_req = bt_gatt_discover_characteristics(client->att, + start, end, + discover_chrcs_cb, + discovery_op_ref(op), + discovery_op_unref); + if (client->discovery_req) + return; + + util_debug(client->debug_callback, client->debug_data, + "Failed to start characteristic discovery"); + discovery_op_unref(op); + +failed: + success = false; + +done: + op->success = success; + op->complete_func(op, success, att_ecode); +} + +static void discover_secondary_cb(bool success, uint8_t att_ecode, + struct bt_gatt_result *result, + void *user_data) +{ + struct discovery_op *op = user_data; + struct bt_gatt_client *client = op->client; + struct bt_gatt_iter iter; + struct gatt_db_attribute *attr; + uint16_t start, end; + uint128_t u128; + bt_uuid_t uuid; + char uuid_str[MAX_LEN_UUID_STR]; + + discovery_req_clear(client); + + if (!success) { + util_debug(client->debug_callback, client->debug_data, + "Secondary service discovery failed." + " ATT ECODE: 0x%02x", att_ecode); + switch (att_ecode) { + case BT_ATT_ERROR_ATTRIBUTE_NOT_FOUND: + case BT_ATT_ERROR_UNSUPPORTED_GROUP_TYPE: + goto next; + default: + goto done; + } + } + + if (!result || !bt_gatt_iter_init(&iter, result)) { + success = false; + goto done; + } + + util_debug(client->debug_callback, client->debug_data, + "Secondary services found: %u", + bt_gatt_result_service_count(result)); + + while (bt_gatt_iter_next_service(&iter, &start, &end, u128.data)) { + bt_uuid128_create(&uuid, u128); + + /* Log debug message */ + bt_uuid_to_string(&uuid, uuid_str, sizeof(uuid_str)); + util_debug(client->debug_callback, client->debug_data, + "start: 0x%04x, end: 0x%04x, uuid: %s", + start, end, uuid_str); + + /* Store the service */ + attr = gatt_db_insert_service(client->db, start, &uuid, false, + end - start + 1); + if (!attr) { + util_debug(client->debug_callback, client->debug_data, + "Failed to create service"); + success = false; + goto done; + } + + queue_push_tail(op->pending_svcs, attr); + } + +next: + /* Sequentially discover included services */ + attr = queue_pop_head(op->pending_svcs); + + /* Complete with success if queue is empty */ + if (!attr) + goto done; + + /* + * Store the service in the tmp queue to be reused during + * characteristics discovery later. + */ + queue_push_tail(op->tmp_queue, attr); + op->cur_svc = attr; + + if (!gatt_db_attribute_get_service_handles(attr, &start, &end)) { + success = false; + goto done; + } + + client->discovery_req = bt_gatt_discover_included_services(client->att, + start, end, + discover_incl_cb, + discovery_op_ref(op), + discovery_op_unref); + if (client->discovery_req) + return; + + util_debug(client->debug_callback, client->debug_data, + "Failed to start included services discovery"); + discovery_op_unref(op); + +done: + op->success = success; + op->complete_func(op, success, att_ecode); +} + +static void discover_primary_cb(bool success, uint8_t att_ecode, + struct bt_gatt_result *result, + void *user_data) +{ + struct discovery_op *op = user_data; + struct bt_gatt_client *client = op->client; + struct bt_gatt_iter iter; + struct gatt_db_attribute *attr; + uint16_t start, end; + uint128_t u128; + bt_uuid_t uuid; + char uuid_str[MAX_LEN_UUID_STR]; + + discovery_req_clear(client); + + if (!success) { + util_debug(client->debug_callback, client->debug_data, + "Primary service discovery failed." + " ATT ECODE: 0x%02x", att_ecode); + goto secondary; + } + + if (!result || !bt_gatt_iter_init(&iter, result)) { + success = false; + goto done; + } + + util_debug(client->debug_callback, client->debug_data, + "Primary services found: %u", + bt_gatt_result_service_count(result)); + + while (bt_gatt_iter_next_service(&iter, &start, &end, u128.data)) { + bt_uuid128_create(&uuid, u128); + + /* Log debug message. */ + bt_uuid_to_string(&uuid, uuid_str, sizeof(uuid_str)); + util_debug(client->debug_callback, client->debug_data, + "start: 0x%04x, end: 0x%04x, uuid: %s", + start, end, uuid_str); + + attr = gatt_db_insert_service(client->db, start, &uuid, true, + end - start + 1); + if (!attr) { + util_debug(client->debug_callback, client->debug_data, + "Failed to store service"); + success = false; + goto done; + } + + queue_push_tail(op->pending_svcs, attr); + } + +secondary: + /* Discover secondary services */ + client->discovery_req = bt_gatt_discover_secondary_services(client->att, + NULL, op->start, op->end, + discover_secondary_cb, + discovery_op_ref(op), + discovery_op_unref); + if (client->discovery_req) + return; + + util_debug(client->debug_callback, client->debug_data, + "Failed to start secondary service discovery"); + discovery_op_unref(op); + success = false; + +done: + op->success = success; + op->complete_func(op, success, att_ecode); +} + +static void notify_client_ready(struct bt_gatt_client *client, bool success, + uint8_t att_ecode) +{ + if (!client->ready_callback) + return; + + bt_gatt_client_ref(client); + client->ready_callback(success, att_ecode, client->ready_data); + bt_gatt_client_unref(client); +} + +static void exchange_mtu_cb(bool success, uint8_t att_ecode, void *user_data) +{ + struct discovery_op *op = user_data; + struct bt_gatt_client *client = op->client; + + op->success = success; + client->mtu_req_id = 0; + + if (!success) { + util_debug(client->debug_callback, client->debug_data, + "MTU Exchange failed. ATT ECODE: 0x%02x", + att_ecode); + + client->in_init = false; + notify_client_ready(client, success, att_ecode); + + return; + } + + util_debug(client->debug_callback, client->debug_data, + "MTU exchange complete, with MTU: %u", + bt_att_get_mtu(client->att)); + + /* Don't do discovery if the database was pre-populated */ + if (!gatt_db_isempty(client->db)) { + op->complete_func(op, true, 0); + return; + } + + client->discovery_req = bt_gatt_discover_all_primary_services( + client->att, NULL, + discover_primary_cb, + discovery_op_ref(op), + discovery_op_unref); + if (client->discovery_req) + return; + + util_debug(client->debug_callback, client->debug_data, + "Failed to initiate primary service discovery"); + + client->in_init = false; + notify_client_ready(client, false, att_ecode); + + discovery_op_unref(op); +} + +struct service_changed_op { + struct bt_gatt_client *client; + uint16_t start_handle; + uint16_t end_handle; +}; + +static void service_changed_reregister_cb(uint16_t att_ecode, void *user_data) +{ + struct bt_gatt_client *client = user_data; + + if (!att_ecode) { + util_debug(client->debug_callback, client->debug_data, + "Re-registered handler for \"Service Changed\" after " + "change in GATT service"); + client->svc_chngd_registered = true; + return; + } + + util_debug(client->debug_callback, client->debug_data, + "Failed to register handler for \"Service Changed\""); + client->svc_chngd_ind_id = 0; +} + +static void process_service_changed(struct bt_gatt_client *client, + uint16_t start_handle, + uint16_t end_handle); +static void service_changed_cb(uint16_t value_handle, const uint8_t *value, + uint16_t length, void *user_data); + +static void get_first_attribute(struct gatt_db_attribute *attrib, + void *user_data) +{ + struct gatt_db_attribute **stored = user_data; + + if (*stored) + return; + + *stored = attrib; +} + +static void service_changed_complete(struct discovery_op *op, bool success, + uint8_t att_ecode) +{ + struct bt_gatt_client *client = op->client; + struct service_changed_op *next_sc_op; + uint16_t start_handle = op->start; + uint16_t end_handle = op->end; + struct gatt_db_attribute *attr = NULL; + bt_uuid_t uuid; + + client->in_svc_chngd = false; + + if (!success && att_ecode != BT_ATT_ERROR_ATTRIBUTE_NOT_FOUND) { + util_debug(client->debug_callback, client->debug_data, + "Failed to discover services within changed range - " + "error: 0x%02x", att_ecode); + + gatt_db_clear_range(client->db, start_handle, end_handle); + } + + /* Notify the upper layer of changed services */ + if (client->svc_chngd_callback) + client->svc_chngd_callback(start_handle, end_handle, + client->svc_chngd_data); + + /* Process any queued events */ + next_sc_op = queue_pop_head(client->svc_chngd_queue); + if (next_sc_op) { + process_service_changed(client, next_sc_op->start_handle, + next_sc_op->end_handle); + free(next_sc_op); + return; + } + + bt_uuid16_create(&uuid, SVC_CHNGD_UUID); + + gatt_db_find_by_type(client->db, start_handle, end_handle, &uuid, + get_first_attribute, &attr); + if (!attr) + return; + + /* The GATT service was modified. Re-register the handler for + * indications from the "Service Changed" characteristic. + */ + client->svc_chngd_registered = false; + client->svc_chngd_ind_id = bt_gatt_client_register_notify(client, + gatt_db_attribute_get_handle(attr), + service_changed_reregister_cb, + service_changed_cb, + client, NULL); + if (client->svc_chngd_ind_id) + return; + + util_debug(client->debug_callback, client->debug_data, + "Failed to re-register handler for \"Service Changed\""); +} + +static void service_changed_failure(struct discovery_op *op) +{ + struct bt_gatt_client *client = op->client; + + gatt_db_clear_range(client->db, op->start, op->end); +} + +static void process_service_changed(struct bt_gatt_client *client, + uint16_t start_handle, + uint16_t end_handle) +{ + struct discovery_op *op; + + /* Invalidate and remove all effected notify callbacks */ + gatt_client_remove_all_notify_in_range(client, start_handle, + end_handle); + gatt_client_remove_notify_chrcs_in_range(client, start_handle, + end_handle); + + /* Remove all services that overlap the modified range since we'll + * rediscover them + */ + gatt_db_clear_range(client->db, start_handle, end_handle); + + op = discovery_op_create(client, start_handle, end_handle, + service_changed_complete, + service_changed_failure); + if (!op) + goto fail; + + client->discovery_req = bt_gatt_discover_primary_services(client->att, + NULL, start_handle, end_handle, + discover_primary_cb, + discovery_op_ref(op), + discovery_op_unref); + if (client->discovery_req) { + client->in_svc_chngd = true; + return; + } + + discovery_op_free(op); + +fail: + util_debug(client->debug_callback, client->debug_data, + "Failed to initiate service discovery" + " after Service Changed"); +} + +static void service_changed_cb(uint16_t value_handle, const uint8_t *value, + uint16_t length, void *user_data) +{ + struct bt_gatt_client *client = user_data; + struct service_changed_op *op; + uint16_t start, end; + + if (length != 4) + return; + + start = get_le16(value); + end = get_le16(value + 2); + + if (start > end) { + util_debug(client->debug_callback, client->debug_data, + "Service Changed received with invalid handles"); + return; + } + + util_debug(client->debug_callback, client->debug_data, + "Service Changed received - start: 0x%04x end: 0x%04x", + start, end); + + if (!client->in_svc_chngd) { + process_service_changed(client, start, end); + return; + } + + op = new0(struct service_changed_op, 1); + if (!op) + return; + + op->start_handle = start; + op->end_handle = end; + + queue_push_tail(client->svc_chngd_queue, op); +} + +static void service_changed_register_cb(uint16_t att_ecode, void *user_data) +{ + bool success; + struct bt_gatt_client *client = user_data; + + if (att_ecode) { + util_debug(client->debug_callback, client->debug_data, + "Failed to register handler for \"Service Changed\""); + success = false; + client->svc_chngd_ind_id = 0; + goto done; + } + + client->svc_chngd_registered = true; + client->ready = true; + success = true; + util_debug(client->debug_callback, client->debug_data, + "Registered handler for \"Service Changed\": %u", + client->svc_chngd_ind_id); + +done: + notify_client_ready(client, success, att_ecode); +} + +static void init_complete(struct discovery_op *op, bool success, + uint8_t att_ecode) +{ + struct bt_gatt_client *client = op->client; + struct gatt_db_attribute *attr = NULL; + bt_uuid_t uuid; + + client->in_init = false; + + if (!success) + goto fail; + + bt_uuid16_create(&uuid, SVC_CHNGD_UUID); + + gatt_db_find_by_type(client->db, 0x0001, 0xffff, &uuid, + get_first_attribute, &attr); + if (!attr) { + client->ready = true; + goto done; + } + + /* Register an indication handler for the "Service Changed" + * characteristic and report ready only if the handler is registered + * successfully. Temporarily set "ready" to true so that we can register + * the handler using the existing framework. + */ + client->ready = true; + client->svc_chngd_ind_id = bt_gatt_client_register_notify(client, + gatt_db_attribute_get_handle(attr), + service_changed_register_cb, + service_changed_cb, + client, NULL); + + if (!client->svc_chngd_registered) + client->ready = false; + + if (client->svc_chngd_ind_id) + return; + + util_debug(client->debug_callback, client->debug_data, + "Failed to register handler for \"Service Changed\""); + success = false; + +fail: + util_debug(client->debug_callback, client->debug_data, + "Failed to initialize gatt-client"); + + op->success = false; + +done: + notify_client_ready(client, success, att_ecode); +} + +static void init_fail(struct discovery_op *op) +{ + struct bt_gatt_client *client = op->client; + + gatt_db_clear(client->db); +} + +static bool gatt_client_init(struct bt_gatt_client *client, uint16_t mtu) +{ + struct discovery_op *op; + + if (client->in_init || client->ready) + return false; + + op = discovery_op_create(client, 0x0001, 0xffff, init_complete, + init_fail); + if (!op) + return false; + + /* Configure the MTU */ + client->mtu_req_id = bt_gatt_exchange_mtu(client->att, + MAX(BT_ATT_DEFAULT_LE_MTU, mtu), + exchange_mtu_cb, + discovery_op_ref(op), + discovery_op_unref); + if (!client->mtu_req_id) { + discovery_op_free(op); + return false; + } + + client->in_init = true; + + return true; +} + +struct pdu_data { + const void *pdu; + uint16_t length; +}; + +static void complete_notify_request(void *data) +{ + struct notify_data *notify_data = data; + + /* Increment the per-characteristic ref count of notify handlers */ + __sync_fetch_and_add(¬ify_data->chrc->notify_count, 1); + + notify_data->att_id = 0; + notify_data->callback(0, notify_data->user_data); +} + +static bool notify_data_write_ccc(struct notify_data *notify_data, bool enable, + bt_att_response_func_t callback) +{ + uint8_t pdu[4]; + unsigned int att_id; + + assert(notify_data->chrc->ccc_handle); + memset(pdu, 0, sizeof(pdu)); + put_le16(notify_data->chrc->ccc_handle, pdu); + + if (enable) { + /* Try to enable notifications and/or indications based on + * whatever the characteristic supports. + */ + if (notify_data->chrc->properties & BT_GATT_CHRC_PROP_NOTIFY) + pdu[2] = 0x01; + + if (notify_data->chrc->properties & BT_GATT_CHRC_PROP_INDICATE) + pdu[2] |= 0x02; + + if (!pdu[2]) + return false; + } + + att_id = bt_att_send(notify_data->client->att, BT_ATT_OP_WRITE_REQ, + pdu, sizeof(pdu), callback, + notify_data_ref(notify_data), + notify_data_unref); + notify_data->chrc->ccc_write_id = notify_data->att_id = att_id; + + return !!att_id; +} + +static uint8_t process_error(const void *pdu, uint16_t length) +{ + const struct bt_att_pdu_error_rsp *error_pdu; + + if (!pdu || length != sizeof(struct bt_att_pdu_error_rsp)) + return 0; + + error_pdu = pdu; + + return error_pdu->ecode; +} + +static void enable_ccc_callback(uint8_t opcode, const void *pdu, + uint16_t length, void *user_data) +{ + struct notify_data *notify_data = user_data; + uint16_t att_ecode; + + assert(!notify_data->chrc->notify_count); + assert(notify_data->chrc->ccc_write_id); + + notify_data->chrc->ccc_write_id = 0; + + if (opcode == BT_ATT_OP_ERROR_RSP) { + att_ecode = process_error(pdu, length); + + /* Failed to enable. Complete the current request and move on to + * the next one in the queue. If there was an error sending the + * write request, then just move on to the next queued entry. + */ + queue_remove(notify_data->client->notify_list, notify_data); + notify_data->callback(att_ecode, notify_data->user_data); + + while ((notify_data = queue_pop_head( + notify_data->chrc->reg_notify_queue))) { + + if (notify_data_write_ccc(notify_data, true, + enable_ccc_callback)) + return; + } + + return; + } + + /* Success! Report success for all remaining requests. */ + bt_gatt_client_ref(notify_data->client); + + complete_notify_request(notify_data); + queue_remove_all(notify_data->chrc->reg_notify_queue, NULL, NULL, + complete_notify_request); + + bt_gatt_client_unref(notify_data->client); +} + +static void disable_ccc_callback(uint8_t opcode, const void *pdu, + uint16_t length, void *user_data) +{ + struct notify_data *notify_data = user_data; + struct notify_data *next_data; + + assert(!notify_data->chrc->notify_count); + assert(notify_data->chrc->ccc_write_id); + + notify_data->chrc->ccc_write_id = 0; + + /* This is a best effort procedure, so ignore errors and process any + * queued requests. + */ + while (1) { + next_data = queue_pop_head(notify_data->chrc->reg_notify_queue); + if (!next_data || notify_data_write_ccc(notify_data, true, + enable_ccc_callback)) + return; + } +} + +static void complete_unregister_notify(void *data) +{ + struct notify_data *notify_data = data; + + /* + * If a procedure to enable the CCC is still pending, then cancel it and + * return. + */ + if (notify_data->att_id) { + bt_att_cancel(notify_data->client->att, notify_data->att_id); + goto done; + } + + if (__sync_sub_and_fetch(¬ify_data->chrc->notify_count, 1) || + !notify_data->chrc->ccc_handle) + goto done; + + if (notify_data_write_ccc(notify_data, false, disable_ccc_callback)) + return; + +done: + notify_data_unref(notify_data); +} + +static void notify_handler(void *data, void *user_data) +{ + struct notify_data *notify_data = data; + struct pdu_data *pdu_data = user_data; + uint16_t value_handle; + const uint8_t *value = NULL; + + value_handle = get_le16(pdu_data->pdu); + + if (notify_data->chrc->value_handle != value_handle) + return; + + if (pdu_data->length > 2) + value = pdu_data->pdu + 2; + + /* + * Even if the notify data has a pending ATT request to write to the + * CCC, there is really no reason not to notify the handlers. + */ + if (notify_data->notify) + notify_data->notify(value_handle, value, pdu_data->length - 2, + notify_data->user_data); +} + +static void notify_cb(uint8_t opcode, const void *pdu, uint16_t length, + void *user_data) +{ + struct bt_gatt_client *client = user_data; + struct pdu_data pdu_data; + + bt_gatt_client_ref(client); + + memset(&pdu_data, 0, sizeof(pdu_data)); + pdu_data.pdu = pdu; + pdu_data.length = length; + + queue_foreach(client->notify_list, notify_handler, &pdu_data); + + if (opcode == BT_ATT_OP_HANDLE_VAL_IND) + bt_att_send(client->att, BT_ATT_OP_HANDLE_VAL_CONF, NULL, 0, + NULL, NULL, NULL); + + bt_gatt_client_unref(client); +} + +static void notify_data_cleanup(void *data) +{ + struct notify_data *notify_data = data; + + if (notify_data->att_id) + bt_att_cancel(notify_data->client->att, notify_data->att_id); + + notify_data_unref(notify_data); +} + +static void bt_gatt_client_free(struct bt_gatt_client *client) +{ + bt_gatt_client_cancel_all(client); + + queue_destroy(client->notify_list, notify_data_cleanup); + + if (client->ready_destroy) + client->ready_destroy(client->ready_data); + + if (client->debug_destroy) + client->debug_destroy(client->debug_data); + + if (client->att) { + bt_att_unregister_disconnect(client->att, client->disc_id); + bt_att_unregister(client->att, client->notify_id); + bt_att_unregister(client->att, client->ind_id); + bt_att_unref(client->att); + } + + gatt_db_unref(client->db); + + queue_destroy(client->svc_chngd_queue, free); + queue_destroy(client->long_write_queue, request_unref); + queue_destroy(client->notify_chrcs, notify_chrc_free); + queue_destroy(client->pending_requests, request_unref); + + free(client); +} + +static void att_disconnect_cb(int err, void *user_data) +{ + struct bt_gatt_client *client = user_data; + bool in_init = client->in_init; + + client->disc_id = 0; + + bt_att_unref(client->att); + client->att = NULL; + + client->in_init = false; + client->ready = false; + + if (in_init) + notify_client_ready(client, false, 0); +} + +struct bt_gatt_client *bt_gatt_client_new(struct gatt_db *db, + struct bt_att *att, + uint16_t mtu) +{ + struct bt_gatt_client *client; + + if (!att || !db) + return NULL; + + client = new0(struct bt_gatt_client, 1); + if (!client) + return NULL; + + client->disc_id = bt_att_register_disconnect(att, att_disconnect_cb, + client, NULL); + if (!client->disc_id) + goto fail; + + client->long_write_queue = queue_new(); + if (!client->long_write_queue) + goto fail; + + client->svc_chngd_queue = queue_new(); + if (!client->svc_chngd_queue) + goto fail; + + client->notify_list = queue_new(); + if (!client->notify_list) + goto fail; + + client->notify_chrcs = queue_new(); + if (!client->notify_chrcs) + goto fail; + + client->pending_requests = queue_new(); + if (!client->pending_requests) + goto fail; + + client->notify_id = bt_att_register(att, BT_ATT_OP_HANDLE_VAL_NOT, + notify_cb, client, NULL); + if (!client->notify_id) + goto fail; + + client->ind_id = bt_att_register(att, BT_ATT_OP_HANDLE_VAL_IND, + notify_cb, client, NULL); + if (!client->ind_id) + goto fail; + + client->att = bt_att_ref(att); + client->db = gatt_db_ref(db); + + if (!gatt_client_init(client, mtu)) + goto fail; + + return bt_gatt_client_ref(client); + +fail: + bt_gatt_client_free(client); + return NULL; +} + +struct bt_gatt_client *bt_gatt_client_ref(struct bt_gatt_client *client) +{ + if (!client) + return NULL; + + __sync_fetch_and_add(&client->ref_count, 1); + + return client; +} + +void bt_gatt_client_unref(struct bt_gatt_client *client) +{ + if (!client) + return; + + if (__sync_sub_and_fetch(&client->ref_count, 1)) + return; + + bt_gatt_client_free(client); +} + +bool bt_gatt_client_is_ready(struct bt_gatt_client *client) +{ + return (client && client->ready); +} + +bool bt_gatt_client_set_ready_handler(struct bt_gatt_client *client, + bt_gatt_client_callback_t callback, + void *user_data, + bt_gatt_client_destroy_func_t destroy) +{ + if (!client) + return false; + + if (client->ready_destroy) + client->ready_destroy(client->ready_data); + + client->ready_callback = callback; + client->ready_destroy = destroy; + client->ready_data = user_data; + + return true; +} + +bool bt_gatt_client_set_service_changed(struct bt_gatt_client *client, + bt_gatt_client_service_changed_callback_t callback, + void *user_data, + bt_gatt_client_destroy_func_t destroy) +{ + if (!client) + return false; + + if (client->svc_chngd_destroy) + client->svc_chngd_destroy(client->svc_chngd_data); + + client->svc_chngd_callback = callback; + client->svc_chngd_destroy = destroy; + client->svc_chngd_data = user_data; + + return true; +} + +bool bt_gatt_client_set_debug(struct bt_gatt_client *client, + bt_gatt_client_debug_func_t callback, + void *user_data, + bt_gatt_client_destroy_func_t destroy) { + if (!client) + return false; + + if (client->debug_destroy) + client->debug_destroy(client->debug_data); + + client->debug_callback = callback; + client->debug_destroy = destroy; + client->debug_data = user_data; + + return true; +} + +uint16_t bt_gatt_client_get_mtu(struct bt_gatt_client *client) +{ + if (!client || !client->att) + return 0; + + return bt_att_get_mtu(client->att); +} + +static bool match_req_id(const void *a, const void *b) +{ + const struct request *req = a; + unsigned int id = PTR_TO_UINT(b); + + return req->id == id; +} + +static void cancel_long_write_cb(uint8_t opcode, const void *pdu, uint16_t len, + void *user_data) +{ + struct bt_gatt_client *client = user_data; + + if (queue_isempty(client->long_write_queue)) + client->in_long_write = false; +} + +static bool cancel_long_write_req(struct bt_gatt_client *client, + struct request *req) +{ + uint8_t pdu = 0x00; + + /* + * att_id == 0 means that request has been queued and no prepare write + * has been sent so far.Let's just remove if from the queue. + * Otherwise execute write needs to be send. + */ + if (!req->att_id) + return queue_remove(client->long_write_queue, req); + + return !!bt_att_send(client->att, BT_ATT_OP_EXEC_WRITE_REQ, &pdu, + sizeof(pdu), + cancel_long_write_cb, + client, NULL); + +} + +static void cancel_prep_write_cb(uint8_t opcode, const void *pdu, uint16_t len, + void *user_data) +{ + struct request *req = user_data; + struct bt_gatt_client *client = req->client; + + client->reliable_write_session_id = 0; +} + +static bool cancel_prep_write_session(struct bt_gatt_client *client, + struct request *req) +{ + uint8_t pdu = 0x00; + + return !!bt_att_send(client->att, BT_ATT_OP_EXEC_WRITE_REQ, &pdu, + sizeof(pdu), + cancel_prep_write_cb, + req, request_unref); +} + +bool bt_gatt_client_cancel(struct bt_gatt_client *client, unsigned int id) +{ + struct request *req; + + if (!client || !id || !client->att) + return false; + + req = queue_remove_if(client->pending_requests, match_req_id, + UINT_TO_PTR(id)); + if (!req) + return false; + + req->removed = true; + + if (!bt_att_cancel(client->att, req->att_id) && !req->long_write && + !req->prep_write) + return false; + + /* If this was a long-write, we need to abort all prepared writes */ + if (req->long_write) + return cancel_long_write_req(client, req); + + if (req->prep_write) + return cancel_prep_write_session(client, req); + + return true; +} + +static void cancel_request(void *data) +{ + struct request *req = data; + + req->removed = true; + + bt_att_cancel(req->client->att, req->att_id); + + if (req->long_write) + cancel_long_write_req(req->client, req); + + if (req->prep_write) + cancel_prep_write_session(req->client, req); +} + +bool bt_gatt_client_cancel_all(struct bt_gatt_client *client) +{ + if (!client || !client->att) + return false; + + queue_remove_all(client->pending_requests, NULL, NULL, cancel_request); + + if (client->discovery_req) { + bt_gatt_request_cancel(client->discovery_req); + bt_gatt_request_unref(client->discovery_req); + client->discovery_req = NULL; + } + + if (client->mtu_req_id) + bt_att_cancel(client->att, client->mtu_req_id); + + return true; +} + +struct read_op { + bt_gatt_client_read_callback_t callback; + void *user_data; + bt_gatt_client_destroy_func_t destroy; +}; + +static void destroy_read_op(void *data) +{ + struct read_op *op = data; + + if (op->destroy) + op->destroy(op->user_data); + + free(op); +} + +static void read_cb(uint8_t opcode, const void *pdu, uint16_t length, + void *user_data) +{ + struct request *req = user_data; + struct read_op *op = req->data; + bool success; + uint8_t att_ecode = 0; + const uint8_t *value = NULL; + uint16_t value_len = 0; + + if (opcode == BT_ATT_OP_ERROR_RSP) { + success = false; + att_ecode = process_error(pdu, length); + goto done; + } + + if (opcode != BT_ATT_OP_READ_RSP || (!pdu && length)) { + success = false; + goto done; + } + + success = true; + value_len = length; + if (value_len) + value = pdu; + +done: + if (op->callback) + op->callback(success, att_ecode, value, length, op->user_data); +} + +unsigned int bt_gatt_client_read_value(struct bt_gatt_client *client, + uint16_t value_handle, + bt_gatt_client_read_callback_t callback, + void *user_data, + bt_gatt_client_destroy_func_t destroy) +{ + struct request *req; + struct read_op *op; + uint8_t pdu[2]; + + if (!client) + return 0; + + op = new0(struct read_op, 1); + if (!op) + return 0; + + req = request_create(client); + if (!req) { + free(op); + return 0; + } + + op->callback = callback; + op->user_data = user_data; + op->destroy = destroy; + + req->data = op; + req->destroy = destroy_read_op; + + put_le16(value_handle, pdu); + + req->att_id = bt_att_send(client->att, BT_ATT_OP_READ_REQ, + pdu, sizeof(pdu), + read_cb, req, + request_unref); + if (!req->att_id) { + op->destroy = NULL; + request_unref(req); + return 0; + } + + return req->id; +} + +static void read_multiple_cb(uint8_t opcode, const void *pdu, uint16_t length, + void *user_data) +{ + struct request *req = user_data; + struct read_op *op = req->data; + uint8_t att_ecode; + bool success; + + if (opcode != BT_ATT_OP_READ_MULT_RSP || (!pdu && length)) { + success = false; + + if (opcode == BT_ATT_OP_ERROR_RSP) + att_ecode = process_error(pdu, length); + else + att_ecode = 0; + + pdu = NULL; + length = 0; + } else { + success = true; + att_ecode = 0; + } + + if (op->callback) + op->callback(success, att_ecode, pdu, length, op->user_data); +} + +unsigned int bt_gatt_client_read_multiple(struct bt_gatt_client *client, + uint16_t *handles, uint8_t num_handles, + bt_gatt_client_read_callback_t callback, + void *user_data, + bt_gatt_client_destroy_func_t destroy) +{ + uint8_t pdu[num_handles * 2]; + struct request *req; + struct read_op *op; + int i; + + if (!client) + return 0; + + if (num_handles < 2) + return 0; + + if (num_handles * 2 > bt_att_get_mtu(client->att) - 1) + return 0; + + op = new0(struct read_op, 1); + if (!op) + return 0; + + req = request_create(client); + if (!req) { + free(op); + return 0; + } + + op->callback = callback; + op->user_data = user_data; + op->destroy = destroy; + + req->data = op; + req->destroy = destroy_read_op; + + for (i = 0; i < num_handles; i++) + put_le16(handles[i], pdu + (2 * i)); + + req->att_id = bt_att_send(client->att, BT_ATT_OP_READ_MULT_REQ, + pdu, sizeof(pdu), + read_multiple_cb, req, + request_unref); + if (!req->att_id) { + op->destroy = NULL; + request_unref(req); + return 0; + } + + return req->id; +} + +struct read_long_op { + struct bt_gatt_client *client; + int ref_count; + uint16_t value_handle; + uint16_t offset; + struct iovec iov; + bt_gatt_client_read_callback_t callback; + void *user_data; + bt_gatt_client_destroy_func_t destroy; +}; + +static void destroy_read_long_op(void *data) +{ + struct read_long_op *op = data; + + if (op->destroy) + op->destroy(op->user_data); + + free(op->iov.iov_base); + free(op); +} + +static bool append_chunk(struct read_long_op *op, const uint8_t *data, + uint16_t len) +{ + void *buf; + + /* Truncate if the data would exceed maximum length */ + if (op->offset + len > BT_ATT_MAX_VALUE_LEN) + len = BT_ATT_MAX_VALUE_LEN - op->offset; + + buf = realloc(op->iov.iov_base, op->iov.iov_len + len); + if (!buf) + return false; + + op->iov.iov_base = buf; + + memcpy(op->iov.iov_base + op->iov.iov_len, data, len); + + op->iov.iov_len += len; + op->offset += len; + + return true; +} + +static void read_long_cb(uint8_t opcode, const void *pdu, + uint16_t length, void *user_data) +{ + struct request *req = user_data; + struct read_long_op *op = req->data; + bool success; + uint8_t att_ecode = 0; + + if (opcode == BT_ATT_OP_ERROR_RSP) { + success = false; + att_ecode = process_error(pdu, length); + goto done; + } + + if (opcode != BT_ATT_OP_READ_BLOB_RSP || (!pdu && length)) { + success = false; + goto done; + } + + if (!length) + goto success; + + if (!append_chunk(op, pdu, length)) { + success = false; + goto done; + } + + if (op->offset >= BT_ATT_MAX_VALUE_LEN) + goto success; + + if (length >= bt_att_get_mtu(op->client->att) - 1) { + uint8_t pdu[4]; + + put_le16(op->value_handle, pdu); + put_le16(op->offset, pdu + 2); + + req->att_id = bt_att_send(op->client->att, + BT_ATT_OP_READ_BLOB_REQ, + pdu, sizeof(pdu), + read_long_cb, + request_ref(req), + request_unref); + if (req->att_id) + return; + + request_unref(req); + success = false; + goto done; + } + +success: + success = true; + +done: + if (op->callback) + op->callback(success, att_ecode, op->iov.iov_base, + op->iov.iov_len, op->user_data); +} + +unsigned int bt_gatt_client_read_long_value(struct bt_gatt_client *client, + uint16_t value_handle, uint16_t offset, + bt_gatt_client_read_callback_t callback, + void *user_data, + bt_gatt_client_destroy_func_t destroy) +{ + struct request *req; + struct read_long_op *op; + uint8_t pdu[4]; + + if (!client) + return 0; + + op = new0(struct read_long_op, 1); + if (!op) + return 0; + + req = request_create(client); + if (!req) { + free(op); + return 0; + } + + op->client = client; + op->value_handle = value_handle; + op->offset = offset; + op->callback = callback; + op->user_data = user_data; + op->destroy = destroy; + + req->data = op; + req->destroy = destroy_read_long_op; + + put_le16(value_handle, pdu); + put_le16(offset, pdu + 2); + + req->att_id = bt_att_send(client->att, BT_ATT_OP_READ_BLOB_REQ, + pdu, sizeof(pdu), + read_long_cb, req, + request_unref); + if (!req->att_id) { + op->destroy = NULL; + request_unref(req); + return 0; + } + + return req->id; +} + +unsigned int bt_gatt_client_write_without_response( + struct bt_gatt_client *client, + uint16_t value_handle, + bool signed_write, + const uint8_t *value, uint16_t length) { + uint8_t pdu[2 + length]; + struct request *req; + int security; + uint8_t op; + + if (!client) + return 0; + + req = request_create(client); + if (!req) + return 0; + + /* Only use signed write if unencrypted */ + if (signed_write) { + security = bt_att_get_sec_level(client->att); + op = security > BT_SECURITY_LOW ? BT_ATT_OP_WRITE_CMD : + BT_ATT_OP_SIGNED_WRITE_CMD; + } else + op = BT_ATT_OP_WRITE_CMD; + + put_le16(value_handle, pdu); + memcpy(pdu + 2, value, length); + + req->att_id = bt_att_send(client->att, op, pdu, sizeof(pdu), NULL, req, + request_unref); + if (!req->att_id) { + request_unref(req); + return 0; + } + + return req->id; +} + +struct write_op { + struct bt_gatt_client *client; + bt_gatt_client_callback_t callback; + void *user_data; + bt_gatt_destroy_func_t destroy; +}; + +static void destroy_write_op(void *data) +{ + struct write_op *op = data; + + if (op->destroy) + op->destroy(op->user_data); + + free(op); +} + +static void write_cb(uint8_t opcode, const void *pdu, uint16_t length, + void *user_data) +{ + struct request *req = user_data; + struct write_op *op = req->data; + bool success = true; + uint8_t att_ecode = 0; + + if (opcode == BT_ATT_OP_ERROR_RSP) { + success = false; + att_ecode = process_error(pdu, length); + goto done; + } + + if (opcode != BT_ATT_OP_WRITE_RSP || pdu || length) + success = false; + +done: + if (op->callback) + op->callback(success, att_ecode, op->user_data); +} + +unsigned int bt_gatt_client_write_value(struct bt_gatt_client *client, + uint16_t value_handle, + const uint8_t *value, uint16_t length, + bt_gatt_client_callback_t callback, + void *user_data, + bt_gatt_client_destroy_func_t destroy) +{ + struct request *req; + struct write_op *op; + uint8_t pdu[2 + length]; + + if (!client) + return 0; + + op = new0(struct write_op, 1); + if (!op) + return 0; + + req = request_create(client); + if (!req) { + free(op); + return 0; + } + + op->callback = callback; + op->user_data = user_data; + op->destroy = destroy; + + req->data = op; + req->destroy = destroy_write_op; + + put_le16(value_handle, pdu); + memcpy(pdu + 2, value, length); + + req->att_id = bt_att_send(client->att, BT_ATT_OP_WRITE_REQ, + pdu, sizeof(pdu), + write_cb, req, + request_unref); + if (!req->att_id) { + op->destroy = NULL; + request_unref(req); + return 0; + } + + return req->id; +} + +struct long_write_op { + struct bt_gatt_client *client; + bool reliable; + bool success; + uint8_t att_ecode; + bool reliable_error; + uint16_t value_handle; + uint8_t *value; + uint16_t length; + uint16_t offset; + uint16_t index; + uint16_t cur_length; + bt_gatt_client_write_long_callback_t callback; + void *user_data; + bt_gatt_client_destroy_func_t destroy; +}; + +static void long_write_op_free(void *data) +{ + struct long_write_op *op = data; + + if (op->destroy) + op->destroy(op->user_data); + + free(op->value); + free(op); +} + +static void prepare_write_cb(uint8_t opcode, const void *pdu, uint16_t length, + void *user_data); +static void complete_write_long_op(struct request *req, bool success, + uint8_t att_ecode, bool reliable_error); + +static void handle_next_prep_write(struct request *req) +{ + struct long_write_op *op = req->data; + bool success = true; + uint8_t *pdu; + + pdu = malloc(op->cur_length + 4); + if (!pdu) { + success = false; + goto done; + } + + put_le16(op->value_handle, pdu); + put_le16(op->offset + op->index, pdu + 2); + memcpy(pdu + 4, op->value + op->index, op->cur_length); + + req->att_id = bt_att_send(op->client->att, BT_ATT_OP_PREP_WRITE_REQ, + pdu, op->cur_length + 4, + prepare_write_cb, + request_ref(req), + request_unref); + if (!req->att_id) { + request_unref(req); + success = false; + } + + free(pdu); + + /* If so far successful, then the operation should continue. + * Otherwise, there was an error and the procedure should be + * completed. + */ + if (success) + return; + +done: + complete_write_long_op(req, success, 0, false); +} + +static void start_next_long_write(struct bt_gatt_client *client) +{ + struct request *req; + + if (queue_isempty(client->long_write_queue)) { + client->in_long_write = false; + return; + } + + req = queue_pop_head(client->long_write_queue); + if (!req) + return; + + handle_next_prep_write(req); + + /* + * send_next_prep_write adds an extra ref. Unref here to clean up if + * necessary, since we also added a ref before pushing to the queue. + */ + request_unref(req); +} + +static void execute_write_cb(uint8_t opcode, const void *pdu, uint16_t length, + void *user_data) +{ + struct request *req = user_data; + struct long_write_op *op = req->data; + bool success = op->success; + uint8_t att_ecode = op->att_ecode; + + if (opcode == BT_ATT_OP_ERROR_RSP) { + success = false; + att_ecode = process_error(pdu, length); + } else if (opcode != BT_ATT_OP_EXEC_WRITE_RSP || pdu || length) + success = false; + + if (op->callback) + op->callback(success, op->reliable_error, att_ecode, + op->user_data); + + start_next_long_write(op->client); +} + +static void complete_write_long_op(struct request *req, bool success, + uint8_t att_ecode, bool reliable_error) +{ + struct long_write_op *op = req->data; + uint8_t pdu; + + op->success = success; + op->att_ecode = att_ecode; + op->reliable_error = reliable_error; + + if (success) + pdu = 0x01; /* Write */ + else + pdu = 0x00; /* Cancel */ + + req->att_id = bt_att_send(op->client->att, BT_ATT_OP_EXEC_WRITE_REQ, + &pdu, sizeof(pdu), + execute_write_cb, + request_ref(req), + request_unref); + if (req->att_id) + return; + + + request_unref(req); + success = false; + + if (op->callback) + op->callback(success, reliable_error, att_ecode, op->user_data); + + start_next_long_write(op->client); +} + +static void prepare_write_cb(uint8_t opcode, const void *pdu, uint16_t length, + void *user_data) +{ + struct request *req = user_data; + struct long_write_op *op = req->data; + bool success = true; + bool reliable_error = false; + uint8_t att_ecode = 0; + uint16_t next_index; + + if (opcode == BT_ATT_OP_ERROR_RSP) { + success = false; + att_ecode = process_error(pdu, length); + goto done; + } + + if (opcode != BT_ATT_OP_PREP_WRITE_RSP) { + success = false; + goto done; + } + + if (op->reliable) { + if (!pdu || length != (op->cur_length + 4)) { + success = false; + reliable_error = true; + goto done; + } + + if (get_le16(pdu) != op->value_handle || + get_le16(pdu + 2) != (op->offset + op->index)) { + success = false; + reliable_error = true; + goto done; + } + + if (memcmp(pdu + 4, op->value + op->index, op->cur_length)) { + success = false; + reliable_error = true; + goto done; + } + } + + next_index = op->index + op->cur_length; + if (next_index == op->length) { + /* All bytes written */ + goto done; + } + + /* If the last written length was greater than or equal to what can fit + * inside a PDU, then there is more data to send. + */ + if (op->cur_length >= bt_att_get_mtu(op->client->att) - 5) { + op->index = next_index; + op->cur_length = MIN(op->length - op->index, + bt_att_get_mtu(op->client->att) - 5); + handle_next_prep_write(req); + return; + } + +done: + complete_write_long_op(req, success, att_ecode, reliable_error); +} + +unsigned int bt_gatt_client_write_long_value(struct bt_gatt_client *client, + bool reliable, + uint16_t value_handle, uint16_t offset, + const uint8_t *value, uint16_t length, + bt_gatt_client_write_long_callback_t callback, + void *user_data, + bt_gatt_client_destroy_func_t destroy) +{ + struct request *req; + struct long_write_op *op; + uint8_t *pdu; + + if (!client) + return 0; + + if ((size_t)(length + offset) > UINT16_MAX) + return 0; + + /* Don't allow writing a 0-length value using this procedure. The + * upper-layer should use bt_gatt_write_value for that instead. + */ + if (!length || !value) + return 0; + + op = new0(struct long_write_op, 1); + if (!op) + return 0; + + op->value = malloc(length); + if (!op->value) { + free(op); + return 0; + } + + req = request_create(client); + if (!req) { + free(op->value); + free(op); + return 0; + } + + memcpy(op->value, value, length); + + op->client = client; + op->reliable = reliable; + op->value_handle = value_handle; + op->length = length; + op->offset = offset; + op->cur_length = MIN(length, bt_att_get_mtu(client->att) - 5); + op->callback = callback; + op->user_data = user_data; + op->destroy = destroy; + + req->data = op; + req->destroy = long_write_op_free; + req->long_write = true; + + if (client->in_long_write || client->reliable_write_session_id > 0) { + queue_push_tail(client->long_write_queue, req); + return req->id; + } + + pdu = malloc(op->cur_length + 4); + if (!pdu) { + free(op->value); + free(op); + return 0; + } + + put_le16(value_handle, pdu); + put_le16(offset, pdu + 2); + memcpy(pdu + 4, op->value, op->cur_length); + + req->att_id = bt_att_send(client->att, BT_ATT_OP_PREP_WRITE_REQ, + pdu, op->cur_length + 4, + prepare_write_cb, req, + request_unref); + free(pdu); + + if (!req->att_id) { + op->destroy = NULL; + request_unref(req); + return 0; + } + + client->in_long_write = true; + + return req->id; +} + +struct prep_write_op { + bt_gatt_client_write_long_callback_t callback; + void *user_data; + bt_gatt_destroy_func_t destroy; + uint8_t *pdu; + uint16_t pdu_len; +}; + +static void destroy_prep_write_op(void *data) +{ + struct prep_write_op *op = data; + + if (op->destroy) + op->destroy(op->user_data); + + free(op->pdu); + free(op); +} + +static void prep_write_cb(uint8_t opcode, const void *pdu, uint16_t length, + void *user_data) +{ + struct request *req = user_data; + struct prep_write_op *op = req->data; + bool success; + uint8_t att_ecode; + bool reliable_error; + + if (opcode == BT_ATT_OP_ERROR_RSP) { + success = false; + reliable_error = false; + att_ecode = process_error(pdu, length); + goto done; + } + + if (opcode != BT_ATT_OP_PREP_WRITE_RSP) { + success = false; + reliable_error = false; + att_ecode = 0; + goto done; + } + + if (!pdu || length != op->pdu_len || + memcmp(pdu, op->pdu, op->pdu_len)) { + success = false; + reliable_error = true; + att_ecode = 0; + goto done; + } + + success = true; + reliable_error = false; + att_ecode = 0; + +done: + if (op->callback) + op->callback(success, reliable_error, att_ecode, op->user_data); +} + +static struct request *get_reliable_request(struct bt_gatt_client *client, + unsigned int id) +{ + struct request *req; + struct prep_write_op *op; + + op = new0(struct prep_write_op, 1); + if (!op) + return NULL; + + /* Following prepare writes */ + if (id != 0) + req = queue_find(client->pending_requests, match_req_id, + UINT_TO_PTR(id)); + else + req = request_create(client); + + if (!req) { + free(op); + return NULL; + } + + req->data = op; + + return req; +} + +unsigned int bt_gatt_client_prepare_write(struct bt_gatt_client *client, + unsigned int id, uint16_t value_handle, + uint16_t offset, uint8_t *value, + uint16_t length, + bt_gatt_client_write_long_callback_t callback, + void *user_data, + bt_gatt_client_destroy_func_t destroy) +{ + struct request *req; + struct prep_write_op *op; + uint8_t pdu[4 + length]; + + if (!client) + return 0; + + if (client->in_long_write) + return 0; + + /* + * Make sure that client who owns reliable session continues with + * prepare writes or this is brand new reliable session (id == 0) + */ + if (id != client->reliable_write_session_id) { + util_debug(client->debug_callback, client->debug_data, + "There is other reliable write session ongoing %u", + client->reliable_write_session_id); + + return 0; + } + + req = get_reliable_request(client, id); + if (!req) + return 0; + + op = (struct prep_write_op *)req->data; + + op->callback = callback; + op->user_data = user_data; + op->destroy = destroy; + + req->destroy = destroy_prep_write_op; + req->prep_write = true; + + put_le16(value_handle, pdu); + put_le16(offset, pdu + 2); + memcpy(pdu + 4, value, length); + + /* + * Before sending command we need to remember pdu as we need to validate + * it in the response. Store handle, offset and value. Therefore + * increase length by 4 (handle + offset) as we need it in couple places + * below + */ + length += 4; + + op->pdu = malloc(length); + if (!op->pdu) { + op->destroy = NULL; + request_unref(req); + return 0; + } + + memcpy(op->pdu, pdu, length); + op->pdu_len = length; + + /* + * Now we are ready to send command + * Note that request_unref will be done on write execute + */ + req->att_id = bt_att_send(client->att, BT_ATT_OP_PREP_WRITE_REQ, pdu, + sizeof(pdu), prep_write_cb, req, + NULL); + if (!req->att_id) { + op->destroy = NULL; + request_unref(req); + return 0; + } + + /* + * Store first request id for prepare write and treat it as a session id + * valid until write execute is done + */ + if (client->reliable_write_session_id == 0) + client->reliable_write_session_id = req->id; + + return client->reliable_write_session_id; +} + +static void exec_write_cb(uint8_t opcode, const void *pdu, uint16_t length, + void *user_data) +{ + struct request *req = user_data; + struct write_op *op = req->data; + bool success; + uint8_t att_ecode; + + if (opcode == BT_ATT_OP_ERROR_RSP) { + success = false; + att_ecode = process_error(pdu, length); + goto done; + } + + if (opcode != BT_ATT_OP_EXEC_WRITE_RSP || pdu || length) { + success = false; + att_ecode = 0; + goto done; + } + + success = true; + att_ecode = 0; + +done: + if (op->callback) + op->callback(success, att_ecode, op->user_data); + + op->client->reliable_write_session_id = 0; + + start_next_long_write(op->client); +} + +unsigned int bt_gatt_client_write_execute(struct bt_gatt_client *client, + unsigned int id, + bt_gatt_client_callback_t callback, + void *user_data, + bt_gatt_client_destroy_func_t destroy) +{ + struct request *req; + struct write_op *op; + uint8_t pdu; + + if (!client) + return 0; + + if (client->in_long_write) + return 0; + + if (client->reliable_write_session_id != id) + return 0; + + op = new0(struct write_op, 1); + if (!op) + return 0; + + req = queue_find(client->pending_requests, match_req_id, + UINT_TO_PTR(id)); + if (!req) { + free(op); + return 0; + } + + op->client = client; + op->callback = callback; + op->user_data = user_data; + op->destroy = destroy; + + pdu = 0x01; + + req->data = op; + req->destroy = destroy_write_op; + + req->att_id = bt_att_send(client->att, BT_ATT_OP_EXEC_WRITE_REQ, &pdu, + sizeof(pdu), exec_write_cb, + req, request_unref); + if (!req->att_id) { + op->destroy = NULL; + request_unref(req); + return 0; + } + + return id; +} + +static bool match_notify_chrc_value_handle(const void *a, const void *b) +{ + const struct notify_chrc *chrc = a; + uint16_t value_handle = PTR_TO_UINT(b); + + return chrc->value_handle == value_handle; +} + +unsigned int bt_gatt_client_register_notify(struct bt_gatt_client *client, + uint16_t chrc_value_handle, + bt_gatt_client_register_callback_t callback, + bt_gatt_client_notify_callback_t notify, + void *user_data, + bt_gatt_client_destroy_func_t destroy) +{ + struct notify_data *notify_data; + struct notify_chrc *chrc = NULL; + + if (!client || !client->db || !chrc_value_handle || !callback) + return 0; + + if (!bt_gatt_client_is_ready(client) || client->in_svc_chngd) + return 0; + + /* Check if a characteristic ref count has been started already */ + chrc = queue_find(client->notify_chrcs, match_notify_chrc_value_handle, + UINT_TO_PTR(chrc_value_handle)); + + if (!chrc) { + /* + * Create an entry if the characteristic is known and has a CCC + * descriptor. + */ + chrc = notify_chrc_create(client, chrc_value_handle); + if (!chrc) + return 0; + } + + /* Fail if we've hit the maximum allowed notify sessions */ + if (chrc->notify_count == INT_MAX) + return 0; + + notify_data = new0(struct notify_data, 1); + if (!notify_data) + return 0; + + notify_data->client = client; + notify_data->ref_count = 1; + notify_data->chrc = chrc; + notify_data->callback = callback; + notify_data->notify = notify; + notify_data->user_data = user_data; + notify_data->destroy = destroy; + + /* Add the handler to the bt_gatt_client's general list */ + queue_push_tail(client->notify_list, notify_data); + + /* Assign an ID to the handler. */ + if (client->next_reg_id < 1) + client->next_reg_id = 1; + + notify_data->id = client->next_reg_id++; + + /* + * If a write to the CCC descriptor is in progress, then queue this + * request. + */ + if (chrc->ccc_write_id) { + queue_push_tail(chrc->reg_notify_queue, notify_data); + return notify_data->id; + } + + /* + * If the ref count is not zero, then notifications are already enabled. + */ + if (chrc->notify_count > 0 || !chrc->ccc_handle) { + complete_notify_request(notify_data); + return notify_data->id; + } + + /* Write to the CCC descriptor */ + if (!notify_data_write_ccc(notify_data, true, enable_ccc_callback)) { + queue_remove(client->notify_list, notify_data); + free(notify_data); + return 0; + } + + return notify_data->id; +} + +bool bt_gatt_client_unregister_notify(struct bt_gatt_client *client, + unsigned int id) +{ + struct notify_data *notify_data; + + if (!client || !id) + return false; + + notify_data = queue_remove_if(client->notify_list, match_notify_data_id, + UINT_TO_PTR(id)); + if (!notify_data) + return false; + + assert(notify_data->chrc->notify_count > 0); + assert(!notify_data->chrc->ccc_write_id); + + complete_unregister_notify(notify_data); + return true; +} + +bool bt_gatt_client_set_sec_level(struct bt_gatt_client *client, + int level) +{ + if (!client) + return false; + + return bt_att_set_sec_level(client->att, level); +} + +int bt_gatt_client_get_sec_level(struct bt_gatt_client *client) +{ + if (!client) + return -1; + + return bt_att_get_sec_level(client->att); +} -- cgit v1.2.3