blob: 77f40d48bd547bf7751739930ade4c5a4442fc9b [file] [log] [blame]
/*
* 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 has_inst, has)
#define HANDLE_IS_VALID(handle) ((handle) != 0x0000)
enum {
HAS_DISCOVER_IN_PROGRESS,
HAS_CP_OPERATION_IN_PROGRESS,
HAS_NUM_FLAGS, /* keep as last */
};
static struct has_inst {
/** Common profile reference object */
struct bt_has has;
/** Profile connection reference */
struct bt_conn *conn;
/** Internal flags */
ATOMIC_DEFINE(flags, HAS_NUM_FLAGS);
/* GATT procedure parameters */
union {
struct {
struct bt_uuid_16 uuid;
union {
struct bt_gatt_read_params read;
struct bt_gatt_discover_params discover;
};
};
struct bt_gatt_write_params write;
} params;
struct bt_gatt_subscribe_params features_subscription;
struct bt_gatt_subscribe_params control_point_subscription;
struct bt_gatt_subscribe_params active_index_subscription;
} has_insts[CONFIG_BT_MAX_CONN];
static const struct bt_has_client_cb *client_cb;
static struct has_inst *inst_by_conn(struct bt_conn *conn)
{
struct has_inst *inst = &has_insts[bt_conn_index(conn)];
if (inst->conn == conn) {
return inst;
}
return NULL;
}
static void inst_cleanup(struct has_inst *inst)
{
bt_conn_unref(inst->conn);
(void)memset(inst, 0, sizeof(*inst));
}
static enum bt_has_capabilities get_capabilities(const struct has_inst *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 has_inst *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 has_inst *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 has_inst *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 has_inst *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 has_inst *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)
{
const struct bt_has_cp_hdr *hdr;
struct net_buf_simple buf;
struct has_inst *inst;
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;
}
inst = inst_by_conn(conn);
if (!inst) {
/* Ignore notification from unknown instance */
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 has_inst *inst)
{
LOG_DBG("conn %p", (void *)inst->conn);
atomic_clear_bit(inst->flags, HAS_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 has_inst *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 has_inst *inst = inst_by_conn(conn);
__ASSERT(inst, "no instance for conn %p", (void *)conn);
LOG_DBG("conn %p err 0x%02x param %p", (void *)conn, err, params);
atomic_clear_bit(inst->flags, HAS_CP_OPERATION_IN_PROGRESS);
if (err) {
client_cb->preset_read_rsp(&inst->has, err, NULL, true);
}
}
static int read_presets_req(struct has_inst *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 has_inst *inst = inst_by_conn(conn);
__ASSERT(inst, "no instance for conn %p", (void *)conn);
LOG_DBG("conn %p err 0x%02x param %p", (void *)conn, err, params);
atomic_clear_bit(inst->flags, HAS_CP_OPERATION_IN_PROGRESS);
if (err) {
client_cb->preset_switch(&inst->has, err, inst->has.active_index);
}
}
static int preset_set(struct has_inst *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 has_inst *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 has_inst *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 has_inst *inst;
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;
}
inst = inst_by_conn(conn);
if (!inst) {
/* Ignore notification from unknown instance */
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_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 has_inst *inst = inst_by_conn(conn);
__ASSERT(inst, "no instance for conn %p", (void *)conn);
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 has_inst *inst, uint16_t value_handle)
{
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);
return bt_gatt_subscribe(inst->conn, &inst->active_index_subscription);
}
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 has_inst *inst = inst_by_conn(conn);
int err = att_err;
__ASSERT(inst, "no instance for conn %p", (void *)conn);
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 has_inst *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 *subscribe)
{
struct has_inst *inst = inst_by_conn(conn);
int err = att_err;
__ASSERT(inst, "no instance for conn %p", (void *)conn);
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 has_inst *inst, uint16_t value_handle,
uint8_t properties)
{
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;
}
return bt_gatt_subscribe(inst->conn, &inst->control_point_subscription);
}
static uint8_t control_point_discover_cb(struct bt_conn *conn, const struct bt_gatt_attr *attr,
struct bt_gatt_discover_params *params)
{
struct has_inst *inst = inst_by_conn(conn);
const struct bt_gatt_chrc *chrc;
int err;
__ASSERT(inst, "no instance for conn %p", (void *)conn);
LOG_DBG("conn %p attr %p params %p", (void *)inst->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 has_inst *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 has_inst *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 has_inst *inst = inst_by_conn(conn);
int err = att_err;
__ASSERT(inst, "no instance for conn %p", (void *)conn);
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 has_inst *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 has_inst *inst = inst_by_conn(conn);
int err = att_err;
__ASSERT(inst, "no instance for conn %p", (void *)conn);
LOG_DBG("conn %p att_err 0x%02x params %p", (void *)inst->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 has_inst *inst;
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;
}
inst = inst_by_conn(conn);
if (!inst) {
/* Ignore notification from unknown instance */
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 has_inst *inst, uint16_t value_handle)
{
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);
return bt_gatt_subscribe(inst->conn, &inst->features_subscription);
}
static uint8_t features_discover_cb(struct bt_conn *conn, const struct bt_gatt_attr *attr,
struct bt_gatt_discover_params *params)
{
struct has_inst *inst = inst_by_conn(conn);
const struct bt_gatt_chrc *chrc;
int err;
__ASSERT(inst, "no instance for conn %p", (void *)conn);
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 has_inst *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 has_inst *inst;
int err;
LOG_DBG("conn %p", (void *)conn);
CHECKIF(!conn || !client_cb || !client_cb->discover) {
return -EINVAL;
}
inst = &has_insts[bt_conn_index(conn)];
if (atomic_test_bit(inst->flags, HAS_CP_OPERATION_IN_PROGRESS) ||
atomic_test_and_set_bit(inst->flags, HAS_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_DISCOVER_IN_PROGRESS);
}
return err;
}
int bt_has_client_conn_get(const struct bt_has *has, struct bt_conn **conn)
{
struct has_inst *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 has_inst *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_DISCOVER_IN_PROGRESS) ||
atomic_test_and_set_bit(inst->flags, HAS_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_CP_OPERATION_IN_PROGRESS);
}
return err;
}
int bt_has_client_preset_set(struct bt_has *has, uint8_t index, bool sync)
{
struct has_inst *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_DISCOVER_IN_PROGRESS) ||
atomic_test_and_set_bit(inst->flags, HAS_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 has_inst *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_DISCOVER_IN_PROGRESS) ||
atomic_test_and_set_bit(inst->flags, HAS_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 has_inst *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_DISCOVER_IN_PROGRESS) ||
atomic_test_and_set_bit(inst->flags, HAS_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 has_inst *inst = inst_by_conn(conn);
if (!inst) {
return;
}
if (atomic_test_bit(inst->flags, HAS_DISCOVER_IN_PROGRESS)) {
discover_failed(conn, -ECONNABORTED);
}
inst_cleanup(inst);
}
BT_CONN_CB_DEFINE(conn_cb) = {
.disconnected = disconnected,
};