| /* |
| * 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 pending_active_index; |
| bool pending_cp; |
| uint8_t preset_changed_index_next; |
| } ntf_bonded; |
| struct bt_has_cp_read_presets_req read_presets_req; |
| struct k_work control_point_work; |
| } 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 void active_preset_work_process(struct k_work *work); |
| static K_WORK_DEFINE(active_preset_work, active_preset_work_process); |
| |
| 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_pending_cp(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.pending_cp = false; |
| client->ntf_bonded.pending_active_index = 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.pending_active_index) { |
| /* Emit active preset notification */ |
| k_work_submit(&active_preset_work); |
| } |
| |
| if (client->ntf_bonded.pending_cp) { |
| /* 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; |
| } |
| } |
| |
| 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_pending_cp(client) || |
| client->ntf_bonded.pending_cp)) { |
| 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) { |
| /* Mark preset changed operation as pending */ |
| client->ntf_bonded.pending_cp = true; |
| /* For simplicity we simply start with the first index, |
| * rather than keeping detailed logs of which clients |
| * have knowledge of which presets |
| */ |
| client->ntf_bonded.preset_changed_index_next = BT_HAS_PRESET_INDEX_FIRST; |
| 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_pending_cp(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.pending_cp) { |
| 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.pending_cp = 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_pending_cp(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(); |
| |
| for (size_t i = 0U; i < ARRAY_SIZE(has_client_list); i++) { |
| struct has_client *client = &has_client_list[i]; |
| int err; |
| |
| if (client->conn == NULL) { |
| /* mark to notify on reconnect */ |
| client->ntf_bonded.pending_active_index = true; |
| continue; |
| } |
| |
| err = bt_gatt_notify(client->conn, ACTIVE_PRESET_INDEX_ATTR, |
| &active_index, sizeof(active_index)); |
| if (err != 0) { |
| LOG_DBG("failed to notify for %p: %d", |
| client->conn, err); |
| } |
| } |
| } |
| |
| 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) |
| { |
| static bool registered; |
| int err; |
| |
| LOG_DBG("param %p", param); |
| |
| CHECKIF(!param) { |
| LOG_DBG("NULL params pointer"); |
| return -EINVAL; |
| } |
| |
| if (registered) { |
| return -EALREADY; |
| } |
| |
| /* 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; |
| } |
| |
| registered = true; |
| |
| return 0; |
| } |