From d297a03b7a6169cb556f7fcdbb1ee81648bb2b5f Mon Sep 17 00:00:00 2001 From: Sathish Narasimman Date: Tue, 22 Nov 2022 15:42:29 +0530 Subject: shared/csip: Add initial code for handling CSIP This adds initial code for Coordinated Set Identification Profile. --- src/shared/csip.c | 866 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/shared/csip.h | 67 +++++ 2 files changed, 933 insertions(+) create mode 100644 src/shared/csip.c create mode 100644 src/shared/csip.h (limited to 'src') diff --git a/src/shared/csip.c b/src/shared/csip.c new file mode 100644 index 000000000..ff2047a4a --- /dev/null +++ b/src/shared/csip.c @@ -0,0 +1,866 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2022 Intel Corporation. All rights reserved. + * + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +#include + +#include "lib/bluetooth.h" +#include "lib/uuid.h" + +#include "src/shared/queue.h" +#include "src/shared/util.h" +#include "src/shared/timeout.h" +#include "src/shared/att.h" +#include "src/shared/gatt-db.h" +#include "src/shared/gatt-server.h" +#include "src/shared/gatt-client.h" +#include "src/shared/crypto.h" +#include "src/shared/csip.h" + +#define DBG(_csip, fmt, arg...) \ + csip_debug(_csip, "%s:%s() " fmt, __FILE__, __func__, ## arg) + +/* SIRK is now hardcoded in the code. This can be moved + * to a configuration file. Since the code is to validate + * the CSIP use case of set member + */ +#define SIRK "761FAE703ED681F0C50B34155B6434FB" +#define CSIS_SIZE 0x02 +#define CSIS_LOCK 0x01 +#define CSIS_RANK 0x01 +#define CSIS_PLAINTEXT 0x01 +#define CSIS_ENC 0x02 + +struct bt_csip_db { + struct gatt_db *db; + struct bt_csis *csis; +}; + +struct csis_sirk { + uint8_t type; + uint8_t val[16]; +} __packed; + +struct bt_csis { + struct bt_csip_db *cdb; + struct csis_sirk *sirk_val; + uint8_t size_val; + uint8_t lock_val; + uint8_t rank_val; + struct gatt_db_attribute *service; + struct gatt_db_attribute *sirk; + struct gatt_db_attribute *size; + struct gatt_db_attribute *lock; + struct gatt_db_attribute *lock_ccc; + struct gatt_db_attribute *rank; +}; + +struct bt_csip_cb { + unsigned int id; + bt_csip_func_t attached; + bt_csip_func_t detached; + void *user_data; +}; + +struct bt_csip_ready { + unsigned int id; + bt_csip_ready_func_t func; + bt_csip_destroy_func_t destroy; + void *data; +}; + +struct bt_csip { + int ref_count; + struct bt_csip_db *ldb; + struct bt_csip_db *rdb; + struct bt_gatt_client *client; + struct bt_att *att; + + unsigned int idle_id; + struct queue *ready_cbs; + + bt_csip_debug_func_t debug_func; + bt_csip_destroy_func_t debug_destroy; + void *debug_data; + + bt_csip_ltk_func_t ltk_func; + void *ltk_data; + + bt_csip_sirk_func_t sirk_func; + void *sirk_data; + + void *user_data; +}; + +static struct queue *csip_db; +static struct queue *csip_cbs; +static struct queue *sessions; + +static void csip_detached(void *data, void *user_data) +{ + struct bt_csip_cb *cb = data; + struct bt_csip *csip = user_data; + + cb->detached(csip, cb->user_data); +} + +void bt_csip_detach(struct bt_csip *csip) +{ + if (!queue_remove(sessions, csip)) + return; + + bt_gatt_client_unref(csip->client); + csip->client = NULL; + + queue_foreach(csip_cbs, csip_detached, csip); +} + +static void csip_db_free(void *data) +{ + struct bt_csip_db *cdb = data; + + if (!cdb) + return; + + gatt_db_unref(cdb->db); + + free(cdb->csis); + free(cdb); +} + +static void csip_ready_free(void *data) +{ + struct bt_csip_ready *ready = data; + + if (ready->destroy) + ready->destroy(ready->data); + + free(ready); +} + +static void csip_free(void *data) +{ + struct bt_csip *csip = data; + + bt_csip_detach(csip); + + csip_db_free(csip->rdb); + + queue_destroy(csip->ready_cbs, csip_ready_free); + + free(csip); +} + +struct bt_att *bt_csip_get_att(struct bt_csip *csip) +{ + if (!csip) + return NULL; + + if (csip->att) + return csip->att; + + return bt_gatt_client_get_att(csip->client); +} + +struct bt_csip *bt_csip_ref(struct bt_csip *csip) +{ + if (!csip) + return NULL; + + __sync_fetch_and_add(&csip->ref_count, 1); + + return csip; +} + +static struct bt_csip *bt_csip_ref_safe(struct bt_csip *csip) +{ + if (!csip || !csip->ref_count) + return NULL; + + return bt_csip_ref(csip); +} + +void bt_csip_unref(struct bt_csip *csip) +{ + if (!csip) + return; + + if (__sync_sub_and_fetch(&csip->ref_count, 1)) + return; + + csip_free(csip); +} + +static void csip_debug(struct bt_csip *csip, const char *format, ...) +{ + va_list ap; + + if (!csip || !format || !csip->debug_func) + return; + + va_start(ap, format); + util_debug_va(csip->debug_func, csip->debug_data, format, ap); + va_end(ap); +} + +static bool csip_match_att(const void *data, const void *match_data) +{ + const struct bt_csip *csip = data; + const struct bt_att *att = match_data; + + return bt_csip_get_att((void *)csip) == att; +} + +static bool csis_sirk_enc(struct bt_csis *csis, struct bt_att *att, + struct csis_sirk *sirk) +{ + struct bt_csip *csip; + uint8_t k[16]; + struct bt_crypto *crypto; + bool ret; + + csip = queue_find(sessions, csip_match_att, att); + if (!csip) + return false; + + if (!csip->ltk_func(csip, k, csip->ltk_data)) { + DBG(csip, "Unable to read sef key"); + return false; + } + + crypto = bt_crypto_new(); + if (!crypto) { + DBG(csip, "Failed to open crypto"); + return false; + } + + ret = bt_crypto_sef(crypto, k, sirk->val, sirk->val); + if (!ret) + DBG(csip, "Failed to encrypt SIRK using sef"); + + bt_crypto_unref(crypto); + + return ret; +} + +static void csis_sirk_read(struct gatt_db_attribute *attrib, + unsigned int id, uint16_t offset, + uint8_t opcode, struct bt_att *att, + void *user_data) +{ + struct bt_csis *csis = user_data; + struct csis_sirk sirk; + struct iovec iov; + + memcpy(&sirk, csis->sirk_val, sizeof(sirk)); + + if (sirk.type == BT_CSIP_SIRK_ENCRYPT && + !csis_sirk_enc(csis, att, &sirk)) { + gatt_db_attribute_read_result(attrib, id, BT_ATT_ERROR_UNLIKELY, + NULL, 0); + return; + } + + iov.iov_base = &sirk; + iov.iov_len = sizeof(sirk); + + gatt_db_attribute_read_result(attrib, id, 0, iov.iov_base, + iov.iov_len); +} + +static void csis_size_read(struct gatt_db_attribute *attrib, + unsigned int id, uint16_t offset, + uint8_t opcode, struct bt_att *att, + void *user_data) +{ + struct bt_csis *csis = user_data; + struct iovec iov; + + iov.iov_base = &csis->size; + iov.iov_len = sizeof(csis->size); + + gatt_db_attribute_read_result(attrib, id, 0, iov.iov_base, + iov.iov_len); +} + +static void csis_lock_read_cb(struct gatt_db_attribute *attrib, + unsigned int id, uint16_t offset, + uint8_t opcode, struct bt_att *att, + void *user_data) +{ + uint8_t value = CSIS_LOCK; + + gatt_db_attribute_read_result(attrib, id, 0, &value, sizeof(value)); +} + +static void csis_lock_write_cb(struct gatt_db_attribute *attrib, + unsigned int id, uint16_t offset, + const uint8_t *value, size_t len, + uint8_t opcode, struct bt_att *att, + void *user_data) +{ + gatt_db_attribute_write_result(attrib, id, 0); +} + +static void csis_rank_read_cb(struct gatt_db_attribute *attrib, + unsigned int id, uint16_t offset, + uint8_t opcode, struct bt_att *att, + void *user_data) +{ + uint8_t value = CSIS_RANK; + + gatt_db_attribute_read_result(attrib, id, 0, &value, sizeof(value)); +} + +static struct bt_csis *csis_new(struct gatt_db *db) +{ + struct bt_csis *csis; + + if (!db) + return NULL; + + csis = new0(struct bt_csis, 1); + + return csis; +} + +static struct bt_csip_db *csip_db_new(struct gatt_db *db) +{ + struct bt_csip_db *cdb; + + if (!db) + return NULL; + + cdb = new0(struct bt_csip_db, 1); + cdb->db = gatt_db_ref(db); + + if (!csip_db) + csip_db = queue_new(); + + cdb->csis = csis_new(db); + cdb->csis->cdb = cdb; + + queue_push_tail(csip_db, cdb); + + return cdb; +} + +bool bt_csip_set_user_data(struct bt_csip *csip, void *user_data) +{ + if (!csip) + return false; + + csip->user_data = user_data; + + return true; +} + +static bool csip_db_match(const void *data, const void *match_data) +{ + const struct bt_csip_db *cdb = data; + const struct gatt_db *db = match_data; + + return (cdb->db == db); +} + +static struct bt_csip_db *csip_get_db(struct gatt_db *db) +{ + struct bt_csip_db *cdb; + + cdb = queue_find(csip_db, csip_db_match, db); + if (cdb) + return cdb; + + return csip_db_new(db); +} + +void bt_csip_add_db(struct gatt_db *db) +{ + csip_db_new(db); +} + +bool bt_csip_set_debug(struct bt_csip *csip, bt_csip_debug_func_t func, + void *user_data, bt_csip_destroy_func_t destroy) +{ + if (!csip) + return false; + + if (csip->debug_destroy) + csip->debug_destroy(csip->debug_data); + + csip->debug_func = func; + csip->debug_destroy = destroy; + csip->debug_data = user_data; + + return true; +} + +unsigned int bt_csip_register(bt_csip_func_t attached, bt_csip_func_t detached, + void *user_data) +{ + struct bt_csip_cb *cb; + static unsigned int id; + + if (!attached && !detached) + return 0; + + if (!csip_cbs) + csip_cbs = queue_new(); + + cb = new0(struct bt_csip_cb, 1); + cb->id = ++id ? id : ++id; + cb->attached = attached; + cb->detached = detached; + cb->user_data = user_data; + + queue_push_tail(csip_cbs, cb); + + return cb->id; +} + +static bool match_id(const void *data, const void *match_data) +{ + const struct bt_csip_cb *cb = data; + unsigned int id = PTR_TO_UINT(match_data); + + return (cb->id == id); +} + +bool bt_csip_unregister(unsigned int id) +{ + struct bt_csip_cb *cb; + + cb = queue_remove_if(csip_cbs, match_id, UINT_TO_PTR(id)); + if (!cb) + return false; + + free(cb); + + return true; +} + +struct bt_csip *bt_csip_new(struct gatt_db *ldb, struct gatt_db *rdb) +{ + struct bt_csip *csip; + struct bt_csip_db *db; + + if (!ldb) + return NULL; + + db = csip_get_db(ldb); + if (!db) + return NULL; + + csip = new0(struct bt_csip, 1); + csip->ldb = db; + csip->ready_cbs = queue_new(); + + if (!rdb) + goto done; + + db = new0(struct bt_csip_db, 1); + db->db = gatt_db_ref(rdb); + + csip->rdb = db; + +done: + bt_csip_ref(csip); + + return csip; +} + +static struct bt_csis *csip_get_csis(struct bt_csip *csip) +{ + if (!csip) + return NULL; + + if (csip->rdb->csis) + return csip->rdb->csis; + + csip->rdb->csis = new0(struct bt_csis, 1); + csip->rdb->csis->cdb = csip->rdb; + + return csip->rdb->csis; +} + +static void read_sirk(bool success, uint8_t att_ecode, const uint8_t *value, + uint16_t length, void *user_data) +{ + struct bt_csip *csip = user_data; + struct bt_csis *csis; + struct csis_sirk *sirk; + struct iovec iov = { + .iov_base = (void *)value, + .iov_len = length + }; + + if (!success) { + DBG(csip, "Unable to read SIRK: error 0x%02x", att_ecode); + return; + } + + csis = csip_get_csis(csip); + if (!csis) + return; + + sirk = util_iov_pull_mem(&iov, sizeof(*sirk)); + if (!sirk) { + DBG(csip, "Invalid size for SIRK: len %u", length); + return; + } + + if (!csis->sirk_val) + csis->sirk_val = new0(struct csis_sirk, 1); + + memcpy(csis->sirk_val, sirk, sizeof(*sirk)); +} + +static void read_size(bool success, uint8_t att_ecode, const uint8_t *value, + uint16_t length, void *user_data) +{ + struct bt_csip *csip = user_data; + struct bt_csis *csis; + + if (!success) { + DBG(csip, "Unable to read Size: error 0x%02x", att_ecode); + return; + } + + csis = csip_get_csis(csip); + if (!csis) + return; + + csis->size_val = *value; +} + +static void read_rank(bool success, uint8_t att_ecode, const uint8_t *value, + uint16_t length, void *user_data) +{ + struct bt_csip *csip = user_data; + struct bt_csis *csis; + + if (!success) { + DBG(csip, "Unable to read Rank: error 0x%02x", att_ecode); + return; + } + + csis = csip_get_csis(csip); + if (!csis) + return; + + csis->rank_val = *value; +} + +static void csip_notify_ready(struct bt_csip *csip) +{ + const struct queue_entry *entry; + + if (!bt_csip_ref_safe(csip)) + return; + + for (entry = queue_get_entries(csip->ready_cbs); entry; + entry = entry->next) { + struct bt_csip_ready *ready = entry->data; + + ready->func(csip, ready->data); + } + + bt_csip_unref(csip); +} + +static void foreach_csis_char(struct gatt_db_attribute *attr, void *user_data) +{ + struct bt_csip *csip = user_data; + uint16_t value_handle; + bt_uuid_t uuid, uuid_sirk, uuid_size, uuid_rank; + struct bt_csis *csis; + + if (!gatt_db_attribute_get_char_data(attr, NULL, &value_handle, + NULL, NULL, &uuid)) + return; + + bt_uuid16_create(&uuid_sirk, CS_SIRK); + bt_uuid16_create(&uuid_size, CS_SIZE); + bt_uuid16_create(&uuid_rank, CS_RANK); + + if (!bt_uuid_cmp(&uuid, &uuid_sirk)) { + DBG(csip, "SIRK found: handle 0x%04x", value_handle); + + csis = csip_get_csis(csip); + if (!csis || csis->sirk) + return; + + csis->sirk = attr; + + bt_gatt_client_read_value(csip->client, value_handle, read_sirk, + csip, NULL); + + return; + } + + if (!bt_uuid_cmp(&uuid, &uuid_size)) { + DBG(csip, "Size found: handle 0x%04x", value_handle); + + csis = csip_get_csis(csip); + if (!csis) + return; + + csis->size = attr; + + bt_gatt_client_read_value(csip->client, value_handle, read_size, + csip, NULL); + } + + if (!bt_uuid_cmp(&uuid, &uuid_rank)) { + DBG(csip, "Rank found: handle 0x%04x", value_handle); + + csis = csip_get_csis(csip); + if (!csis) + return; + + csis->rank = attr; + + bt_gatt_client_read_value(csip->client, value_handle, read_rank, + csip, NULL); + } +} +static void foreach_csis_service(struct gatt_db_attribute *attr, + void *user_data) +{ + struct bt_csip *csip = user_data; + struct bt_csis *csis = csip_get_csis(csip); + + csis->service = attr; + + gatt_db_service_set_claimed(attr, true); + + gatt_db_service_foreach_char(attr, foreach_csis_char, csip); +} + +static void csip_idle(void *data) +{ + struct bt_csip *csip = data; + + csip->idle_id = 0; + + csip_notify_ready(csip); +} + +bool bt_csip_attach(struct bt_csip *csip, struct bt_gatt_client *client) +{ + bt_uuid_t uuid; + + if (!sessions) + sessions = queue_new(); + + queue_push_tail(sessions, csip); + + if (!client) + return true; + + if (csip->client) + return false; + + csip->client = bt_gatt_client_clone(client); + if (!csip->client) + return false; + + csip->idle_id = bt_gatt_client_idle_register(csip->client, csip_idle, + csip, NULL); + + bt_uuid16_create(&uuid, CSIS_UUID); + gatt_db_foreach_service(csip->rdb->db, &uuid, foreach_csis_service, + csip); + + return true; +} + +static struct csis_sirk *sirk_new(struct bt_csis *csis, struct gatt_db *db, + uint8_t type, uint8_t k[16], + uint8_t size, uint8_t rank) +{ + struct csis_sirk *sirk; + bt_uuid_t uuid; + struct gatt_db_attribute *cas; + + if (!csis) + return NULL; + + if (csis->sirk) + sirk = csis->sirk_val; + else + sirk = new0(struct csis_sirk, 1); + + sirk->type = type; + memcpy(sirk->val, k, sizeof(sirk->val)); + csis->sirk_val = sirk; + csis->size_val = size; + csis->lock_val = 1; + csis->rank_val = rank; + + /* Check if service already active as that means the attributes have + * already been registered. + */ + if (gatt_db_service_get_active(csis->service)) + return sirk; + + /* Populate DB with CSIS attributes */ + bt_uuid16_create(&uuid, CSIS_UUID); + csis->service = gatt_db_add_service(db, &uuid, true, 10); + + bt_uuid16_create(&uuid, CS_SIRK); + csis->sirk = gatt_db_service_add_characteristic(csis->service, + &uuid, + BT_ATT_PERM_READ, + BT_GATT_CHRC_PROP_READ, + csis_sirk_read, NULL, + csis); + + bt_uuid16_create(&uuid, CS_SIZE); + csis->size = gatt_db_service_add_characteristic(csis->service, + &uuid, + BT_ATT_PERM_READ, + BT_GATT_CHRC_PROP_READ, + csis_size_read, NULL, + csis); + + /* Lock */ + bt_uuid16_create(&uuid, CS_LOCK); + csis->lock = gatt_db_service_add_characteristic(csis->service, &uuid, + BT_ATT_PERM_READ, + BT_GATT_CHRC_PROP_READ | + BT_GATT_CHRC_PROP_WRITE | + BT_GATT_CHRC_PROP_NOTIFY, + csis_lock_read_cb, + csis_lock_write_cb, + csis); + + csis->lock_ccc = gatt_db_service_add_ccc(csis->service, + BT_ATT_PERM_READ | BT_ATT_PERM_WRITE); + + /* Rank */ + bt_uuid16_create(&uuid, CS_RANK); + csis->rank = gatt_db_service_add_characteristic(csis->service, &uuid, + BT_ATT_PERM_READ, + BT_GATT_CHRC_PROP_READ, + csis_rank_read_cb, + NULL, csis); + + /* Add the CAS service */ + bt_uuid16_create(&uuid, 0x1853); + cas = gatt_db_add_service(db, &uuid, true, 2); + gatt_db_service_add_included(cas, csis->service); + gatt_db_service_set_active(cas, true); + gatt_db_service_add_included(cas, csis->service); + + gatt_db_service_set_active(csis->service, true); + + return sirk; +} + +bool bt_csip_set_sirk(struct bt_csip *csip, bool encrypt, + uint8_t k[16], uint8_t size, uint8_t rank, + bt_csip_ltk_func_t func, void *user_data) +{ + uint8_t zero[16] = {}; + uint8_t type; + + if (!csip || !csip->ldb || !memcmp(k, zero, sizeof(zero))) + return false; + + type = encrypt ? BT_CSIP_SIRK_ENCRYPT : BT_CSIP_SIRK_CLEARTEXT; + + /* In case of encrypted type requires sef key function */ + if (type == BT_CSIP_SIRK_ENCRYPT && !func) + return false; + + if (!sirk_new(csip->ldb->csis, csip->ldb->db, type, k, size, rank)) + return false; + + csip->ltk_func = func; + csip->ltk_data = user_data; + + return true; +} + +bool bt_csip_get_sirk(struct bt_csip *csip, uint8_t *type, + uint8_t k[16], uint8_t *size, uint8_t *rank) +{ + struct bt_csis *csis; + + if (!csip) + return false; + + csis = csip_get_csis(csip); + if (!csis) + return false; + + if (type) + *type = csis->sirk_val->type; + + memcpy(k, csis->sirk_val->val, sizeof(csis->sirk_val->val)); + + if (size) + *size = csis->size_val; + + if (rank) + *rank = csis->rank_val; + + return true; +} + +unsigned int bt_csip_ready_register(struct bt_csip *csip, + bt_csip_ready_func_t func, void *user_data, + bt_csip_destroy_func_t destroy) +{ + struct bt_csip_ready *ready; + static unsigned int id; + + if (!csip) + return 0; + + ready = new0(struct bt_csip_ready, 1); + ready->id = ++id ? id : ++id; + ready->func = func; + ready->destroy = destroy; + ready->data = user_data; + + queue_push_tail(csip->ready_cbs, ready); + + return ready->id; +} + +static bool match_ready_id(const void *data, const void *match_data) +{ + const struct bt_csip_ready *ready = data; + unsigned int id = PTR_TO_UINT(match_data); + + return (ready->id == id); +} + +bool bt_csip_ready_unregister(struct bt_csip *csip, unsigned int id) +{ + struct bt_csip_ready *ready; + + ready = queue_remove_if(csip->ready_cbs, match_ready_id, + UINT_TO_PTR(id)); + if (!ready) + return false; + + csip_ready_free(ready); + + return true; +} diff --git a/src/shared/csip.h b/src/shared/csip.h new file mode 100644 index 000000000..0f8acb1ca --- /dev/null +++ b/src/shared/csip.h @@ -0,0 +1,67 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2022 Intel Corporation. All rights reserved. + * + */ + +#include +#include + +#include "src/shared/io.h" + +#ifndef __packed +#define __packed __attribute__((packed)) +#endif + +struct bt_csip; + +enum { + BT_CSIP_SIRK_ENCRYPT = 0x00, + BT_CSIP_SIRK_CLEARTEXT = 0x01 +}; + +typedef void (*bt_csip_ready_func_t)(struct bt_csip *csip, void *user_data); +typedef void (*bt_csip_destroy_func_t)(void *user_data); +typedef void (*bt_csip_debug_func_t)(const char *str, void *user_data); +typedef void (*bt_csip_func_t)(struct bt_csip *csip, void *user_data); +typedef bool (*bt_csip_ltk_func_t)(struct bt_csip *csip, uint8_t k[16], + void *user_data); +typedef bool (*bt_csip_sirk_func_t)(struct bt_csip *csip, uint8_t type, + uint8_t k[16], uint8_t size, uint8_t rank, + void *user_data); + +struct bt_csip *bt_csip_ref(struct bt_csip *csip); +void bt_csip_unref(struct bt_csip *csip); + +void bt_csip_add_db(struct gatt_db *db); + +bool bt_csip_attach(struct bt_csip *csip, struct bt_gatt_client *client); +void bt_csip_detach(struct bt_csip *csip); + +bool bt_csip_set_debug(struct bt_csip *csip, bt_csip_debug_func_t func, + void *user_data, bt_csip_destroy_func_t destroy); + +struct bt_att *bt_csip_get_att(struct bt_csip *csip); + +bool bt_csip_set_user_data(struct bt_csip *csip, void *user_data); + +/* Session related function */ +unsigned int bt_csip_register(bt_csip_func_t added, bt_csip_func_t removed, + void *user_data); +bool bt_csip_unregister(unsigned int id); +struct bt_csip *bt_csip_new(struct gatt_db *ldb, struct gatt_db *rdb); + +bool bt_csip_set_sirk(struct bt_csip *csip, bool encrypt, + uint8_t k[16], uint8_t size, uint8_t rank, + bt_csip_ltk_func_t func, void *user_data); + +bool bt_csip_get_sirk(struct bt_csip *csip, uint8_t *type, + uint8_t k[16], uint8_t *size, uint8_t *rank); + +unsigned int bt_csip_ready_register(struct bt_csip *csip, + bt_csip_ready_func_t func, void *user_data, + bt_csip_destroy_func_t destroy); +bool bt_csip_ready_unregister(struct bt_csip *csip, unsigned int id); -- cgit v1.2.1