diff --git a/include/bluetooth/audio/vocs.h b/include/bluetooth/audio/vocs.h new file mode 100644 index 0000000000000..3477d78b5e5a1 --- /dev/null +++ b/include/bluetooth/audio/vocs.h @@ -0,0 +1,307 @@ +/* + * Copyright (c) 2020 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_VOCS_H_ +#define ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_VOCS_H_ + +/** + * @brief Volume Offset Control Service (VOCS) + * + * @defgroup bt_gatt_vocs Volume Offset Control Service (VOCS) + * + * @ingroup bluetooth + * @{ + * + * The Volume Offset Control Service is a secondary service, and as such should not be used own its + * own, but rather in the context of another (primary) service. + * + * This API implements both the server and client functionality. + * Note that the API abstracts away the change counter in the volume offset control state and will + * automatically handle any changes to that. If out of date, the client implementation will + * autonomously read the change counter value when executing a write request. + * + * [Experimental] Users should note that the APIs can change as a part of ongoing development. + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** Volume Offset Control Service Error codes */ +#define BT_VOCS_ERR_INVALID_COUNTER 0x80 +#define BT_VOCS_ERR_OP_NOT_SUPPORTED 0x81 +#define BT_VOCS_ERR_OUT_OF_RANGE 0x82 + +#define BT_VOCS_MIN_OFFSET -255 +#define BT_VOCS_MAX_OFFSET 255 + +/** @brief Opaque Volume Offset Control Service instance. */ +struct bt_vocs; + +/** @brief Structure for initializing a Volume Offset Control Service instance. */ +struct bt_vocs_init_param { + /** Audio Location bitmask */ + uint32_t location; + + /** Boolean to set whether the location is writable by clients */ + bool location_writable; + + /** Initial volume offset (-255 to 255) */ + int16_t offset; + + /** Initial audio output description */ + char *output_desc; + + /** Boolean to set whether the description is writable by clients */ + bool desc_writable; +}; + +/** @brief Structure for discovering a Volume Offset Control Service instance. */ +struct bt_vocs_discover_param { + /** + * @brief The start handle of the discovering. + * + * Typically the @p start_handle of a @ref bt_gatt_include. + */ + uint16_t start_handle; + /** + * @brief The end handle of the discovering. + * + * Typically the @p end_handle of a @ref bt_gatt_include. + */ + uint16_t end_handle; +}; + +/** + * @brief Get a free service instance of Volume Offset Control Service from the pool. + * + * @return Volume Offset Control Service instance in case of success or NULL in case of error. + */ +struct bt_vocs *bt_vocs_free_instance_get(void); + +/** + * @brief Get the service declaration attribute. + * + * The first service attribute returned can be included in any other GATT service. + * + * @param vocs Volume Offset Control Service instance. + * + * @return Pointer to the attributes of the service. + */ +void *bt_vocs_svc_decl_get(struct bt_vocs *vocs); + +/** + * @brief Initialize the Volume Offset Control Service instance. + * + * @param vocs Volume Offset Control Service instance. + * @param init Volume Offset Control Service initialization structure. + * May be NULL to use default values. + * + * @return 0 if success, errno on failure. + */ +int bt_vocs_init(struct bt_vocs *vocs, const struct bt_vocs_init_param *init); + +/** + * @brief Callback function for the offset state. + * + * Called when the value is read, or if the value is changed by either the server or client. + * + * @param conn Connection to peer device, or NULL if local server read. + * @param inst The instance pointer. + * @param err Error value. 0 on success, GATT error on positive value + * or errno on negative value. + * For notifications, this will always be 0. + * @param offset The offset value. + */ +typedef void (*bt_vocs_state_cb_t)(struct bt_conn *conn, struct bt_vocs *inst, + int err, int16_t offset); + +/** + * @brief Callback function for setting offset. + * + * @param conn Connection to peer device, or NULL if local server write. + * @param inst The instance pointer. + * @param err Error value. 0 on success, GATT error on positive value + * or errno on negative value. + */ +typedef void (*bt_vocs_set_offset_cb_t)(struct bt_conn *conn, struct bt_vocs *inst, int err); + +/** + * @brief Callback function for the location. + * + * Called when the value is read, or if the value is changed by either the server or client. + * + * @param conn Connection to peer device, or NULL if local server read. + * @param inst The instance pointer. + * @param err Error value. 0 on success, GATT error on positive value + * or errno on negative value. + * For notifications, this will always be 0. + * @param location The location value. + */ +typedef void (*bt_vocs_location_cb_t)(struct bt_conn *conn, struct bt_vocs *inst, int err, + uint32_t location); + +/** + * @brief Callback function for the description. + * + * Called when the value is read, or if the value is changed by either the server or client. + * + * @param conn Connection to peer device, or NULL if local server read. + * @param inst The instance pointer. + * @param err Error value. 0 on success, GATT error on positive value + * or errno on negative value. + * For notifications, this will always be 0. + * @param description The description as an UTF-8 encoded string. + */ +typedef void (*bt_vocs_description_cb_t)(struct bt_conn *conn, struct bt_vocs *inst, int err, + char *description); + +/** + * @brief Callback function for bt_vocs_discover. + * + * This callback will usually be overwritten by the primary service that + * includes the Volume Control Offset Service client. + * + * @param conn Connection to peer device, or NULL if local server read. + * @param inst The instance pointer. + * @param err Error value. 0 on success, GATT error on positive value + * or errno on negative value. + * For notifications, this will always be 0. + */ +typedef void (*bt_vocs_discover_cb_t)(struct bt_conn *conn, struct bt_vocs *inst, int err); + +struct bt_vocs_cb { + bt_vocs_state_cb_t state; + bt_vocs_location_cb_t location; + bt_vocs_description_cb_t description; + +#if defined(CONFIG_BT_VOCS_CLIENT) + /* Client only */ + bt_vocs_discover_cb_t discover; + bt_vocs_set_offset_cb_t set_offset; +#endif /* CONFIG_BT_VOCS_CLIENT */ +}; + +/** + * @brief Read the Volume Offset Control Service offset state. + * + * The value is returned in the bt_vocs_cb.state callback. + * + * @param conn Connection to peer device, or NULL to read local server value. + * @param inst Pointer to the Volume Offset Control Service instance. + * + * @return 0 on success, GATT error value on fail. + */ +int bt_vocs_state_get(struct bt_conn *conn, struct bt_vocs *inst); + +/** + * @brief Set the Volume Offset Control Service offset state. + * + * @param conn Connection to peer device, or NULL to set local server value. + * @param inst Pointer to the Volume Offset Control Service instance. + * @param offset The offset to set (-255 to 255). + * + * @return 0 on success, GATT error value on fail. + */ +int bt_vocs_state_set(struct bt_conn *conn, struct bt_vocs *inst, int16_t offset); + +/** + * @brief Read the Volume Offset Control Service location. + * + * The value is returned in the bt_vocs_cb.location callback. + * + * @param conn Connection to peer device, or NULL to read local server value. + * @param inst Pointer to the Volume Offset Control Service instance. + * + * @return 0 on success, GATT error value on fail. + */ +int bt_vocs_location_get(struct bt_conn *conn, struct bt_vocs *inst); + +/** + * @brief Set the Volume Offset Control Service location. + * + * @param conn Connection to peer device, or NULL to read local server value. + * @param inst Pointer to the Volume Offset Control Service instance. + * @param location The location to set. + * + * @return 0 on success, GATT error value on fail. + */ +int bt_vocs_location_set(struct bt_conn *conn, struct bt_vocs *inst, uint32_t location); + +/** + * @brief Read the Volume Offset Control Service output description. + * + * The value is returned in the bt_vocs_cb.description callback. + * + * @param conn Connection to peer device, or NULL to read local server value. + * @param inst Pointer to the Volume Offset Control Service instance. + * + * @return 0 on success, GATT error value on fail. + */ +int bt_vocs_description_get(struct bt_conn *conn, struct bt_vocs *inst); + +/** + * @brief Set the Volume Offset Control Service description. + * + * @param conn Connection to peer device, or NULL to set local server value. + * @param inst Pointer to the Volume Offset Control Service instance. + * @param description The UTF-8 encoded string description to set. + * + * @return 0 on success, GATT error value on fail. + */ +int bt_vocs_description_set(struct bt_conn *conn, struct bt_vocs *inst, + const char *description); + +/** + * @brief Register callbacks for the Volume Offset Control Service. + * + * @param inst Pointer to the Volume Offset Control Service instance. + * @param cb Pointer to the callback structure. + * + * @return 0 on success, GATT error value on fail. + */ +int bt_vocs_cb_register(struct bt_vocs *inst, struct bt_vocs_cb *cb); + +/** + * @brief Registers the callbacks for the Volume Offset Control Service client. + * + * @param inst Pointer to the Volume Offset Control Service client instance. + * @param cb Pointer to the callback structure. + */ +void bt_vocs_client_cb_register(struct bt_vocs *inst, struct bt_vocs_cb *cb); + +/** + * @brief Returns a pointer to a Volume Offset Control Service client instance. + * + * @return Pointer to the instance, or NULL if no free instances are left. + */ +struct bt_vocs *bt_vocs_client_free_instance_get(void); + +/** + * @brief Discover a Volume Offset Control Service. + * + * Attempts to discover a Volume Offset Control Service on a server given the @p param. + * + * @param conn Connection to the peer with the Volume Offset Control Service. + * @param inst Pointer to the Volume Offset Control Service client instance. + * @param param Pointer to the parameters. + * + * @return 0 on success, errno on fail. + */ +int bt_vocs_discover(struct bt_conn *conn, struct bt_vocs *inst, + const struct bt_vocs_discover_param *param); + +#ifdef __cplusplus +} +#endif + +/** + * @} + */ + +#endif /* ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_VOCS_H_ */ diff --git a/include/bluetooth/uuid.h b/include/bluetooth/uuid.h index 27d1feba2bcbc..3589098b6a829 100644 --- a/include/bluetooth/uuid.h +++ b/include/bluetooth/uuid.h @@ -383,6 +383,15 @@ struct bt_uuid_128 { */ #define BT_UUID_MESH_PROXY \ BT_UUID_DECLARE_16(BT_UUID_MESH_PROXY_VAL) +/** @def BT_UUID_VOCS_VAL + * @brief Volume Offset Control Service value + */ +#define BT_UUID_VOCS_VAL 0x1845 +/** @def BT_UUID_VOCS + * @brief Volume Offset Control Service + */ +#define BT_UUID_VOCS \ + BT_UUID_DECLARE_16(BT_UUID_VOCS_VAL) /** @def BT_UUID_GATT_PRIMARY_VAL * @brief GATT Primary Service UUID value */ @@ -1214,6 +1223,43 @@ struct bt_uuid_128 { #define BT_UUID_GATT_SERVER_FEATURES \ BT_UUID_DECLARE_16(BT_UUID_GATT_SERVER_FEATURES_VAL) +/** @def BT_UUID_VOCS_STATE_VAL + * @brief Volume Offset State value + */ +#define BT_UUID_VOCS_STATE_VAL 0x2B80 +/** @def BT_UUID_VOCS_STATE + * @brief Volume Offset State + */ +#define BT_UUID_VOCS_STATE \ + BT_UUID_DECLARE_16(BT_UUID_VOCS_STATE_VAL) +/** @def BT_UUID_VOCS_LOCATION_VAL + * @brief Audio Location value + */ +#define BT_UUID_VOCS_LOCATION_VAL 0x2B81 +/** @def BT_UUID_VOCS_LOCATION + * @brief Audio Location + */ +#define BT_UUID_VOCS_LOCATION \ + BT_UUID_DECLARE_16(BT_UUID_VOCS_LOCATION_VAL) +/** @def BT_UUID_VOCS_CONTROL_VAL + * @brief Volume Offset Control Point value + */ +#define BT_UUID_VOCS_CONTROL_VAL 0x2B82 +/** @def BT_UUID_VOCS_CONTROL + * @brief Volume Offset Control Point + */ +#define BT_UUID_VOCS_CONTROL \ + BT_UUID_DECLARE_16(BT_UUID_VOCS_CONTROL_VAL) +/** @def BT_UUID_VOCS_DESCRIPTION_VAL + * @brief Volume Offset Audio Output Description value + */ +#define BT_UUID_VOCS_DESCRIPTION_VAL 0x2B83 +/** @def BT_UUID_VOCS_DESCRIPTION + * @brief Volume Offset Audio Output Description + */ +#define BT_UUID_VOCS_DESCRIPTION \ + BT_UUID_DECLARE_16(BT_UUID_VOCS_DESCRIPTION_VAL) + /* * Protocol UUIDs */ diff --git a/subsys/bluetooth/CMakeLists.txt b/subsys/bluetooth/CMakeLists.txt index eaca29d734194..9cc0d7a43d720 100644 --- a/subsys/bluetooth/CMakeLists.txt +++ b/subsys/bluetooth/CMakeLists.txt @@ -9,6 +9,7 @@ add_subdirectory_ifdef(CONFIG_BT_HCI host) add_subdirectory_ifdef(CONFIG_BT_SHELL shell) add_subdirectory_ifdef(CONFIG_BT_CONN services) add_subdirectory_ifdef(CONFIG_BT_MESH mesh) +add_subdirectory_ifdef(CONFIG_BT_AUDIO audio) if(CONFIG_BT_CTLR AND CONFIG_BT_LL_SW_SPLIT) add_subdirectory(controller) diff --git a/subsys/bluetooth/audio/CMakeLists.txt b/subsys/bluetooth/audio/CMakeLists.txt new file mode 100644 index 0000000000000..272add10b9165 --- /dev/null +++ b/subsys/bluetooth/audio/CMakeLists.txt @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 + +zephyr_library() +zephyr_library_link_libraries(subsys__bluetooth) + +if (CONFIG_BT_VOCS OR CONFIG_BT_VOCS_CLIENT) + zephyr_library_sources(vocs.c) +endif() +zephyr_library_sources_ifdef(CONFIG_BT_VOCS_CLIENT vocs_client.c) diff --git a/subsys/bluetooth/audio/Kconfig b/subsys/bluetooth/audio/Kconfig index f2ffa07fb4f38..2324d2c369435 100644 --- a/subsys/bluetooth/audio/Kconfig +++ b/subsys/bluetooth/audio/Kconfig @@ -50,4 +50,7 @@ config BT_AUDIO_DEBUG Use this option to enable debug logs for the Bluetooth Audio functionality. + +source "subsys/bluetooth/audio/Kconfig.vocs" + endif # BT_AUDIO diff --git a/subsys/bluetooth/audio/Kconfig.vocs b/subsys/bluetooth/audio/Kconfig.vocs new file mode 100644 index 0000000000000..e60ad1ec836f6 --- /dev/null +++ b/subsys/bluetooth/audio/Kconfig.vocs @@ -0,0 +1,76 @@ +# Bluetooth Audio - Volume Offset Control Service options +# +# Copyright (c) 2020 Bose Corporation +# +# SPDX-License-Identifier: Apache-2.0 +# + +if BT_AUDIO + +##################### Volume Offset Control Service ##################### + +config BT_VOCS_MAX_INSTANCE_COUNT + int "Volume Offset Control Service max instance count" + default 0 + range 0 15 + help + This option sets the maximum number of instances of Volume Offset + Control Services. + +config BT_VOCS + bool # hidden + default y if BT_VOCS_MAX_INSTANCE_COUNT > 0 + help + This hidden option enables support for Volume Control Service. + +if BT_VOCS + +config BT_VOCS_MAX_OUTPUT_DESCRIPTION_SIZE + int "Volume Offset Control Service max output description size" + default 32 + range 0 512 + help + This option sets the maximum output description size in octets. + +############# DEBUG ############# + +config BT_DEBUG_VOCS + bool "Volume Offset Control Service debug" + depends on BT_AUDIO_DEBUG + help + Use this option to enable Volume Offset Control Service debug logs for + the Bluetooth Audio functionality. + +endif # BT_VOCS + +##################### Volume Offset Control Service Client ##################### + +config BT_VOCS_CLIENT_MAX_INSTANCE_COUNT + int "Volume Offset Control Service client max instance count" + default 0 + range 0 15 + help + This option sets the maximum number of instances of Volume Offset + Control Service clients. + +config BT_VOCS_CLIENT + bool # hidden + default y if BT_VOCS_CLIENT_MAX_INSTANCE_COUNT > 0 + help + This hidden option enables support for Volume Offset Control Service. + + +if BT_VOCS_CLIENT + +############# DEBUG ############# + +config BT_DEBUG_VOCS_CLIENT + bool "Volume Offset Control Service client debug" + depends on BT_AUDIO_DEBUG + help + Use this option to enable Volume Offset Control Service client debug + logs for the Bluetooth Audio functionality. + +endif # BT_VOCS_CLIENT + +endif # BT_AUDIO diff --git a/subsys/bluetooth/audio/vocs.c b/subsys/bluetooth/audio/vocs.c new file mode 100644 index 0000000000000..25f47d9065f34 --- /dev/null +++ b/subsys/bluetooth/audio/vocs.c @@ -0,0 +1,482 @@ +/* Bluetooth VOCS - Volume offset Control Service + * + * Copyright (c) 2021 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include "vocs_internal.h" + +#define BT_DBG_ENABLED IS_ENABLED(CONFIG_BT_DEBUG_VOCS) +#define LOG_MODULE_NAME bt_vocs +#include "common/log.h" + +#define VALID_VOCS_OPCODE(opcode) ((opcode) == VOCS_OPCODE_SET_OFFSET) + +#if defined(CONFIG_BT_VOCS) +static void offset_state_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +static ssize_t read_offset_state(struct bt_conn *conn, const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) +{ + struct vocs_server *inst = attr->user_data; + + BT_DBG("offset %d, counter %u", inst->state.offset, inst->state.change_counter); + return bt_gatt_attr_read(conn, attr, buf, len, offset, &inst->state, sizeof(inst->state)); +} + +static void location_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +#endif /* CONFIG_BT_VOCS */ + +static ssize_t write_location(struct bt_conn *conn, const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, uint8_t flags) +{ + struct vocs_server *inst = attr->user_data; + uint32_t old_location = inst->location; + + if (len != sizeof(inst->location)) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + memcpy(&inst->location, buf, len); + BT_DBG("%02x", inst->location); + + if (old_location != inst->location) { + bt_gatt_notify_uuid(NULL, BT_UUID_VOCS_LOCATION, inst->service_p->attrs, + &inst->location, sizeof(inst->location)); + + if (inst->cb && inst->cb->location) { + inst->cb->location(NULL, (struct bt_vocs *)inst, 0, inst->location); + } + } + + return len; +} + +#if defined(CONFIG_BT_VOCS) +static ssize_t read_location(struct bt_conn *conn, const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) +{ + struct vocs_server *inst = attr->user_data; + + BT_DBG("0x%02x", inst->location); + return bt_gatt_attr_read(conn, attr, buf, len, offset, &inst->location, + sizeof(inst->location)); +} +#endif /* CONFIG_BT_VOCS */ + +static ssize_t write_vocs_control(struct bt_conn *conn, const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, uint8_t flags) +{ + struct vocs_server *inst = attr->user_data; + const struct vocs_control_t *cp = buf; + bool notify = false; + + if (!len || !buf) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + /* Check opcode before length */ + if (!VALID_VOCS_OPCODE(cp->opcode)) { + BT_DBG("Invalid opcode %u", cp->opcode); + return BT_GATT_ERR(BT_VOCS_ERR_OP_NOT_SUPPORTED); + } + + if (offset) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + + if (len != sizeof(struct vocs_control_t)) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + BT_DBG("Opcode %u, counter %u", cp->opcode, cp->counter); + + + if (cp->counter != inst->state.change_counter) { + return BT_GATT_ERR(BT_VOCS_ERR_INVALID_COUNTER); + } + + switch (cp->opcode) { + case VOCS_OPCODE_SET_OFFSET: + BT_DBG("Set offset %d", cp->offset); + if (cp->offset > BT_VOCS_MAX_OFFSET || cp->offset < BT_VOCS_MIN_OFFSET) { + return BT_GATT_ERR(BT_VOCS_ERR_OUT_OF_RANGE); + } + + if (inst->state.offset != sys_le16_to_cpu(cp->offset)) { + inst->state.offset = sys_le16_to_cpu(cp->offset); + notify = true; + } + break; + default: + return BT_GATT_ERR(BT_VOCS_ERR_OP_NOT_SUPPORTED); + } + + if (notify) { + inst->state.change_counter++; + BT_DBG("New state: offset %d, counter %u", + inst->state.offset, inst->state.change_counter); + bt_gatt_notify_uuid(NULL, BT_UUID_VOCS_STATE, inst->service_p->attrs, + &inst->state, sizeof(inst->state)); + + if (inst->cb && inst->cb->state) { + inst->cb->state(NULL, (struct bt_vocs *)inst, 0, inst->state.offset); + } + + } + + return len; +} + +#if defined(CONFIG_BT_VOCS) +static void output_desc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} +#endif /* CONFIG_BT_VOCS */ + +static ssize_t write_output_desc(struct bt_conn *conn, const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, uint8_t flags) +{ + struct vocs_server *inst = attr->user_data; + + if (len >= sizeof(inst->output_desc)) { + BT_DBG("Output desc was clipped from length %u to %zu", + len, sizeof(inst->output_desc) - 1); + /* We just clip the string value if it's too long */ + len = (uint16_t)sizeof(inst->output_desc) - 1; + } + + if (len != strlen(inst->output_desc) || memcmp(buf, inst->output_desc, len)) { + memcpy(inst->output_desc, buf, len); + inst->output_desc[len] = '\0'; + + bt_gatt_notify_uuid(NULL, BT_UUID_VOCS_DESCRIPTION, inst->service_p->attrs, + &inst->output_desc, strlen(inst->output_desc)); + + if (inst->cb && inst->cb->description) { + inst->cb->description(NULL, (struct bt_vocs *)inst, 0, inst->output_desc); + } + } + + BT_DBG("%s", log_strdup(inst->output_desc)); + + return len; +} + +#if defined(CONFIG_BT_VOCS) +static ssize_t read_output_desc(struct bt_conn *conn, const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) +{ + struct vocs_server *inst = attr->user_data; + + BT_DBG("%s", log_strdup(inst->output_desc)); + return bt_gatt_attr_read(conn, attr, buf, len, offset, &inst->output_desc, + strlen(inst->output_desc)); +} + +#define BT_VOCS_SERVICE_DEFINITION(_vocs) { \ + BT_GATT_SECONDARY_SERVICE(BT_UUID_VOCS), \ + BT_GATT_CHARACTERISTIC(BT_UUID_VOCS_STATE, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT, \ + read_offset_state, NULL, &_vocs), \ + BT_GATT_CCC(offset_state_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + BT_GATT_CHARACTERISTIC(BT_UUID_VOCS_LOCATION, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT, \ + read_location, NULL, &_vocs), \ + BT_GATT_CCC(location_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + BT_GATT_CHARACTERISTIC(BT_UUID_VOCS_CONTROL, \ + BT_GATT_CHRC_WRITE, \ + BT_GATT_PERM_WRITE_ENCRYPT, \ + NULL, write_vocs_control, &_vocs), \ + BT_GATT_CHARACTERISTIC(BT_UUID_VOCS_DESCRIPTION, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT, \ + read_output_desc, NULL, &_vocs), \ + BT_GATT_CCC(output_desc_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT) \ + } + +static struct vocs_server vocs_insts[CONFIG_BT_VOCS_MAX_INSTANCE_COUNT]; +BT_GATT_SERVICE_INSTANCE_DEFINE(vocs_service_list, vocs_insts, CONFIG_BT_VOCS_MAX_INSTANCE_COUNT, + BT_VOCS_SERVICE_DEFINITION); + +struct bt_vocs *bt_vocs_free_instance_get(void) +{ + static uint32_t instance_cnt; + + if (instance_cnt >= CONFIG_BT_VOCS_MAX_INSTANCE_COUNT) { + return NULL; + } + + return (struct bt_vocs *)&vocs_insts[instance_cnt++]; +} + +void *bt_vocs_svc_decl_get(struct bt_vocs *vocs) +{ + CHECKIF(!vocs) { + BT_DBG("Null VOCS pointer"); + return NULL; + } + + return vocs->srv.service_p->attrs; +} + +static void prepare_vocs_instances(void) +{ + for (int i = 0; i < ARRAY_SIZE(vocs_insts); i++) { + vocs_insts[i].service_p = &vocs_service_list[i]; + } +} + +int bt_vocs_init(struct bt_vocs *vocs, const struct bt_vocs_init_param *init) +{ + int err; + struct bt_gatt_attr *attr; + struct bt_gatt_chrc *chrc; + static bool instances_prepared; + + CHECKIF(!vocs) { + BT_DBG("Null VOCS pointer"); + return -EINVAL; + } + + if (!instances_prepared) { + prepare_vocs_instances(); + instances_prepared = true; + } + + CHECKIF(vocs->srv.initialized) { + BT_DBG("Already initialized VOCS instance"); + return -EALREADY; + } + + CHECKIF(init->offset > BT_VOCS_MAX_OFFSET || init->offset < BT_VOCS_MIN_OFFSET) { + BT_DBG("Invalid offset %d", init->offset); + return -EINVAL; + } + + vocs->srv.location = init->location; + vocs->srv.state.offset = init->offset; + + if (init->output_desc) { + strncpy(vocs->srv.output_desc, init->output_desc, + sizeof(vocs->srv.output_desc) - 1); + /* strncpy may not always null-terminate */ + vocs->srv.output_desc[sizeof(vocs->srv.output_desc) - 1] = '\0'; + if (IS_ENABLED(CONFIG_BT_DEBUG_VOCS) && + strcmp(vocs->srv.output_desc, init->output_desc)) { + BT_DBG("Output desc clipped to %s", log_strdup(vocs->srv.output_desc)); + } + } + + /* Iterate over the attributes in VOCS (starting from i = 1 to skip the service declaration) + * to find the BT_UUID_VOCS_DESCRIPTION or BT_UUID_VOCS_LOCATION and update the + * characteristic value (at [i]), update with the write permission and callback, and + * also update the characteristic declaration (always found at [i - 1]) with the + * BT_GATT_CHRC_WRITE_WITHOUT_RESP property. + */ + for (int i = 1; i < vocs->srv.service_p->attr_count; i++) { + attr = &vocs->srv.service_p->attrs[i]; + + if (init->location_writable && !bt_uuid_cmp(attr->uuid, BT_UUID_VOCS_LOCATION)) { + /* Update attr and chrc to be writable */ + chrc = vocs->srv.service_p->attrs[i - 1].user_data; + attr->write = write_location; + attr->perm |= BT_GATT_PERM_WRITE_ENCRYPT; + chrc->properties |= BT_GATT_CHRC_WRITE_WITHOUT_RESP; + } else if (init->desc_writable && + !bt_uuid_cmp(attr->uuid, BT_UUID_VOCS_DESCRIPTION)) { + /* Update attr and chrc to be writable */ + chrc = vocs->srv.service_p->attrs[i - 1].user_data; + attr->write = write_output_desc; + attr->perm |= BT_GATT_PERM_WRITE_ENCRYPT; + chrc->properties |= BT_GATT_CHRC_WRITE_WITHOUT_RESP; + } + } + + err = bt_gatt_service_register(vocs->srv.service_p); + if (err) { + BT_DBG("Could not register VOCS service"); + return err; + } + + vocs->srv.initialized = true; + return 0; +} +#endif /* CONFIG_BT_VOCS */ + +#if defined(CONFIG_BT_VOCS) || defined(CONFIG_BT_VOCS_CLIENT) + +int bt_vocs_state_get(struct bt_conn *conn, struct bt_vocs *inst) +{ + CHECKIF(!inst) { + BT_DBG("Null VOCS pointer"); + return -EINVAL; + } + + if (IS_ENABLED(CONFIG_BT_VOCS_CLIENT) && conn) { + return bt_vocs_client_state_get(conn, inst); + } else if (IS_ENABLED(CONFIG_BT_VOCS) && !conn) { + if (inst->srv.cb && inst->srv.cb->state) { + inst->srv.cb->state(NULL, inst, 0, inst->srv.state.offset); + } + return 0; + } + + return -EOPNOTSUPP; +} + +int bt_vocs_location_get(struct bt_conn *conn, struct bt_vocs *inst) +{ + CHECKIF(!inst) { + BT_DBG("Null VOCS pointer"); + return -EINVAL; + } + + if (IS_ENABLED(CONFIG_BT_VOCS_CLIENT) && conn) { + return bt_vocs_client_location_get(conn, inst); + } else if (IS_ENABLED(CONFIG_BT_VOCS) && !conn) { + if (inst->srv.cb && inst->srv.cb->location) { + inst->srv.cb->location(NULL, inst, 0, inst->srv.location); + } + return 0; + } + + return -EOPNOTSUPP; +} + +int bt_vocs_location_set(struct bt_conn *conn, struct bt_vocs *inst, uint32_t location) +{ + CHECKIF(!inst) { + BT_DBG("Null VOCS pointer"); + return -EINVAL; + } + + if (IS_ENABLED(CONFIG_BT_VOCS_CLIENT) && conn) { + return bt_vocs_client_location_set(conn, inst, location); + } else if (IS_ENABLED(CONFIG_BT_VOCS) && !conn) { + struct bt_gatt_attr attr; + int err; + + attr.user_data = inst; + + err = write_location(NULL, &attr, &location, sizeof(location), 0, 0); + + return err > 0 ? 0 : err; + } + + return -EOPNOTSUPP; +} + +int bt_vocs_state_set(struct bt_conn *conn, struct bt_vocs *inst, int16_t offset) +{ + CHECKIF(!inst) { + BT_DBG("Null VOCS pointer"); + return -EINVAL; + } + + if (IS_ENABLED(CONFIG_BT_VOCS_CLIENT) && conn) { + return bt_vocs_client_state_set(conn, inst, offset); + } else if (IS_ENABLED(CONFIG_BT_VOCS) && !conn) { + struct bt_gatt_attr attr; + struct vocs_control_t cp; + int err; + + cp.opcode = VOCS_OPCODE_SET_OFFSET; + cp.counter = inst->srv.state.change_counter; + cp.offset = sys_cpu_to_le16(offset); + + attr.user_data = inst; + + err = write_vocs_control(NULL, &attr, &cp, sizeof(cp), 0, 0); + + return err > 0 ? 0 : err; + } + + return -EOPNOTSUPP; +} + +int bt_vocs_description_get(struct bt_conn *conn, struct bt_vocs *inst) +{ + CHECKIF(!inst) { + BT_DBG("Null VOCS pointer"); + return -EINVAL; + } + + if (IS_ENABLED(CONFIG_BT_VOCS_CLIENT) && conn) { + return bt_vocs_client_description_get(conn, inst); + } else if (IS_ENABLED(CONFIG_BT_VOCS) && !conn) { + if (inst->srv.cb && inst->srv.cb->description) { + inst->srv.cb->description(NULL, inst, 0, inst->srv.output_desc); + } + return 0; + } + + return -EOPNOTSUPP; +} + +int bt_vocs_description_set(struct bt_conn *conn, struct bt_vocs *inst, const char *description) +{ + CHECKIF(!inst) { + BT_DBG("Null VOCS pointer"); + return -EINVAL; + } + + CHECKIF(!description) { + BT_DBG("Null description pointer"); + return -EINVAL; + } + + if (IS_ENABLED(CONFIG_BT_VOCS_CLIENT) && conn) { + return bt_vocs_client_description_set(conn, inst, description); + } else if (IS_ENABLED(CONFIG_BT_VOCS) && !conn) { + struct bt_gatt_attr attr; + int err; + + attr.user_data = inst; + + err = write_output_desc(NULL, &attr, description, strlen(description), 0, 0); + return err > 0 ? 0 : err; + } + + return -EOPNOTSUPP; +} + +int bt_vocs_cb_register(struct bt_vocs *inst, struct bt_vocs_cb *cb) +{ + CHECKIF(!inst) { + BT_DBG("Null VOCS pointer"); + return -EINVAL; + } + + inst->srv.cb = cb; + + return 0; +} + +#endif /* CONFIG_BT_VOCS || CONFIG_BT_VOCS_CLIENT */ diff --git a/subsys/bluetooth/audio/vocs_client.c b/subsys/bluetooth/audio/vocs_client.c new file mode 100644 index 0000000000000..2bc6bc06ec66a --- /dev/null +++ b/subsys/bluetooth/audio/vocs_client.c @@ -0,0 +1,681 @@ +/* Bluetooth VOCS - Volume Offset Control Service - Client */ + +/* + * Copyright (c) 2021 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include "vocs_internal.h" + +#define BT_DBG_ENABLED IS_ENABLED(CONFIG_BT_DEBUG_VOCS_CLIENT) +#define LOG_MODULE_NAME bt_vocs_client +#include "common/log.h" + +static struct bt_vocs vocs_insts[CONFIG_BT_MAX_CONN * CONFIG_BT_VOCS_CLIENT_MAX_INSTANCE_COUNT]; + +static struct bt_vocs *lookup_vocs_by_handle(struct bt_conn *conn, uint16_t handle) +{ + __ASSERT(handle != 0, "Handle cannot be 0"); + __ASSERT(conn, "Conn cannot be NULL"); + + for (int i = 0; i < ARRAY_SIZE(vocs_insts); i++) { + if (vocs_insts[i].cli.conn == conn && + vocs_insts[i].cli.active && + vocs_insts[i].cli.start_handle <= handle && + vocs_insts[i].cli.end_handle >= handle) { + return &vocs_insts[i]; + } + } + + BT_DBG("Could not find VOCS instance with handle 0x%04x", handle); + return NULL; +} + +uint8_t vocs_client_notify_handler(struct bt_conn *conn, struct bt_gatt_subscribe_params *params, + const void *data, uint16_t length) +{ + uint16_t handle = params->value_handle; + struct bt_vocs *inst = lookup_vocs_by_handle(conn, handle); + + if (!inst) { + BT_DBG("Instance not found"); + return BT_GATT_ITER_STOP; + } + + if (!data || !length) { + return BT_GATT_ITER_CONTINUE; + } + + if (handle == inst->cli.state_handle) { + if (length == sizeof(inst->cli.state)) { + memcpy(&inst->cli.state, data, length); + BT_DBG("Inst %p: Offset %d, counter %u", inst, inst->cli.state.offset, + inst->cli.state.change_counter); + if (inst->cli.cb && inst->cli.cb->state) { + inst->cli.cb->state(conn, inst, 0, inst->cli.state.offset); + } + } else { + BT_DBG("Invalid state length %u", length); + } + } else if (handle == inst->cli.desc_handle) { + char desc[MIN(CONFIG_BT_L2CAP_RX_MTU, BT_ATT_MAX_ATTRIBUTE_LEN) + 1]; + + /* Truncate if too large */ + length = MIN(sizeof(desc) - 1, length); + + memcpy(desc, data, length); + desc[length] = '\0'; + BT_DBG("Inst %p: Output description: %s", inst, log_strdup(desc)); + if (inst->cli.cb && inst->cli.cb->description) { + inst->cli.cb->description(conn, inst, 0, desc); + } + } else if (handle == inst->cli.location_handle) { + if (length == sizeof(inst->cli.location)) { + memcpy(&inst->cli.location, data, length); + BT_DBG("Inst %p: Location %u", inst, inst->cli.location); + if (inst->cli.cb && inst->cli.cb->location) { + inst->cli.cb->location(conn, inst, 0, inst->cli.location); + } + } else { + BT_DBG("Invalid location length %u", length); + } + } + + return BT_GATT_ITER_CONTINUE; +} + +static uint8_t vocs_client_read_offset_state_cb(struct bt_conn *conn, uint8_t err, + struct bt_gatt_read_params *params, + const void *data, uint16_t length) +{ + int cb_err = err; + struct bt_vocs *inst = lookup_vocs_by_handle(conn, params->single.handle); + + memset(params, 0, sizeof(*params)); + + if (!inst) { + BT_DBG("Instance not found"); + return BT_GATT_ITER_STOP; + } + + BT_DBG("Inst %p: err: 0x%02X", inst, err); + inst->cli.busy = false; + + if (data) { + if (length == sizeof(inst->cli.state)) { + memcpy(&inst->cli.state, data, length); + BT_DBG("Offset %d, counter %u", + inst->cli.state.offset, inst->cli.state.change_counter); + } else { + BT_DBG("Invalid length %u (expected %zu)", length, sizeof(inst->cli.state)); + cb_err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; + } + } else { + BT_DBG("Invalid state"); + cb_err = BT_ATT_ERR_UNLIKELY; + } + + if (inst->cli.cb && inst->cli.cb->state) { + inst->cli.cb->state(conn, inst, cb_err, inst->cli.state.offset); + } + + return BT_GATT_ITER_STOP; +} + +static uint8_t vocs_client_read_location_cb(struct bt_conn *conn, uint8_t err, + struct bt_gatt_read_params *params, + const void *data, uint16_t length) +{ + int cb_err = err; + struct bt_vocs *inst = lookup_vocs_by_handle(conn, params->single.handle); + + memset(params, 0, sizeof(*params)); + + if (!inst) { + BT_DBG("Instance not found"); + return BT_GATT_ITER_STOP; + } + + BT_DBG("Inst %p: err: 0x%02X", inst, err); + inst->cli.busy = false; + + if (data) { + if (length == sizeof(inst->cli.location)) { + memcpy(&inst->cli.location, data, length); + BT_DBG("Location %u", inst->cli.location); + } else { + BT_DBG("Invalid length %u (expected %zu)", + length, sizeof(inst->cli.location)); + cb_err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; + } + } else { + BT_DBG("Invalid location"); + cb_err = BT_ATT_ERR_UNLIKELY; + } + + if (inst->cli.cb && inst->cli.cb->location) { + inst->cli.cb->location(conn, inst, cb_err, inst->cli.location); + } + + return BT_GATT_ITER_STOP; +} + +static uint8_t internal_read_volume_offset_state_cb(struct bt_conn *conn, uint8_t err, + struct bt_gatt_read_params *params, + const void *data, uint16_t length) +{ + int cb_err = err; + struct bt_vocs *inst = lookup_vocs_by_handle(conn, params->single.handle); + + memset(params, 0, sizeof(*params)); + + if (!inst) { + BT_ERR("Instance not found"); + return BT_GATT_ITER_STOP; + } + + if (err) { + BT_WARN("Volume state read failed: %d", err); + cb_err = BT_ATT_ERR_UNLIKELY; + } else if (data) { + if (length == sizeof(inst->cli.state)) { + int write_err; + + memcpy(&inst->cli.state, data, length); + BT_DBG("Offset %d, counter %u", + inst->cli.state.offset, + inst->cli.state.change_counter); + + /* clear busy flag to reuse function */ + inst->cli.busy = false; + write_err = bt_vocs_client_state_set(conn, inst, inst->cli.cp.offset); + if (write_err) { + cb_err = BT_ATT_ERR_UNLIKELY; + } + } else { + BT_DBG("Invalid length %u (expected %zu)", length, sizeof(inst->cli.state)); + cb_err = BT_ATT_ERR_UNLIKELY; + } + } else { + BT_DBG("Invalid location"); + cb_err = BT_ATT_ERR_UNLIKELY; + } + + if (cb_err) { + inst->cli.busy = false; + + if (inst->cli.cb && inst->cli.cb->set_offset) { + inst->cli.cb->set_offset(conn, inst, err); + } + } + + return BT_GATT_ITER_STOP; +} + +static void vcs_client_write_vocs_cp_cb(struct bt_conn *conn, uint8_t err, + struct bt_gatt_write_params *params) +{ + int cb_err = err; + struct bt_vocs *inst = lookup_vocs_by_handle(conn, params->handle); + + memset(params, 0, sizeof(*params)); + + if (!inst) { + BT_DBG("Instance not found"); + return; + } + + BT_DBG("Inst %p: err: 0x%02X", inst, err); + + /* If the change counter is out of data when a write was attempted from the application, + * we automatically initiate a read to get the newest state and try again. Once the + * change counter has been read, we restart the applications write request. If it fails + * the second time, we return an error to the application. + */ + if (cb_err == BT_VOCS_ERR_INVALID_COUNTER && inst->cli.state_handle) { + BT_DBG("Invalid change counter. Reading volume state from server."); + + inst->cli.read_params.func = internal_read_volume_offset_state_cb; + inst->cli.read_params.handle_count = 1; + inst->cli.read_params.single.handle = inst->cli.state_handle; + + cb_err = bt_gatt_read(conn, &inst->cli.read_params); + if (cb_err) { + BT_WARN("Could not read Volume state: %d", cb_err); + } + } else { + BT_DBG("Invalid location"); + cb_err = BT_ATT_ERR_UNLIKELY; + } + + inst->cli.busy = false; + + if (inst->cli.cb && inst->cli.cb->set_offset) { + inst->cli.cb->set_offset(conn, inst, cb_err); + } +} + +static uint8_t vcs_client_read_output_desc_cb(struct bt_conn *conn, uint8_t err, + struct bt_gatt_read_params *params, + const void *data, uint16_t length) +{ + int cb_err = err; + struct bt_vocs *inst = lookup_vocs_by_handle(conn, params->single.handle); + char desc[MIN(CONFIG_BT_L2CAP_RX_MTU, BT_ATT_MAX_ATTRIBUTE_LEN) + 1]; + + memset(params, 0, sizeof(*params)); + + if (!inst) { + BT_DBG("Instance not found"); + return BT_GATT_ITER_STOP; + } + + BT_DBG("Inst %p: err: 0x%02X", inst, err); + inst->cli.busy = false; + + if (data) { + BT_HEXDUMP_DBG(data, length, "Output description read"); + length = MIN(sizeof(desc) - 1, length); + + /* TODO: Handle long reads */ + memcpy(desc, data, length); + desc[length] = '\0'; + BT_DBG("Output description: %s", log_strdup(desc)); + } else { + BT_DBG("Invalid location"); + cb_err = BT_ATT_ERR_UNLIKELY; + } + + if (inst->cli.cb && inst->cli.cb->description) { + inst->cli.cb->description(conn, inst, cb_err, desc); + } + + return BT_GATT_ITER_STOP; +} + +static bool valid_inst_discovered(struct bt_vocs *inst) +{ + return inst->cli.state_handle && + inst->cli.control_handle && + inst->cli.location_handle && + inst->cli.desc_handle; +} + +static uint8_t vocs_discover_func(struct bt_conn *conn, const struct bt_gatt_attr *attr, + struct bt_gatt_discover_params *params) +{ + struct bt_vocs *inst = (struct bt_vocs *)CONTAINER_OF( + params, struct vocs_client, discover_params); + + if (!attr) { + BT_DBG("Discovery complete for VOCS %p", inst); + inst->cli.busy = false; + (void)memset(params, 0, sizeof(*params)); + + if (inst->cli.cb && inst->cli.cb->discover) { + int err = valid_inst_discovered(inst) ? 0 : -ENOENT; + + inst->cli.cb->discover(conn, inst, err); + } + + return BT_GATT_ITER_STOP; + } + + BT_DBG("[ATTRIBUTE] handle 0x%04X", attr->handle); + + if (params->type == BT_GATT_DISCOVER_CHARACTERISTIC) { + struct bt_gatt_subscribe_params *sub_params = NULL; + struct bt_gatt_chrc *chrc; + + chrc = (struct bt_gatt_chrc *)attr->user_data; + if (inst->cli.start_handle == 0) { + inst->cli.start_handle = chrc->value_handle; + } + inst->cli.end_handle = chrc->value_handle; + + if (!bt_uuid_cmp(chrc->uuid, BT_UUID_VOCS_STATE)) { + BT_DBG("Volume offset state"); + inst->cli.state_handle = chrc->value_handle; + sub_params = &inst->cli.state_sub_params; + } else if (!bt_uuid_cmp(chrc->uuid, BT_UUID_VOCS_LOCATION)) { + BT_DBG("Location"); + inst->cli.location_handle = chrc->value_handle; + if (chrc->properties & BT_GATT_CHRC_NOTIFY) { + sub_params = &inst->cli.location_sub_params; + } + if (chrc->properties & BT_GATT_CHRC_WRITE_WITHOUT_RESP) { + inst->cli.location_writable = true; + } + } else if (!bt_uuid_cmp(chrc->uuid, BT_UUID_VOCS_CONTROL)) { + BT_DBG("Control point"); + inst->cli.control_handle = chrc->value_handle; + } else if (!bt_uuid_cmp(chrc->uuid, BT_UUID_VOCS_DESCRIPTION)) { + BT_DBG("Description"); + inst->cli.desc_handle = chrc->value_handle; + if (chrc->properties & BT_GATT_CHRC_NOTIFY) { + sub_params = &inst->cli.desc_sub_params; + } + if (chrc->properties & BT_GATT_CHRC_WRITE_WITHOUT_RESP) { + inst->cli.desc_writable = true; + } + } + + if (sub_params) { + int err; + + sub_params->value = BT_GATT_CCC_NOTIFY; + sub_params->value_handle = chrc->value_handle; + /* + * TODO: Don't assume that CCC is at handle + 2; + * do proper discovery; + */ + sub_params->ccc_handle = attr->handle + 2; + sub_params->notify = vocs_client_notify_handler; + err = bt_gatt_subscribe(conn, sub_params); + if (err) { + BT_WARN("Could not subscribe to handle %u", + sub_params->ccc_handle); + } + } + } + + return BT_GATT_ITER_CONTINUE; +} + +int bt_vocs_client_state_get(struct bt_conn *conn, struct bt_vocs *inst) +{ + int err; + + CHECKIF(!conn) { + BT_DBG("NULL conn"); + return -ENOTCONN; + } + + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (!inst->cli.state_handle) { + BT_DBG("Handle not set"); + return -EINVAL; + } + + if (inst->cli.busy) { + BT_DBG("Handle not set"); + return -EBUSY; + } + + inst->cli.read_params.func = vocs_client_read_offset_state_cb; + inst->cli.read_params.handle_count = 1; + inst->cli.read_params.single.handle = inst->cli.state_handle; + inst->cli.read_params.single.offset = 0U; + + err = bt_gatt_read(conn, &inst->cli.read_params); + if (!err) { + inst->cli.busy = true; + } + + return err; +} + +int bt_vocs_client_location_set(struct bt_conn *conn, struct bt_vocs *inst, uint32_t location) +{ + CHECKIF(!conn) { + BT_DBG("NULL conn"); + return -ENOTCONN; + } + + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (!inst->cli.location_handle) { + BT_DBG("Handle not set"); + return -EINVAL; + } else if (inst->cli.busy) { + return -EBUSY; + } else if (!inst->cli.location_writable) { + BT_DBG("Location is not writable on peer service instance"); + return -EPERM; + } + + return bt_gatt_write_without_response(conn, inst->cli.location_handle, + &location, sizeof(location), + false); +} + +int bt_vocs_client_location_get(struct bt_conn *conn, struct bt_vocs *inst) +{ + int err; + + CHECKIF(!conn) { + BT_DBG("NULL conn"); + return -ENOTCONN; + } + + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (!inst->cli.location_handle) { + BT_DBG("Handle not set"); + return -EINVAL; + } else if (inst->cli.busy) { + return -EBUSY; + } + + inst->cli.read_params.func = vocs_client_read_location_cb; + inst->cli.read_params.handle_count = 1; + inst->cli.read_params.single.handle = inst->cli.location_handle; + inst->cli.read_params.single.offset = 0U; + + err = bt_gatt_read(conn, &inst->cli.read_params); + if (!err) { + inst->cli.busy = true; + } + + return err; +} + +int bt_vocs_client_state_set(struct bt_conn *conn, struct bt_vocs *inst, int16_t offset) +{ + int err; + + CHECKIF(!conn) { + BT_DBG("NULL conn"); + return -ENOTCONN; + } + + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (!inst->cli.control_handle) { + BT_DBG("Handle not set"); + return -EINVAL; + } else if (inst->cli.busy) { + return -EBUSY; + } + + inst->cli.cp.opcode = VOCS_OPCODE_SET_OFFSET; + inst->cli.cp.counter = inst->cli.state.change_counter; + inst->cli.cp.offset = offset; + + inst->cli.write_params.offset = 0; + inst->cli.write_params.data = &inst->cli.cp; + inst->cli.write_params.length = sizeof(inst->cli.cp); + inst->cli.write_params.handle = inst->cli.control_handle; + inst->cli.write_params.func = vcs_client_write_vocs_cp_cb; + + err = bt_gatt_write(conn, &inst->cli.write_params); + if (!err) { + inst->cli.busy = true; + } + + return err; +} + +int bt_vocs_client_description_get(struct bt_conn *conn, struct bt_vocs *inst) +{ + int err; + + CHECKIF(!conn) { + BT_DBG("NULL conn"); + return -ENOTCONN; + } + + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (!inst->cli.desc_handle) { + BT_DBG("Handle not set"); + return -EINVAL; + } else if (inst->cli.busy) { + return -EBUSY; + } + + inst->cli.read_params.func = vcs_client_read_output_desc_cb; + inst->cli.read_params.handle_count = 1; + inst->cli.read_params.single.handle = inst->cli.desc_handle; + inst->cli.read_params.single.offset = 0U; + + err = bt_gatt_read(conn, &inst->cli.read_params); + if (!err) { + inst->cli.busy = true; + } + + return err; +} + +int bt_vocs_client_description_set(struct bt_conn *conn, struct bt_vocs *inst, + const char *description) +{ + CHECKIF(!conn) { + BT_DBG("NULL conn"); + return -ENOTCONN; + } + + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (!inst->cli.desc_handle) { + BT_DBG("Handle not set"); + return -EINVAL; + } else if (inst->cli.busy) { + return -EBUSY; + } else if (!inst->cli.desc_writable) { + BT_DBG("Description is not writable on peer service instance"); + return -EPERM; + } + + return bt_gatt_write_without_response(conn, inst->cli.desc_handle, + description, + strlen(description), false); +} + +struct bt_vocs *bt_vocs_client_free_instance_get(void) +{ + for (int i = 0; i < ARRAY_SIZE(vocs_insts); i++) { + if (!vocs_insts[i].cli.active) { + vocs_insts[i].cli.active = true; + return &vocs_insts[i]; + } + } + + return NULL; +} + +static void vocs_client_reset(struct bt_vocs *inst, struct bt_conn *conn) +{ + memset(&inst->cli.state, 0, sizeof(inst->cli.state)); + inst->cli.location_writable = 0; + inst->cli.location = 0; + inst->cli.desc_writable = 0; + inst->cli.start_handle = 0; + inst->cli.end_handle = 0; + inst->cli.state_handle = 0; + inst->cli.location_handle = 0; + inst->cli.control_handle = 0; + inst->cli.desc_handle = 0; + + /* It's okay if these fail */ + (void)bt_gatt_unsubscribe(conn, &inst->cli.state_sub_params); + (void)bt_gatt_unsubscribe(conn, &inst->cli.location_sub_params); + (void)bt_gatt_unsubscribe(conn, &inst->cli.desc_sub_params); +} + +int bt_vocs_discover(struct bt_conn *conn, struct bt_vocs *inst, + const struct bt_vocs_discover_param *param) +{ + int err = 0; + + CHECKIF(!inst || !conn || !param) { + BT_DBG("%s cannot be NULL", + inst == NULL ? "inst" : conn == NULL ? "conn" : "param"); + return -EINVAL; + } + + CHECKIF(param->end_handle < param->start_handle) { + BT_DBG("start_handle (%u) shall be less than end_handle (%u)", + param->start_handle, param->end_handle); + return -EINVAL; + } + + CHECKIF(!inst->cli.active) { + BT_DBG("Inactive instance"); + return -EINVAL; + } + + if (inst->cli.busy) { + BT_DBG("Instance is busy"); + return -EBUSY; + } + + vocs_client_reset(inst, conn); + + inst->cli.conn = conn; + inst->cli.discover_params.start_handle = param->start_handle; + inst->cli.discover_params.end_handle = param->end_handle; + inst->cli.discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; + inst->cli.discover_params.func = vocs_discover_func; + + err = bt_gatt_discover(conn, &inst->cli.discover_params); + if (err) { + BT_DBG("Discover failed (err %d)", err); + } else { + inst->cli.busy = true; + } + + return err; +} + +void bt_vocs_client_cb_register(struct bt_vocs *inst, struct bt_vocs_cb *cb) +{ + CHECKIF(!inst) { + BT_DBG("inst cannot be NULL"); + return; + } + + inst->cli.cb = cb; +} diff --git a/subsys/bluetooth/audio/vocs_internal.h b/subsys/bluetooth/audio/vocs_internal.h new file mode 100644 index 0000000000000..dd9712018a405 --- /dev/null +++ b/subsys/bluetooth/audio/vocs_internal.h @@ -0,0 +1,85 @@ +/** @file + * @brief Internal APIs for Bluetooth VOCS. + * + * Copyright (c) 2020 Bose Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_BLUETOOTH_AUDIO_VOCS_INTERNAL_ +#define ZEPHYR_INCLUDE_BLUETOOTH_AUDIO_VOCS_INTERNAL_ +#include + +#if defined(CONFIG_BT_VOCS) +#define VOCS_MAX_DESC_SIZE CONFIG_BT_VOCS_MAX_OUTPUT_DESCRIPTION_SIZE +#else +#define VOCS_MAX_DESC_SIZE 1 +#endif /* CONFIG_BT_VOCS */ + +/* VOCS opcodes */ +#define VOCS_OPCODE_SET_OFFSET 0x01 + +struct vocs_control_t { + uint8_t opcode; + uint8_t counter; + int16_t offset; +} __packed; + +struct vocs_state_t { + int16_t offset; + uint8_t change_counter; +} __packed; + +struct vocs_client { + struct vocs_state_t state; + bool location_writable; + uint32_t location; + bool desc_writable; + bool active; + + uint16_t start_handle; + uint16_t end_handle; + uint16_t state_handle; + uint16_t location_handle; + uint16_t control_handle; + uint16_t desc_handle; + struct bt_gatt_subscribe_params state_sub_params; + struct bt_gatt_subscribe_params location_sub_params; + struct bt_gatt_subscribe_params desc_sub_params; + uint8_t subscribe_cnt; + + bool busy; + struct vocs_control_t cp; + struct bt_gatt_write_params write_params; + struct bt_gatt_read_params read_params; + struct bt_vocs_cb *cb; + struct bt_gatt_discover_params discover_params; + struct bt_conn *conn; +}; + +struct vocs_server { + struct vocs_state_t state; + uint32_t location; + bool initialized; + char output_desc[VOCS_MAX_DESC_SIZE]; + struct bt_vocs_cb *cb; + + struct bt_gatt_service *service_p; +}; + +struct bt_vocs { + union { + struct vocs_server srv; + struct vocs_client cli; + }; +}; + +int bt_vocs_client_state_get(struct bt_conn *conn, struct bt_vocs *inst); +int bt_vocs_client_state_set(struct bt_conn *conn, struct bt_vocs *inst, int16_t offset); +int bt_vocs_client_location_get(struct bt_conn *conn, struct bt_vocs *inst); +int bt_vocs_client_location_set(struct bt_conn *conn, struct bt_vocs *inst, uint32_t location); +int bt_vocs_client_description_get(struct bt_conn *conn, struct bt_vocs *inst); +int bt_vocs_client_description_set(struct bt_conn *conn, struct bt_vocs *inst, + const char *description); + +#endif /* ZEPHYR_INCLUDE_BLUETOOTH_AUDIO_VOCS_INTERNAL_ */ diff --git a/tests/bluetooth/shell/prj.conf b/tests/bluetooth/shell/prj.conf index 32c7877ae6034..8f934ce24b6af 100644 --- a/tests/bluetooth/shell/prj.conf +++ b/tests/bluetooth/shell/prj.conf @@ -50,3 +50,5 @@ CONFIG_BT_AUTO_PHY_UPDATE=y CONFIG_BT_AUDIO=y CONFIG_BT_AUDIO_UNICAST=y CONFIG_BT_AUDIO_BROADCAST=y +CONFIG_BT_VOCS_MAX_INSTANCE_COUNT=1 +CONFIG_BT_VOCS_CLIENT_MAX_INSTANCE_COUNT=1