The bt_has_register_param's preset_sync_support and independent_presets make sense only if CONFIG_BT_HAS_PRESET_COUNT is non-zero meaning the CONFIG_BT_HAS_PRESET_SUPPORT is enabled. Otherwise, those parameters shall be skipped. Signed-off-by: Mariusz Skamra <mariusz.skamra@codecoup.pl>
1183 lines
29 KiB
C
1183 lines
29 KiB
C
/*
|
|
* Copyright (c) 2022 Codecoup
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#include <stdlib.h>
|
|
#include <zephyr/kernel.h>
|
|
#include <zephyr/sys/check.h>
|
|
|
|
#include <zephyr/device.h>
|
|
|
|
#include <zephyr/bluetooth/bluetooth.h>
|
|
#include <zephyr/bluetooth/gatt.h>
|
|
#include <zephyr/bluetooth/audio/audio.h>
|
|
#include <zephyr/bluetooth/audio/pacs.h>
|
|
#include <zephyr/bluetooth/audio/has.h>
|
|
#include <zephyr/sys/check.h>
|
|
|
|
#include "../bluetooth/host/conn_internal.h"
|
|
#include "../bluetooth/host/hci_core.h"
|
|
#include "audio_internal.h"
|
|
#include "has_internal.h"
|
|
|
|
#include <zephyr/logging/log.h>
|
|
|
|
LOG_MODULE_REGISTER(bt_has, CONFIG_BT_HAS_LOG_LEVEL);
|
|
|
|
/* The service allows operations with paired devices only.
|
|
* For now, the context is kept for connected devices only, thus the number of contexts is
|
|
* equal to maximum number of simultaneous connections to paired devices.
|
|
*/
|
|
#define BT_HAS_MAX_CONN MIN(CONFIG_BT_MAX_CONN, CONFIG_BT_MAX_PAIRED)
|
|
|
|
static struct bt_has has;
|
|
|
|
#if defined(CONFIG_BT_HAS_PRESET_SUPPORT)
|
|
static ssize_t write_control_point(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
|
const void *data, uint16_t len, uint16_t offset, uint8_t flags);
|
|
|
|
static ssize_t read_active_preset_index(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
|
void *buf, uint16_t len, uint16_t offset)
|
|
{
|
|
LOG_DBG("conn %p attr %p offset %d", (void *)conn, attr, offset);
|
|
|
|
if (offset > sizeof(has.active_index)) {
|
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
|
|
}
|
|
|
|
return bt_gatt_attr_read(conn, attr, buf, len, offset, &has.active_index,
|
|
sizeof(has.active_index));
|
|
}
|
|
|
|
static void ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value)
|
|
{
|
|
LOG_DBG("attr %p value 0x%04x", attr, value);
|
|
}
|
|
#endif /* CONFIG_BT_HAS_PRESET_SUPPORT */
|
|
|
|
static ssize_t read_features(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf,
|
|
uint16_t len, uint16_t offset)
|
|
{
|
|
LOG_DBG("conn %p attr %p offset %d", (void *)conn, attr, offset);
|
|
|
|
if (offset > sizeof(has.features)) {
|
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
|
|
}
|
|
|
|
return bt_gatt_attr_read(conn, attr, buf, len, offset, &has.features,
|
|
sizeof(has.features));
|
|
}
|
|
|
|
/* Hearing Access Service GATT Attributes */
|
|
static struct bt_gatt_attr has_attrs[] = {
|
|
BT_GATT_PRIMARY_SERVICE(BT_UUID_HAS),
|
|
BT_AUDIO_CHRC(BT_UUID_HAS_HEARING_AID_FEATURES,
|
|
BT_GATT_CHRC_READ,
|
|
BT_GATT_PERM_READ_ENCRYPT,
|
|
read_features, NULL, NULL),
|
|
#if defined(CONFIG_BT_HAS_PRESET_SUPPORT)
|
|
BT_AUDIO_CHRC(BT_UUID_HAS_PRESET_CONTROL_POINT,
|
|
#if defined(CONFIG_BT_EATT)
|
|
BT_GATT_CHRC_WRITE | BT_GATT_CHRC_INDICATE | BT_GATT_CHRC_NOTIFY,
|
|
#else
|
|
BT_GATT_CHRC_WRITE | BT_GATT_CHRC_INDICATE,
|
|
#endif /* CONFIG_BT_EATT */
|
|
BT_GATT_PERM_WRITE_ENCRYPT,
|
|
NULL, write_control_point, NULL),
|
|
BT_AUDIO_CCC(ccc_cfg_changed),
|
|
BT_AUDIO_CHRC(BT_UUID_HAS_ACTIVE_PRESET_INDEX,
|
|
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
|
|
BT_GATT_PERM_READ_ENCRYPT,
|
|
read_active_preset_index, NULL, NULL),
|
|
BT_AUDIO_CCC(ccc_cfg_changed),
|
|
#endif /* CONFIG_BT_HAS_PRESET_SUPPORT */
|
|
};
|
|
|
|
static struct bt_gatt_service has_svc;
|
|
|
|
#if defined(CONFIG_BT_HAS_PRESET_SUPPORT)
|
|
#define PRESET_CONTROL_POINT_ATTR &has_attrs[4]
|
|
#define ACTIVE_PRESET_INDEX_ATTR &has_attrs[7]
|
|
|
|
static struct has_client {
|
|
struct bt_conn *conn;
|
|
union {
|
|
struct bt_gatt_indicate_params ind;
|
|
#if defined(CONFIG_BT_EATT)
|
|
struct bt_gatt_notify_params ntf;
|
|
#endif /* CONFIG_BT_EATT */
|
|
} params;
|
|
|
|
struct {
|
|
bool is_pending;
|
|
uint8_t preset_changed_index_next;
|
|
} ntf_bonded;
|
|
struct bt_has_cp_read_presets_req read_presets_req;
|
|
struct k_work control_point_work;
|
|
struct k_work_sync control_point_work_sync;
|
|
} has_client_list[BT_HAS_MAX_CONN];
|
|
|
|
/* HAS internal preset representation */
|
|
static struct has_preset {
|
|
uint8_t index;
|
|
enum bt_has_properties properties;
|
|
#if defined(CONFIG_BT_HAS_PRESET_NAME_DYNAMIC)
|
|
char name[BT_HAS_PRESET_NAME_MAX + 1]; /* +1 byte for NULL-terminator */
|
|
#else
|
|
const char *name;
|
|
#endif /* CONFIG_BT_HAS_PRESET_NAME_DYNAMIC */
|
|
const struct bt_has_preset_ops *ops;
|
|
} has_preset_list[CONFIG_BT_HAS_PRESET_COUNT];
|
|
|
|
/* Number of registered presets */
|
|
static uint8_t has_preset_num;
|
|
|
|
/* Active preset notification work */
|
|
static struct k_work active_preset_work;
|
|
|
|
static void process_control_point_work(struct k_work *work);
|
|
|
|
static struct has_client *client_get_or_new(struct bt_conn *conn)
|
|
{
|
|
struct has_client *client = NULL;
|
|
|
|
for (size_t i = 0; i < ARRAY_SIZE(has_client_list); i++) {
|
|
if (conn == has_client_list[i].conn) {
|
|
return &has_client_list[i];
|
|
}
|
|
|
|
/* first free slot */
|
|
if (!client && !has_client_list[i].conn) {
|
|
client = &has_client_list[i];
|
|
}
|
|
}
|
|
|
|
__ASSERT(client, "failed to get client for conn %p", (void *)conn);
|
|
|
|
client->conn = bt_conn_ref(conn);
|
|
|
|
k_work_init(&client->control_point_work, process_control_point_work);
|
|
|
|
return client;
|
|
}
|
|
|
|
static bool read_presets_req_is_pending(struct has_client *client)
|
|
{
|
|
return client->read_presets_req.num_presets > 0;
|
|
}
|
|
|
|
static void read_presets_req_free(struct has_client *client)
|
|
{
|
|
client->read_presets_req.num_presets = 0;
|
|
}
|
|
|
|
static void client_free(struct has_client *client)
|
|
{
|
|
(void)k_work_cancel(&client->control_point_work);
|
|
|
|
read_presets_req_free(client);
|
|
|
|
client->ntf_bonded.is_pending = false;
|
|
|
|
bt_conn_unref(client->conn);
|
|
|
|
client->conn = NULL;
|
|
}
|
|
|
|
static struct has_client *client_get(struct bt_conn *conn)
|
|
{
|
|
for (size_t i = 0; i < ARRAY_SIZE(has_client_list); i++) {
|
|
if (conn == has_client_list[i].conn) {
|
|
return &has_client_list[i];
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void security_changed(struct bt_conn *conn, bt_security_t level, enum bt_security_err err)
|
|
{
|
|
struct has_client *client;
|
|
|
|
LOG_DBG("conn %p level %d err %d", (void *)conn, level, err);
|
|
|
|
if (err != BT_SECURITY_ERR_SUCCESS ||
|
|
!bt_addr_le_is_bonded(conn->id, &conn->le.dst)) {
|
|
return;
|
|
}
|
|
|
|
client = client_get_or_new(conn);
|
|
if (unlikely(!client)) {
|
|
LOG_ERR("Failed to allocate client");
|
|
return;
|
|
}
|
|
|
|
/* Notify after reconnection */
|
|
if (client->ntf_bonded.is_pending) {
|
|
/* Emit active preset notification */
|
|
k_work_submit(&active_preset_work);
|
|
|
|
/* Emit preset changed notifications */
|
|
k_work_submit(&client->control_point_work);
|
|
}
|
|
}
|
|
|
|
static void connected(struct bt_conn *conn, uint8_t err)
|
|
{
|
|
struct has_client *client;
|
|
|
|
LOG_DBG("conn %p err %d", conn, err);
|
|
|
|
if (err != 0 || !bt_addr_le_is_bonded(conn->id, &conn->le.dst)) {
|
|
return;
|
|
}
|
|
|
|
client = client_get_or_new(conn);
|
|
if (unlikely(!client)) {
|
|
LOG_ERR("Failed to allocate client");
|
|
return;
|
|
}
|
|
|
|
/* Mark preset changed operation as pending */
|
|
client->ntf_bonded.is_pending = true;
|
|
client->ntf_bonded.preset_changed_index_next = BT_HAS_PRESET_INDEX_FIRST;
|
|
}
|
|
|
|
static void disconnected(struct bt_conn *conn, uint8_t reason)
|
|
{
|
|
struct has_client *client;
|
|
|
|
LOG_DBG("conn %p reason %d", (void *)conn, reason);
|
|
|
|
client = client_get(conn);
|
|
if (client) {
|
|
client_free(client);
|
|
}
|
|
}
|
|
|
|
BT_CONN_CB_DEFINE(conn_cb) = {
|
|
.connected = connected,
|
|
.disconnected = disconnected,
|
|
.security_changed = security_changed,
|
|
};
|
|
|
|
typedef uint8_t (*preset_func_t)(const struct has_preset *preset, void *user_data);
|
|
|
|
static void preset_foreach(uint8_t start_index, uint8_t end_index, preset_func_t func,
|
|
void *user_data)
|
|
{
|
|
for (size_t i = 0; i < ARRAY_SIZE(has_preset_list); i++) {
|
|
const struct has_preset *preset = &has_preset_list[i];
|
|
|
|
if (preset->index < start_index) {
|
|
continue;
|
|
}
|
|
|
|
if (preset->index > end_index) {
|
|
return;
|
|
}
|
|
|
|
if (func(preset, user_data) == BT_HAS_PRESET_ITER_STOP) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static uint8_t preset_found(const struct has_preset *preset, void *user_data)
|
|
{
|
|
const struct has_preset **found = user_data;
|
|
|
|
*found = preset;
|
|
|
|
return BT_HAS_PRESET_ITER_STOP;
|
|
}
|
|
|
|
static int preset_index_compare(const void *p1, const void *p2)
|
|
{
|
|
const struct has_preset *preset_1 = p1;
|
|
const struct has_preset *preset_2 = p2;
|
|
|
|
if (preset_1->index == BT_HAS_PRESET_INDEX_NONE) {
|
|
return 1;
|
|
}
|
|
|
|
if (preset_2->index == BT_HAS_PRESET_INDEX_NONE) {
|
|
return -1;
|
|
}
|
|
|
|
return preset_1->index - preset_2->index;
|
|
}
|
|
|
|
static struct has_preset *preset_alloc(uint8_t index, enum bt_has_properties properties,
|
|
const char *name, const struct bt_has_preset_ops *ops)
|
|
{
|
|
struct has_preset *preset = NULL;
|
|
|
|
if (has_preset_num < ARRAY_SIZE(has_preset_list)) {
|
|
preset = &has_preset_list[has_preset_num];
|
|
preset->index = index;
|
|
preset->properties = properties;
|
|
#if defined(CONFIG_BT_HAS_PRESET_NAME_DYNAMIC)
|
|
utf8_lcpy(preset->name, name, ARRAY_SIZE(preset->name));
|
|
#else
|
|
preset->name = name;
|
|
#endif /* CONFIG_BT_HAS_PRESET_NAME_DYNAMIC */
|
|
preset->ops = ops;
|
|
|
|
has_preset_num++;
|
|
|
|
/* sort the presets in index ascending order */
|
|
qsort(has_preset_list, has_preset_num, sizeof(*preset), preset_index_compare);
|
|
}
|
|
|
|
return preset;
|
|
}
|
|
|
|
static void preset_free(struct has_preset *preset)
|
|
{
|
|
preset->index = BT_HAS_PRESET_INDEX_NONE;
|
|
|
|
/* sort the presets in index ascending order */
|
|
if (has_preset_num > 1) {
|
|
qsort(has_preset_list, has_preset_num, sizeof(*preset), preset_index_compare);
|
|
}
|
|
|
|
has_preset_num--;
|
|
}
|
|
|
|
static void control_point_ntf_complete(struct bt_conn *conn, void *user_data)
|
|
{
|
|
struct has_client *client = client_get(conn);
|
|
|
|
LOG_DBG("conn %p", (void *)conn);
|
|
|
|
/* Resubmit if needed */
|
|
if (client != NULL &&
|
|
(read_presets_req_is_pending(client) || client->ntf_bonded.is_pending)) {
|
|
k_work_submit(&client->control_point_work);
|
|
}
|
|
}
|
|
|
|
static void control_point_ind_complete(struct bt_conn *conn,
|
|
struct bt_gatt_indicate_params *params,
|
|
uint8_t err)
|
|
{
|
|
if (err) {
|
|
/* TODO: Handle error somehow */
|
|
LOG_ERR("conn %p err 0x%02x", (void *)conn, err);
|
|
}
|
|
|
|
control_point_ntf_complete(conn, NULL);
|
|
}
|
|
|
|
static int control_point_send(struct has_client *client, struct net_buf_simple *buf)
|
|
{
|
|
#if defined(CONFIG_BT_EATT)
|
|
if (bt_eatt_count(client->conn) > 0 &&
|
|
bt_gatt_is_subscribed(client->conn, PRESET_CONTROL_POINT_ATTR, BT_GATT_CCC_NOTIFY)) {
|
|
client->params.ntf.attr = PRESET_CONTROL_POINT_ATTR;
|
|
client->params.ntf.func = control_point_ntf_complete;
|
|
client->params.ntf.data = buf->data;
|
|
client->params.ntf.len = buf->len;
|
|
|
|
return bt_gatt_notify_cb(client->conn, &client->params.ntf);
|
|
}
|
|
#endif /* CONFIG_BT_EATT */
|
|
|
|
if (bt_gatt_is_subscribed(client->conn, PRESET_CONTROL_POINT_ATTR, BT_GATT_CCC_INDICATE)) {
|
|
client->params.ind.attr = PRESET_CONTROL_POINT_ATTR;
|
|
client->params.ind.func = control_point_ind_complete;
|
|
client->params.ind.destroy = NULL;
|
|
client->params.ind.data = buf->data;
|
|
client->params.ind.len = buf->len;
|
|
|
|
return bt_gatt_indicate(client->conn, &client->params.ind);
|
|
}
|
|
|
|
return -ECANCELED;
|
|
}
|
|
|
|
static int control_point_send_all(struct net_buf_simple *buf)
|
|
{
|
|
int result = 0;
|
|
|
|
for (size_t i = 0; i < ARRAY_SIZE(has_client_list); i++) {
|
|
struct has_client *client = &has_client_list[i];
|
|
int err;
|
|
|
|
if (!client->conn) {
|
|
continue;
|
|
}
|
|
|
|
if (!bt_gatt_is_subscribed(client->conn, PRESET_CONTROL_POINT_ATTR,
|
|
BT_GATT_CCC_NOTIFY | BT_GATT_CCC_INDICATE)) {
|
|
continue;
|
|
}
|
|
|
|
err = control_point_send(client, buf);
|
|
if (err) {
|
|
result = err;
|
|
/* continue anyway */
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static int bt_has_cp_read_preset_rsp(struct has_client *client, const struct has_preset *preset,
|
|
bool is_last)
|
|
{
|
|
struct bt_has_cp_hdr *hdr;
|
|
struct bt_has_cp_read_preset_rsp *rsp;
|
|
|
|
NET_BUF_SIMPLE_DEFINE(buf, sizeof(*hdr) + sizeof(*rsp) + BT_HAS_PRESET_NAME_MAX);
|
|
|
|
LOG_DBG("conn %p preset %p is_last 0x%02x", (void *)client->conn, preset, is_last);
|
|
|
|
hdr = net_buf_simple_add(&buf, sizeof(*hdr));
|
|
hdr->opcode = BT_HAS_OP_READ_PRESET_RSP;
|
|
rsp = net_buf_simple_add(&buf, sizeof(*rsp));
|
|
rsp->is_last = is_last ? 0x01 : 0x00;
|
|
rsp->index = preset->index;
|
|
rsp->properties = preset->properties;
|
|
net_buf_simple_add_mem(&buf, preset->name, strlen(preset->name));
|
|
|
|
return control_point_send(client, &buf);
|
|
}
|
|
|
|
static uint8_t get_prev_preset_index(const struct has_preset *preset)
|
|
{
|
|
const struct has_preset *prev = NULL;
|
|
|
|
for (size_t i = 0; i < ARRAY_SIZE(has_preset_list); i++) {
|
|
const struct has_preset *tmp = &has_preset_list[i];
|
|
|
|
if (tmp->index == BT_HAS_PRESET_INDEX_NONE || tmp == preset) {
|
|
break;
|
|
}
|
|
|
|
prev = tmp;
|
|
}
|
|
|
|
return prev ? prev->index : BT_HAS_PRESET_INDEX_NONE;
|
|
}
|
|
|
|
static void preset_changed_prepare(struct net_buf_simple *buf, uint8_t change_id, uint8_t is_last)
|
|
{
|
|
struct bt_has_cp_hdr *hdr;
|
|
struct bt_has_cp_preset_changed *preset_changed;
|
|
|
|
hdr = net_buf_simple_add(buf, sizeof(*hdr));
|
|
hdr->opcode = BT_HAS_OP_PRESET_CHANGED;
|
|
preset_changed = net_buf_simple_add(buf, sizeof(*preset_changed));
|
|
preset_changed->change_id = change_id;
|
|
preset_changed->is_last = is_last;
|
|
}
|
|
|
|
static int bt_has_cp_generic_update(struct has_client *client, const struct has_preset *preset,
|
|
uint8_t is_last)
|
|
{
|
|
struct bt_has_cp_generic_update *generic_update;
|
|
|
|
NET_BUF_SIMPLE_DEFINE(buf, sizeof(struct bt_has_cp_hdr) +
|
|
sizeof(struct bt_has_cp_preset_changed) +
|
|
sizeof(struct bt_has_cp_generic_update) + BT_HAS_PRESET_NAME_MAX);
|
|
|
|
preset_changed_prepare(&buf, BT_HAS_CHANGE_ID_GENERIC_UPDATE, is_last);
|
|
|
|
generic_update = net_buf_simple_add(&buf, sizeof(*generic_update));
|
|
generic_update->prev_index = get_prev_preset_index(preset);
|
|
generic_update->index = preset->index;
|
|
generic_update->properties = preset->properties;
|
|
net_buf_simple_add_mem(&buf, preset->name, strlen(preset->name));
|
|
|
|
if (client) {
|
|
return control_point_send(client, &buf);
|
|
} else {
|
|
return control_point_send_all(&buf);
|
|
}
|
|
}
|
|
|
|
static void process_control_point_work(struct k_work *work)
|
|
{
|
|
struct has_client *client = CONTAINER_OF(work, struct has_client, control_point_work);
|
|
int err;
|
|
|
|
if (!client->conn) {
|
|
return;
|
|
}
|
|
|
|
if (read_presets_req_is_pending(client)) {
|
|
const struct has_preset *preset = NULL;
|
|
bool is_last = true;
|
|
|
|
preset_foreach(client->read_presets_req.start_index, BT_HAS_PRESET_INDEX_LAST,
|
|
preset_found, &preset);
|
|
|
|
if (unlikely(preset == NULL)) {
|
|
(void)bt_has_cp_read_preset_rsp(client, NULL, 0x01);
|
|
|
|
return;
|
|
}
|
|
|
|
if (client->read_presets_req.num_presets > 1) {
|
|
const struct has_preset *next = NULL;
|
|
|
|
preset_foreach(preset->index + 1, BT_HAS_PRESET_INDEX_LAST,
|
|
preset_found, &next);
|
|
|
|
is_last = next == NULL;
|
|
|
|
}
|
|
|
|
err = bt_has_cp_read_preset_rsp(client, preset, is_last);
|
|
if (err) {
|
|
LOG_ERR("bt_has_cp_read_preset_rsp failed (err %d)", err);
|
|
}
|
|
|
|
if (err || is_last) {
|
|
read_presets_req_free(client);
|
|
} else {
|
|
client->read_presets_req.start_index = preset->index + 1;
|
|
client->read_presets_req.num_presets--;
|
|
}
|
|
} else if (client->ntf_bonded.is_pending) {
|
|
const struct has_preset *preset = NULL;
|
|
const struct has_preset *next = NULL;
|
|
bool is_last = true;
|
|
|
|
preset_foreach(client->ntf_bonded.preset_changed_index_next,
|
|
BT_HAS_PRESET_INDEX_LAST, preset_found, &preset);
|
|
|
|
if (preset == NULL) {
|
|
return;
|
|
}
|
|
|
|
preset_foreach(preset->index + 1, BT_HAS_PRESET_INDEX_LAST,
|
|
preset_found, &next);
|
|
|
|
is_last = next == NULL;
|
|
|
|
err = bt_has_cp_generic_update(client, preset, is_last);
|
|
if (err) {
|
|
LOG_ERR("bt_has_cp_read_preset_rsp failed (err %d)", err);
|
|
}
|
|
|
|
if (err || is_last) {
|
|
client->ntf_bonded.is_pending = false;
|
|
} else {
|
|
client->ntf_bonded.preset_changed_index_next = preset->index + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
static uint8_t handle_read_preset_req(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_has_cp_read_presets_req *req;
|
|
const struct has_preset *preset = NULL;
|
|
struct has_client *client;
|
|
|
|
if (buf->len < sizeof(*req)) {
|
|
return BT_HAS_ERR_INVALID_PARAM_LEN;
|
|
}
|
|
|
|
/* As per HAS_d1.0r00 Client Characteristic Configuration Descriptor Improperly Configured
|
|
* shall be returned if client writes Read Presets Request but is not registered for
|
|
* indications.
|
|
*/
|
|
if (!bt_gatt_is_subscribed(conn, PRESET_CONTROL_POINT_ATTR, BT_GATT_CCC_INDICATE)) {
|
|
return BT_ATT_ERR_CCC_IMPROPER_CONF;
|
|
}
|
|
|
|
client = client_get(conn);
|
|
if (!client) {
|
|
return BT_ATT_ERR_UNLIKELY;
|
|
}
|
|
|
|
req = net_buf_simple_pull_mem(buf, sizeof(*req));
|
|
|
|
LOG_DBG("start_index %d num_presets %d", req->start_index, req->num_presets);
|
|
|
|
/* Abort if there is no preset in requested index range */
|
|
preset_foreach(req->start_index, BT_HAS_PRESET_INDEX_LAST, preset_found, &preset);
|
|
|
|
if (preset == NULL) {
|
|
return BT_ATT_ERR_OUT_OF_RANGE;
|
|
}
|
|
|
|
/* Reject if already in progress */
|
|
if (read_presets_req_is_pending(client)) {
|
|
return BT_HAS_ERR_OPERATION_NOT_POSSIBLE;
|
|
}
|
|
|
|
/* Store the request */
|
|
client->read_presets_req.start_index = req->start_index;
|
|
client->read_presets_req.num_presets = req->num_presets;
|
|
|
|
k_work_submit(&client->control_point_work);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int set_preset_name(uint8_t index, const char *name, size_t len)
|
|
{
|
|
struct has_preset *preset = NULL;
|
|
|
|
LOG_DBG("index %d name_len %zu", index, len);
|
|
|
|
if (len < BT_HAS_PRESET_NAME_MIN || len > BT_HAS_PRESET_NAME_MAX) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* Abort if there is no preset in requested index range */
|
|
preset_foreach(index, BT_HAS_PRESET_INDEX_LAST, preset_found, &preset);
|
|
|
|
if (preset == NULL) {
|
|
return -ENOENT;
|
|
}
|
|
|
|
if (!(preset->properties & BT_HAS_PROP_WRITABLE)) {
|
|
return -EPERM;
|
|
}
|
|
|
|
IF_ENABLED(CONFIG_BT_HAS_PRESET_NAME_DYNAMIC, (
|
|
__ASSERT(len < ARRAY_SIZE(preset->name), "No space for name");
|
|
|
|
(void)memcpy(preset->name, name, len);
|
|
|
|
/* NULL-terminate string */
|
|
preset->name[len] = '\0';
|
|
|
|
/* Properly truncate a NULL-terminated UTF-8 string */
|
|
utf8_trunc(preset->name);
|
|
));
|
|
|
|
if (preset->ops->name_changed) {
|
|
preset->ops->name_changed(index, preset->name);
|
|
}
|
|
|
|
return bt_has_cp_generic_update(NULL, preset, BT_HAS_IS_LAST);
|
|
}
|
|
|
|
static uint8_t handle_write_preset_name(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_has_cp_write_preset_name *req;
|
|
struct has_client *client;
|
|
int err;
|
|
|
|
if (buf->len < sizeof(*req)) {
|
|
return BT_HAS_ERR_INVALID_PARAM_LEN;
|
|
}
|
|
|
|
/* As per HAS_v1.0 Client Characteristic Configuration Descriptor Improperly Configured
|
|
* shall be returned if client writes Write Preset Name opcode but is not registered for
|
|
* indications.
|
|
*/
|
|
if (!bt_gatt_is_subscribed(conn, PRESET_CONTROL_POINT_ATTR, BT_GATT_CCC_INDICATE)) {
|
|
return BT_ATT_ERR_CCC_IMPROPER_CONF;
|
|
}
|
|
|
|
client = client_get(conn);
|
|
if (!client) {
|
|
return BT_ATT_ERR_UNLIKELY;
|
|
}
|
|
|
|
req = net_buf_simple_pull_mem(buf, sizeof(*req));
|
|
|
|
err = set_preset_name(req->index, req->name, buf->len);
|
|
if (err == -EINVAL) {
|
|
return BT_HAS_ERR_INVALID_PARAM_LEN;
|
|
} else if (err == -ENOENT) {
|
|
return BT_ATT_ERR_OUT_OF_RANGE;
|
|
} else if (err == -EPERM) {
|
|
return BT_HAS_ERR_WRITE_NAME_NOT_ALLOWED;
|
|
} else if (err) {
|
|
return BT_ATT_ERR_UNLIKELY;
|
|
}
|
|
|
|
return BT_ATT_ERR_SUCCESS;
|
|
}
|
|
|
|
static void active_preset_work_process(struct k_work *work)
|
|
{
|
|
const uint8_t active_index = bt_has_preset_active_get();
|
|
|
|
bt_gatt_notify(NULL, ACTIVE_PRESET_INDEX_ATTR, &active_index, sizeof(active_index));
|
|
}
|
|
|
|
static void preset_active_set(uint8_t index)
|
|
{
|
|
if (index != has.active_index) {
|
|
has.active_index = index;
|
|
|
|
/* Emit active preset notification */
|
|
k_work_submit(&active_preset_work);
|
|
}
|
|
}
|
|
|
|
static uint8_t preset_select(const struct has_preset *preset, bool sync)
|
|
{
|
|
const int err = preset->ops->select(preset->index, sync);
|
|
|
|
if (err == -EINPROGRESS) {
|
|
/* User has to confirm once the requested preset becomes active by
|
|
* calling bt_has_preset_active_set.
|
|
*/
|
|
return 0;
|
|
}
|
|
|
|
if (err == -EBUSY) {
|
|
return BT_HAS_ERR_OPERATION_NOT_POSSIBLE;
|
|
}
|
|
|
|
if (err) {
|
|
return BT_ATT_ERR_UNLIKELY;
|
|
}
|
|
|
|
preset_active_set(preset->index);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static uint8_t handle_set_active_preset(struct net_buf_simple *buf, bool sync)
|
|
{
|
|
const struct bt_has_cp_set_active_preset *pdu;
|
|
const struct has_preset *preset = NULL;
|
|
|
|
if (buf->len < sizeof(*pdu)) {
|
|
return BT_HAS_ERR_INVALID_PARAM_LEN;
|
|
}
|
|
|
|
pdu = net_buf_simple_pull_mem(buf, sizeof(*pdu));
|
|
|
|
preset_foreach(pdu->index, pdu->index, preset_found, &preset);
|
|
if (preset == NULL) {
|
|
return BT_ATT_ERR_OUT_OF_RANGE;
|
|
}
|
|
|
|
if (!(preset->properties & BT_HAS_PROP_AVAILABLE)) {
|
|
return BT_HAS_ERR_OPERATION_NOT_POSSIBLE;
|
|
}
|
|
|
|
return preset_select(preset, sync);
|
|
}
|
|
|
|
static uint8_t handle_set_next_preset(bool sync)
|
|
{
|
|
const struct has_preset *next_avail = NULL;
|
|
const struct has_preset *first_avail = NULL;
|
|
|
|
for (size_t i = 0; i < has_preset_num; i++) {
|
|
const struct has_preset *tmp = &has_preset_list[i];
|
|
|
|
if (tmp->index == BT_HAS_PRESET_INDEX_NONE) {
|
|
break;
|
|
}
|
|
|
|
if (!(tmp->properties & BT_HAS_PROP_AVAILABLE)) {
|
|
continue;
|
|
}
|
|
|
|
if (tmp->index < has.active_index && !first_avail) {
|
|
first_avail = tmp;
|
|
continue;
|
|
}
|
|
|
|
if (tmp->index > has.active_index) {
|
|
next_avail = tmp;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (next_avail) {
|
|
return preset_select(next_avail, sync);
|
|
}
|
|
|
|
if (first_avail) {
|
|
return preset_select(first_avail, sync);
|
|
}
|
|
|
|
return BT_HAS_ERR_OPERATION_NOT_POSSIBLE;
|
|
}
|
|
|
|
static uint8_t handle_set_prev_preset(bool sync)
|
|
{
|
|
const struct has_preset *prev_available = NULL;
|
|
const struct has_preset *last_available = NULL;
|
|
|
|
for (size_t i = 0; i < ARRAY_SIZE(has_preset_list); i++) {
|
|
const struct has_preset *tmp = &has_preset_list[i];
|
|
|
|
if (tmp->index == BT_HAS_PRESET_INDEX_NONE) {
|
|
break;
|
|
}
|
|
|
|
if (!(tmp->properties & BT_HAS_PROP_AVAILABLE)) {
|
|
continue;
|
|
}
|
|
|
|
if (tmp->index < has.active_index) {
|
|
prev_available = tmp;
|
|
continue;
|
|
}
|
|
|
|
if (prev_available) {
|
|
break;
|
|
}
|
|
|
|
if (tmp->index > has.active_index) {
|
|
last_available = tmp;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (prev_available) {
|
|
return preset_select(prev_available, sync);
|
|
}
|
|
|
|
if (last_available) {
|
|
return preset_select(last_available, sync);
|
|
}
|
|
|
|
return BT_HAS_ERR_OPERATION_NOT_POSSIBLE;
|
|
}
|
|
|
|
static uint8_t handle_control_point_op(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_has_cp_hdr *hdr;
|
|
|
|
hdr = net_buf_simple_pull_mem(buf, sizeof(*hdr));
|
|
|
|
LOG_DBG("conn %p opcode %s (0x%02x)", (void *)conn, bt_has_op_str(hdr->opcode),
|
|
hdr->opcode);
|
|
|
|
switch (hdr->opcode) {
|
|
case BT_HAS_OP_READ_PRESET_REQ:
|
|
return handle_read_preset_req(conn, buf);
|
|
case BT_HAS_OP_WRITE_PRESET_NAME:
|
|
if (IS_ENABLED(CONFIG_BT_HAS_PRESET_NAME_DYNAMIC)) {
|
|
return handle_write_preset_name(conn, buf);
|
|
}
|
|
break;
|
|
case BT_HAS_OP_SET_ACTIVE_PRESET:
|
|
return handle_set_active_preset(buf, false);
|
|
case BT_HAS_OP_SET_NEXT_PRESET:
|
|
return handle_set_next_preset(false);
|
|
case BT_HAS_OP_SET_PREV_PRESET:
|
|
return handle_set_prev_preset(false);
|
|
case BT_HAS_OP_SET_ACTIVE_PRESET_SYNC:
|
|
if ((has.features & BT_HAS_FEAT_PRESET_SYNC_SUPP) != 0) {
|
|
return handle_set_active_preset(buf, true);
|
|
} else {
|
|
return BT_HAS_ERR_PRESET_SYNC_NOT_SUPP;
|
|
}
|
|
case BT_HAS_OP_SET_NEXT_PRESET_SYNC:
|
|
if ((has.features & BT_HAS_FEAT_PRESET_SYNC_SUPP) != 0) {
|
|
return handle_set_next_preset(true);
|
|
} else {
|
|
return BT_HAS_ERR_PRESET_SYNC_NOT_SUPP;
|
|
}
|
|
case BT_HAS_OP_SET_PREV_PRESET_SYNC:
|
|
if ((has.features & BT_HAS_FEAT_PRESET_SYNC_SUPP) != 0) {
|
|
return handle_set_prev_preset(true);
|
|
} else {
|
|
return BT_HAS_ERR_PRESET_SYNC_NOT_SUPP;
|
|
}
|
|
};
|
|
|
|
return BT_HAS_ERR_INVALID_OPCODE;
|
|
}
|
|
|
|
static ssize_t write_control_point(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
|
const void *data, uint16_t len, uint16_t offset, uint8_t flags)
|
|
{
|
|
struct net_buf_simple buf;
|
|
uint8_t err;
|
|
|
|
LOG_DBG("conn %p attr %p data %p len %d offset %d flags 0x%02x", (void *)conn, attr, data,
|
|
len, offset, flags);
|
|
|
|
if (offset > 0) {
|
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
|
|
}
|
|
|
|
if (len == 0) {
|
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
|
}
|
|
|
|
net_buf_simple_init_with_data(&buf, (void *)data, len);
|
|
|
|
err = handle_control_point_op(conn, &buf);
|
|
if (err) {
|
|
LOG_WRN("err 0x%02x", err);
|
|
return BT_GATT_ERR(err);
|
|
}
|
|
|
|
return len;
|
|
}
|
|
|
|
int bt_has_preset_register(const struct bt_has_preset_register_param *param)
|
|
{
|
|
struct has_preset *preset = NULL;
|
|
size_t name_len;
|
|
|
|
CHECKIF(param == NULL) {
|
|
LOG_ERR("param is NULL");
|
|
return -EINVAL;
|
|
}
|
|
|
|
CHECKIF(param->index == BT_HAS_PRESET_INDEX_NONE) {
|
|
LOG_ERR("param->index is invalid");
|
|
return -EINVAL;
|
|
}
|
|
|
|
CHECKIF(param->name == NULL) {
|
|
LOG_ERR("param->name is NULL");
|
|
return -EINVAL;
|
|
}
|
|
|
|
name_len = strlen(param->name);
|
|
|
|
CHECKIF(name_len < BT_HAS_PRESET_NAME_MIN) {
|
|
LOG_ERR("param->name is too short (%zu < %u)", name_len, BT_HAS_PRESET_NAME_MIN);
|
|
return -EINVAL;
|
|
}
|
|
|
|
CHECKIF(name_len > BT_HAS_PRESET_NAME_MAX) {
|
|
LOG_WRN("param->name is too long (%zu > %u)", name_len, BT_HAS_PRESET_NAME_MAX);
|
|
}
|
|
|
|
CHECKIF(param->ops == NULL) {
|
|
LOG_ERR("param->ops is NULL");
|
|
return -EINVAL;
|
|
}
|
|
|
|
CHECKIF(param->ops->select == NULL) {
|
|
LOG_ERR("param->ops->select is NULL");
|
|
return -EINVAL;
|
|
}
|
|
|
|
preset_foreach(param->index, param->index, preset_found, &preset);
|
|
if (preset != NULL) {
|
|
return -EALREADY;
|
|
}
|
|
|
|
preset = preset_alloc(param->index, param->properties, param->name, param->ops);
|
|
if (preset == NULL) {
|
|
return -ENOMEM;
|
|
}
|
|
|
|
return bt_has_cp_generic_update(NULL, preset, BT_HAS_IS_LAST);
|
|
}
|
|
|
|
int bt_has_preset_unregister(uint8_t index)
|
|
{
|
|
struct has_preset *preset = NULL;
|
|
|
|
NET_BUF_SIMPLE_DEFINE(buf, sizeof(struct bt_has_cp_hdr) +
|
|
sizeof(struct bt_has_cp_preset_changed) + sizeof(uint8_t));
|
|
|
|
CHECKIF(index == BT_HAS_PRESET_INDEX_NONE) {
|
|
LOG_ERR("index is invalid");
|
|
return -EINVAL;
|
|
}
|
|
|
|
preset_foreach(index, index, preset_found, &preset);
|
|
if (preset == NULL) {
|
|
return -ENOENT;
|
|
}
|
|
|
|
preset_changed_prepare(&buf, BT_HAS_CHANGE_ID_PRESET_DELETED, BT_HAS_IS_LAST);
|
|
net_buf_simple_add_u8(&buf, preset->index);
|
|
|
|
preset_free(preset);
|
|
|
|
return control_point_send_all(&buf);
|
|
}
|
|
|
|
int bt_has_preset_available(uint8_t index)
|
|
{
|
|
struct has_preset *preset = NULL;
|
|
|
|
CHECKIF(index == BT_HAS_PRESET_INDEX_NONE) {
|
|
LOG_ERR("index is invalid");
|
|
return -EINVAL;
|
|
}
|
|
|
|
preset_foreach(index, index, preset_found, &preset);
|
|
if (preset == NULL) {
|
|
return -ENOENT;
|
|
}
|
|
|
|
/* toggle property bit if needed */
|
|
if (!(preset->properties & BT_HAS_PROP_AVAILABLE)) {
|
|
NET_BUF_SIMPLE_DEFINE(buf, sizeof(struct bt_has_cp_hdr) +
|
|
sizeof(struct bt_has_cp_preset_changed) + sizeof(uint8_t));
|
|
|
|
preset->properties ^= BT_HAS_PROP_AVAILABLE;
|
|
|
|
preset_changed_prepare(&buf, BT_HAS_CHANGE_ID_PRESET_AVAILABLE, BT_HAS_IS_LAST);
|
|
net_buf_simple_add_u8(&buf, preset->index);
|
|
|
|
return control_point_send_all(&buf);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int bt_has_preset_unavailable(uint8_t index)
|
|
{
|
|
struct has_preset *preset = NULL;
|
|
|
|
CHECKIF(index == BT_HAS_PRESET_INDEX_NONE) {
|
|
LOG_ERR("index is invalid");
|
|
return -EINVAL;
|
|
}
|
|
|
|
preset_foreach(index, index, preset_found, &preset);
|
|
if (preset == NULL) {
|
|
return -ENOENT;
|
|
}
|
|
|
|
/* toggle property bit if needed */
|
|
if (preset->properties & BT_HAS_PROP_AVAILABLE) {
|
|
NET_BUF_SIMPLE_DEFINE(buf, sizeof(struct bt_has_cp_hdr) +
|
|
sizeof(struct bt_has_cp_preset_changed) + sizeof(uint8_t));
|
|
|
|
preset->properties ^= BT_HAS_PROP_AVAILABLE;
|
|
|
|
preset_changed_prepare(&buf, BT_HAS_CHANGE_ID_PRESET_UNAVAILABLE, BT_HAS_IS_LAST);
|
|
net_buf_simple_add_u8(&buf, preset->index);
|
|
|
|
return control_point_send_all(&buf);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
struct bt_has_preset_foreach_data {
|
|
bt_has_preset_func_t func;
|
|
void *user_data;
|
|
};
|
|
|
|
static uint8_t bt_has_preset_foreach_func(const struct has_preset *preset, void *user_data)
|
|
{
|
|
const struct bt_has_preset_foreach_data *data = user_data;
|
|
|
|
return data->func(preset->index, preset->properties, preset->name, data->user_data);
|
|
}
|
|
|
|
void bt_has_preset_foreach(uint8_t index, bt_has_preset_func_t func, void *user_data)
|
|
{
|
|
uint8_t start_index, end_index;
|
|
struct bt_has_preset_foreach_data data = {
|
|
.func = func,
|
|
.user_data = user_data,
|
|
};
|
|
|
|
if (index == BT_HAS_PRESET_INDEX_NONE) {
|
|
start_index = BT_HAS_PRESET_INDEX_FIRST;
|
|
end_index = BT_HAS_PRESET_INDEX_LAST;
|
|
} else {
|
|
start_index = end_index = index;
|
|
}
|
|
|
|
preset_foreach(start_index, end_index, bt_has_preset_foreach_func, &data);
|
|
}
|
|
|
|
int bt_has_preset_active_set(uint8_t index)
|
|
{
|
|
if (index != BT_HAS_PRESET_INDEX_NONE) {
|
|
struct has_preset *preset = NULL;
|
|
|
|
preset_foreach(index, index, preset_found, &preset);
|
|
if (preset == NULL) {
|
|
return -ENOENT;
|
|
}
|
|
|
|
if (!(preset->properties & BT_HAS_PROP_AVAILABLE)) {
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
preset_active_set(index);
|
|
|
|
return 0;
|
|
}
|
|
|
|
uint8_t bt_has_preset_active_get(void)
|
|
{
|
|
return has.active_index;
|
|
}
|
|
|
|
int bt_has_preset_name_change(uint8_t index, const char *name)
|
|
{
|
|
CHECKIF(name == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (IS_ENABLED(CONFIG_BT_HAS_PRESET_NAME_DYNAMIC)) {
|
|
return set_preset_name(index, name, strlen(name));
|
|
} else {
|
|
return -EOPNOTSUPP;
|
|
}
|
|
}
|
|
#endif /* CONFIG_BT_HAS_PRESET_SUPPORT */
|
|
|
|
int bt_has_register(const struct bt_has_register_param *param)
|
|
{
|
|
int err;
|
|
|
|
LOG_DBG("param %p", param);
|
|
|
|
CHECKIF(!param) {
|
|
LOG_DBG("NULL params pointer");
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* Initialize the supported features characteristic value */
|
|
has.features = param->type;
|
|
|
|
if (IS_ENABLED(CONFIG_BT_HAS_PRESET_SUPPORT)) {
|
|
has.features |= BT_HAS_FEAT_DYNAMIC_PRESETS;
|
|
|
|
if (param->preset_sync_support) {
|
|
if (param->type != BT_HAS_HEARING_AID_TYPE_BINAURAL) {
|
|
LOG_DBG("Preset sync support only available "
|
|
"for binaural hearing aid type");
|
|
return -EINVAL;
|
|
}
|
|
|
|
has.features |= BT_HAS_FEAT_PRESET_SYNC_SUPP;
|
|
}
|
|
|
|
if (param->independent_presets) {
|
|
if (param->type != BT_HAS_HEARING_AID_TYPE_BINAURAL) {
|
|
LOG_DBG("Independent presets only available "
|
|
"for binaural hearing aid type");
|
|
return -EINVAL;
|
|
}
|
|
|
|
has.features |= BT_HAS_FEAT_INDEPENDENT_PRESETS;
|
|
}
|
|
}
|
|
|
|
if (IS_ENABLED(CONFIG_BT_HAS_PRESET_NAME_DYNAMIC)) {
|
|
has.features |= BT_HAS_FEAT_WRITABLE_PRESETS_SUPP;
|
|
}
|
|
|
|
has_svc = (struct bt_gatt_service)BT_GATT_SERVICE(has_attrs);
|
|
err = bt_gatt_service_register(&has_svc);
|
|
if (err != 0) {
|
|
LOG_DBG("HAS service register failed: %d", err);
|
|
return err;
|
|
}
|
|
|
|
#if defined(CONFIG_BT_HAS_PRESET_SUPPORT)
|
|
k_work_init(&active_preset_work, active_preset_work_process);
|
|
#endif /* CONFIG_BT_HAS_PRESET_SUPPORT */
|
|
|
|
return 0;
|
|
}
|