diff --git a/tests/bluetooth/tester/CMakeLists.txt b/tests/bluetooth/tester/CMakeLists.txt index 24b3353b5134..6ec469dc0fdb 100644 --- a/tests/bluetooth/tester/CMakeLists.txt +++ b/tests/bluetooth/tester/CMakeLists.txt @@ -63,3 +63,7 @@ endif() if(CONFIG_BT_MCC OR CONFIG_BT_MCS) target_sources(app PRIVATE src/btp_mcp.c) endif() + +if(CONFIG_BT_HAS) + target_sources(app PRIVATE src/btp_hap.c) +endif() diff --git a/tests/bluetooth/tester/overlay-le-audio.conf b/tests/bluetooth/tester/overlay-le-audio.conf index 3536a2712da7..330ea3c6b997 100644 --- a/tests/bluetooth/tester/overlay-le-audio.conf +++ b/tests/bluetooth/tester/overlay-le-audio.conf @@ -90,6 +90,8 @@ CONFIG_BT_HAS=y CONFIG_BT_HAS_PRESET_COUNT=6 CONFIG_BT_HAS_PRESET_NAME_DYNAMIC=y +CONFIG_BT_HAS_CLIENT=y + # CSIS CONFIG_BT_CSIP_SET_MEMBER=y diff --git a/tests/bluetooth/tester/src/btp/btp.h b/tests/bluetooth/tester/src/btp/btp.h index a88118e895dd..e9c6f5ce65fe 100644 --- a/tests/bluetooth/tester/src/btp/btp.h +++ b/tests/bluetooth/tester/src/btp/btp.h @@ -32,6 +32,7 @@ #include "btp_cas.h" #include "btp_mcp.h" #include "btp_mcs.h" +#include "btp_hap.h" #define BTP_MTU 1024 #define BTP_DATA_MAX_SIZE (BTP_MTU - sizeof(struct btp_hdr)) @@ -63,8 +64,9 @@ #define BTP_SERVICE_ID_CAS 21 #define BTP_SERVICE_ID_MCP 22 #define BTP_SERVICE_ID_GMCS 23 +#define BTP_SERVICE_ID_HAP 24 -#define BTP_SERVICE_ID_MAX BTP_SERVICE_ID_GMCS +#define BTP_SERVICE_ID_MAX BTP_SERVICE_ID_HAP #define BTP_STATUS_SUCCESS 0x00 #define BTP_STATUS_FAILED 0x01 diff --git a/tests/bluetooth/tester/src/btp/btp_hap.h b/tests/bluetooth/tester/src/btp/btp_hap.h new file mode 100644 index 000000000000..a304a6b87a6f --- /dev/null +++ b/tests/bluetooth/tester/src/btp/btp_hap.h @@ -0,0 +1,61 @@ +/* btp_hap.h - Bluetooth tester headers */ + +/* + * Copyright (c) 2023 Codecoup + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include + +/* HAP commands */ +#define BTP_HAP_READ_SUPPORTED_COMMANDS 0x01 +struct btp_hap_read_supported_commands_rp { + uint8_t data[0]; +} __packed; + +#define BTP_HAP_HA_OPT_PRESETS_SYNC 0x01 +#define BTP_HAP_HA_OPT_PRESETS_INDEPENDENT 0x02 +#define BTP_HAP_HA_OPT_PRESETS_DYNAMIC 0x04 +#define BTP_HAP_HA_OPT_PRESETS_WRITABLE 0x08 + +#define BTP_HAP_HA_INIT 0x02 +struct btp_hap_ha_init_cmd { + uint8_t type; + uint16_t opts; +} __packed; + +#define BTP_HAP_HARC_INIT 0x03 +#define BTP_HAP_HAUC_INIT 0x04 +#define BTP_HAP_IAC_INIT 0x05 + +#define BTP_HAP_IAC_DISCOVER 0x06 +struct btp_hap_iac_discover_cmd { + bt_addr_le_t address; +} __packed; + +#define BTP_HAP_IAC_SET_ALERT 0x07 +struct btp_hap_iac_set_alert_cmd { + bt_addr_le_t address; + uint8_t alert; +} __packed; + +#define BTP_HAP_HAUC_DISCOVER 0x08 +struct btp_hap_hauc_discover_cmd { + bt_addr_le_t address; +} __packed; + +/* HAP events */ +#define BT_HAP_EV_IAC_DISCOVERY_COMPLETE 0x80 +struct btp_hap_iac_discovery_complete_ev { + bt_addr_le_t address; + uint8_t status; +} __packed; + +#define BT_HAP_EV_HAUC_DISCOVERY_COMPLETE 0x81 +struct btp_hap_hauc_discovery_complete_ev { + bt_addr_le_t address; + uint8_t status; + uint16_t has_hearing_aid_features_handle; + uint16_t has_control_point_handle; + uint16_t has_active_preset_index_handle; +} __packed; diff --git a/tests/bluetooth/tester/src/btp/bttester.h b/tests/bluetooth/tester/src/btp/bttester.h index 9c545cd4a46a..c4e6397bebb0 100644 --- a/tests/bluetooth/tester/src/btp/bttester.h +++ b/tests/bluetooth/tester/src/btp/bttester.h @@ -120,3 +120,6 @@ uint8_t tester_unregister_mcp(void); uint8_t tester_init_mcs(void); uint8_t tester_unregister_mcs(void); + +uint8_t tester_init_hap(void); +uint8_t tester_unregister_hap(void); diff --git a/tests/bluetooth/tester/src/btp_core.c b/tests/bluetooth/tester/src/btp_core.c index 757d8816c8e2..b76bd8841057 100644 --- a/tests/bluetooth/tester/src/btp_core.c +++ b/tests/bluetooth/tester/src/btp_core.c @@ -98,6 +98,9 @@ static uint8_t supported_services(const void *cmd, uint16_t cmd_len, #if defined(CONFIG_BT_MCS) tester_set_bit(rp->data, BTP_SERVICE_ID_GMCS); #endif /* CONFIG_BT_MCS */ +#if defined(CONFIG_BT_HAS) + tester_set_bit(rp->data, BTP_SERVICE_ID_HAP); +#endif /* CONFIG_BT_HAS */ *rsp_len = sizeof(*rp) + 2; @@ -211,6 +214,11 @@ static uint8_t register_service(const void *cmd, uint16_t cmd_len, status = tester_init_mcs(); break; #endif /* CONFIG_BT_MCS */ +#if defined(CONFIG_BT_HAS) + case BTP_SERVICE_ID_HAP: + status = tester_init_hap(); + break; +#endif /* CONFIG_BT_HAS */ default: LOG_WRN("unknown id: 0x%02x", cp->id); status = BTP_STATUS_FAILED; @@ -328,6 +336,11 @@ static uint8_t unregister_service(const void *cmd, uint16_t cmd_len, status = tester_unregister_mcs(); break; #endif /* CONFIG_BT_MCS */ +#if defined(CONFIG_BT_HAS) + case BTP_SERVICE_ID_HAP: + status = tester_unregister_hap(); + break; +#endif /* CONFIG_BT_HAS */ default: LOG_WRN("unknown id: 0x%x", cp->id); status = BTP_STATUS_FAILED; diff --git a/tests/bluetooth/tester/src/btp_hap.c b/tests/bluetooth/tester/src/btp_hap.c new file mode 100644 index 000000000000..88dba7e8fbbb --- /dev/null +++ b/tests/bluetooth/tester/src/btp_hap.c @@ -0,0 +1,277 @@ +/* btp_hap.c - Bluetooth HAP Tester */ + +/* + * Copyright (c) 2023 Codecoup + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include + +#include "../bluetooth/audio/has_internal.h" +#include "btp/btp.h" + +#include +LOG_MODULE_REGISTER(bttester_hap, CONFIG_BTTESTER_LOG_LEVEL); + +static uint8_t read_supported_commands(const void *cmd, uint16_t cmd_len, void *rsp, + uint16_t *rsp_len) +{ + struct btp_hap_read_supported_commands_rp *rp = rsp; + + tester_set_bit(rp->data, BTP_HAP_READ_SUPPORTED_COMMANDS); + tester_set_bit(rp->data, BTP_HAP_HA_INIT); + tester_set_bit(rp->data, BTP_HAP_HAUC_INIT); + tester_set_bit(rp->data, BTP_HAP_IAC_INIT); + tester_set_bit(rp->data, BTP_HAP_IAC_DISCOVER); + tester_set_bit(rp->data, BTP_HAP_IAC_SET_ALERT); + tester_set_bit(rp->data, BTP_HAP_HAUC_DISCOVER); + + *rsp_len = sizeof(*rp) + 1; + + return BTP_STATUS_SUCCESS; +} + +static uint8_t ha_init(const void *cmd, uint16_t cmd_len, void *rsp, uint16_t *rsp_len) +{ + const struct btp_hap_ha_init_cmd *cp = cmd; + struct bt_has_features_param params; + const uint16_t opts = sys_le16_to_cpu(cp->opts); + const bool presets_sync = (opts & BTP_HAP_HA_OPT_PRESETS_SYNC) > 0; + const bool presets_independent = (opts & BTP_HAP_HA_OPT_PRESETS_INDEPENDENT) > 0; + const bool presets_writable = (opts & BTP_HAP_HA_OPT_PRESETS_WRITABLE) > 0; + const bool presets_dynamic = (opts & BTP_HAP_HA_OPT_PRESETS_DYNAMIC) > 0; + int err; + + if (!IS_ENABLED(CONFIG_BT_HAS_PRESET_SUPPORT) && + (presets_sync || presets_independent || presets_writable || presets_dynamic)) { + return BTP_STATUS_VAL(-ENOTSUP); + } + + /* Only dynamic presets are supported */ + if (!presets_dynamic) { + return BTP_STATUS_VAL(-ENOTSUP); + } + + /* Preset name writable support mismatch */ + if (presets_writable != IS_ENABLED(CONFIG_BT_HAS_PRESET_NAME_DYNAMIC)) { + return BTP_STATUS_VAL(-ENOTSUP); + } + + params.type = cp->type; + params.preset_sync_support = presets_sync; + params.independent_presets = presets_independent; + + if (cp->type == BT_HAS_HEARING_AID_TYPE_BANDED) { + err = bt_pacs_set_location(BT_AUDIO_DIR_SINK, BT_AUDIO_LOCATION_FRONT_LEFT | + BT_AUDIO_LOCATION_FRONT_RIGHT); + } else { + err = bt_pacs_set_location(BT_AUDIO_DIR_SINK, BT_AUDIO_LOCATION_FRONT_LEFT); + } + + if (err != 0) { + return BTP_STATUS_VAL(err); + } + + err = bt_has_register(¶ms); + if (err != 0) { + return BTP_STATUS_VAL(err); + } + + return BTP_STATUS_SUCCESS; +} + +static void has_client_discover_cb(struct bt_conn *conn, int err, struct bt_has *has, + enum bt_has_hearing_aid_type type, enum bt_has_capabilities caps) +{ + struct btp_hap_hauc_discovery_complete_ev ev = { 0 }; + + LOG_DBG("conn %p err %d", (void *)conn, err); + + bt_addr_le_copy(&ev.address, bt_conn_get_dst(conn)); + ev.status = BTP_STATUS_VAL(err); + + if (err != 0 && err != BT_ATT_ERR_ATTRIBUTE_NOT_FOUND) { + LOG_DBG("Client discovery failed: %d", err); + } else { + struct bt_has_client *inst = CONTAINER_OF(has, struct bt_has_client, has); + + ev.has_hearing_aid_features_handle = inst->features_subscription.value_handle; + ev.has_control_point_handle = inst->control_point_subscription.value_handle; + ev.has_active_preset_index_handle = inst->active_index_subscription.value_handle; + } + + tester_event(BTP_SERVICE_ID_HAP, BT_HAP_EV_HAUC_DISCOVERY_COMPLETE, &ev, sizeof(ev)); +} + +static void has_client_preset_switch_cb(struct bt_has *has, int err, uint8_t index) +{ + +} + +static const struct bt_has_client_cb has_client_cb = { + .discover = has_client_discover_cb, + .preset_switch = has_client_preset_switch_cb, +}; + +static uint8_t hauc_init(const void *cmd, uint16_t cmd_len, void *rsp, uint16_t *rsp_len) +{ + int err; + + err = bt_has_client_cb_register(&has_client_cb); + if (err != 0) { + LOG_DBG("Failed to register client callbacks: %d", err); + return BTP_STATUS_FAILED; + } + + return BTP_STATUS_SUCCESS; +} + +static void ias_client_discover_cb(struct bt_conn *conn, int err) +{ + struct btp_hap_iac_discovery_complete_ev ev; + struct bt_conn_info info; + + bt_conn_get_info(conn, &info); + + bt_addr_le_copy(&ev.address, info.le.dst); + if (err < 0) { + ev.status = BT_ATT_ERR_UNLIKELY; + } else { + ev.status = (uint8_t)err; + } + + tester_event(BTP_SERVICE_ID_HAP, BT_HAP_EV_IAC_DISCOVERY_COMPLETE, &ev, sizeof(ev)); +} + +static const struct bt_ias_client_cb ias_client_cb = { + .discover = ias_client_discover_cb, +}; + +static uint8_t iac_init(const void *cmd, uint16_t cmd_len, void *rsp, uint16_t *rsp_len) +{ + int err; + + err = bt_ias_client_cb_register(&ias_client_cb); + if (err != 0) { + return BTP_STATUS_VAL(err); + } + + return BTP_STATUS_SUCCESS; +} + +static uint8_t iac_discover(const void *cmd, uint16_t cmd_len, void *rsp, uint16_t *rsp_len) +{ + const struct btp_hap_iac_discover_cmd *cp = cmd; + struct bt_conn *conn; + int err; + + conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, &cp->address); + if (!conn) { + LOG_ERR("Unknown connection"); + return BTP_STATUS_FAILED; + } + + err = bt_ias_discover(conn); + + bt_conn_unref(conn); + + return BTP_STATUS_VAL(err); +} + +static uint8_t iac_set_alert(const void *cmd, uint16_t cmd_len, void *rsp, uint16_t *rsp_len) +{ + const struct btp_hap_iac_set_alert_cmd *cp = cmd; + struct bt_conn *conn; + int err; + + conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, &cp->address); + if (!conn) { + LOG_ERR("Unknown connection"); + return BTP_STATUS_FAILED; + } + + err = bt_ias_client_alert_write(conn, (enum bt_ias_alert_lvl)cp->alert); + + bt_conn_unref(conn); + + return BTP_STATUS_VAL(err); +} + +static uint8_t hauc_discover(const void *cmd, uint16_t cmd_len, void *rsp, uint16_t *rsp_len) +{ + const struct btp_hap_hauc_discover_cmd *cp = cmd; + struct bt_conn *conn; + int err; + + conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, &cp->address); + if (!conn) { + LOG_ERR("Unknown connection"); + return BTP_STATUS_FAILED; + } + + err = bt_has_client_discover(conn); + if (err != 0) { + LOG_DBG("Failed to discover remote HAS: %d", err); + } + + bt_conn_unref(conn); + + return BTP_STATUS_VAL(err); +} + +static const struct btp_handler hap_handlers[] = { + { + .opcode = BTP_HAP_READ_SUPPORTED_COMMANDS, + .index = BTP_INDEX_NONE, + .expect_len = 0, + .func = read_supported_commands, + }, + { + .opcode = BTP_HAP_HA_INIT, + .expect_len = sizeof(struct btp_hap_ha_init_cmd), + .func = ha_init, + }, + { + .opcode = BTP_HAP_HAUC_INIT, + .expect_len = 0, + .func = hauc_init, + }, + { + .opcode = BTP_HAP_IAC_INIT, + .expect_len = 0, + .func = iac_init, + }, + { + .opcode = BTP_HAP_IAC_DISCOVER, + .expect_len = sizeof(struct btp_hap_iac_discover_cmd), + .func = iac_discover, + }, + { + .opcode = BTP_HAP_IAC_SET_ALERT, + .expect_len = sizeof(struct btp_hap_iac_set_alert_cmd), + .func = iac_set_alert, + }, + { + .opcode = BTP_HAP_HAUC_DISCOVER, + .expect_len = sizeof(struct btp_hap_hauc_discover_cmd), + .func = hauc_discover, + }, +}; + +uint8_t tester_init_hap(void) +{ + tester_register_command_handlers(BTP_SERVICE_ID_HAP, hap_handlers, + ARRAY_SIZE(hap_handlers)); + + return BTP_STATUS_SUCCESS; +} + +uint8_t tester_unregister_hap(void) +{ + return BTP_STATUS_SUCCESS; +}