diff --git a/include/bluetooth/audio/aics.h b/include/bluetooth/audio/aics.h new file mode 100644 index 0000000000000..b410200a415cd --- /dev/null +++ b/include/bluetooth/audio/aics.h @@ -0,0 +1,451 @@ +/* + * Copyright (c) 2020 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_AICS_H_ +#define ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_AICS_H_ + +/** + * @brief Audio Input Control Service (AICS) + * + * @defgroup bt_gatt_aics Audio Input Control Service (AICS) + * + * @ingroup bluetooth + * @{ + * + * The Audio Input Control Service is a secondary service, and as such should not be used on 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 audio input 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 +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** Audio Input Control Service mute states */ +#define BT_AICS_STATE_UNMUTED 0x00 +#define BT_AICS_STATE_MUTED 0x01 +#define BT_AICS_STATE_MUTE_DISABLED 0x02 + +/** Audio Input Control Service input modes */ +#define BT_AICS_MODE_MANUAL_ONLY 0x00 +#define BT_AICS_MODE_AUTO_ONLY 0x01 +#define BT_AICS_MODE_MANUAL 0x02 +#define BT_AICS_MODE_AUTO 0x03 + +/** Audio Input Control Service input types */ +#define BT_AICS_INPUT_TYPE_UNSPECIFIED 0x00 +#define BT_AICS_INPUT_TYPE_BLUETOOTH 0x01 +#define BT_AICS_INPUT_TYPE_MICROPHONE 0x02 +#define BT_AICS_INPUT_TYPE_ANALOG 0x03 +#define BT_AICS_INPUT_TYPE_DIGITAL 0x04 +#define BT_AICS_INPUT_TYPE_RADIO 0x05 +#define BT_AICS_INPUT_TYPE_STREAMING 0x06 + +/** Audio Input Control Service Error codes */ +#define BT_AICS_ERR_INVALID_COUNTER 0x80 +#define BT_AICS_ERR_OP_NOT_SUPPORTED 0x81 +#define BT_AICS_ERR_MUTE_DISABLED 0x82 +#define BT_AICS_ERR_OUT_OF_RANGE 0x83 +#define BT_AICS_ERR_GAIN_MODE_NOT_ALLOWED 0x84 + +/** @brief Opaque Audio Input Control Service instance. */ +struct bt_aics; + +/** @brief Structure for initializing a Audio Input Control Service instance. */ +struct bt_aics_register_param { + /** Initial audio input gain (-128 to 127) */ + int8_t gain; + + /** Initial audio input mute state */ + uint8_t mute; + + /** Initial audio input mode */ + uint8_t gain_mode; + + /** Initial audio input gain units (N * 0.1 dB) */ + uint8_t units; + + /** Initial audio input minimum gain */ + int8_t min_gain; + + /** Initial audio input maximum gain */ + int8_t max_gain; + + /** Initial audio input type */ + uint8_t type; + + /** Initial audio input status (active/inactive) */ + bool status; + + /** Boolean to set whether the description is writable by clients */ + bool desc_writable; + + /** Initial audio input description */ + char *description; + + /** Pointer to the callback structure. */ + struct bt_aics_cb *cb; +}; + +/** @brief Structure for discovering a Audio Input Control Service instance. */ +struct bt_aics_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 instance of Audio Input Control Service from the pool. + * + * @return Audio Input Control Service instance in case of success or NULL in case of error. + */ +struct bt_aics *bt_aics_free_instance_get(void); + +/** + * @brief Get the service declaration attribute. + * + * The first service attribute returned can be included in any other GATT service. + * + * @param aics Audio Input Control Service instance. + * + * @return Pointer to the attributes of the service. + */ +void *bt_aics_svc_decl_get(struct bt_aics *aics); + +/** + * @brief Initialize the Audio Input Control Service instance. + * + * @param aics Audio Input Control Service instance. + * @param init Audio Input Control Service initialization structure. + * + * @return 0 if success, ERRNO on failure. + */ +int bt_aics_register(struct bt_aics *aics, struct bt_aics_register_param *init); + +/** + * @brief Callback function for writes. + * + * @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_aics_write_cb)(struct bt_conn *conn, struct bt_aics *inst, + int err); + +/** + * @brief Callback function for the input 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 gain The gain setting value. + * @param mute The mute value. + * @param mode The mode value. + */ +typedef void (*bt_aics_state_cb)(struct bt_conn *conn, struct bt_aics *inst, + int err, int8_t gain, uint8_t mute, + uint8_t mode); + +/** + * @brief Callback function for the gain settings. + * + * 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 units The value that reflect the size of a single increment or decrement of the + * Gain Setting value in 0.1 decibel units. + * @param minimum The minimum gain allowed for the gain setting. + * @param maximum The maximum gain allowed for the gain setting. + */ +typedef void (*bt_aics_gain_setting_cb)(struct bt_conn *conn, + struct bt_aics *inst, int err, + uint8_t units, int8_t minimum, + int8_t maximum); + +/** + * @brief Callback function for the input type. + * + * 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 type The input type. + */ +typedef void (*bt_aics_type_cb)(struct bt_conn *conn, struct bt_aics *inst, + int err, uint8_t type); + +/** + * @brief Callback function for the input status. + * + * 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 active Whether the instance is active or inactive. + */ +typedef void (*bt_aics_status_cb)(struct bt_conn *conn, struct bt_aics *inst, + int err, bool active); + +/** + * @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 (may have been clipped). + */ +typedef void (*bt_aics_description_cb)(struct bt_conn *conn, + struct bt_aics *inst, int err, + char *description); + +/** + * @brief Callback function for bt_aics_discover. + * + * This callback will usually be overwritten by the primary service that + * includes the Audio Input Control 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_aics_discover_cb)(struct bt_conn *conn, struct bt_aics *inst, + int err); + +struct bt_aics_cb { + bt_aics_state_cb state; + bt_aics_gain_setting_cb gain_setting; + bt_aics_type_cb type; + bt_aics_status_cb status; + bt_aics_description_cb description; + +#if defined(CONFIG_BT_AICS_CLIENT) + bt_aics_discover_cb discover; + bt_aics_write_cb set_gain; + bt_aics_write_cb unmute; + bt_aics_write_cb mute; + bt_aics_write_cb set_manual_mode; + bt_aics_write_cb set_auto_mode; +#endif /* CONFIG_BT_AICS_CLIENT */ +}; + + +/** + * @brief Discover a Audio Input Control Service. + * + * Attempts to discover a Audio Input Control Service on a server given the + * @p param. + * + * @param conn Connection to the peer with the Audio Input Control Service. + * @param inst The instance pointer. + * @param param Pointer to the parameters. + * + * @return 0 on success, ERRNO on fail. + */ +int bt_aics_discover(struct bt_conn *conn, struct bt_aics *inst, + const struct bt_aics_discover_param *param); + +/** + * @brief Deactivates a Audio Input Control Service instance. + * + * Audio Input Control Services are activated by default, but this will allow + * the server to deactivate an Audio Input Control Service. + * + * @param inst The instance pointer. + * + * @return 0 if success, ERRNO on failure. + */ +int bt_aics_deactivate(struct bt_aics *inst); + +/** + * @brief Activates a Audio Input Control Service instance. + * + * Audio Input Control Services are activated by default, but this will allow + * the server reactivate a Audio Input Control Service instance after it has + * been deactivated with @ref bt_aics_deactivate. + * + * @param inst The instance pointer. + * + * @return 0 if success, ERRNO on failure. + */ +int bt_aics_activate(struct bt_aics *inst); + +/** + * @brief Read the Audio Input Control Service input state. + * + * @param conn Connection to peer device, or NULL to read local server value. + * @param inst The instance pointer. + * + * @return 0 on success, GATT error value on fail. + */ +int bt_aics_state_get(struct bt_conn *conn, struct bt_aics *inst); + +/** + * @brief Read the Audio Input Control Service gain settings. + * + * @param conn Connection to peer device, or NULL to read local server value. + * @param inst The instance pointer. + * + * @return 0 on success, GATT error value on fail. + */ +int bt_aics_gain_setting_get(struct bt_conn *conn, struct bt_aics *inst); + +/** + * @brief Read the Audio Input Control Service input type. + * + * @param conn Connection to peer device, or NULL to read local server value. + * @param inst The instance pointer. + * + * @return 0 on success, GATT error value on fail. + */ +int bt_aics_type_get(struct bt_conn *conn, struct bt_aics *inst); + +/** + * @brief Read the Audio Input Control Service input status. + * + * @param conn Connection to peer device, or NULL to read local server value. + * @param inst The instance pointer. + * + * @return 0 on success, GATT error value on fail. + */ +int bt_aics_status_get(struct bt_conn *conn, struct bt_aics *inst); + +/** + * @brief Unmute the Audio Input Control Service input. + * + * @param conn Connection to peer device, or NULL to read local server value. + * @param inst The instance pointer. + * + * @return 0 on success, GATT error value on fail. + */ +int bt_aics_unmute(struct bt_conn *conn, struct bt_aics *inst); + +/** + * @brief Mute the Audio Input Control Service input. + * + * @param conn Connection to peer device, or NULL to read local server value. + * @param inst The instance pointer. + * + * @return 0 on success, GATT error value on fail. + */ +int bt_aics_mute(struct bt_conn *conn, struct bt_aics *inst); + +/** + * @brief Set input gain to manual. + * + * @param conn Connection to peer device, or NULL to set local server value. + * @param inst The instance pointer. + * + * @return 0 on success, GATT error value on fail. + */ +int bt_aics_manual_gain_set(struct bt_conn *conn, struct bt_aics *inst); + +/** + * @brief Set the input gain to automatic. + * + * @param conn Connection to peer device, or NULL to set local server value. + * @param inst The instance pointer. + * + * @return 0 on success, GATT error value on fail. + */ +int bt_aics_automatic_gain_set(struct bt_conn *conn, struct bt_aics *inst); + +/** + * @brief Set the input gain. + * + * @param conn Connection to peer device, or NULL to set local server value. + * @param inst The instance pointer. + * @param gain The gain to set (-128 to 127) in gain setting units + * (see @ref bt_aics_gain_setting_cb). + * + * @return 0 on success, GATT error value on fail. + */ +int bt_aics_gain_set(struct bt_conn *conn, struct bt_aics *inst, int8_t gain); + +/** + * @brief Read the Audio Input Control Service description. + * + * @param conn Connection to peer device, or NULL to read local server value. + * @param inst The instance pointer. + * + * @return 0 on success, GATT error value on fail. + */ +int bt_aics_description_get(struct bt_conn *conn, struct bt_aics *inst); + +/** + * @brief Set the Audio Input Control Service description. + * + * @param conn Connection to peer device, or NULL to set local server value. + * @param inst The instance pointer. + * @param description The description as an UTF-8 encoded string. + * + * @return 0 on success, GATT error value on fail. + */ +int bt_aics_description_set(struct bt_conn *conn, struct bt_aics *inst, + const char *description); + +/** + * @brief Get a new Audio Input Control Service client instance. + * + * @return Pointer to the instance, or NULL if no free instances are left. + */ +struct bt_aics *bt_aics_client_free_instance_get(void); + +/** + * @brief Registers the callbacks for the Audio Input Control Service client. + * + * @param inst The instance pointer. + * @param cb Pointer to the callback structure. + */ +void bt_aics_client_cb_register(struct bt_aics *inst, struct bt_aics_cb *cb); + +#ifdef __cplusplus +} +#endif + +/** + * @} + */ + +#endif /* ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_AICS_H_ */ diff --git a/include/bluetooth/uuid.h b/include/bluetooth/uuid.h index 1efa4d6a19d98..1fc33f7067797 100644 --- a/include/bluetooth/uuid.h +++ b/include/bluetooth/uuid.h @@ -410,6 +410,15 @@ struct bt_uuid_128 { */ #define BT_UUID_MESH_PROXY \ BT_UUID_DECLARE_16(BT_UUID_MESH_PROXY_VAL) +/** @def BT_UUID_AICS_VAL + * @brief Audio Input Control Service value + */ +#define BT_UUID_AICS_VAL 0x1843 +/** @def BT_UUID_AICS + * @brief Audio Input Control Service + */ +#define BT_UUID_AICS \ + BT_UUID_DECLARE_16(BT_UUID_AICS_VAL) /** @def BT_UUID_VOCS_VAL * @brief Volume Offset Control Service value */ @@ -1313,6 +1322,60 @@ struct bt_uuid_128 { #define BT_UUID_GATT_SERVER_FEATURES \ BT_UUID_DECLARE_16(BT_UUID_GATT_SERVER_FEATURES_VAL) +/** @def BT_UUID_AICS_STATE_VAL + * @brief Audio Input Control Service State value + */ +#define BT_UUID_AICS_STATE_VAL 0x2B77 +/** @def BT_UUID_AICS_STATE + * @brief Audio Input Control Service State + */ +#define BT_UUID_AICS_STATE \ + BT_UUID_DECLARE_16(BT_UUID_AICS_STATE_VAL) +/** @def BT_UUID_AICS_GAIN_SETTINGS_VAL + * @brief Audio Input Control Service Gain Settings Properties value + */ +#define BT_UUID_AICS_GAIN_SETTINGS_VAL 0x2B78 +/** @def BT_UUID_AICS_GAIN_SETTINGS + * @brief Audio Input Control Service Gain Settings Properties + */ +#define BT_UUID_AICS_GAIN_SETTINGS \ + BT_UUID_DECLARE_16(BT_UUID_AICS_GAIN_SETTINGS_VAL) +/** @def BT_UUID_AICS_INPUT_TYPE_VAL + * @brief Audio Input Control Service Input Type value + */ +#define BT_UUID_AICS_INPUT_TYPE_VAL 0x2B79 +/** @def BT_UUID_AICS_INPUT_TYPE + * @brief Audio Input Control Service Input Type + */ +#define BT_UUID_AICS_INPUT_TYPE \ + BT_UUID_DECLARE_16(BT_UUID_AICS_INPUT_TYPE_VAL) +/** @def BT_UUID_AICS_INPUT_STATUS_VAL + * @brief Audio Input Control Service Input Status value + */ +#define BT_UUID_AICS_INPUT_STATUS_VAL 0x2B7A +/** @def BT_UUID_AICS_INPUT_STATUS + * @brief Audio Input Control Service Input Status + */ +#define BT_UUID_AICS_INPUT_STATUS \ + BT_UUID_DECLARE_16(BT_UUID_AICS_INPUT_STATUS_VAL) +/** @def BT_UUID_AICS_CONTROL_VAL + * @brief Audio Input Control Service Control Point value + */ +#define BT_UUID_AICS_CONTROL_VAL 0x2B7B +/** @def BT_UUID_AICS_CONTROL + * @brief Audio Input Control Service Control Point + */ +#define BT_UUID_AICS_CONTROL \ + BT_UUID_DECLARE_16(BT_UUID_AICS_CONTROL_VAL) +/** @def BT_UUID_AICS_DESCRIPTION_VAL + * @brief Audio Input Control Service Input Description value + */ +#define BT_UUID_AICS_DESCRIPTION_VAL 0x2B7C +/** @def BT_UUID_AICS_DESCRIPTION + * @brief Audio Input Control Service Input Description + */ +#define BT_UUID_AICS_DESCRIPTION \ + BT_UUID_DECLARE_16(BT_UUID_AICS_DESCRIPTION_VAL) /** @def BT_UUID_VOCS_STATE_VAL * @brief Volume Offset State value */ @@ -1349,7 +1412,6 @@ struct bt_uuid_128 { */ #define BT_UUID_VOCS_DESCRIPTION \ BT_UUID_DECLARE_16(BT_UUID_VOCS_DESCRIPTION_VAL) - /* * Protocol UUIDs */ diff --git a/subsys/bluetooth/audio/CMakeLists.txt b/subsys/bluetooth/audio/CMakeLists.txt index dfc3475eeb95a..7fe02c7576018 100644 --- a/subsys/bluetooth/audio/CMakeLists.txt +++ b/subsys/bluetooth/audio/CMakeLists.txt @@ -4,3 +4,8 @@ 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) + +if (CONFIG_BT_AICS OR CONFIG_BT_AICS_CLIENT) + zephyr_library_sources(aics.c) +endif() +zephyr_library_sources_ifdef(CONFIG_BT_AICS_CLIENT aics_client.c) diff --git a/subsys/bluetooth/audio/Kconfig b/subsys/bluetooth/audio/Kconfig index 2324d2c369435..a114ad0742a54 100644 --- a/subsys/bluetooth/audio/Kconfig +++ b/subsys/bluetooth/audio/Kconfig @@ -50,7 +50,7 @@ config BT_AUDIO_DEBUG Use this option to enable debug logs for the Bluetooth Audio functionality. - source "subsys/bluetooth/audio/Kconfig.vocs" +source "subsys/bluetooth/audio/Kconfig.aics" endif # BT_AUDIO diff --git a/subsys/bluetooth/audio/Kconfig.aics b/subsys/bluetooth/audio/Kconfig.aics new file mode 100644 index 0000000000000..00495029420f3 --- /dev/null +++ b/subsys/bluetooth/audio/Kconfig.aics @@ -0,0 +1,76 @@ +# Bluetooth Audio - Audio Input Control Service options +# +# Copyright (c) 2020 Bose Corporation +# Copyright (c) 2020 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +if BT_AUDIO + +##################### Audio Input Control Service ##################### + +config BT_AICS_MAX_INSTANCE_COUNT + int "Audio Input Control Service max instance count" + default 0 + range 0 15 + help + This option sets the maximum number of instances of Audio Input + Control Services. + +config BT_AICS + bool # hidden + default y if BT_AICS_MAX_INSTANCE_COUNT > 0 + help + This hidden option enables support for Audio Input Control Service. + +if BT_AICS + +config BT_AICS_MAX_INPUT_DESCRIPTION_SIZE + int "Audio Input Control Service max input description size" + default 32 + range 0 512 + help + This option sets the maximum input description size in octets. +############# DEBUG ############# + +config BT_DEBUG_AICS + bool "Audio Input Control Service debug" + depends on BT_AUDIO_DEBUG + help + Use this option to enable Audio Input Control Service debug logs for + the Bluetooth Audio functionality. + +endif # BT_AICS + +##################### Audio Input Control Service Client ##################### + +config BT_AICS_CLIENT_MAX_INSTANCE_COUNT + int "Audio Input Control Service client max instance count" + default 0 + range 0 15 + help + This option sets the maximum number of instances of Audio Input + Control Services. + +config BT_AICS_CLIENT + bool # hidden + default y if BT_AICS_CLIENT_MAX_INSTANCE_COUNT > 0 + help + This hidden option enables support for Audio Input Control Service. + + +if BT_AICS_CLIENT + +############# DEBUG ############# + +config BT_DEBUG_AICS_CLIENT + bool "Audio Input Control Service client debug" + depends on BT_AUDIO_DEBUG + help + Use this option to enable Audio Input Control Service client debug + logs for the Bluetooth Audio functionality. + +endif # BT_AICS_CLIENT + +endif # BT_AUDIO diff --git a/subsys/bluetooth/audio/aics.c b/subsys/bluetooth/audio/aics.c new file mode 100644 index 0000000000000..7ed12d67afd41 --- /dev/null +++ b/subsys/bluetooth/audio/aics.c @@ -0,0 +1,814 @@ +/* + * Copyright (c) 2020 Bose Corporation + * Copyright (c) 2020 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include "aics_internal.h" + +#define BT_DBG_ENABLED IS_ENABLED(CONFIG_BT_DEBUG_AICS) +#define LOG_MODULE_NAME bt_aics +#include "common/log.h" + +#define VALID_AICS_OPCODE(opcode) \ + ((opcode) >= BT_AICS_OPCODE_SET_GAIN && (opcode) <= BT_AICS_OPCODE_SET_AUTO) + +#define AICS_CP_LEN 0x02 +#define AICS_CP_SET_GAIN_LEN 0x03 + +static void aics_state_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value); +static ssize_t read_aics_state(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset); +static ssize_t read_aics_gain_settings(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + void *buf, uint16_t len, + uint16_t offset); +static ssize_t read_type(struct bt_conn *conn, const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset); +static void aics_input_status_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value); +static ssize_t read_input_status(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset); +static ssize_t write_aics_control(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, + uint16_t offset, uint8_t flags); +static void aics_description_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value); +static ssize_t write_description(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, + uint8_t flags); +static ssize_t read_description(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset); + +#if defined(CONFIG_BT_AICS) +#define BT_AICS_SERVICE_DEFINITION(_aics) { \ + BT_GATT_SECONDARY_SERVICE(BT_UUID_AICS), \ + BT_GATT_CHARACTERISTIC(BT_UUID_AICS_STATE, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT, \ + read_aics_state, NULL, &_aics), \ + BT_GATT_CCC(aics_state_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + BT_GATT_CHARACTERISTIC(BT_UUID_AICS_GAIN_SETTINGS, \ + BT_GATT_CHRC_READ, \ + BT_GATT_PERM_READ_ENCRYPT, \ + read_aics_gain_settings, NULL, &_aics), \ + BT_GATT_CHARACTERISTIC(BT_UUID_AICS_INPUT_TYPE, \ + BT_GATT_CHRC_READ, \ + BT_GATT_PERM_READ_ENCRYPT, \ + read_type, NULL, &_aics), \ + BT_GATT_CHARACTERISTIC(BT_UUID_AICS_INPUT_STATUS, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT, \ + read_input_status, NULL, &_aics), \ + BT_GATT_CCC(aics_input_status_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), \ + BT_GATT_CHARACTERISTIC(BT_UUID_AICS_CONTROL, \ + BT_GATT_CHRC_WRITE, \ + BT_GATT_PERM_WRITE_ENCRYPT, \ + NULL, write_aics_control, &_aics), \ + BT_GATT_CHARACTERISTIC(BT_UUID_AICS_DESCRIPTION, \ + BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_READ_ENCRYPT, \ + read_description, NULL, &_aics), \ + BT_GATT_CCC(aics_description_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT) \ + } + + +static struct bt_aics_server aics_insts[CONFIG_BT_AICS_MAX_INSTANCE_COUNT]; +static uint32_t instance_cnt; +BT_GATT_SERVICE_INSTANCE_DEFINE(aics_service_list, aics_insts, + CONFIG_BT_AICS_MAX_INSTANCE_COUNT, + BT_AICS_SERVICE_DEFINITION); + +static void aics_state_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +static ssize_t read_aics_state(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + struct bt_aics_server *inst = attr->user_data; + + BT_DBG("gain %d, mute %u, gain_mode %u, counter %u", + inst->state.gain, inst->state.mute, + inst->state.gain_mode, inst->state.change_counter); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &inst->state, + sizeof(inst->state)); +} + +static ssize_t read_aics_gain_settings(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) +{ + struct bt_aics_server *inst = attr->user_data; + + BT_DBG("units %u, min %d, max %d", + inst->gain_settings.units, inst->gain_settings.minimum, + inst->gain_settings.maximum); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, + &inst->gain_settings, + sizeof(inst->gain_settings)); +} + +static ssize_t read_type(struct bt_conn *conn, const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) +{ + struct bt_aics_server *inst = attr->user_data; + + BT_DBG("%u", inst->type); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &inst->type, + sizeof(inst->type)); +} + +static void aics_input_status_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} + +static ssize_t read_input_status(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + struct bt_aics_server *inst = attr->user_data; + + BT_DBG("%u", inst->status); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &inst->status, + sizeof(inst->status)); +} + +#endif /* CONFIG_BT_AICS */ + +static ssize_t write_aics_control(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, + uint16_t offset, uint8_t flags) +{ + struct bt_aics_server *inst = attr->user_data; + const struct bt_aics_gain_control *cp = buf; + bool notify = false; + + if (offset) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + + if (!len || !buf) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + /* Check opcode before length */ + if (!VALID_AICS_OPCODE(cp->cp.opcode)) { + BT_DBG("Invalid opcode %u", cp->cp.opcode); + return BT_GATT_ERR(BT_AICS_ERR_OP_NOT_SUPPORTED); + } + + if ((len < AICS_CP_LEN) || + (len == AICS_CP_SET_GAIN_LEN && cp->cp.opcode != BT_AICS_OPCODE_SET_GAIN) || + (len > AICS_CP_SET_GAIN_LEN)) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + BT_DBG("Opcode %u, counter %u", cp->cp.opcode, cp->cp.counter); + if (cp->cp.counter != inst->state.change_counter) { + return BT_GATT_ERR(BT_AICS_ERR_INVALID_COUNTER); + } + + switch (cp->cp.opcode) { + case BT_AICS_OPCODE_SET_GAIN: + BT_DBG("Set gain %d", cp->gain_setting); + if (cp->gain_setting < inst->gain_settings.minimum || + cp->gain_setting > inst->gain_settings.maximum) { + return BT_GATT_ERR(BT_AICS_ERR_OUT_OF_RANGE); + } + if (BT_AICS_INPUT_MODE_SETTABLE(inst->state.gain_mode) && + inst->state.gain != cp->gain_setting) { + inst->state.gain = cp->gain_setting; + notify = true; + } + break; + case BT_AICS_OPCODE_UNMUTE: + BT_DBG("Unmute"); + if (inst->state.mute == BT_AICS_STATE_MUTE_DISABLED) { + return BT_GATT_ERR(BT_AICS_ERR_MUTE_DISABLED); + } + if (inst->state.mute != BT_AICS_STATE_UNMUTED) { + inst->state.mute = BT_AICS_STATE_UNMUTED; + notify = true; + } + break; + case BT_AICS_OPCODE_MUTE: + BT_DBG("Mute"); + if (inst->state.mute == BT_AICS_STATE_MUTE_DISABLED) { + return BT_GATT_ERR(BT_AICS_ERR_MUTE_DISABLED); + } + if (inst->state.mute != BT_AICS_STATE_MUTED) { + inst->state.mute = BT_AICS_STATE_MUTED; + notify = true; + } + break; + case BT_AICS_OPCODE_SET_MANUAL: + BT_DBG("Set manual mode"); + if (BT_AICS_INPUT_MODE_IMMUTABLE(inst->state.gain_mode)) { + return BT_GATT_ERR(BT_AICS_ERR_GAIN_MODE_NOT_ALLOWED); + } + if (inst->state.gain_mode != BT_AICS_MODE_MANUAL) { + inst->state.gain_mode = BT_AICS_MODE_MANUAL; + notify = true; + } + break; + case BT_AICS_OPCODE_SET_AUTO: + BT_DBG("Set automatic mode"); + if (BT_AICS_INPUT_MODE_IMMUTABLE(inst->state.gain_mode)) { + return BT_GATT_ERR(BT_AICS_ERR_GAIN_MODE_NOT_ALLOWED); + } + if (inst->state.gain_mode != BT_AICS_MODE_AUTO) { + inst->state.gain_mode = BT_AICS_MODE_AUTO; + notify = true; + } + break; + default: + return BT_GATT_ERR(BT_AICS_ERR_OP_NOT_SUPPORTED); + } + + if (notify) { + inst->state.change_counter++; + + BT_DBG("New state: gain %d, mute %u, gain_mode %u, counter %u", + inst->state.gain, inst->state.mute, + inst->state.gain_mode, inst->state.change_counter); + + bt_gatt_notify_uuid(NULL, BT_UUID_AICS_STATE, + inst->service_p->attrs, &inst->state, + sizeof(inst->state)); + + if (inst->cb && inst->cb->state) { + inst->cb->state(NULL, (struct bt_aics *)inst, 0, + inst->state.gain, inst->state.mute, + inst->state.gain_mode); + } else { + BT_DBG("Callback not registered for instance %p", inst); + } + } + + return len; +} + +#if defined(CONFIG_BT_AICS) +static void aics_description_cfg_changed(const struct bt_gatt_attr *attr, + uint16_t value) +{ + BT_DBG("value 0x%04x", value); +} +#endif /* CONFIG_BT_AICS */ + +static ssize_t write_description(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, + uint8_t flags) +{ + struct bt_aics_server *inst = attr->user_data; + + if (len >= sizeof(inst->description)) { + BT_DBG("Output desc was clipped from length %u to %zu", + len, sizeof(inst->description) - 1); + /* We just clip the string value if it's too long */ + len = (uint16_t)sizeof(inst->description) - 1; + } + + if (memcmp(buf, inst->description, len)) { + memcpy(inst->description, buf, len); + inst->description[len] = '\0'; + + bt_gatt_notify_uuid(NULL, BT_UUID_AICS_DESCRIPTION, + inst->service_p->attrs, &inst->description, + strlen(inst->description)); + + if (inst->cb && inst->cb->description) { + inst->cb->description(NULL, (struct bt_aics *)inst, 0, + inst->description); + } else { + BT_DBG("Callback not registered for instance %p", inst); + } + } + + BT_DBG("%s", log_strdup(inst->description)); + + return len; +} + +#if defined(CONFIG_BT_AICS) +static ssize_t read_description(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + struct bt_aics_server *inst = attr->user_data; + + BT_DBG("%s", log_strdup(inst->description)); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, + &inst->description, strlen(inst->description)); +} + +/************************ PUBLIC API ************************/ +void *bt_aics_svc_decl_get(struct bt_aics *aics) +{ + CHECKIF(!aics) { + BT_DBG("NULL instance"); + return NULL; + } + + return aics->srv.service_p->attrs; +} + +static void prepare_aics_instances(void) +{ + for (int i = 0; i < ARRAY_SIZE(aics_insts); i++) { + aics_insts[i].service_p = &aics_service_list[i]; + } +} + +int bt_aics_register(struct bt_aics *aics, struct bt_aics_register_param *param) +{ + int err; + static bool instance_prepared; + + CHECKIF(!aics) { + BT_DBG("NULL aics pointer"); + return -ENOTCONN; + } + + CHECKIF(!param) { + BT_DBG("NULL param"); + return -EINVAL; + } + + if (!instance_prepared) { + prepare_aics_instances(); + instance_prepared = true; + } + + CHECKIF(aics->srv.initialized) { + return -EALREADY; + } + + CHECKIF(param->mute > BT_AICS_STATE_MUTE_DISABLED) { + BT_DBG("Invalid AICS mute value: %u", param->mute); + return -EINVAL; + } + + CHECKIF(param->gain_mode > BT_AICS_MODE_AUTO) { + BT_DBG("Invalid AICS mode value: %u", param->gain_mode); + return -EINVAL; + } + + CHECKIF(param->type > BT_AICS_INPUT_TYPE_STREAMING) { + BT_DBG("Invalid AICS input type value: %u", param->type); + return -EINVAL; + } + + CHECKIF(param->units == 0) { + BT_DBG("AICS units value shall not be 0"); + return -EINVAL; + } + + CHECKIF(!(param->min_gain <= param->max_gain)) { + BT_DBG("AICS min gain (%d) shall be lower than or equal to max gain (%d)", + param->min_gain, param->max_gain); + return -EINVAL; + } + + CHECKIF(param->gain < param->min_gain || param->gain > param->max_gain) { + BT_DBG("AICS gain (%d) shall be not lower than min gain (%d) " + "or higher than max gain (%d)", + param->gain, param->min_gain, param->max_gain); + return -EINVAL; + } + + aics->srv.state.gain = param->gain; + aics->srv.state.mute = param->mute; + aics->srv.state.gain_mode = param->gain_mode; + aics->srv.gain_settings.units = param->units; + aics->srv.gain_settings.minimum = param->min_gain; + aics->srv.gain_settings.maximum = param->max_gain; + aics->srv.type = param->type; + aics->srv.status = param->status ? BT_AICS_STATUS_ACTIVE : BT_AICS_STATUS_INACTIVE; + aics->srv.cb = param->cb; + + if (param->description) { + strncpy(aics->srv.description, param->description, + sizeof(aics->srv.description) - 1); + /* strncpy may not always null-terminate */ + aics->srv.description[sizeof(aics->srv.description) - 1] = '\0'; + if (IS_ENABLED(CONFIG_BT_DEBUG_AICS) && + strcmp(aics->srv.description, param->description)) { + BT_DBG("Input desc clipped to %s", + log_strdup(aics->srv.description)); + } + } + + /* Iterate over the attributes in AICS (starting from i = 1 to skip the + * service declaration) to find the BT_UUID_AICS_DESCRIPTION and update + * the characteristic value (at [i]), update that 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. + */ + if (param->desc_writable) { + for (int i = 1; i < aics->srv.service_p->attr_count; i++) { + struct bt_gatt_attr *attr; + + attr = &aics->srv.service_p->attrs[i]; + + if (!bt_uuid_cmp(attr->uuid, BT_UUID_AICS_DESCRIPTION)) { + /* Update attr and chrc to be writable */ + struct bt_gatt_chrc *chrc; + + chrc = aics->srv.service_p->attrs[i - 1].user_data; + attr->write = write_description; + attr->perm |= BT_GATT_PERM_WRITE_ENCRYPT; + chrc->properties |= BT_GATT_CHRC_WRITE_WITHOUT_RESP; + + break; + } + } + } + + err = bt_gatt_service_register(aics->srv.service_p); + if (err) { + BT_DBG("Could not register AICS service"); + return err; + } + + aics->srv.initialized = true; + + return 0; +} + +struct bt_aics *bt_aics_free_instance_get(void) +{ + if (instance_cnt >= CONFIG_BT_AICS_MAX_INSTANCE_COUNT) { + return NULL; + } + + return (struct bt_aics *)&aics_insts[instance_cnt++]; +} + +/****************************** PUBLIC API ******************************/ +int bt_aics_deactivate(struct bt_aics *inst) +{ + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (inst->srv.status == BT_AICS_STATUS_ACTIVE) { + inst->srv.status = BT_AICS_STATUS_INACTIVE; + BT_DBG("Instance %p: Status was set to inactive", inst); + + bt_gatt_notify_uuid(NULL, BT_UUID_AICS_INPUT_STATUS, + inst->srv.service_p->attrs, + &inst->srv.status, + sizeof(inst->srv.status)); + + if (inst->srv.cb && inst->srv.cb->status) { + inst->srv.cb->status(NULL, inst, 0, inst->srv.status); + } else { + BT_DBG("Callback not registered for instance %p", inst); + } + } + + return 0; +} + +int bt_aics_activate(struct bt_aics *inst) +{ + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (inst->srv.status == BT_AICS_STATUS_INACTIVE) { + inst->srv.status = BT_AICS_STATUS_ACTIVE; + BT_DBG("Instance %p: Status was set to active", inst); + + bt_gatt_notify_uuid(NULL, BT_UUID_AICS_INPUT_STATUS, + inst->srv.service_p->attrs, + &inst->srv.status, + sizeof(inst->srv.status)); + + if (inst->srv.cb && inst->srv.cb->status) { + inst->srv.cb->status(NULL, inst, 0, inst->srv.status); + } else { + BT_DBG("Callback not registered for instance %p", inst); + } + } + + return 0; +} + +#endif /* CONFIG_BT_AICS */ + +int bt_aics_state_get(struct bt_conn *conn, struct bt_aics *inst) +{ + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (IS_ENABLED(CONFIG_BT_AICS_CLIENT) && conn) { + return bt_aics_client_state_get(conn, inst); + } else if (IS_ENABLED(CONFIG_BT_AICS) && !conn) { + if (inst->srv.cb && inst->srv.cb->state) { + inst->srv.cb->state(NULL, inst, 0, inst->srv.state.gain, + inst->srv.state.mute, + inst->srv.state.gain_mode); + } else { + BT_DBG("Callback not registered for instance %p", inst); + } + return 0; + } + + return -ENOTSUP; +} + +int bt_aics_gain_setting_get(struct bt_conn *conn, struct bt_aics *inst) +{ + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (IS_ENABLED(CONFIG_BT_AICS_CLIENT) && conn) { + return bt_aics_client_gain_setting_get(conn, inst); + } else if (IS_ENABLED(CONFIG_BT_AICS) && !conn) { + if (inst->srv.cb && inst->srv.cb->gain_setting) { + inst->srv.cb->gain_setting(NULL, inst, 0, + inst->srv.gain_settings.units, + inst->srv.gain_settings.minimum, + inst->srv.gain_settings.maximum); + } else { + BT_DBG("Callback not registered for instance %p", inst); + } + return 0; + } + + return -ENOTSUP; +} + +int bt_aics_type_get(struct bt_conn *conn, struct bt_aics *inst) +{ + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (IS_ENABLED(CONFIG_BT_AICS_CLIENT) && conn) { + return bt_aics_client_type_get(conn, inst); + } else if (IS_ENABLED(CONFIG_BT_AICS) && !conn) { + if (inst->srv.cb && inst->srv.cb->type) { + inst->srv.cb->type(NULL, inst, 0, inst->srv.type); + } else { + BT_DBG("Callback not registered for instance %p", inst); + } + return 0; + } + + return -ENOTSUP; +} + +int bt_aics_status_get(struct bt_conn *conn, struct bt_aics *inst) +{ + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (IS_ENABLED(CONFIG_BT_AICS_CLIENT) && conn) { + return bt_aics_client_status_get(conn, inst); + } else if (IS_ENABLED(CONFIG_BT_AICS) && !conn) { + if (inst->srv.cb && inst->srv.cb->status) { + inst->srv.cb->status(NULL, inst, 0, inst->srv.status); + } else { + BT_DBG("Callback not registered for instance %p", inst); + } + return 0; + } + + return -ENOTSUP; +} + +int bt_aics_unmute(struct bt_conn *conn, struct bt_aics *inst) +{ + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (IS_ENABLED(CONFIG_BT_AICS_CLIENT) && conn) { + return bt_aics_client_unmute(conn, inst); + } else if (IS_ENABLED(CONFIG_BT_AICS) && !conn) { + struct bt_gatt_attr attr; + struct bt_aics_control cp; + int err; + + cp.opcode = BT_AICS_OPCODE_UNMUTE; + cp.counter = inst->srv.state.change_counter; + + attr.user_data = inst; + + err = write_aics_control(NULL, &attr, &cp, sizeof(cp), 0, 0); + + return err > 0 ? 0 : err; + } + + return -ENOTSUP; +} + +int bt_aics_mute(struct bt_conn *conn, struct bt_aics *inst) +{ + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (IS_ENABLED(CONFIG_BT_AICS_CLIENT) && conn) { + return bt_aics_client_mute(conn, inst); + } else if (IS_ENABLED(CONFIG_BT_AICS) && !conn) { + struct bt_gatt_attr attr; + struct bt_aics_control cp; + int err; + + cp.opcode = BT_AICS_OPCODE_MUTE; + cp.counter = inst->srv.state.change_counter; + + attr.user_data = inst; + + err = write_aics_control(NULL, &attr, &cp, sizeof(cp), 0, 0); + + return err > 0 ? 0 : err; + } + + return -ENOTSUP; +} + +int bt_aics_manual_gain_set(struct bt_conn *conn, struct bt_aics *inst) +{ + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (IS_ENABLED(CONFIG_BT_AICS_CLIENT) && conn) { + return bt_aics_client_manual_gain_set(conn, inst); + } else if (IS_ENABLED(CONFIG_BT_AICS) && !conn) { + struct bt_gatt_attr attr; + struct bt_aics_control cp; + int err; + + cp.opcode = BT_AICS_OPCODE_SET_MANUAL; + cp.counter = inst->srv.state.change_counter; + + attr.user_data = inst; + + err = write_aics_control(NULL, &attr, &cp, sizeof(cp), 0, 0); + + return err > 0 ? 0 : err; + } + + return -ENOTSUP; +} + +int bt_aics_automatic_gain_set(struct bt_conn *conn, struct bt_aics *inst) +{ + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (IS_ENABLED(CONFIG_BT_AICS_CLIENT) && conn) { + return bt_aics_client_automatic_gain_set(conn, inst); + } else if (IS_ENABLED(CONFIG_BT_AICS) && !conn) { + struct bt_gatt_attr attr; + struct bt_aics_control cp; + int err; + + cp.opcode = BT_AICS_OPCODE_SET_AUTO; + cp.counter = inst->srv.state.change_counter; + + attr.user_data = inst; + + err = write_aics_control(NULL, &attr, &cp, sizeof(cp), 0, 0); + + return err > 0 ? 0 : err; + } + + return -ENOTSUP; +} + +int bt_aics_gain_set(struct bt_conn *conn, struct bt_aics *inst, int8_t gain) +{ + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (IS_ENABLED(CONFIG_BT_AICS_CLIENT) && conn) { + return bt_aics_client_gain_set(conn, inst, gain); + } else if (IS_ENABLED(CONFIG_BT_AICS) && !conn) { + struct bt_gatt_attr attr; + struct bt_aics_gain_control cp; + int err; + + cp.cp.opcode = BT_AICS_OPCODE_SET_GAIN; + cp.cp.counter = inst->srv.state.change_counter; + cp.gain_setting = gain; + + attr.user_data = inst; + + err = write_aics_control(NULL, &attr, &cp, sizeof(cp), 0, 0); + + return err > 0 ? 0 : err; + } + + return -ENOTSUP; +} + +int bt_aics_description_get(struct bt_conn *conn, struct bt_aics *inst) +{ + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (IS_ENABLED(CONFIG_BT_AICS_CLIENT) && conn) { + return bt_aics_client_description_get(conn, inst); + } else if (IS_ENABLED(CONFIG_BT_AICS) && !conn) { + if (inst->srv.cb && inst->srv.cb->description) { + inst->srv.cb->description(NULL, inst, 0, + inst->srv.description); + } else { + BT_DBG("Callback not registered for instance %p", inst); + } + return 0; + } + + return -ENOTSUP; +} + +int bt_aics_description_set(struct bt_conn *conn, struct bt_aics *inst, + const char *description) +{ + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + CHECKIF(!description) { + BT_DBG("NULL description"); + return -EINVAL; + } + + if (IS_ENABLED(CONFIG_BT_AICS_CLIENT) && conn) { + return bt_aics_client_description_set(conn, inst, description); + } else if (IS_ENABLED(CONFIG_BT_AICS) && !conn) { + struct bt_gatt_attr attr; + int err; + + attr.user_data = inst; + + err = write_description(NULL, &attr, description, + strlen(description), 0, 0); + + return err > 0 ? 0 : err; + } + + return -ENOTSUP; +} diff --git a/subsys/bluetooth/audio/aics_client.c b/subsys/bluetooth/audio/aics_client.c new file mode 100644 index 0000000000000..3cfe4114064ba --- /dev/null +++ b/subsys/bluetooth/audio/aics_client.c @@ -0,0 +1,939 @@ +/* Bluetooth AICS client */ + +/* + * Copyright (c) 2020 Bose Corporation + * Copyright (c) 2020 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include + +#include "aics_internal.h" + +#define BT_DBG_ENABLED IS_ENABLED(CONFIG_BT_DEBUG_AICS_CLIENT) +#define LOG_MODULE_NAME bt_aics_client +#include "common/log.h" + +static struct bt_aics aics_insts[CONFIG_BT_MAX_CONN * CONFIG_BT_AICS_CLIENT_MAX_INSTANCE_COUNT]; + +static int aics_client_common_control(struct bt_conn *conn, uint8_t opcode, struct bt_aics *inst); + +static struct bt_aics *lookup_aics_by_handle(struct bt_conn *conn, uint16_t handle) +{ + for (int i = 0; i < ARRAY_SIZE(aics_insts); i++) { + if (aics_insts[i].cli.conn == conn && + aics_insts[i].cli.active && + aics_insts[i].cli.start_handle <= handle && + aics_insts[i].cli.end_handle >= handle) { + return &aics_insts[i]; + } + } + + BT_DBG("Could not find AICS instance with handle 0x%04x", handle); + + return NULL; +} + +uint8_t aics_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_aics *inst = lookup_aics_by_handle(conn, handle); + struct bt_aics_state *state; + uint8_t *status; + + 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(*state)) { + state = (struct bt_aics_state *)data; + BT_DBG("Inst %p: Gain %d, mute %u, gain_mode %u, counter %u", + inst, state->gain, state->mute, state->gain_mode, + state->change_counter); + + inst->cli.change_counter = state->change_counter; + + if (inst->cli.cb && inst->cli.cb->state) { + inst->cli.cb->state(conn, inst, 0, state->gain, + state->mute, state->gain_mode); + } + } + } else if (handle == inst->cli.status_handle) { + if (length == sizeof(*status)) { + status = (uint8_t *)data; + BT_DBG("Inst %p: Status %u", inst, *status); + if (inst->cli.cb && inst->cli.cb->status) { + inst->cli.cb->status(conn, inst, 0, *status); + } + } + } 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 */ + if (length > sizeof(desc) - 1) { + BT_DBG("Description truncated from %u to %zu octets", + length, sizeof(desc) - 1); + } + length = MIN(sizeof(desc) - 1, length); + + memcpy(desc, data, length); + desc[length] = '\0'; + BT_DBG("Inst %p: Input description: %s", inst, log_strdup(desc)); + if (inst->cli.cb && inst->cli.cb->description) { + inst->cli.cb->description(conn, inst, 0, desc); + } + } + + return BT_GATT_ITER_CONTINUE; +} + +static uint8_t aics_client_read_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_aics *inst = lookup_aics_by_handle(conn, params->single.handle); + struct bt_aics_state *state = (struct bt_aics_state *)data; + + 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 (cb_err) { + BT_DBG("State read failed: %d", err); + if (inst->cli.cb && inst->cli.cb->state) { + inst->cli.cb->state(conn, inst, cb_err, 0, 0, 0); + } + return BT_GATT_ITER_STOP; + } + + if (data) { + if (length == sizeof(*state)) { + BT_DBG("Gain %d, mute %u, gain_mode %u, counter %u", + state->gain, state->mute, state->gain_mode, + state->change_counter); + + inst->cli.change_counter = state->change_counter; + } else { + BT_DBG("Invalid length %u (expected %zu)", + length, sizeof(*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, state->gain, + state->mute, state->gain_mode); + } + + return BT_GATT_ITER_STOP; +} + +static uint8_t aics_client_read_gain_settings_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_aics *inst = lookup_aics_by_handle(conn, params->single.handle); + struct bt_aics_gain_settings *gain_settings = (struct bt_aics_gain_settings *)data; + + 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 (cb_err) { + BT_DBG("Gain settings read failed: %d", err); + if (inst->cli.cb && inst->cli.cb->gain_setting) { + inst->cli.cb->gain_setting(conn, inst, cb_err, 0, 0, 0); + } + return BT_GATT_ITER_STOP; + } + + if (data) { + if (length == sizeof(*gain_settings)) { + BT_DBG("Units %u, Max %d, Min %d", gain_settings->units, + gain_settings->maximum, gain_settings->minimum); + } else { + BT_DBG("Invalid length %u (expected %zu)", length, sizeof(*gain_settings)); + cb_err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; + } + } else { + BT_DBG("Invalid gain settings"); + cb_err = BT_ATT_ERR_UNLIKELY; + } + + if (inst->cli.cb && inst->cli.cb->gain_setting) { + inst->cli.cb->gain_setting(conn, inst, cb_err, gain_settings->units, + gain_settings->minimum, gain_settings->maximum); + } + + return BT_GATT_ITER_STOP; +} + +static uint8_t aics_client_read_type_cb(struct bt_conn *conn, uint8_t err, + struct bt_gatt_read_params *params, + const void *data, uint16_t length) +{ + int cb_err = err; + uint8_t *type = (uint8_t *)data; + struct bt_aics *inst = lookup_aics_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 (cb_err) { + BT_DBG("Type read failed: %d", err); + if (inst->cli.cb && inst->cli.cb->type) { + inst->cli.cb->type(conn, inst, cb_err, 0); + } + return BT_GATT_ITER_STOP; + } + + if (data) { + if (length == sizeof(*type)) { + BT_DBG("Type %u", *type); + } else { + BT_DBG("Invalid length %u (expected %zu)", length, sizeof(*type)); + cb_err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; + } + } else { + BT_DBG("Invalid type"); + cb_err = BT_ATT_ERR_UNLIKELY; + } + + if (inst->cli.cb && inst->cli.cb->type) { + inst->cli.cb->type(conn, inst, cb_err, *type); + } + + return BT_GATT_ITER_STOP; +} + +static uint8_t aics_client_read_status_cb(struct bt_conn *conn, uint8_t err, + struct bt_gatt_read_params *params, + const void *data, uint16_t length) +{ + int cb_err = err; + uint8_t *status = (uint8_t *)data; + struct bt_aics *inst = lookup_aics_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 (cb_err) { + BT_DBG("Status read failed: %d", err); + if (inst->cli.cb && inst->cli.cb->status) { + inst->cli.cb->status(conn, inst, cb_err, 0); + } + return BT_GATT_ITER_STOP; + } + + if (data) { + if (length == sizeof(*status)) { + BT_DBG("Status %u", *status); + } else { + BT_DBG("Invalid length %u (expected %zu)", length, sizeof(*status)); + cb_err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; + } + } else { + BT_DBG("Invalid status"); + cb_err = BT_ATT_ERR_UNLIKELY; + } + + if (inst->cli.cb && inst->cli.cb->status) { + inst->cli.cb->status(conn, inst, cb_err, *status); + } + + return BT_GATT_ITER_STOP; +} + +static void aics_cp_notify_app(struct bt_conn *conn, struct bt_aics *inst, uint8_t err) +{ + if (!inst->cli.cb) { + return; + } + + switch (inst->cli.cp_val.cp.opcode) { + case BT_AICS_OPCODE_SET_GAIN: + if (inst->cli.cb->set_gain) { + inst->cli.cb->set_gain(conn, inst, err); + } + break; + case BT_AICS_OPCODE_UNMUTE: + if (inst->cli.cb->unmute) { + inst->cli.cb->unmute(conn, inst, err); + } + break; + case BT_AICS_OPCODE_MUTE: + if (inst->cli.cb->mute) { + inst->cli.cb->mute(conn, inst, err); + } + break; + case BT_AICS_OPCODE_SET_MANUAL: + if (inst->cli.cb->set_manual_mode) { + inst->cli.cb->set_manual_mode(conn, inst, err); + } + break; + case BT_AICS_OPCODE_SET_AUTO: + if (inst->cli.cb->set_auto_mode) { + inst->cli.cb->set_auto_mode(conn, inst, err); + } + break; + default: + BT_DBG("Unknown opcode 0x%02x", inst->cli.cp_val.cp.opcode); + break; + } +} + +static uint8_t internal_read_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_aics *inst = lookup_aics_by_handle(conn, params->single.handle); + struct bt_aics_state *state = (struct bt_aics_state *)data; + + memset(params, 0, sizeof(*params)); + + if (!inst) { + BT_ERR("Instance not found"); + return BT_GATT_ITER_STOP; + } + + if (err) { + BT_WARN("State read failed: %d", err); + } else if (data) { + if (length == sizeof(*state)) { + int write_err; + + BT_DBG("Gain %d, mute %u, gain_mode %u, counter %u", + state->gain, state->mute, + state->gain_mode, state->change_counter); + inst->cli.change_counter = state->change_counter; + + /* clear busy flag to reuse function */ + inst->cli.busy = false; + + if (inst->cli.cp_val.cp.opcode == BT_AICS_OPCODE_SET_GAIN) { + write_err = bt_aics_client_gain_set(conn, inst, + inst->cli.cp_val.gain_setting); + } else { + write_err = aics_client_common_control(conn, + inst->cli.cp_val.cp.opcode, + inst); + } + + if (write_err) { + cb_err = BT_ATT_ERR_UNLIKELY; + } + } else { + BT_DBG("Invalid length %u (expected %zu)", length, sizeof(*state)); + cb_err = BT_ATT_ERR_UNLIKELY; + } + } + + if (cb_err) { + inst->cli.busy = false; + aics_cp_notify_app(conn, inst, cb_err); + } + + return BT_GATT_ITER_STOP; +} + + +static void aics_client_write_aics_cp_cb(struct bt_conn *conn, uint8_t err, + struct bt_gatt_write_params *params) +{ + int cb_err = err; + struct bt_aics *inst = lookup_aics_by_handle(conn, params->handle); + + memset(params, 0, sizeof(*params)); + + if (!inst) { + BT_DBG("Instance not found"); + return; + } + + BT_DBG("Inst %p: err: %d", inst, cb_err); + + if (cb_err == BT_AICS_ERR_INVALID_COUNTER && inst->cli.cp_retried) { + cb_err = BT_ATT_ERR_UNLIKELY; + } else if (cb_err == BT_AICS_ERR_INVALID_COUNTER && inst->cli.state_handle) { + inst->cli.read_params.func = internal_read_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; + + cb_err = bt_gatt_read(conn, &inst->cli.read_params); + + if (cb_err) { + BT_WARN("Could not read state: %d", cb_err); + } else { + inst->cli.cp_retried = true; + /* Wait for read callback */ + return; + } + } + + inst->cli.busy = false; + inst->cli.cp_retried = false; + + aics_cp_notify_app(conn, inst, cb_err); +} + +static int aics_client_common_control(struct bt_conn *conn, uint8_t opcode, struct bt_aics *inst) +{ + int err; + + if (!conn) { + return -ENOTCONN; + } else if (!inst->cli.control_handle) { + BT_DBG("Handle not set for opcode %u", opcode); + return -EINVAL; + } else if (inst->cli.busy) { + return -EBUSY; + } + + inst->cli.cp_val.cp.opcode = opcode; + inst->cli.cp_val.cp.counter = inst->cli.change_counter; + + inst->cli.write_params.offset = 0; + inst->cli.write_params.data = &inst->cli.cp_val.cp; + inst->cli.write_params.length = sizeof(inst->cli.cp_val.cp); + inst->cli.write_params.handle = inst->cli.control_handle; + inst->cli.write_params.func = aics_client_write_aics_cp_cb; + + err = bt_gatt_write(conn, &inst->cli.write_params); + if (!err) { + inst->cli.busy = true; + } + + return err; +} + + +static uint8_t aics_client_read_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; + char desc[MIN(CONFIG_BT_L2CAP_RX_MTU, BT_ATT_MAX_ATTRIBUTE_LEN) + 1]; + struct bt_aics *inst = lookup_aics_by_handle(conn, params->single.handle); + + memset(params, 0, sizeof(*params)); + + if (!inst) { + BT_DBG("Instance not found"); + return BT_GATT_ITER_STOP; + } + + inst->cli.busy = false; + + if (cb_err) { + BT_DBG("Description read failed: %d", err); + if (inst->cli.cb && inst->cli.cb->description) { + inst->cli.cb->description(conn, inst, cb_err, NULL); + } + return BT_GATT_ITER_STOP; + } + + if (data) { + BT_HEXDUMP_DBG(data, length, "Input description read"); + + /* Truncate if too large */ + if (length > sizeof(desc) - 1) { + BT_DBG("Description truncated from %u to %zu octets", + length, sizeof(desc) - 1); + } + length = MIN(sizeof(desc) - 1, length); + + /* TODO: Handle long reads */ + + memcpy(desc, data, length); + } + + desc[length] = '\0'; + BT_DBG("Input description: %s", log_strdup(desc)); + + 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_aics *inst) +{ + return inst->cli.state_handle && + inst->cli.gain_handle && + inst->cli.type_handle && + inst->cli.status_handle && + inst->cli.control_handle && + inst->cli.desc_handle; +} + +static uint8_t aics_discover_func(struct bt_conn *conn, const struct bt_gatt_attr *attr, + struct bt_gatt_discover_params *params) +{ + struct bt_aics *inst = (struct bt_aics *)CONTAINER_OF( + params, struct bt_aics_client, discover_params); + + if (!attr) { + BT_DBG("Discovery complete for AICS %p", inst); + + memset(params, 0, sizeof(*params)); + + inst->cli.busy = false; + + 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_AICS_STATE)) { + BT_DBG("Audio Input state"); + inst->cli.state_handle = chrc->value_handle; + sub_params = &inst->cli.state_sub_params; + } else if (!bt_uuid_cmp(chrc->uuid, BT_UUID_AICS_GAIN_SETTINGS)) { + BT_DBG("Gain settings"); + inst->cli.gain_handle = chrc->value_handle; + } else if (!bt_uuid_cmp(chrc->uuid, BT_UUID_AICS_INPUT_TYPE)) { + BT_DBG("Input type"); + inst->cli.type_handle = chrc->value_handle; + } else if (!bt_uuid_cmp(chrc->uuid, BT_UUID_AICS_INPUT_STATUS)) { + BT_DBG("Input status"); + inst->cli.status_handle = chrc->value_handle; + sub_params = &inst->cli.status_sub_params; + } else if (!bt_uuid_cmp(chrc->uuid, BT_UUID_AICS_CONTROL)) { + BT_DBG("Control point"); + inst->cli.control_handle = chrc->value_handle; + } else if (!bt_uuid_cmp(chrc->uuid, BT_UUID_AICS_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) { + 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 = aics_client_notify_handler; + bt_gatt_subscribe(conn, sub_params); + } + } + + return BT_GATT_ITER_CONTINUE; +} + +static void aics_client_reset(struct bt_aics *inst, struct bt_conn *conn) +{ + inst->cli.desc_writable = 0; + inst->cli.change_counter = 0; + inst->cli.gain_mode = 0; + inst->cli.start_handle = 0; + inst->cli.end_handle = 0; + inst->cli.state_handle = 0; + inst->cli.gain_handle = 0; + inst->cli.type_handle = 0; + inst->cli.status_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.status_sub_params); + (void)bt_gatt_unsubscribe(conn, &inst->cli.desc_sub_params); +} + + +int bt_aics_discover(struct bt_conn *conn, struct bt_aics *inst, + const struct bt_aics_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; + } + + aics_client_reset(inst, conn); + + (void)memset(&inst->cli.discover_params, 0, sizeof(inst->cli.discover_params)); + 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 = aics_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; +} + +struct bt_aics *bt_aics_client_free_instance_get(void) +{ + for (int i = 0; i < ARRAY_SIZE(aics_insts); i++) { + if (!aics_insts[i].cli.active) { + aics_insts[i].cli.active = true; + return &aics_insts[i]; + } + } + + return NULL; +} + +int bt_aics_client_state_get(struct bt_conn *conn, struct bt_aics *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; + } else if (inst->cli.busy) { + return -EBUSY; + } + + inst->cli.read_params.func = aics_client_read_state_cb; + inst->cli.read_params.handle_count = 1; + inst->cli.read_params.single.handle = inst->cli.state_handle; + + err = bt_gatt_read(conn, &inst->cli.read_params); + if (!err) { + inst->cli.busy = true; + } + + return err; +} + +int bt_aics_client_gain_setting_get(struct bt_conn *conn, struct bt_aics *inst) +{ + int err; + + CHECKIF(!conn) { + BT_DBG("NULL conn"); + return -ENOTCONN; + } + + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (!inst->cli.gain_handle) { + BT_DBG("Handle not set"); + return -EINVAL; + } else if (inst->cli.busy) { + return -EBUSY; + } + + inst->cli.read_params.func = aics_client_read_gain_settings_cb; + inst->cli.read_params.handle_count = 1; + inst->cli.read_params.single.handle = inst->cli.gain_handle; + + err = bt_gatt_read(conn, &inst->cli.read_params); + if (!err) { + inst->cli.busy = true; + } + + return err; +} + +int bt_aics_client_type_get(struct bt_conn *conn, struct bt_aics *inst) +{ + int err; + + CHECKIF(!conn) { + BT_DBG("NULL conn"); + return -ENOTCONN; + } + + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (!inst->cli.type_handle) { + BT_DBG("Handle not set"); + return -EINVAL; + } else if (inst->cli.busy) { + return -EBUSY; + } + + inst->cli.read_params.func = aics_client_read_type_cb; + inst->cli.read_params.handle_count = 1; + inst->cli.read_params.single.handle = inst->cli.type_handle; + + err = bt_gatt_read(conn, &inst->cli.read_params); + if (!err) { + inst->cli.busy = true; + } + + return err; +} + +int bt_aics_client_status_get(struct bt_conn *conn, struct bt_aics *inst) +{ + int err; + + CHECKIF(!conn) { + BT_DBG("NULL conn"); + return -ENOTCONN; + } + + CHECKIF(!inst) { + BT_DBG("NULL instance"); + return -EINVAL; + } + + if (!inst->cli.status_handle) { + BT_DBG("Handle not set"); + return -EINVAL; + } else if (inst->cli.busy) { + return -EBUSY; + } + + inst->cli.read_params.func = aics_client_read_status_cb; + inst->cli.read_params.handle_count = 1; + inst->cli.read_params.single.handle = inst->cli.status_handle; + + err = bt_gatt_read(conn, &inst->cli.read_params); + if (!err) { + inst->cli.busy = true; + } + + return err; +} + +int bt_aics_client_unmute(struct bt_conn *conn, struct bt_aics *inst) +{ + return aics_client_common_control(conn, BT_AICS_OPCODE_UNMUTE, inst); +} + +int bt_aics_client_mute(struct bt_conn *conn, struct bt_aics *inst) +{ + return aics_client_common_control(conn, BT_AICS_OPCODE_MUTE, inst); +} + +int bt_aics_client_manual_gain_set(struct bt_conn *conn, struct bt_aics *inst) +{ + return aics_client_common_control(conn, BT_AICS_OPCODE_SET_MANUAL, inst); +} + +int bt_aics_client_automatic_gain_set(struct bt_conn *conn, struct bt_aics *inst) +{ + return aics_client_common_control(conn, BT_AICS_OPCODE_SET_AUTO, inst); +} + +int bt_aics_client_gain_set(struct bt_conn *conn, struct bt_aics *inst, int8_t gain) +{ + 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_val.cp.opcode = BT_AICS_OPCODE_SET_GAIN; + inst->cli.cp_val.cp.counter = inst->cli.change_counter; + inst->cli.cp_val.gain_setting = gain; + + inst->cli.write_params.data = &inst->cli.cp_val; + inst->cli.write_params.length = sizeof(inst->cli.cp_val); + inst->cli.write_params.handle = inst->cli.control_handle; + inst->cli.write_params.func = aics_client_write_aics_cp_cb; + + err = bt_gatt_write(conn, &inst->cli.write_params); + if (!err) { + inst->cli.busy = true; + } + + return err; +} + +int bt_aics_client_description_get(struct bt_conn *conn, struct bt_aics *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 = aics_client_read_desc_cb; + inst->cli.read_params.handle_count = 1; + inst->cli.read_params.single.handle = inst->cli.desc_handle; + + err = bt_gatt_read(conn, &inst->cli.read_params); + if (!err) { + inst->cli.busy = true; + } + + return err; +} + +int bt_aics_client_description_set(struct bt_conn *conn, struct bt_aics *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); +} + +void bt_aics_client_cb_register(struct bt_aics *inst, struct bt_aics_cb *cb) +{ + CHECKIF(!inst) { + BT_DBG("inst cannot be NULL"); + return; + } + + inst->cli.cb = cb; +} diff --git a/subsys/bluetooth/audio/aics_internal.h b/subsys/bluetooth/audio/aics_internal.h new file mode 100644 index 0000000000000..7190549613d60 --- /dev/null +++ b/subsys/bluetooth/audio/aics_internal.h @@ -0,0 +1,132 @@ +/** @file + * @brief Internal APIs for Bluetooth AICS. + */ + +/* + * Copyright (c) 2020 Bose Corporation + * Copyright (c) 2020 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_BLUETOOTH_AUDIO_AICS_INTERNAL_ +#define ZEPHYR_INCLUDE_BLUETOOTH_AUDIO_AICS_INTERNAL_ +#include +#include + +#if defined(CONFIG_BT_AICS) +#define BT_AICS_MAX_DESC_SIZE CONFIG_BT_AICS_MAX_INPUT_DESCRIPTION_SIZE +#else +#define BT_AICS_MAX_DESC_SIZE 1 +#endif /* CONFIG_BT_AICS */ + +/* AICS opcodes */ +#define BT_AICS_OPCODE_SET_GAIN 0x01 +#define BT_AICS_OPCODE_UNMUTE 0x02 +#define BT_AICS_OPCODE_MUTE 0x03 +#define BT_AICS_OPCODE_SET_MANUAL 0x04 +#define BT_AICS_OPCODE_SET_AUTO 0x05 + +/* AICS status */ +#define BT_AICS_STATUS_INACTIVE 0x00 +#define BT_AICS_STATUS_ACTIVE 0x01 + +#define BT_AICS_INPUT_MODE_IMMUTABLE(gain_mode) \ + ((gain_mode) == BT_AICS_MODE_MANUAL_ONLY || (gain_mode) == BT_AICS_MODE_AUTO_ONLY) + +#define BT_AICS_INPUT_MODE_SETTABLE(gain_mode) \ + ((gain_mode) == BT_AICS_MODE_AUTO || (gain_mode) == BT_AICS_MODE_MANUAL) + +struct bt_aics_control { + uint8_t opcode; + uint8_t counter; +} __packed; + +struct bt_aics_gain_control { + struct bt_aics_control cp; + int8_t gain_setting; +} __packed; + +struct bt_aics_client { + uint8_t change_counter; + uint8_t gain_mode; + bool desc_writable; + bool active; + + uint16_t start_handle; + uint16_t end_handle; + uint16_t state_handle; + uint16_t gain_handle; + uint16_t type_handle; + uint16_t status_handle; + uint16_t control_handle; + uint16_t desc_handle; + struct bt_gatt_subscribe_params state_sub_params; + struct bt_gatt_subscribe_params status_sub_params; + struct bt_gatt_subscribe_params desc_sub_params; + uint8_t subscribe_cnt; + bool cp_retried; + + bool busy; + struct bt_aics_gain_control cp_val; + struct bt_gatt_write_params write_params; + struct bt_gatt_read_params read_params; + struct bt_gatt_discover_params discover_params; + struct bt_aics_cb *cb; + struct bt_conn *conn; +}; + +struct bt_aics_state { + int8_t gain; + uint8_t mute; + uint8_t gain_mode; + uint8_t change_counter; +} __packed; + +struct bt_aics_gain_settings { + uint8_t units; + int8_t minimum; + int8_t maximum; +} __packed; + +struct bt_aics_server { + struct bt_aics_state state; + struct bt_aics_gain_settings gain_settings; + bool initialized; + uint8_t type; + uint8_t status; + struct bt_aics *inst; + char description[BT_AICS_MAX_DESC_SIZE]; + struct bt_aics_cb *cb; + + struct bt_gatt_service *service_p; +}; + +/* Struct used as a common type for the api */ +struct bt_aics { + union { + struct bt_aics_server srv; + struct bt_aics_client cli; + }; + +}; + +uint8_t aics_client_notify_handler(struct bt_conn *conn, + struct bt_gatt_subscribe_params *params, + const void *data, uint16_t length); +int bt_aics_client_register(struct bt_aics *inst); +int bt_aics_client_unregister(struct bt_aics *inst); +int bt_aics_client_state_get(struct bt_conn *conn, struct bt_aics *inst); +int bt_aics_client_gain_setting_get(struct bt_conn *conn, struct bt_aics *inst); +int bt_aics_client_type_get(struct bt_conn *conn, struct bt_aics *inst); +int bt_aics_client_status_get(struct bt_conn *conn, struct bt_aics *inst); +int bt_aics_client_unmute(struct bt_conn *conn, struct bt_aics *inst); +int bt_aics_client_mute(struct bt_conn *conn, struct bt_aics *inst); +int bt_aics_client_manual_gain_set(struct bt_conn *conn, struct bt_aics *inst); +int bt_aics_client_automatic_gain_set(struct bt_conn *conn, struct bt_aics *inst); +int bt_aics_client_gain_set(struct bt_conn *conn, struct bt_aics *inst, int8_t gain); +int bt_aics_client_description_get(struct bt_conn *conn, struct bt_aics *inst); +int bt_aics_client_description_set(struct bt_conn *conn, struct bt_aics *inst, + const char *description); + +#endif /* ZEPHYR_INCLUDE_BLUETOOTH_AUDIO_AICS_INTERNAL_ */ diff --git a/tests/bluetooth/shell/prj.conf b/tests/bluetooth/shell/prj.conf index 8f934ce24b6af..ff877e7103c12 100644 --- a/tests/bluetooth/shell/prj.conf +++ b/tests/bluetooth/shell/prj.conf @@ -50,5 +50,9 @@ 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 + +CONFIG_BT_AICS_MAX_INSTANCE_COUNT=1 +CONFIG_BT_AICS_CLIENT_MAX_INSTANCE_COUNT=1