The LE Audio implementations do not really support bonding yet, and removing subs on disconnect is the most effective (and correct) way of ensuring that we do not subscribe more than once when we re-discover after reconnection. The broadcast assistant and the media control client does not support multiple connections as of this commit, so they needed special treatment. In the case that we do discovery on multiple ACL connections, it is important that the existing subscriptions are removed correctly by calling bt_gatt_unsubscribe. In order to implement this change properly on some of the clients, thet had no proper connection references or support for clearing the data on disconnects, they had to be updated as well. The csip_notify.sh test has been disabled, as that expected a notification in the client, but since this commit removes that (until bonding is properly supported in the clients), then the test will fail. Signed-off-by: Emil Gydesen <emil.gydesen@nordicsemi.no>
999 lines
26 KiB
C
999 lines
26 KiB
C
/*
|
|
* Copyright (c) 2022 Codecoup
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#include <zephyr/kernel.h>
|
|
|
|
#include <zephyr/bluetooth/bluetooth.h>
|
|
#include <zephyr/bluetooth/gatt.h>
|
|
#include <zephyr/bluetooth/audio/has.h>
|
|
#include <zephyr/net/buf.h>
|
|
#include <zephyr/sys/atomic.h>
|
|
#include <zephyr/sys/check.h>
|
|
|
|
#include "has_internal.h"
|
|
|
|
#include <zephyr/logging/log.h>
|
|
|
|
LOG_MODULE_REGISTER(bt_has_client, CONFIG_BT_HAS_CLIENT_LOG_LEVEL);
|
|
|
|
#define HAS_INST(_has) CONTAINER_OF(_has, struct bt_has_client, has)
|
|
#define HANDLE_IS_VALID(handle) ((handle) != 0x0000)
|
|
static struct bt_has_client clients[CONFIG_BT_MAX_CONN];
|
|
static const struct bt_has_client_cb *client_cb;
|
|
|
|
static struct bt_has_client *inst_by_conn(struct bt_conn *conn)
|
|
{
|
|
struct bt_has_client *inst = &clients[bt_conn_index(conn)];
|
|
|
|
if (inst->conn == conn) {
|
|
return inst;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void inst_cleanup(struct bt_has_client *inst)
|
|
{
|
|
bt_conn_unref(inst->conn);
|
|
|
|
(void)memset(inst, 0, sizeof(*inst));
|
|
}
|
|
|
|
static enum bt_has_capabilities get_capabilities(const struct bt_has_client *inst)
|
|
{
|
|
enum bt_has_capabilities caps = 0;
|
|
|
|
/* The Control Point support is optional, as the server might have no presets support */
|
|
if (HANDLE_IS_VALID(inst->control_point_subscription.value_handle)) {
|
|
caps |= BT_HAS_PRESET_SUPPORT;
|
|
}
|
|
|
|
return caps;
|
|
}
|
|
|
|
static void handle_read_preset_rsp(struct bt_has_client *inst, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_has_cp_read_preset_rsp *pdu;
|
|
struct bt_has_preset_record record;
|
|
char name[BT_HAS_PRESET_NAME_MAX + 1]; /* + 1 byte for null-terminator */
|
|
size_t name_len;
|
|
|
|
LOG_DBG("conn %p buf %p", (void *)inst->conn, buf);
|
|
|
|
if (buf->len < sizeof(*pdu)) {
|
|
LOG_ERR("malformed PDU");
|
|
return;
|
|
}
|
|
|
|
pdu = net_buf_simple_pull_mem(buf, sizeof(*pdu));
|
|
|
|
if (pdu->is_last > BT_HAS_IS_LAST) {
|
|
LOG_WRN("unexpected is_last value 0x%02x", pdu->is_last);
|
|
}
|
|
|
|
record.index = pdu->index;
|
|
record.properties = pdu->properties;
|
|
record.name = name;
|
|
|
|
name_len = buf->len + 1; /* + 1 byte for NULL terminator */
|
|
if (name_len > ARRAY_SIZE(name)) {
|
|
LOG_WRN("name is too long (%zu > %u)", buf->len, BT_HAS_PRESET_NAME_MAX);
|
|
|
|
name_len = ARRAY_SIZE(name);
|
|
}
|
|
|
|
utf8_lcpy(name, pdu->name, name_len);
|
|
|
|
client_cb->preset_read_rsp(&inst->has, 0, &record, !!pdu->is_last);
|
|
}
|
|
|
|
static void handle_generic_update(struct bt_has_client *inst, struct net_buf_simple *buf,
|
|
bool is_last)
|
|
{
|
|
const struct bt_has_cp_generic_update *pdu;
|
|
struct bt_has_preset_record record;
|
|
char name[BT_HAS_PRESET_NAME_MAX + 1]; /* + 1 byte for null-terminator */
|
|
size_t name_len;
|
|
|
|
if (buf->len < sizeof(*pdu)) {
|
|
LOG_ERR("malformed PDU");
|
|
return;
|
|
}
|
|
|
|
pdu = net_buf_simple_pull_mem(buf, sizeof(*pdu));
|
|
|
|
record.index = pdu->index;
|
|
record.properties = pdu->properties;
|
|
record.name = name;
|
|
|
|
name_len = buf->len + 1; /* + 1 byte for NULL terminator */
|
|
if (name_len > ARRAY_SIZE(name)) {
|
|
LOG_WRN("name is too long (%zu > %u)", buf->len, BT_HAS_PRESET_NAME_MAX);
|
|
|
|
name_len = ARRAY_SIZE(name);
|
|
}
|
|
|
|
utf8_lcpy(name, pdu->name, name_len);
|
|
|
|
client_cb->preset_update(&inst->has, pdu->prev_index, &record, is_last);
|
|
}
|
|
|
|
static void handle_preset_deleted(struct bt_has_client *inst, struct net_buf_simple *buf,
|
|
bool is_last)
|
|
{
|
|
if (buf->len < sizeof(uint8_t)) {
|
|
LOG_ERR("malformed PDU");
|
|
return;
|
|
}
|
|
|
|
client_cb->preset_deleted(&inst->has, net_buf_simple_pull_u8(buf), is_last);
|
|
}
|
|
|
|
static void handle_preset_availability(struct bt_has_client *inst, struct net_buf_simple *buf,
|
|
bool available, bool is_last)
|
|
{
|
|
if (buf->len < sizeof(uint8_t)) {
|
|
LOG_ERR("malformed PDU");
|
|
return;
|
|
}
|
|
|
|
client_cb->preset_availability(&inst->has, net_buf_simple_pull_u8(buf), available,
|
|
is_last);
|
|
}
|
|
|
|
static void handle_preset_changed(struct bt_has_client *inst, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_has_cp_preset_changed *pdu;
|
|
|
|
LOG_DBG("conn %p buf %p", (void *)inst->conn, buf);
|
|
|
|
if (buf->len < sizeof(*pdu)) {
|
|
LOG_ERR("malformed PDU");
|
|
return;
|
|
}
|
|
|
|
pdu = net_buf_simple_pull_mem(buf, sizeof(*pdu));
|
|
|
|
if (pdu->is_last > BT_HAS_IS_LAST) {
|
|
LOG_WRN("unexpected is_last 0x%02x", pdu->is_last);
|
|
}
|
|
|
|
switch (pdu->change_id) {
|
|
case BT_HAS_CHANGE_ID_GENERIC_UPDATE:
|
|
if (client_cb->preset_update) {
|
|
handle_generic_update(inst, buf, !!pdu->is_last);
|
|
}
|
|
break;
|
|
case BT_HAS_CHANGE_ID_PRESET_DELETED:
|
|
if (client_cb->preset_deleted) {
|
|
handle_preset_deleted(inst, buf, !!pdu->is_last);
|
|
}
|
|
break;
|
|
case BT_HAS_CHANGE_ID_PRESET_AVAILABLE:
|
|
if (client_cb->preset_availability) {
|
|
handle_preset_availability(inst, buf, !!pdu->is_last, true);
|
|
}
|
|
return;
|
|
case BT_HAS_CHANGE_ID_PRESET_UNAVAILABLE:
|
|
if (client_cb->preset_availability) {
|
|
handle_preset_availability(inst, buf, !!pdu->is_last, false);
|
|
}
|
|
return;
|
|
default:
|
|
LOG_WRN("unknown change_id 0x%02x", pdu->change_id);
|
|
}
|
|
}
|
|
|
|
static uint8_t control_point_notify_cb(struct bt_conn *conn,
|
|
struct bt_gatt_subscribe_params *params, const void *data,
|
|
uint16_t len)
|
|
{
|
|
struct bt_has_client *inst = CONTAINER_OF(params, struct bt_has_client,
|
|
control_point_subscription);
|
|
const struct bt_has_cp_hdr *hdr;
|
|
struct net_buf_simple buf;
|
|
|
|
LOG_DBG("conn %p params %p data %p len %u", (void *)conn, params, data, len);
|
|
|
|
if (!conn) { /* Unpaired, continue receiving notifications */
|
|
return BT_GATT_ITER_CONTINUE;
|
|
}
|
|
|
|
if (!data) { /* Unsubscribed */
|
|
params->value_handle = 0u;
|
|
|
|
return BT_GATT_ITER_STOP;
|
|
}
|
|
|
|
if (len < sizeof(*hdr)) { /* Ignore malformed notification */
|
|
return BT_GATT_ITER_CONTINUE;
|
|
}
|
|
|
|
net_buf_simple_init_with_data(&buf, (void *)data, len);
|
|
|
|
hdr = net_buf_simple_pull_mem(&buf, sizeof(*hdr));
|
|
|
|
switch (hdr->opcode) {
|
|
case BT_HAS_OP_READ_PRESET_RSP:
|
|
handle_read_preset_rsp(inst, &buf);
|
|
break;
|
|
case BT_HAS_OP_PRESET_CHANGED:
|
|
handle_preset_changed(inst, &buf);
|
|
break;
|
|
};
|
|
|
|
return BT_GATT_ITER_CONTINUE;
|
|
}
|
|
|
|
static void discover_complete(struct bt_has_client *inst)
|
|
{
|
|
LOG_DBG("conn %p", (void *)inst->conn);
|
|
|
|
atomic_clear_bit(inst->flags, HAS_CLIENT_DISCOVER_IN_PROGRESS);
|
|
|
|
client_cb->discover(inst->conn, 0, &inst->has,
|
|
inst->has.features & BT_HAS_FEAT_HEARING_AID_TYPE_MASK,
|
|
get_capabilities(inst));
|
|
|
|
/* If Active Preset Index supported, notify it's value */
|
|
if (client_cb->preset_switch &&
|
|
HANDLE_IS_VALID(inst->active_index_subscription.value_handle)) {
|
|
client_cb->preset_switch(&inst->has, 0, inst->has.active_index);
|
|
}
|
|
}
|
|
|
|
static void discover_failed(struct bt_conn *conn, int err)
|
|
{
|
|
LOG_DBG("conn %p", (void *)conn);
|
|
|
|
client_cb->discover(conn, err, NULL, 0, 0);
|
|
}
|
|
|
|
static int cp_write(struct bt_has_client *inst, struct net_buf_simple *buf,
|
|
bt_gatt_write_func_t func)
|
|
{
|
|
const uint16_t value_handle = inst->control_point_subscription.value_handle;
|
|
|
|
if (!HANDLE_IS_VALID(value_handle)) {
|
|
return -ENOTSUP;
|
|
}
|
|
|
|
inst->params.write.func = func;
|
|
inst->params.write.handle = value_handle;
|
|
inst->params.write.offset = 0U;
|
|
inst->params.write.data = buf->data;
|
|
inst->params.write.length = buf->len;
|
|
|
|
return bt_gatt_write(inst->conn, &inst->params.write);
|
|
}
|
|
|
|
static void read_presets_req_cb(struct bt_conn *conn, uint8_t err,
|
|
struct bt_gatt_write_params *params)
|
|
{
|
|
struct bt_has_client *inst = CONTAINER_OF(params, struct bt_has_client, params.write);
|
|
|
|
LOG_DBG("conn %p err 0x%02x param %p", (void *)conn, err, params);
|
|
|
|
atomic_clear_bit(inst->flags, HAS_CLIENT_CP_OPERATION_IN_PROGRESS);
|
|
|
|
if (err) {
|
|
client_cb->preset_read_rsp(&inst->has, err, NULL, true);
|
|
}
|
|
}
|
|
|
|
static int read_presets_req(struct bt_has_client *inst, uint8_t start_index, uint8_t num_presets)
|
|
{
|
|
struct bt_has_cp_hdr *hdr;
|
|
struct bt_has_cp_read_presets_req *req;
|
|
|
|
NET_BUF_SIMPLE_DEFINE(buf, sizeof(*hdr) + sizeof(*req));
|
|
|
|
LOG_DBG("conn %p start_index 0x%02x num_presets %d", (void *)inst->conn, start_index,
|
|
num_presets);
|
|
|
|
hdr = net_buf_simple_add(&buf, sizeof(*hdr));
|
|
hdr->opcode = BT_HAS_OP_READ_PRESET_REQ;
|
|
req = net_buf_simple_add(&buf, sizeof(*req));
|
|
req->start_index = start_index;
|
|
req->num_presets = num_presets;
|
|
|
|
return cp_write(inst, &buf, read_presets_req_cb);
|
|
}
|
|
|
|
static void set_active_preset_cb(struct bt_conn *conn, uint8_t err,
|
|
struct bt_gatt_write_params *params)
|
|
{
|
|
struct bt_has_client *inst = CONTAINER_OF(params, struct bt_has_client, params.write);
|
|
|
|
LOG_DBG("conn %p err 0x%02x param %p", (void *)conn, err, params);
|
|
|
|
atomic_clear_bit(inst->flags, HAS_CLIENT_CP_OPERATION_IN_PROGRESS);
|
|
|
|
if (err) {
|
|
client_cb->preset_switch(&inst->has, err, inst->has.active_index);
|
|
}
|
|
}
|
|
|
|
static int preset_set(struct bt_has_client *inst, uint8_t opcode, uint8_t index)
|
|
{
|
|
struct bt_has_cp_hdr *hdr;
|
|
struct bt_has_cp_set_active_preset *req;
|
|
|
|
NET_BUF_SIMPLE_DEFINE(buf, sizeof(*hdr) + sizeof(*req));
|
|
|
|
LOG_DBG("conn %p opcode 0x%02x index 0x%02x", (void *)inst->conn, opcode, index);
|
|
|
|
hdr = net_buf_simple_add(&buf, sizeof(*hdr));
|
|
hdr->opcode = opcode;
|
|
req = net_buf_simple_add(&buf, sizeof(*req));
|
|
req->index = index;
|
|
|
|
return cp_write(inst, &buf, set_active_preset_cb);
|
|
}
|
|
|
|
static int preset_set_next_or_prev(struct bt_has_client *inst, uint8_t opcode)
|
|
{
|
|
struct bt_has_cp_hdr *hdr;
|
|
|
|
NET_BUF_SIMPLE_DEFINE(buf, sizeof(*hdr));
|
|
|
|
LOG_DBG("conn %p opcode 0x%02x", (void *)inst->conn, opcode);
|
|
|
|
hdr = net_buf_simple_add(&buf, sizeof(*hdr));
|
|
hdr->opcode = opcode;
|
|
|
|
return cp_write(inst, &buf, set_active_preset_cb);
|
|
}
|
|
|
|
static uint8_t active_index_update(struct bt_has_client *inst, const void *data, uint16_t len)
|
|
{
|
|
struct net_buf_simple buf;
|
|
const uint8_t prev = inst->has.active_index;
|
|
|
|
net_buf_simple_init_with_data(&buf, (void *)data, len);
|
|
|
|
inst->has.active_index = net_buf_simple_pull_u8(&buf);
|
|
|
|
LOG_DBG("conn %p index 0x%02x", (void *)inst->conn, inst->has.active_index);
|
|
|
|
return prev;
|
|
}
|
|
|
|
static uint8_t active_preset_notify_cb(struct bt_conn *conn,
|
|
struct bt_gatt_subscribe_params *params, const void *data,
|
|
uint16_t len)
|
|
{
|
|
struct bt_has_client *inst = CONTAINER_OF(params, struct bt_has_client,
|
|
active_index_subscription);
|
|
uint8_t prev;
|
|
|
|
LOG_DBG("conn %p params %p data %p len %u", (void *)conn, params, data, len);
|
|
|
|
if (!conn) {
|
|
/* Unpaired, stop receiving notifications from device */
|
|
return BT_GATT_ITER_STOP;
|
|
}
|
|
|
|
if (!data) {
|
|
/* Unsubscribed */
|
|
params->value_handle = 0u;
|
|
|
|
return BT_GATT_ITER_STOP;
|
|
}
|
|
|
|
if (len == 0) {
|
|
/* Ignore empty notification */
|
|
return BT_GATT_ITER_CONTINUE;
|
|
}
|
|
|
|
prev = active_index_update(inst, data, len);
|
|
|
|
if (atomic_test_bit(inst->flags, HAS_CLIENT_DISCOVER_IN_PROGRESS)) {
|
|
/* Got notification during discovery process, postpone the active_index callback
|
|
* until discovery is complete.
|
|
*/
|
|
return BT_GATT_ITER_CONTINUE;
|
|
}
|
|
|
|
if (client_cb && client_cb->preset_switch && inst->has.active_index != prev) {
|
|
client_cb->preset_switch(&inst->has, 0, inst->has.active_index);
|
|
}
|
|
|
|
return BT_GATT_ITER_CONTINUE;
|
|
}
|
|
|
|
static void active_index_subscribe_cb(struct bt_conn *conn, uint8_t att_err,
|
|
struct bt_gatt_subscribe_params *params)
|
|
{
|
|
struct bt_has_client *inst = CONTAINER_OF(params, struct bt_has_client,
|
|
active_index_subscription);
|
|
|
|
LOG_DBG("conn %p att_err 0x%02x params %p", (void *)inst->conn, att_err, params);
|
|
|
|
if (att_err != BT_ATT_ERR_SUCCESS) {
|
|
/* Cleanup instance so that it can be reused */
|
|
inst_cleanup(inst);
|
|
|
|
discover_failed(conn, att_err);
|
|
} else {
|
|
discover_complete(inst);
|
|
}
|
|
}
|
|
|
|
static int active_index_subscribe(struct bt_has_client *inst, uint16_t value_handle)
|
|
{
|
|
int err;
|
|
|
|
LOG_DBG("conn %p handle 0x%04x", (void *)inst->conn, value_handle);
|
|
|
|
inst->active_index_subscription.notify = active_preset_notify_cb;
|
|
inst->active_index_subscription.subscribe = active_index_subscribe_cb;
|
|
inst->active_index_subscription.value_handle = value_handle;
|
|
inst->active_index_subscription.ccc_handle = 0x0000;
|
|
inst->active_index_subscription.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE;
|
|
inst->active_index_subscription.disc_params = &inst->params.discover;
|
|
inst->active_index_subscription.value = BT_GATT_CCC_NOTIFY;
|
|
atomic_set_bit(inst->active_index_subscription.flags, BT_GATT_SUBSCRIBE_FLAG_VOLATILE);
|
|
|
|
err = bt_gatt_subscribe(inst->conn, &inst->active_index_subscription);
|
|
if (err != 0 && err != -EALREADY) {
|
|
return err;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static uint8_t active_index_read_cb(struct bt_conn *conn, uint8_t att_err,
|
|
struct bt_gatt_read_params *params, const void *data,
|
|
uint16_t len)
|
|
{
|
|
struct bt_has_client *inst = CONTAINER_OF(params, struct bt_has_client, params.read);
|
|
int err = att_err;
|
|
|
|
LOG_DBG("conn %p att_err 0x%02x params %p data %p len %u", (void *)conn, att_err, params,
|
|
data, len);
|
|
|
|
if (att_err != BT_ATT_ERR_SUCCESS || len == 0) {
|
|
goto fail;
|
|
}
|
|
|
|
active_index_update(inst, data, len);
|
|
|
|
err = active_index_subscribe(inst, params->by_uuid.start_handle);
|
|
if (err) {
|
|
LOG_ERR("Subscribe failed (err %d)", err);
|
|
goto fail;
|
|
}
|
|
|
|
return BT_GATT_ITER_STOP;
|
|
|
|
fail:
|
|
/* Cleanup instance so that it can be reused */
|
|
inst_cleanup(inst);
|
|
|
|
discover_failed(conn, err);
|
|
|
|
return BT_GATT_ITER_STOP;
|
|
}
|
|
|
|
static int active_index_read(struct bt_has_client *inst)
|
|
{
|
|
LOG_DBG("conn %p", (void *)inst->conn);
|
|
|
|
(void)memset(&inst->params.read, 0, sizeof(inst->params.read));
|
|
|
|
(void)memcpy(&inst->params.uuid, BT_UUID_HAS_ACTIVE_PRESET_INDEX,
|
|
sizeof(inst->params.uuid));
|
|
inst->params.read.func = active_index_read_cb;
|
|
inst->params.read.handle_count = 0u;
|
|
inst->params.read.by_uuid.uuid = &inst->params.uuid.uuid;
|
|
inst->params.read.by_uuid.start_handle = BT_ATT_FIRST_ATTRIBUTE_HANDLE;
|
|
inst->params.read.by_uuid.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE;
|
|
|
|
return bt_gatt_read(inst->conn, &inst->params.read);
|
|
}
|
|
|
|
static void control_point_subscribe_cb(struct bt_conn *conn, uint8_t att_err,
|
|
struct bt_gatt_subscribe_params *params)
|
|
{
|
|
struct bt_has_client *inst = CONTAINER_OF(params, struct bt_has_client,
|
|
control_point_subscription);
|
|
int err = att_err;
|
|
|
|
LOG_DBG("conn %p att_err 0x%02x", (void *)inst->conn, att_err);
|
|
|
|
if (att_err != BT_ATT_ERR_SUCCESS) {
|
|
goto fail;
|
|
}
|
|
|
|
err = active_index_read(inst);
|
|
if (err) {
|
|
LOG_ERR("Active Preset Index read failed (err %d)", err);
|
|
goto fail;
|
|
}
|
|
|
|
return;
|
|
|
|
fail:
|
|
/* Cleanup instance so that it can be reused */
|
|
inst_cleanup(inst);
|
|
|
|
discover_failed(conn, err);
|
|
}
|
|
|
|
static int control_point_subscribe(struct bt_has_client *inst, uint16_t value_handle,
|
|
uint8_t properties)
|
|
{
|
|
int err;
|
|
|
|
LOG_DBG("conn %p handle 0x%04x", (void *)inst->conn, value_handle);
|
|
|
|
inst->control_point_subscription.notify = control_point_notify_cb;
|
|
inst->control_point_subscription.subscribe = control_point_subscribe_cb;
|
|
inst->control_point_subscription.value_handle = value_handle;
|
|
inst->control_point_subscription.ccc_handle = 0x0000;
|
|
inst->control_point_subscription.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE;
|
|
inst->control_point_subscription.disc_params = &inst->params.discover;
|
|
atomic_set_bit(inst->control_point_subscription.flags, BT_GATT_SUBSCRIBE_FLAG_VOLATILE);
|
|
|
|
if (IS_ENABLED(CONFIG_BT_EATT) && properties & BT_GATT_CHRC_NOTIFY) {
|
|
inst->control_point_subscription.value = BT_GATT_CCC_INDICATE | BT_GATT_CCC_NOTIFY;
|
|
} else {
|
|
inst->control_point_subscription.value = BT_GATT_CCC_INDICATE;
|
|
}
|
|
|
|
err = bt_gatt_subscribe(inst->conn, &inst->control_point_subscription);
|
|
if (err != 0 && err != -EALREADY) {
|
|
return err;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static uint8_t control_point_discover_cb(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
|
struct bt_gatt_discover_params *params)
|
|
{
|
|
struct bt_has_client *inst = CONTAINER_OF(params, struct bt_has_client, params.discover);
|
|
const struct bt_gatt_chrc *chrc;
|
|
int err;
|
|
|
|
LOG_DBG("conn %p attr %p params %p", (void *)conn, attr, params);
|
|
|
|
if (!attr) {
|
|
LOG_INF("Control Point not found");
|
|
discover_complete(inst);
|
|
return BT_GATT_ITER_STOP;
|
|
}
|
|
|
|
chrc = attr->user_data;
|
|
|
|
err = control_point_subscribe(inst, chrc->value_handle, chrc->properties);
|
|
if (err) {
|
|
LOG_ERR("Subscribe failed (err %d)", err);
|
|
|
|
/* Cleanup instance so that it can be reused */
|
|
inst_cleanup(inst);
|
|
|
|
discover_failed(conn, err);
|
|
}
|
|
|
|
return BT_GATT_ITER_STOP;
|
|
}
|
|
|
|
static int control_point_discover(struct bt_has_client *inst)
|
|
{
|
|
LOG_DBG("conn %p", (void *)inst->conn);
|
|
|
|
(void)memset(&inst->params.discover, 0, sizeof(inst->params.discover));
|
|
|
|
(void)memcpy(&inst->params.uuid, BT_UUID_HAS_PRESET_CONTROL_POINT,
|
|
sizeof(inst->params.uuid));
|
|
inst->params.discover.uuid = &inst->params.uuid.uuid;
|
|
inst->params.discover.func = control_point_discover_cb;
|
|
inst->params.discover.start_handle = BT_ATT_FIRST_ATTRIBUTE_HANDLE;
|
|
inst->params.discover.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE;
|
|
inst->params.discover.type = BT_GATT_DISCOVER_CHARACTERISTIC;
|
|
|
|
return bt_gatt_discover(inst->conn, &inst->params.discover);
|
|
}
|
|
|
|
static void features_update(struct bt_has_client *inst, const void *data, uint16_t len)
|
|
{
|
|
struct net_buf_simple buf;
|
|
|
|
net_buf_simple_init_with_data(&buf, (void *)data, len);
|
|
|
|
inst->has.features = net_buf_simple_pull_u8(&buf);
|
|
|
|
LOG_DBG("conn %p features 0x%02x", (void *)inst->conn, inst->has.features);
|
|
}
|
|
|
|
static uint8_t features_read_cb(struct bt_conn *conn, uint8_t att_err,
|
|
struct bt_gatt_read_params *params, const void *data, uint16_t len)
|
|
{
|
|
struct bt_has_client *inst = CONTAINER_OF(params, struct bt_has_client, params.read);
|
|
int err = att_err;
|
|
|
|
LOG_DBG("conn %p att_err 0x%02x params %p data %p len %u", (void *)conn, att_err, params,
|
|
data, len);
|
|
|
|
if (att_err != BT_ATT_ERR_SUCCESS || len == 0) {
|
|
goto fail;
|
|
}
|
|
|
|
features_update(inst, data, len);
|
|
|
|
if (!client_cb->preset_switch) {
|
|
/* Complete the discovery if client is not interested in active preset changes */
|
|
discover_complete(inst);
|
|
return BT_GATT_ITER_STOP;
|
|
}
|
|
|
|
err = control_point_discover(inst);
|
|
if (err) {
|
|
LOG_ERR("Control Point discover failed (err %d)", err);
|
|
goto fail;
|
|
}
|
|
|
|
return BT_GATT_ITER_STOP;
|
|
|
|
fail:
|
|
/* Cleanup instance so that it can be reused */
|
|
inst_cleanup(inst);
|
|
|
|
discover_failed(conn, err);
|
|
|
|
return BT_GATT_ITER_STOP;
|
|
}
|
|
|
|
static int features_read(struct bt_has_client *inst, uint16_t value_handle)
|
|
{
|
|
LOG_DBG("conn %p handle 0x%04x", (void *)inst->conn, value_handle);
|
|
|
|
(void)memset(&inst->params.read, 0, sizeof(inst->params.read));
|
|
|
|
inst->params.read.func = features_read_cb;
|
|
inst->params.read.handle_count = 1u;
|
|
inst->params.read.single.handle = value_handle;
|
|
inst->params.read.single.offset = 0u;
|
|
|
|
return bt_gatt_read(inst->conn, &inst->params.read);
|
|
}
|
|
|
|
static void features_subscribe_cb(struct bt_conn *conn, uint8_t att_err,
|
|
struct bt_gatt_subscribe_params *params)
|
|
{
|
|
struct bt_has_client *inst = CONTAINER_OF(params, struct bt_has_client,
|
|
features_subscription);
|
|
int err = att_err;
|
|
|
|
LOG_DBG("conn %p att_err 0x%02x params %p", (void *)conn, att_err, params);
|
|
|
|
if (att_err != BT_ATT_ERR_SUCCESS) {
|
|
goto fail;
|
|
}
|
|
|
|
err = features_read(inst, inst->features_subscription.value_handle);
|
|
if (err) {
|
|
LOG_ERR("Read failed (err %d)", err);
|
|
goto fail;
|
|
}
|
|
|
|
return;
|
|
|
|
fail:
|
|
/* Cleanup instance so that it can be reused */
|
|
inst_cleanup(inst);
|
|
|
|
discover_failed(conn, err);
|
|
}
|
|
|
|
static uint8_t features_notify_cb(struct bt_conn *conn, struct bt_gatt_subscribe_params *params,
|
|
const void *data, uint16_t len)
|
|
{
|
|
struct bt_has_client *inst = CONTAINER_OF(params, struct bt_has_client,
|
|
features_subscription);
|
|
|
|
LOG_DBG("conn %p params %p data %p len %u", (void *)conn, params, data, len);
|
|
|
|
if (!conn) {
|
|
/* Unpaired, stop receiving notifications from device */
|
|
return BT_GATT_ITER_STOP;
|
|
}
|
|
|
|
if (!data) {
|
|
/* Unsubscribed */
|
|
params->value_handle = 0u;
|
|
|
|
return BT_GATT_ITER_STOP;
|
|
}
|
|
|
|
if (len == 0) {
|
|
/* Ignore empty notification */
|
|
return BT_GATT_ITER_CONTINUE;
|
|
}
|
|
|
|
features_update(inst, data, len);
|
|
|
|
return BT_GATT_ITER_CONTINUE;
|
|
}
|
|
|
|
static int features_subscribe(struct bt_has_client *inst, uint16_t value_handle)
|
|
{
|
|
int err;
|
|
|
|
LOG_DBG("conn %p handle 0x%04x", (void *)inst->conn, value_handle);
|
|
|
|
inst->features_subscription.notify = features_notify_cb;
|
|
inst->features_subscription.subscribe = features_subscribe_cb;
|
|
inst->features_subscription.value_handle = value_handle;
|
|
inst->features_subscription.ccc_handle = 0x0000;
|
|
inst->features_subscription.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE;
|
|
inst->features_subscription.disc_params = &inst->params.discover;
|
|
inst->features_subscription.value = BT_GATT_CCC_NOTIFY;
|
|
atomic_set_bit(inst->features_subscription.flags, BT_GATT_SUBSCRIBE_FLAG_VOLATILE);
|
|
|
|
err = bt_gatt_subscribe(inst->conn, &inst->features_subscription);
|
|
if (err != 0 && err != -EALREADY) {
|
|
return err;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static uint8_t features_discover_cb(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
|
struct bt_gatt_discover_params *params)
|
|
{
|
|
struct bt_has_client *inst = CONTAINER_OF(params, struct bt_has_client, params.discover);
|
|
const struct bt_gatt_chrc *chrc;
|
|
int err;
|
|
|
|
LOG_DBG("conn %p attr %p params %p", (void *)conn, attr, params);
|
|
|
|
if (!attr) {
|
|
err = -ENOENT;
|
|
goto fail;
|
|
}
|
|
|
|
chrc = attr->user_data;
|
|
|
|
/* Subscribe first if notifications are supported, otherwise read the features */
|
|
if (chrc->properties & BT_GATT_CHRC_NOTIFY) {
|
|
err = features_subscribe(inst, chrc->value_handle);
|
|
if (err) {
|
|
LOG_ERR("Subscribe failed (err %d)", err);
|
|
goto fail;
|
|
}
|
|
} else {
|
|
err = features_read(inst, chrc->value_handle);
|
|
if (err) {
|
|
LOG_ERR("Read failed (err %d)", err);
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
return BT_GATT_ITER_STOP;
|
|
|
|
fail:
|
|
/* Cleanup instance so that it can be reused */
|
|
inst_cleanup(inst);
|
|
|
|
discover_failed(conn, err);
|
|
|
|
return BT_GATT_ITER_STOP;
|
|
}
|
|
|
|
static int features_discover(struct bt_has_client *inst)
|
|
{
|
|
LOG_DBG("conn %p", (void *)inst->conn);
|
|
|
|
(void)memset(&inst->params.discover, 0, sizeof(inst->params.discover));
|
|
|
|
(void)memcpy(&inst->params.uuid, BT_UUID_HAS_HEARING_AID_FEATURES,
|
|
sizeof(inst->params.uuid));
|
|
inst->params.discover.uuid = &inst->params.uuid.uuid;
|
|
inst->params.discover.func = features_discover_cb;
|
|
inst->params.discover.start_handle = BT_ATT_FIRST_ATTRIBUTE_HANDLE;
|
|
inst->params.discover.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE;
|
|
inst->params.discover.type = BT_GATT_DISCOVER_CHARACTERISTIC;
|
|
|
|
return bt_gatt_discover(inst->conn, &inst->params.discover);
|
|
}
|
|
|
|
int bt_has_client_cb_register(const struct bt_has_client_cb *cb)
|
|
{
|
|
CHECKIF(!cb) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
CHECKIF(client_cb) {
|
|
return -EALREADY;
|
|
}
|
|
|
|
client_cb = cb;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Hearing Access Service discovery
|
|
*
|
|
* This will initiate a discover procedure. The procedure will do the following sequence:
|
|
* 1) HAS related characteristic discovery
|
|
* 2) CCC subscription
|
|
* 3) Hearing Aid Features and Active Preset Index characteristic read
|
|
* 5) When everything above have been completed, the callback is called
|
|
*/
|
|
int bt_has_client_discover(struct bt_conn *conn)
|
|
{
|
|
struct bt_has_client *inst;
|
|
int err;
|
|
|
|
LOG_DBG("conn %p", (void *)conn);
|
|
|
|
CHECKIF(!conn || !client_cb || !client_cb->discover) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
inst = &clients[bt_conn_index(conn)];
|
|
|
|
if (atomic_test_bit(inst->flags, HAS_CLIENT_CP_OPERATION_IN_PROGRESS) ||
|
|
atomic_test_and_set_bit(inst->flags, HAS_CLIENT_DISCOVER_IN_PROGRESS)) {
|
|
return -EBUSY;
|
|
}
|
|
|
|
if (inst->conn) {
|
|
return -EALREADY;
|
|
}
|
|
|
|
inst->conn = bt_conn_ref(conn);
|
|
|
|
err = features_discover(inst);
|
|
if (err) {
|
|
atomic_clear_bit(inst->flags, HAS_CLIENT_DISCOVER_IN_PROGRESS);
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
int bt_has_client_conn_get(const struct bt_has *has, struct bt_conn **conn)
|
|
{
|
|
struct bt_has_client *inst = HAS_INST(has);
|
|
|
|
*conn = bt_conn_ref(inst->conn);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int bt_has_client_presets_read(struct bt_has *has, uint8_t start_index, uint8_t count)
|
|
{
|
|
struct bt_has_client *inst = HAS_INST(has);
|
|
int err;
|
|
|
|
LOG_DBG("conn %p start_index 0x%02x count %d", (void *)inst->conn, start_index, count);
|
|
|
|
if (!inst->conn) {
|
|
return -ENOTCONN;
|
|
}
|
|
|
|
if (atomic_test_bit(inst->flags, HAS_CLIENT_DISCOVER_IN_PROGRESS) ||
|
|
atomic_test_and_set_bit(inst->flags, HAS_CLIENT_CP_OPERATION_IN_PROGRESS)) {
|
|
return -EBUSY;
|
|
}
|
|
|
|
CHECKIF(start_index == BT_HAS_PRESET_INDEX_NONE) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
CHECKIF(count == 0u) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
err = read_presets_req(inst, start_index, count);
|
|
if (err) {
|
|
atomic_clear_bit(inst->flags, HAS_CLIENT_CP_OPERATION_IN_PROGRESS);
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
int bt_has_client_preset_set(struct bt_has *has, uint8_t index, bool sync)
|
|
{
|
|
struct bt_has_client *inst = HAS_INST(has);
|
|
uint8_t opcode;
|
|
|
|
LOG_DBG("conn %p index 0x%02x", (void *)inst->conn, index);
|
|
|
|
if (!inst->conn) {
|
|
return -ENOTCONN;
|
|
}
|
|
|
|
CHECKIF(index == BT_HAS_PRESET_INDEX_NONE) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (sync && (inst->has.features & BT_HAS_FEAT_PRESET_SYNC_SUPP) == 0) {
|
|
return -EOPNOTSUPP;
|
|
}
|
|
|
|
if (atomic_test_bit(inst->flags, HAS_CLIENT_DISCOVER_IN_PROGRESS) ||
|
|
atomic_test_and_set_bit(inst->flags, HAS_CLIENT_CP_OPERATION_IN_PROGRESS)) {
|
|
return -EBUSY;
|
|
}
|
|
|
|
opcode = sync ? BT_HAS_OP_SET_ACTIVE_PRESET_SYNC : BT_HAS_OP_SET_ACTIVE_PRESET;
|
|
|
|
return preset_set(inst, opcode, index);
|
|
}
|
|
|
|
int bt_has_client_preset_next(struct bt_has *has, bool sync)
|
|
{
|
|
struct bt_has_client *inst = HAS_INST(has);
|
|
uint8_t opcode;
|
|
|
|
LOG_DBG("conn %p sync %d", (void *)inst->conn, sync);
|
|
|
|
if (!inst->conn) {
|
|
return -ENOTCONN;
|
|
}
|
|
|
|
if (sync && (inst->has.features & BT_HAS_FEAT_PRESET_SYNC_SUPP) == 0) {
|
|
return -EOPNOTSUPP;
|
|
}
|
|
|
|
if (atomic_test_bit(inst->flags, HAS_CLIENT_DISCOVER_IN_PROGRESS) ||
|
|
atomic_test_and_set_bit(inst->flags, HAS_CLIENT_CP_OPERATION_IN_PROGRESS)) {
|
|
return -EBUSY;
|
|
}
|
|
|
|
opcode = sync ? BT_HAS_OP_SET_NEXT_PRESET_SYNC : BT_HAS_OP_SET_NEXT_PRESET;
|
|
|
|
return preset_set_next_or_prev(inst, opcode);
|
|
}
|
|
|
|
int bt_has_client_preset_prev(struct bt_has *has, bool sync)
|
|
{
|
|
struct bt_has_client *inst = HAS_INST(has);
|
|
uint8_t opcode;
|
|
|
|
LOG_DBG("conn %p sync %d", (void *)inst->conn, sync);
|
|
|
|
if (!inst->conn) {
|
|
return -ENOTCONN;
|
|
}
|
|
|
|
if (sync && (inst->has.features & BT_HAS_FEAT_PRESET_SYNC_SUPP) == 0) {
|
|
return -EOPNOTSUPP;
|
|
}
|
|
|
|
if (atomic_test_bit(inst->flags, HAS_CLIENT_DISCOVER_IN_PROGRESS) ||
|
|
atomic_test_and_set_bit(inst->flags, HAS_CLIENT_CP_OPERATION_IN_PROGRESS)) {
|
|
return -EBUSY;
|
|
}
|
|
|
|
opcode = sync ? BT_HAS_OP_SET_PREV_PRESET_SYNC : BT_HAS_OP_SET_PREV_PRESET;
|
|
|
|
return preset_set_next_or_prev(inst, opcode);
|
|
}
|
|
|
|
static void disconnected(struct bt_conn *conn, uint8_t reason)
|
|
{
|
|
struct bt_has_client *inst = inst_by_conn(conn);
|
|
|
|
if (!inst) {
|
|
return;
|
|
}
|
|
|
|
if (atomic_test_bit(inst->flags, HAS_CLIENT_DISCOVER_IN_PROGRESS)) {
|
|
discover_failed(conn, -ECONNABORTED);
|
|
}
|
|
|
|
inst_cleanup(inst);
|
|
}
|
|
|
|
BT_CONN_CB_DEFINE(conn_cb) = {
|
|
.disconnected = disconnected,
|
|
};
|