| /* |
| * 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/audio.h> |
| #include <zephyr/bluetooth/audio/pacs.h> |
| #include <zephyr/bluetooth/audio/has.h> |
| #include <zephyr/sys/check.h> |
| #include <zephyr/sys/slist.h> |
| |
| #include "../bluetooth/host/hci_core.h" |
| #include "../bluetooth/host/settings.h" |
| #include "audio_internal.h" |
| #include "has_internal.h" |
| |
| #include "common/bt_str.h" |
| |
| #include <zephyr/logging/log.h> |
| |
| LOG_MODULE_REGISTER(bt_has, CONFIG_BT_HAS_LOG_LEVEL); |
| |
| /* The service allows operations with paired devices only. |
| * The number of clients is set to maximum number of simultaneous connections to paired devices. |
| */ |
| #define MAX_INSTS MIN(CONFIG_BT_MAX_CONN, CONFIG_BT_MAX_PAIRED) |
| |
| #define BITS_CHANGED(_new_value, _old_value) ((_new_value) ^ (_old_value)) |
| #define FEATURE_DEVICE_TYPE_UNCHANGED(_new_value) \ |
| !BITS_CHANGED(_new_value, (has.features & BT_HAS_FEAT_HEARING_AID_TYPE_MASK)) |
| #define FEATURE_SYNC_SUPPORT_UNCHANGED(_new_value) \ |
| !BITS_CHANGED(_new_value, ((has.features & BT_HAS_FEAT_PRESET_SYNC_SUPP) != 0 ? 1 : 0)) |
| #define FEATURE_IND_PRESETS_UNCHANGED(_new_value) \ |
| !BITS_CHANGED(_new_value, ((has.features & BT_HAS_FEAT_INDEPENDENT_PRESETS) != 0 ? 1 : 0)) |
| #define BONDED_CLIENT_INIT_FLAGS \ |
| (BIT(FLAG_ACTIVE_INDEX_CHANGED) | BIT(FLAG_NOTIFY_PRESET_LIST) | BIT(FLAG_FEATURES_CHANGED)) |
| |
| static struct bt_has has; |
| |
| #if defined(CONFIG_BT_HAS_ACTIVE_PRESET_INDEX) |
| static void active_preset_index_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) |
| { |
| LOG_DBG("attr %p value 0x%04x", attr, value); |
| } |
| #endif /* CONFIG_BT_HAS_ACTIVE_PRESET_INDEX */ |
| |
| #if defined(CONFIG_BT_HAS_PRESET_SUPPORT) |
| struct has_client; |
| |
| static int read_preset_response(struct has_client *client); |
| static int preset_list_changed(struct has_client *client); |
| static int preset_list_changed_generic_update_tail(struct has_client *client); |
| static int preset_list_changed_record_deleted_last(struct has_client *client); |
| 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 void preset_cp_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) |
| { |
| LOG_DBG("attr %p value 0x%04x", attr, value); |
| } |
| |
| 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) |
| { |
| uint8_t active_index; |
| |
| LOG_DBG("conn %p attr %p offset %d", (void *)conn, attr, offset); |
| |
| if (offset > sizeof(active_index)) { |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); |
| } |
| |
| active_index = bt_has_preset_active_get(); |
| |
| return bt_gatt_attr_read(conn, attr, buf, len, offset, &active_index, sizeof(active_index)); |
| } |
| #endif /* CONFIG_BT_HAS_PRESET_SUPPORT */ |
| |
| #if defined(CONFIG_BT_HAS_FEATURES_NOTIFIABLE) |
| static void features_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) |
| { |
| LOG_DBG("attr %p value 0x%04x", attr, value); |
| } |
| #endif /* CONFIG_BT_HAS_FEATURES_NOTIFIABLE */ |
| |
| 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)); |
| } |
| |
| #if defined(CONFIG_BT_HAS_FEATURES_NOTIFIABLE) |
| #define BT_HAS_CHR_FEATURES \ |
| BT_AUDIO_CHRC(BT_UUID_HAS_HEARING_AID_FEATURES, \ |
| BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, \ |
| BT_GATT_PERM_READ_ENCRYPT, \ |
| read_features, NULL, NULL), \ |
| BT_AUDIO_CCC(features_cfg_changed), |
| #else |
| #define BT_HAS_CHR_FEATURES \ |
| BT_AUDIO_CHRC(BT_UUID_HAS_HEARING_AID_FEATURES, \ |
| BT_GATT_CHRC_READ, \ |
| BT_GATT_PERM_READ_ENCRYPT, \ |
| read_features, NULL, NULL), |
| #endif /* CONFIG_BT_HAS_FEATURES_NOTIFIABLE */ |
| |
| #if defined(CONFIG_BT_HAS_PRESET_SUPPORT) |
| #if defined(CONFIG_BT_HAS_PRESET_CONTROL_POINT_NOTIFIABLE) |
| #define BT_HAS_CHR_PRESET_CONTROL_POINT \ |
| BT_AUDIO_CHRC(BT_UUID_HAS_PRESET_CONTROL_POINT, \ |
| BT_GATT_CHRC_WRITE | BT_GATT_CHRC_INDICATE | BT_GATT_CHRC_NOTIFY, \ |
| BT_GATT_PERM_WRITE_ENCRYPT, \ |
| NULL, write_control_point, NULL), \ |
| BT_AUDIO_CCC(preset_cp_cfg_changed), |
| #else |
| #define BT_HAS_CHR_PRESET_CONTROL_POINT \ |
| BT_AUDIO_CHRC(BT_UUID_HAS_PRESET_CONTROL_POINT, \ |
| BT_GATT_CHRC_WRITE | BT_GATT_CHRC_INDICATE, \ |
| BT_GATT_PERM_WRITE_ENCRYPT, \ |
| NULL, write_control_point, NULL), \ |
| BT_AUDIO_CCC(preset_cp_cfg_changed), |
| #endif /* CONFIG_BT_HAS_PRESET_CONTROL_POINT_NOTIFIABLE */ |
| #else |
| #define BT_HAS_CHR_PRESET_CONTROL_POINT |
| #endif /* CONFIG_BT_HAS_PRESET_SUPPORT */ |
| |
| #if defined(CONFIG_BT_HAS_ACTIVE_PRESET_INDEX) |
| #define BT_HAS_CHR_ACTIVE_PRESET_INDEX \ |
| 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(active_preset_index_cfg_changed) |
| #else |
| #define BT_HAS_CHR_ACTIVE_PRESET_INDEX |
| #endif /* CONFIG_BT_HAS_ACTIVE_PRESET_INDEX */ |
| |
| /* Hearing Access Service GATT Attributes */ |
| static struct bt_gatt_attr has_attrs[] = { |
| BT_GATT_PRIMARY_SERVICE(BT_UUID_HAS), |
| BT_HAS_CHR_FEATURES |
| BT_HAS_CHR_PRESET_CONTROL_POINT |
| BT_HAS_CHR_ACTIVE_PRESET_INDEX |
| }; |
| |
| static struct bt_gatt_service has_svc; |
| static struct bt_gatt_attr *hearing_aid_features_attr; |
| static struct bt_gatt_attr *preset_control_point_attr; |
| static struct bt_gatt_attr *active_preset_index_attr; |
| |
| #if defined(CONFIG_BT_HAS_PRESET_SUPPORT) || defined(CONFIG_BT_HAS_FEATURES_NOTIFIABLE) |
| static void notify_work_handler(struct k_work *work); |
| |
| enum flag_internal { |
| FLAG_ACTIVE_INDEX_CHANGED, |
| FLAG_PENDING_READ_PRESET_RESPONSE, |
| FLAG_NOTIFY_PRESET_LIST, |
| FLAG_NOTIFY_PRESET_LIST_GENERIC_UPDATE_TAIL, |
| FLAG_NOTIFY_PRESET_LIST_RECORD_DELETED_LAST, |
| FLAG_FEATURES_CHANGED, |
| FLAG_NUM, |
| }; |
| |
| /* Stored client context */ |
| static struct client_context { |
| bt_addr_le_t addr; |
| |
| /* Pending notification flags */ |
| ATOMIC_DEFINE(flags, FLAG_NUM); |
| |
| /* Last notified preset index */ |
| uint8_t last_preset_index_known; |
| } contexts[CONFIG_BT_MAX_PAIRED]; |
| |
| /* Connected client clientance */ |
| static struct has_client { |
| struct bt_conn *conn; |
| #if defined(CONFIG_BT_HAS_PRESET_SUPPORT) |
| union { |
| struct bt_gatt_indicate_params ind; |
| #if defined(CONFIG_BT_HAS_PRESET_CONTROL_POINT_NOTIFIABLE) |
| struct bt_gatt_notify_params ntf; |
| #endif /* CONFIG_BT_HAS_PRESET_CONTROL_POINT_NOTIFIABLE */ |
| } params; |
| |
| uint8_t preset_changed_index_next; |
| struct bt_has_cp_read_presets_req read_presets_req; |
| #endif /* CONFIG_BT_HAS_PRESET_SUPPORT */ |
| struct k_work_delayable notify_work; |
| struct client_context *context; |
| } has_client_list[MAX_INSTS]; |
| |
| |
| static struct client_context *context_find(const bt_addr_le_t *addr) |
| { |
| __ASSERT_NO_MSG(addr != NULL); |
| |
| for (size_t i = 0; i < ARRAY_SIZE(contexts); i++) { |
| if (bt_addr_le_eq(&contexts[i].addr, addr)) { |
| return &contexts[i]; |
| } |
| } |
| |
| return NULL; |
| } |
| |
| static struct client_context *context_alloc(const bt_addr_le_t *addr) |
| { |
| struct client_context *context; |
| |
| __ASSERT_NO_MSG(addr != NULL); |
| |
| /* Free contexts has BT_ADDR_LE_ANY as the address */ |
| context = context_find(BT_ADDR_LE_ANY); |
| if (context == NULL) { |
| return NULL; |
| } |
| |
| memset(context, 0, sizeof(*context)); |
| |
| bt_addr_le_copy(&context->addr, addr); |
| |
| return context; |
| } |
| |
| static void context_free(struct client_context *context) |
| { |
| bt_addr_le_copy(&context->addr, BT_ADDR_LE_ANY); |
| } |
| |
| static void client_free(struct has_client *client) |
| { |
| struct bt_conn_info info = { 0 }; |
| int err; |
| |
| #if defined(CONFIG_BT_HAS_PRESET_SUPPORT) || defined(CONFIG_BT_HAS_FEATURES_NOTIFIABLE) |
| (void)k_work_cancel_delayable(&client->notify_work); |
| #endif /* CONFIG_BT_HAS_PRESET_SUPPORT || CONFIG_BT_HAS_FEATURES_NOTIFIABLE */ |
| |
| err = bt_conn_get_info(client->conn, &info); |
| __ASSERT_NO_MSG(err == 0); |
| |
| if (client->context != NULL && !bt_addr_le_is_bonded(info.id, info.le.dst)) { |
| /* Free stored context of non-bonded client */ |
| context_free(client->context); |
| client->context = NULL; |
| } |
| |
| bt_conn_unref(client->conn); |
| client->conn = NULL; |
| } |
| |
| static struct has_client *client_alloc(struct bt_conn *conn) |
| { |
| struct bt_conn_info info = { 0 }; |
| struct has_client *client = NULL; |
| int err; |
| |
| 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 == NULL) { |
| client = &has_client_list[i]; |
| } |
| } |
| |
| __ASSERT(client, "failed to get client for conn %p", (void *)conn); |
| |
| memset(client, 0, sizeof(*client)); |
| |
| client->conn = bt_conn_ref(conn); |
| |
| #if defined(CONFIG_BT_HAS_PRESET_SUPPORT) || defined(CONFIG_BT_HAS_FEATURES_NOTIFIABLE) |
| k_work_init_delayable(&client->notify_work, notify_work_handler); |
| #endif /* CONFIG_BT_HAS_PRESET_SUPPORT || CONFIG_BT_HAS_FEATURES_NOTIFIABLE */ |
| |
| err = bt_conn_get_info(conn, &info); |
| if (err != 0) { |
| LOG_DBG("Could not get conn info: %d", err); |
| |
| return NULL; |
| } |
| |
| client->context = context_find(info.le.dst); |
| if (client->context == NULL) { |
| client->context = context_alloc(info.le.dst); |
| if (client->context == NULL) { |
| LOG_ERR("Failed to allocate client_context for %s", |
| bt_addr_le_str(info.le.dst)); |
| |
| client_free(client); |
| |
| return NULL; |
| } |
| |
| LOG_DBG("New client_context for %s", bt_addr_le_str(info.le.dst)); |
| } |
| |
| return client; |
| } |
| |
| static struct has_client *client_find_by_conn(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 notify_work_reschedule(struct has_client *client, k_timeout_t delay) |
| { |
| int err; |
| |
| __ASSERT(client->conn, "Not connected"); |
| |
| if (k_work_delayable_remaining_get(&client->notify_work) > 0) { |
| return; |
| } |
| |
| err = k_work_reschedule(&client->notify_work, delay); |
| if (err < 0) { |
| LOG_ERR("Failed to reschedule notification work err %d", err); |
| } |
| } |
| |
| static void security_changed(struct bt_conn *conn, bt_security_t level, enum bt_security_err err) |
| { |
| struct has_client *client; |
| struct bt_conn_info info; |
| int ret; |
| |
| LOG_DBG("conn %p level %d err %d", (void *)conn, level, err); |
| |
| if (err != BT_SECURITY_ERR_SUCCESS) { |
| return; |
| } |
| |
| client = client_alloc(conn); |
| if (unlikely(!client)) { |
| LOG_ERR("Failed to allocate client"); |
| return; |
| } |
| |
| ret = bt_conn_get_info(client->conn, &info); |
| if (ret < 0) { |
| LOG_ERR("bt_conn_get_info err %d", ret); |
| return; |
| } |
| |
| if (!bt_addr_le_is_bonded(info.id, info.le.dst)) { |
| return; |
| } |
| |
| if (atomic_get(client->context->flags) != 0) { |
| notify_work_reschedule(client, K_NO_WAIT); |
| } |
| } |
| |
| 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_find_by_conn(conn); |
| if (client) { |
| client_free(client); |
| } |
| } |
| |
| static void identity_resolved(struct bt_conn *conn, const bt_addr_le_t *rpa, |
| const bt_addr_le_t *identity) |
| { |
| struct has_client *client; |
| |
| LOG_DBG("conn %p %s -> %s", (void *)conn, bt_addr_le_str(rpa), bt_addr_le_str(identity)); |
| |
| client = client_find_by_conn(conn); |
| if (client == NULL) { |
| return; |
| } |
| |
| bt_addr_le_copy(&client->context->addr, identity); |
| } |
| |
| BT_CONN_CB_DEFINE(conn_cb) = { |
| .disconnected = disconnected, |
| .security_changed = security_changed, |
| .identity_resolved = identity_resolved, |
| }; |
| |
| static void notify_work_handler(struct k_work *work) |
| { |
| struct k_work_delayable *dwork = k_work_delayable_from_work(work); |
| struct has_client *client = CONTAINER_OF(dwork, struct has_client, notify_work); |
| int err; |
| |
| if (IS_ENABLED(CONFIG_BT_HAS_FEATURES_NOTIFIABLE) && |
| atomic_test_and_clear_bit(client->context->flags, FLAG_FEATURES_CHANGED) && |
| bt_gatt_is_subscribed(client->conn, hearing_aid_features_attr, BT_GATT_CCC_NOTIFY)) { |
| err = bt_gatt_notify(client->conn, hearing_aid_features_attr, &has.features, |
| sizeof(has.features)); |
| if (err == -ENOMEM) { |
| atomic_set_bit(client->context->flags, FLAG_FEATURES_CHANGED); |
| notify_work_reschedule(client, K_USEC(BT_AUDIO_NOTIFY_RETRY_DELAY_US)); |
| } else if (err < 0) { |
| LOG_ERR("Notify features err %d", err); |
| } |
| } |
| |
| #if defined(CONFIG_BT_HAS_PRESET_SUPPORT) |
| if (atomic_test_and_clear_bit(client->context->flags, FLAG_PENDING_READ_PRESET_RESPONSE)) { |
| err = read_preset_response(client); |
| if (err == -ENOMEM) { |
| atomic_set_bit(client->context->flags, FLAG_PENDING_READ_PRESET_RESPONSE); |
| notify_work_reschedule(client, K_USEC(BT_AUDIO_NOTIFY_RETRY_DELAY_US)); |
| } else if (err < 0) { |
| LOG_ERR("Notify read preset response err %d", err); |
| } |
| } else if (atomic_test_and_clear_bit(client->context->flags, FLAG_NOTIFY_PRESET_LIST)) { |
| err = preset_list_changed(client); |
| if (err == -ENOMEM) { |
| atomic_set_bit(client->context->flags, FLAG_NOTIFY_PRESET_LIST); |
| notify_work_reschedule(client, K_USEC(BT_AUDIO_NOTIFY_RETRY_DELAY_US)); |
| } else if (err < 0) { |
| LOG_ERR("Notify preset list changed err %d", err); |
| } |
| } else if (atomic_test_and_clear_bit(client->context->flags, |
| FLAG_NOTIFY_PRESET_LIST_GENERIC_UPDATE_TAIL)) { |
| err = preset_list_changed_generic_update_tail(client); |
| if (err == -ENOMEM) { |
| atomic_set_bit(client->context->flags, |
| FLAG_NOTIFY_PRESET_LIST_GENERIC_UPDATE_TAIL); |
| notify_work_reschedule(client, K_USEC(BT_AUDIO_NOTIFY_RETRY_DELAY_US)); |
| } else if (err < 0) { |
| LOG_ERR("Notify preset list changed generic update tail err %d", err); |
| } |
| } else if (atomic_test_and_clear_bit(client->context->flags, |
| FLAG_NOTIFY_PRESET_LIST_RECORD_DELETED_LAST)) { |
| err = preset_list_changed_record_deleted_last(client); |
| if (err == -ENOMEM) { |
| atomic_set_bit(client->context->flags, |
| FLAG_NOTIFY_PRESET_LIST_RECORD_DELETED_LAST); |
| notify_work_reschedule(client, K_USEC(BT_AUDIO_NOTIFY_RETRY_DELAY_US)); |
| } else if (err < 0) { |
| LOG_ERR("Notify preset list changed recoed deleted last err %d", err); |
| } |
| } |
| |
| #endif /* CONFIG_BT_HAS_PRESET_SUPPORT */ |
| |
| if (IS_ENABLED(CONFIG_BT_HAS_PRESET_SUPPORT) && |
| atomic_test_and_clear_bit(client->context->flags, FLAG_ACTIVE_INDEX_CHANGED) && |
| bt_gatt_is_subscribed(client->conn, active_preset_index_attr, BT_GATT_CCC_NOTIFY)) { |
| uint8_t active_index; |
| |
| active_index = bt_has_preset_active_get(); |
| |
| err = bt_gatt_notify(client->conn, active_preset_index_attr, |
| &active_index, sizeof(active_index)); |
| if (err == -ENOMEM) { |
| atomic_set_bit(client->context->flags, FLAG_ACTIVE_INDEX_CHANGED); |
| notify_work_reschedule(client, K_USEC(BT_AUDIO_NOTIFY_RETRY_DELAY_US)); |
| } else if (err < 0) { |
| LOG_ERR("Notify active index err %d", err); |
| } |
| } |
| } |
| |
| static void notify(struct has_client *client, enum flag_internal flag) |
| { |
| if (client != NULL) { |
| atomic_set_bit(client->context->flags, flag); |
| notify_work_reschedule(client, K_NO_WAIT); |
| return; |
| } |
| |
| /* Mark notification to be sent to all clients */ |
| for (size_t i = 0U; i < ARRAY_SIZE(contexts); i++) { |
| atomic_set_bit(contexts[i].flags, flag); |
| } |
| |
| for (size_t i = 0U; i < ARRAY_SIZE(has_client_list); i++) { |
| client = &has_client_list[i]; |
| |
| if (client->conn == NULL) { |
| continue; |
| } |
| |
| notify_work_reschedule(client, K_NO_WAIT); |
| } |
| } |
| |
| static void bond_deleted_cb(uint8_t id, const bt_addr_le_t *addr) |
| { |
| struct client_context *context; |
| |
| context = context_find(addr); |
| if (context != NULL) { |
| context_free(context); |
| } |
| |
| if (IS_ENABLED(CONFIG_BT_SETTINGS)) { |
| bt_settings_delete("has", 0, addr); |
| } |
| } |
| |
| static struct bt_conn_auth_info_cb auth_info_cb = { |
| .bond_deleted = bond_deleted_cb, |
| }; |
| #endif /* CONFIG_BT_HAS_PRESET_SUPPORT || CONFIG_BT_HAS_FEATURES_NOTIFIABLE */ |
| |
| #if defined(CONFIG_BT_HAS_PRESET_SUPPORT) |
| static struct has_preset *active_preset; |
| |
| /* 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; |
| sys_snode_t node; |
| } preset_pool[CONFIG_BT_HAS_PRESET_COUNT]; |
| |
| static sys_slist_t preset_list = SYS_SLIST_STATIC_INIT(&preset_list); |
| static sys_slist_t preset_free_list = SYS_SLIST_STATIC_INIT(&preset_free_list); |
| |
| 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) |
| { |
| struct has_preset *preset, *tmp; |
| |
| SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&preset_list, preset, tmp, node) { |
| 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 void preset_insert(struct has_preset *preset) |
| { |
| struct has_preset *tmp, *prev = NULL; |
| |
| SYS_SLIST_FOR_EACH_CONTAINER(&preset_list, tmp, node) { |
| if (tmp->index > preset->index) { |
| if (prev) { |
| sys_slist_insert(&preset_list, &prev->node, &preset->node); |
| } else { |
| sys_slist_prepend(&preset_list, &preset->node); |
| } |
| return; |
| } |
| |
| prev = tmp; |
| } |
| |
| sys_slist_append(&preset_list, &preset->node); |
| } |
| |
| 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; |
| sys_snode_t *node; |
| |
| node = sys_slist_get(&preset_free_list); |
| if (node == NULL) { |
| return NULL; |
| } |
| |
| preset = CONTAINER_OF(node, struct has_preset, node); |
| 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; |
| |
| preset_insert(preset); |
| |
| return preset; |
| } |
| |
| static void preset_free(struct has_preset *preset) |
| { |
| bool removed; |
| |
| removed = sys_slist_find_and_remove(&preset_list, &preset->node); |
| if (removed) { |
| sys_slist_append(&preset_free_list, &preset->node); |
| } |
| } |
| |
| static struct has_preset *preset_get_head(void) |
| { |
| struct has_preset *next; |
| |
| return SYS_SLIST_PEEK_HEAD_CONTAINER(&preset_list, next, node); |
| } |
| |
| static struct has_preset *preset_get_tail(void) |
| { |
| struct has_preset *prev; |
| |
| return SYS_SLIST_PEEK_TAIL_CONTAINER(&preset_list, prev, node); |
| } |
| |
| static struct has_preset *preset_get_prev(const struct has_preset *preset) |
| { |
| struct has_preset *prev; |
| |
| SYS_SLIST_FOR_EACH_CONTAINER(&preset_list, prev, node) { |
| if (SYS_SLIST_PEEK_NEXT_CONTAINER(prev, node) == preset) { |
| return prev; |
| } |
| } |
| |
| prev = preset_get_tail(); |
| if (prev == preset) { |
| return NULL; |
| } |
| |
| return prev; |
| } |
| |
| static struct has_preset *preset_lookup_index(uint8_t index) |
| { |
| struct has_preset *preset; |
| |
| SYS_SLIST_FOR_EACH_CONTAINER(&preset_list, preset, node) { |
| if (preset->index == index) { |
| return preset; |
| } |
| } |
| |
| return NULL; |
| } |
| |
| static struct has_preset *preset_get_next(struct has_preset *preset) |
| { |
| struct has_preset *next; |
| |
| next = SYS_SLIST_PEEK_NEXT_CONTAINER(preset, node); |
| if (next == NULL) { |
| next = preset_get_head(); |
| if (next == preset) { |
| return NULL; |
| } |
| } |
| |
| return next; |
| } |
| |
| static uint8_t preset_get_prev_index(const struct has_preset *preset) |
| { |
| const struct has_preset *prev; |
| |
| prev = preset_get_prev(preset); |
| if (prev == NULL || prev->index >= preset->index) { |
| return BT_HAS_PRESET_INDEX_NONE; |
| } |
| |
| return prev->index; |
| } |
| |
| static void control_point_ntf_complete(struct bt_conn *conn, void *user_data) |
| { |
| struct has_client *client = client_find_by_conn(conn); |
| |
| LOG_DBG("conn %p", (void *)conn); |
| |
| /* Resubmit if needed */ |
| if (client != NULL && atomic_get(client->context->flags) != 0) { |
| notify_work_reschedule(client, K_NO_WAIT); |
| } |
| } |
| |
| 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) |
| { |
| const uint16_t mtu_size = bt_gatt_get_mtu(client->conn); |
| /* PDU structure is [Opcode (1)] [Handle (2)] [...] */ |
| const uint16_t pdu_size = 3 + buf->len; |
| |
| if (mtu_size < pdu_size) { |
| LOG_WRN("Sending truncated control point PDU %d < %d", mtu_size, pdu_size); |
| buf->len -= (pdu_size - mtu_size); |
| } |
| |
| #if defined(CONFIG_BT_HAS_PRESET_CONTROL_POINT_NOTIFIABLE) |
| if (bt_eatt_count(client->conn) > 0 && |
| bt_gatt_is_subscribed(client->conn, preset_control_point_attr, BT_GATT_CCC_NOTIFY)) { |
| memset(&client->params.ntf, 0, sizeof(client->params.ntf)); |
| 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_HAS_PRESET_CONTROL_POINT_NOTIFIABLE */ |
| |
| if (bt_gatt_is_subscribed(client->conn, preset_control_point_attr, BT_GATT_CCC_INDICATE)) { |
| memset(&client->params.ind, 0, sizeof(client->params.ind)); |
| 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 = 0U; i < ARRAY_SIZE(contexts); i++) { |
| struct client_context *context = &contexts[i]; |
| struct has_client *client = NULL; |
| int err; |
| |
| for (size_t j = 0U; j < ARRAY_SIZE(has_client_list); j++) { |
| if (has_client_list[j].context == context) { |
| client = &has_client_list[j]; |
| break; |
| } |
| } |
| |
| if (client == NULL || client->conn == NULL) { |
| /* Mark preset changed operation as pending */ |
| atomic_set_bit(context->flags, FLAG_NOTIFY_PRESET_LIST); |
| 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 index 0x%02x prop 0x%02x %s is_last 0x%02x", (void *)client->conn, |
| preset->index, preset->properties, preset->name, 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 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, uint8_t prev_index, uint8_t index, |
| uint8_t properties, const char *name, 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); |
| |
| LOG_DBG("client %p prev_index 0x%02x index 0x%02x prop 0x%02x %s is_last %d", |
| client, prev_index, index, properties, name, is_last); |
| |
| 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 = prev_index; |
| generic_update->index = index; |
| generic_update->properties = properties; |
| net_buf_simple_add_mem(&buf, name, strlen(name)); |
| |
| if (client) { |
| return control_point_send(client, &buf); |
| } else { |
| return control_point_send_all(&buf); |
| } |
| } |
| |
| #if defined(CONFIG_BT_SETTINGS) |
| struct client_context_store { |
| /* Last notified preset index */ |
| uint8_t last_preset_index_known; |
| } __packed; |
| |
| static int settings_set_cb(const char *name, size_t len_rd, settings_read_cb read_cb, void *cb_arg) |
| { |
| struct client_context_store store; |
| struct client_context *context; |
| bt_addr_le_t addr; |
| ssize_t len; |
| int err; |
| |
| if (!name) { |
| LOG_ERR("Insufficient number of arguments"); |
| return -EINVAL; |
| } |
| |
| err = bt_settings_decode_key(name, &addr); |
| if (err) { |
| LOG_ERR("Unable to decode address %s", name); |
| return -EINVAL; |
| } |
| |
| context = context_find(&addr); |
| if (context == NULL) { |
| /* Find and initialize a free entry */ |
| context = context_alloc(&addr); |
| if (context == NULL) { |
| LOG_ERR("Failed to allocate client_context for %s", bt_addr_le_str(&addr)); |
| return -ENOMEM; |
| } |
| } |
| |
| if (len_rd) { |
| len = read_cb(cb_arg, &store, sizeof(store)); |
| if (len < 0) { |
| LOG_ERR("Failed to decode value (err %zd)", len); |
| return len; |
| } |
| |
| context->last_preset_index_known = store.last_preset_index_known; |
| } else { |
| context->last_preset_index_known = 0x00; |
| } |
| |
| /* Notify all the characteristics values after reboot */ |
| atomic_set(context->flags, BONDED_CLIENT_INIT_FLAGS); |
| |
| return 0; |
| } |
| |
| BT_SETTINGS_DEFINE(has, "has", settings_set_cb, NULL); |
| |
| static void store_client_context(struct client_context *context) |
| { |
| struct client_context_store store = { |
| .last_preset_index_known = context->last_preset_index_known, |
| }; |
| int err; |
| |
| LOG_DBG("%s last_preset_index_known 0x%02x", |
| bt_addr_le_str(&context->addr), store.last_preset_index_known); |
| |
| err = bt_settings_store("has", 0, &context->addr, &store, sizeof(store)); |
| if (err != 0) { |
| LOG_ERR("Failed to store err %d", err); |
| } |
| } |
| #else |
| #define store_client_context(...) |
| #endif /* CONFIG_BT_SETTINGS */ |
| |
| static void update_last_preset_index_known(struct has_client *client, uint8_t index) |
| { |
| if (client != NULL && client->context != NULL && |
| client->context->last_preset_index_known != index) { |
| client->context->last_preset_index_known = index; |
| store_client_context(client->context); |
| return; |
| } |
| |
| for (size_t i = 0; i < ARRAY_SIZE(has_client_list); i++) { |
| client = &has_client_list[i]; |
| |
| /* For each connected client */ |
| if (client->conn != NULL && client->context != NULL && |
| client->context->last_preset_index_known != index) { |
| client->context->last_preset_index_known = index; |
| store_client_context(client->context); |
| } |
| } |
| } |
| |
| static int read_preset_response(struct has_client *client) |
| { |
| const struct has_preset *preset = NULL; |
| bool is_last = true; |
| int err; |
| |
| __ASSERT_NO_MSG(client != NULL); |
| |
| preset_foreach(client->read_presets_req.start_index, BT_HAS_PRESET_INDEX_LAST, |
| preset_found, &preset); |
| |
| if (unlikely(preset == NULL)) { |
| return bt_has_cp_read_preset_rsp(client, NULL, BT_HAS_IS_LAST); |
| } |
| |
| 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 != 0) { |
| return err; |
| } |
| |
| if (preset->index > client->context->last_preset_index_known) { |
| update_last_preset_index_known(client, preset->index); |
| } |
| |
| if (!is_last) { |
| client->read_presets_req.start_index = preset->index + 1; |
| client->read_presets_req.num_presets--; |
| |
| atomic_set_bit(client->context->flags, FLAG_PENDING_READ_PRESET_RESPONSE); |
| notify_work_reschedule(client, K_USEC(BT_AUDIO_NOTIFY_RETRY_DELAY_US)); |
| } |
| |
| return 0; |
| } |
| |
| static int bt_has_cp_preset_record_deleted(struct has_client *client, uint8_t index) |
| { |
| NET_BUF_SIMPLE_DEFINE(buf, sizeof(struct bt_has_cp_hdr) + |
| sizeof(struct bt_has_cp_preset_changed) + sizeof(uint8_t)); |
| |
| LOG_DBG("client %p index 0x%02x", client, index); |
| |
| preset_changed_prepare(&buf, BT_HAS_CHANGE_ID_PRESET_DELETED, BT_HAS_IS_LAST); |
| net_buf_simple_add_u8(&buf, index); |
| |
| if (client != NULL) { |
| return control_point_send(client, &buf); |
| } else { |
| return control_point_send_all(&buf); |
| } |
| } |
| |
| /* Generic Update the last (already deleted) preset */ |
| static int preset_list_changed_generic_update_tail(struct has_client *client) |
| { |
| const struct has_preset *prev; |
| struct has_preset last = { |
| /* The index value of the last preset the client knew about. */ |
| .index = client->context->last_preset_index_known, |
| |
| /* As the properties of deleted preset is not available anymore, we set this value |
| * to 0x00 meaning the preset is unavailable and non-writable which is actually true |
| */ |
| .properties = BT_HAS_PROP_NONE, |
| |
| /* As the name of deleted preset are not available anymore, we set this value |
| * to the value what is compliant with specification. |
| * As per HAS_v1.0 the Name is 1-40 octet value. |
| */ |
| .name = "N/A", |
| }; |
| int err; |
| |
| prev = preset_get_tail(); |
| |
| err = bt_has_cp_generic_update(client, prev ? prev->index : BT_HAS_PRESET_INDEX_NONE, |
| last.index, last.properties, last.name, false); |
| if (err != 0) { |
| return err; |
| } |
| |
| return 0; |
| } |
| |
| static int preset_list_changed_record_deleted_last(struct has_client *client) |
| { |
| const struct has_preset *last; |
| int err; |
| |
| err = bt_has_cp_preset_record_deleted(client, client->context->last_preset_index_known); |
| if (err != 0) { |
| return err; |
| } |
| |
| last = preset_get_tail(); |
| |
| update_last_preset_index_known(client, last ? last->index : BT_HAS_PRESET_INDEX_NONE); |
| |
| return 0; |
| } |
| |
| static int preset_list_changed(struct has_client *client) |
| { |
| const struct has_preset *preset = NULL; |
| const struct has_preset *next = NULL; |
| bool is_last = true; |
| int err; |
| |
| if (sys_slist_is_empty(&preset_list)) { |
| /* The preset list is empty. We need to indicate deletion of all presets */ |
| atomic_set_bit(client->context->flags, |
| FLAG_NOTIFY_PRESET_LIST_GENERIC_UPDATE_TAIL); |
| atomic_set_bit(client->context->flags, |
| FLAG_NOTIFY_PRESET_LIST_RECORD_DELETED_LAST); |
| notify_work_reschedule(client, K_USEC(BT_AUDIO_NOTIFY_RETRY_DELAY_US)); |
| |
| return 0; |
| } |
| |
| preset_foreach(client->preset_changed_index_next, BT_HAS_PRESET_INDEX_LAST, |
| preset_found, &preset); |
| |
| if (preset == NULL) { |
| return 0; |
| } |
| |
| preset_foreach(preset->index + 1, BT_HAS_PRESET_INDEX_LAST, preset_found, &next); |
| |
| /* It is last Preset Changed notification if there are no presets left to notify and the |
| * currently notified preset have the highest index known to the client. |
| */ |
| is_last = next == NULL && preset->index >= client->context->last_preset_index_known; |
| |
| err = bt_has_cp_generic_update(client, preset_get_prev_index(preset), preset->index, |
| preset->properties, preset->name, is_last); |
| if (err != 0) { |
| return err; |
| } |
| |
| if (is_last) { |
| client->preset_changed_index_next = 0; |
| |
| /* It's the last preset notified, so update the highest index known to the client */ |
| update_last_preset_index_known(client, preset->index); |
| |
| return 0; |
| } |
| |
| if (next == NULL) { |
| /* If we end up here, the last preset known to the client has been removed. |
| * As we do not hold the information about the deleted presets, we need to use |
| * Generic Update procedure to: |
| * 1. Notify the presets that have been removed in range |
| * (PrevIndex = current_preset_last, Index=previous_preset_last) |
| * 2. Notify deletion of preset Index=previous_preset_last. |
| */ |
| atomic_set_bit(client->context->flags, |
| FLAG_NOTIFY_PRESET_LIST_GENERIC_UPDATE_TAIL); |
| atomic_set_bit(client->context->flags, |
| FLAG_NOTIFY_PRESET_LIST_RECORD_DELETED_LAST); |
| } else { |
| client->preset_changed_index_next = preset->index + 1; |
| |
| atomic_set_bit(client->context->flags, FLAG_NOTIFY_PRESET_LIST); |
| } |
| |
| notify_work_reschedule(client, K_USEC(BT_AUDIO_NOTIFY_RETRY_DELAY_US)); |
| |
| return 0; |
| } |
| |
| 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_find_by_conn(conn); |
| if (client == NULL) { |
| 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 (atomic_test_bit(client->context->flags, FLAG_PENDING_READ_PRESET_RESPONSE)) { |
| 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; |
| |
| notify(client, FLAG_PENDING_READ_PRESET_RESPONSE); |
| |
| 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_get_prev_index(preset), preset->index, |
| preset->properties, preset->name, 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_find_by_conn(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 preset_set_active(struct has_preset *preset) |
| { |
| if (active_preset != preset) { |
| active_preset = preset; |
| |
| notify(NULL, FLAG_ACTIVE_INDEX_CHANGED); |
| } |
| } |
| |
| static uint8_t preset_select(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_set_active(preset); |
| |
| return 0; |
| } |
| |
| static bool is_preset_available(const struct has_preset *preset) |
| { |
| return (preset->properties & BT_HAS_PROP_AVAILABLE) != 0; |
| } |
| |
| static uint8_t handle_set_active_preset(struct net_buf_simple *buf, bool sync) |
| { |
| const struct bt_has_cp_set_active_preset *pdu; |
| struct has_preset *preset; |
| |
| if (buf->len < sizeof(*pdu)) { |
| return BT_HAS_ERR_INVALID_PARAM_LEN; |
| } |
| |
| pdu = net_buf_simple_pull_mem(buf, sizeof(*pdu)); |
| |
| preset = preset_lookup_index(pdu->index); |
| if (preset == NULL) { |
| return BT_ATT_ERR_OUT_OF_RANGE; |
| } |
| |
| if (!is_preset_available(preset)) { |
| return BT_HAS_ERR_OPERATION_NOT_POSSIBLE; |
| } |
| |
| return preset_select(preset, sync); |
| } |
| |
| static uint8_t handle_set_next_preset(bool sync) |
| { |
| struct has_preset *next, *tmp; |
| |
| if (active_preset == NULL) { |
| next = preset_get_head(); |
| } else { |
| next = preset_get_next(active_preset); |
| } |
| |
| tmp = next; |
| do { |
| if (next == NULL) { |
| break; |
| } |
| |
| if (is_preset_available(next)) { |
| return preset_select(next, sync); |
| } |
| |
| next = preset_get_next(next); |
| } while (tmp != next); |
| |
| return BT_HAS_ERR_OPERATION_NOT_POSSIBLE; |
| } |
| |
| static uint8_t handle_set_prev_preset(bool sync) |
| { |
| struct has_preset *prev, *tmp; |
| |
| if (active_preset == NULL) { |
| prev = preset_get_tail(); |
| } else { |
| prev = preset_get_prev(active_preset); |
| } |
| |
| tmp = prev; |
| do { |
| if (prev == NULL) { |
| break; |
| } |
| |
| if (is_preset_available(prev)) { |
| return preset_select(prev, sync); |
| } |
| |
| prev = preset_get_prev(prev); |
| } while (tmp != prev); |
| |
| 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); |
| } else { |
| return BT_HAS_ERR_WRITE_NAME_NOT_ALLOWED; |
| } |
| 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("handle_control_point_op 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; |
| 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 = preset_lookup_index(param->index); |
| if (preset != NULL) { |
| return -EALREADY; |
| } |
| |
| CHECKIF(!IS_ENABLED(CONFIG_BT_HAS_PRESET_NAME_DYNAMIC) && |
| (param->properties & BT_HAS_PROP_WRITABLE) > 0) { |
| LOG_ERR("Writable presets are not supported"); |
| return -ENOTSUP; |
| } |
| |
| preset = preset_alloc(param->index, param->properties, param->name, param->ops); |
| if (preset == NULL) { |
| return -ENOMEM; |
| } |
| |
| if (preset == preset_get_tail()) { |
| update_last_preset_index_known(NULL, preset->index); |
| } |
| |
| return bt_has_cp_generic_update(NULL, preset_get_prev_index(preset), preset->index, |
| preset->properties, preset->name, BT_HAS_IS_LAST); |
| } |
| |
| int bt_has_preset_unregister(uint8_t index) |
| { |
| struct has_preset *preset; |
| int err; |
| |
| CHECKIF(index == BT_HAS_PRESET_INDEX_NONE) { |
| LOG_ERR("index is invalid"); |
| return -EINVAL; |
| } |
| |
| preset = preset_lookup_index(index); |
| if (preset == NULL) { |
| return -ENOENT; |
| } |
| |
| if (preset == active_preset) { |
| return -EADDRINUSE; |
| } |
| |
| err = bt_has_cp_preset_record_deleted(NULL, preset->index); |
| if (err != 0) { |
| return err; |
| } |
| |
| if (preset == preset_get_tail()) { |
| update_last_preset_index_known(NULL, preset_get_prev_index(preset)); |
| } |
| |
| preset_free(preset); |
| |
| return 0; |
| } |
| |
| static int set_preset_availability(uint8_t index, bool available) |
| { |
| NET_BUF_SIMPLE_DEFINE(buf, sizeof(struct bt_has_cp_hdr) + |
| sizeof(struct bt_has_cp_preset_changed) + sizeof(uint8_t)); |
| struct has_preset *preset; |
| uint8_t change_id; |
| |
| CHECKIF(index == BT_HAS_PRESET_INDEX_NONE) { |
| LOG_ERR("index is invalid"); |
| return -EINVAL; |
| } |
| |
| preset = preset_lookup_index(index); |
| if (preset == NULL) { |
| return -ENOENT; |
| } |
| |
| if (is_preset_available(preset) == available) { |
| /* availability not changed */ |
| return 0; |
| } |
| |
| preset->properties ^= BT_HAS_PROP_AVAILABLE; |
| |
| if (is_preset_available(preset)) { |
| change_id = BT_HAS_CHANGE_ID_PRESET_AVAILABLE; |
| } else { |
| change_id = BT_HAS_CHANGE_ID_PRESET_UNAVAILABLE; |
| } |
| |
| preset_changed_prepare(&buf, change_id, BT_HAS_IS_LAST); |
| net_buf_simple_add_u8(&buf, preset->index); |
| |
| return control_point_send_all(&buf); |
| } |
| |
| int bt_has_preset_available(uint8_t index) |
| { |
| return set_preset_availability(index, true); |
| } |
| |
| int bt_has_preset_unavailable(uint8_t index) |
| { |
| return set_preset_availability(index, false); |
| } |
| |
| 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) |
| { |
| struct has_preset *preset; |
| |
| if (index == BT_HAS_PRESET_INDEX_NONE) { |
| preset_set_active(NULL); |
| return 0; |
| } |
| |
| preset = preset_lookup_index(index); |
| if (preset == NULL) { |
| return -ENOENT; |
| } |
| |
| if (!is_preset_available(preset)) { |
| return -EINVAL; |
| } |
| |
| preset_set_active(preset); |
| |
| return 0; |
| } |
| |
| uint8_t bt_has_preset_active_get(void) |
| { |
| if (active_preset == NULL) { |
| return BT_HAS_PRESET_INDEX_NONE; |
| } |
| |
| return active_preset->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 */ |
| |
| static int has_features_register(const struct bt_has_features_param *features) |
| { |
| /* Initialize the supported features characteristic value */ |
| has.features = features->type; |
| |
| if (IS_ENABLED(CONFIG_BT_HAS_PRESET_SUPPORT)) { |
| has.features |= BT_HAS_FEAT_DYNAMIC_PRESETS; |
| |
| if (features->preset_sync_support) { |
| if (features->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 (features->independent_presets) { |
| if (features->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; |
| } |
| |
| return 0; |
| } |
| |
| #if defined(CONFIG_BT_HAS_FEATURES_NOTIFIABLE) |
| int bt_has_features_set(const struct bt_has_features_param *features) |
| { |
| int err; |
| |
| if (!has.registered) { |
| return -ENOTSUP; |
| } |
| |
| /* Check whether any features will change, otherwise we don't want to notify clients */ |
| if (FEATURE_DEVICE_TYPE_UNCHANGED(features->type) && |
| FEATURE_SYNC_SUPPORT_UNCHANGED(features->preset_sync_support) && |
| FEATURE_IND_PRESETS_UNCHANGED(features->independent_presets)) { |
| return 0; |
| } |
| |
| err = has_features_register(features); |
| if (err != 0) { |
| LOG_DBG("Failed to register features"); |
| return err; |
| } |
| |
| notify(NULL, FLAG_FEATURES_CHANGED); |
| |
| return 0; |
| } |
| #endif /* CONFIG_BT_HAS_FEATURES_NOTIFIABLE */ |
| |
| int bt_has_register(const struct bt_has_features_param *features) |
| { |
| int err; |
| |
| LOG_DBG("features %p", features); |
| |
| CHECKIF(!features) { |
| LOG_DBG("NULL params pointer"); |
| return -EINVAL; |
| } |
| |
| if (has.registered) { |
| return -EALREADY; |
| } |
| |
| err = has_features_register(features); |
| if (err != 0) { |
| LOG_DBG("HAS service failed to register features: %d", err); |
| return err; |
| } |
| |
| 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 (IS_ENABLED(CONFIG_BT_HAS_FEATURES_NOTIFIABLE)) { |
| hearing_aid_features_attr = bt_gatt_find_by_uuid(has_svc.attrs, has_svc.attr_count, |
| BT_UUID_HAS_HEARING_AID_FEATURES); |
| __ASSERT_NO_MSG(hearing_aid_features_attr != NULL); |
| } |
| |
| if (IS_ENABLED(CONFIG_BT_HAS_PRESET_SUPPORT)) { |
| preset_control_point_attr = bt_gatt_find_by_uuid(has_svc.attrs, has_svc.attr_count, |
| BT_UUID_HAS_PRESET_CONTROL_POINT); |
| __ASSERT_NO_MSG(preset_control_point_attr != NULL); |
| |
| active_preset_index_attr = bt_gatt_find_by_uuid(has_svc.attrs, has_svc.attr_count, |
| BT_UUID_HAS_ACTIVE_PRESET_INDEX); |
| __ASSERT_NO_MSG(active_preset_index_attr != NULL); |
| } |
| |
| #if defined(CONFIG_BT_HAS_PRESET_SUPPORT) |
| for (size_t i = 0; i < ARRAY_SIZE(preset_pool); i++) { |
| struct has_preset *preset = &preset_pool[i]; |
| |
| sys_slist_append(&preset_free_list, &preset->node); |
| } |
| #endif /* CONFIG_BT_HAS_PRESET_SUPPORT */ |
| |
| #if defined(CONFIG_BT_HAS_PRESET_SUPPORT) || defined(CONFIG_BT_HAS_FEATURES_NOTIFIABLE) |
| bt_conn_auth_info_cb_register(&auth_info_cb); |
| #endif /* CONFIG_BT_HAS_PRESET_SUPPORT || CONFIG_BT_HAS_FEATURES_NOTIFIABLE */ |
| |
| has.registered = true; |
| |
| return 0; |
| } |