From baa5683e593953224af384fd821c1731013bb482 Mon Sep 17 00:00:00 2001 From: Nithin Ramesh Myliattil Date: Thu, 8 Aug 2024 17:49:46 +0200 Subject: [PATCH] bluetooth: BAS: add battery level status char to bas service Added the battery level status char to bas service as per bas_1.1 spec. Added BSIM test for BAS service to test the NTF/INDs of BAS characteristics. Signed-off-by: Nithin Ramesh Myliattil --- include/zephyr/bluetooth/services/bas.h | 238 ++++++++ samples/bluetooth/hap_ha/prj.conf | 5 + subsys/bluetooth/services/CMakeLists.txt | 6 +- subsys/bluetooth/services/Kconfig | 4 +- subsys/bluetooth/services/Kconfig.bas | 7 - subsys/bluetooth/services/bas.c | 98 --- subsys/bluetooth/services/bas/CMakeLists.txt | 3 + subsys/bluetooth/services/bas/Kconfig.bas | 30 + subsys/bluetooth/services/bas/bas.c | 134 +++++ subsys/bluetooth/services/bas/bas_bls.c | 236 ++++++++ subsys/bluetooth/services/bas/bas_internal.h | 146 +++++ .../samples/battery_service/CMakeLists.txt | 29 + .../samples/battery_service/prj.conf | 14 + .../battery_service/src/central_test.c | 562 ++++++++++++++++++ .../samples/battery_service/src/main.c | 22 + .../battery_service/src/peripheral_test.c | 183 ++++++ .../battery_service/tests_scripts/bas.sh | 29 + tests/bsim/bluetooth/samples/compile.sh | 5 + .../tests.nrf5340bsim_nrf5340_cpuapp.txt | 2 +- 19 files changed, 1643 insertions(+), 110 deletions(-) delete mode 100644 subsys/bluetooth/services/Kconfig.bas delete mode 100644 subsys/bluetooth/services/bas.c create mode 100644 subsys/bluetooth/services/bas/CMakeLists.txt create mode 100644 subsys/bluetooth/services/bas/Kconfig.bas create mode 100644 subsys/bluetooth/services/bas/bas.c create mode 100644 subsys/bluetooth/services/bas/bas_bls.c create mode 100644 subsys/bluetooth/services/bas/bas_internal.h create mode 100644 tests/bsim/bluetooth/samples/battery_service/CMakeLists.txt create mode 100644 tests/bsim/bluetooth/samples/battery_service/prj.conf create mode 100644 tests/bsim/bluetooth/samples/battery_service/src/central_test.c create mode 100644 tests/bsim/bluetooth/samples/battery_service/src/main.c create mode 100644 tests/bsim/bluetooth/samples/battery_service/src/peripheral_test.c create mode 100755 tests/bsim/bluetooth/samples/battery_service/tests_scripts/bas.sh diff --git a/include/zephyr/bluetooth/services/bas.h b/include/zephyr/bluetooth/services/bas.h index 59413346401..67932d4b6cc 100644 --- a/include/zephyr/bluetooth/services/bas.h +++ b/include/zephyr/bluetooth/services/bas.h @@ -1,4 +1,5 @@ /* + * Copyright (c) 2024 Demant A/S * Copyright (c) 2018 Nordic Semiconductor ASA * Copyright (c) 2016 Intel Corporation * @@ -19,11 +20,173 @@ */ #include +#include #ifdef __cplusplus extern "C" { #endif +/** + * @brief Battery Level Status Characteristic flags. + * + * Enumeration for the flags indicating the presence + * of various fields in the Battery Level Status characteristic. + */ +enum bt_bas_bls_flags { + /** Bit indicating that the Battery Level Status identifier is present. */ + BT_BAS_BLS_FLAG_IDENTIFIER_PRESENT = BIT(0), + + /** Bit indicating that the Battery Level is present. */ + BT_BAS_BLS_FLAG_BATTERY_LEVEL_PRESENT = BIT(1), + + /** Bit indicating that additional status information is present. */ + BT_BAS_BLS_FLAG_ADDITIONAL_STATUS_PRESENT = BIT(2), +}; + +/** @brief Battery Present Status + * + * Enumeration for the presence of the battery. + */ +enum bt_bas_bls_battery_present { + /** Battery is not present. */ + BT_BAS_BLS_BATTERY_NOT_PRESENT = 0, + + /** Battery is present. */ + BT_BAS_BLS_BATTERY_PRESENT = 1 +}; + +/** @brief Wired External Power Source Status + * + * Enumeration for the status of the wired external power source. + */ +enum bt_bas_bls_wired_power_source { + /** Wired external power source is not connected. */ + BT_BAS_BLS_WIRED_POWER_NOT_CONNECTED = 0, + + /** Wired external power source is connected. */ + BT_BAS_BLS_WIRED_POWER_CONNECTED = 1, + + /** Wired external power source status is unknown. */ + BT_BAS_BLS_WIRED_POWER_UNKNOWN = 2 +}; + +/** @brief Wireless External Power Source Status + * + * Enumeration for the status of the wireless external power source. + */ +enum bt_bas_bls_wireless_power_source { + /** Wireless external power source is not connected. */ + BT_BAS_BLS_WIRELESS_POWER_NOT_CONNECTED = 0, + + /** Wireless external power source is connected. */ + BT_BAS_BLS_WIRELESS_POWER_CONNECTED = 1, + + /** Wireless external power source status is unknown. */ + BT_BAS_BLS_WIRELESS_POWER_UNKNOWN = 2 +}; + +/** @brief Battery Charge State + * + * Enumeration for the charge state of the battery. + */ +enum bt_bas_bls_battery_charge_state { + /** Battery charge state is unknown. */ + BT_BAS_BLS_CHARGE_STATE_UNKNOWN = 0, + + /** Battery is currently charging. */ + BT_BAS_BLS_CHARGE_STATE_CHARGING = 1, + + /** Battery is discharging actively. */ + BT_BAS_BLS_CHARGE_STATE_DISCHARGING_ACTIVE = 2, + + /** Battery is discharging but inactive. */ + BT_BAS_BLS_CHARGE_STATE_DISCHARGING_INACTIVE = 3 +}; + +/** @brief Battery Charge Level + * + * Enumeration for the level of charge in the battery. + */ +enum bt_bas_bls_battery_charge_level { + /** Battery charge level is unknown. */ + BT_BAS_BLS_CHARGE_LEVEL_UNKNOWN = 0, + + /** Battery charge level is good. */ + BT_BAS_BLS_CHARGE_LEVEL_GOOD = 1, + + /** Battery charge level is low. */ + BT_BAS_BLS_CHARGE_LEVEL_LOW = 2, + + /** Battery charge level is critical. */ + BT_BAS_BLS_CHARGE_LEVEL_CRITICAL = 3 +}; + +/** @brief Battery Charge Type + * + * Enumeration for the type of charging applied to the battery. + */ +enum bt_bas_bls_battery_charge_type { + /** Battery charge type is unknown or not charging. */ + BT_BAS_BLS_CHARGE_TYPE_UNKNOWN = 0, + + /** Battery is charged using constant current. */ + BT_BAS_BLS_CHARGE_TYPE_CONSTANT_CURRENT = 1, + + /** Battery is charged using constant voltage. */ + BT_BAS_BLS_CHARGE_TYPE_CONSTANT_VOLTAGE = 2, + + /** Battery is charged using trickle charge. */ + BT_BAS_BLS_CHARGE_TYPE_TRICKLE = 3, + + /** Battery is charged using float charge. */ + BT_BAS_BLS_CHARGE_TYPE_FLOAT = 4 +}; + +/** @brief Charging Fault Reason + * + * Enumeration for the reasons of charging faults. + */ +enum bt_bas_bls_charging_fault_reason { + /** No charging fault. */ + BT_BAS_BLS_FAULT_REASON_NONE = 0, + + /** Charging fault due to battery issue. */ + BT_BAS_BLS_FAULT_REASON_BATTERY = BIT(0), + + /** Charging fault due to external power source issue. */ + BT_BAS_BLS_FAULT_REASON_EXTERNAL_POWER = BIT(1), + + /** Charging fault for other reasons. */ + BT_BAS_BLS_FAULT_REASON_OTHER = BIT(2) +}; + +/** @brief Service Required Status + * + * Enumeration for whether the service is required. + */ +enum bt_bas_bls_service_required { + /** Service is not required. */ + BT_BAS_BLS_SERVICE_REQUIRED_FALSE = 0, + + /** Service is required. */ + BT_BAS_BLS_SERVICE_REQUIRED_TRUE = 1, + + /** Service requirement is unknown. */ + BT_BAS_BLS_SERVICE_REQUIRED_UNKNOWN = 2 +}; + +/** @brief Battery Fault Status + * + * Enumeration for the fault status of the battery. + */ +enum bt_bas_bls_battery_fault { + /** No battery fault. */ + BT_BAS_BLS_BATTERY_FAULT_NO = 0, + + /** Battery fault present. */ + BT_BAS_BLS_BATTERY_FAULT_YES = 1 +}; + /** @brief Read battery level value. * * Read the characteristic value of the battery level @@ -43,6 +206,81 @@ uint8_t bt_bas_get_battery_level(void); */ int bt_bas_set_battery_level(uint8_t level); +/** + * @brief Set the battery present status. + * + * @param present The battery present status to set. + */ +void bt_bas_bls_set_battery_present(enum bt_bas_bls_battery_present present); + +/** + * @brief Set the wired external power source status. + * + * @param source The wired external power source status to set. + */ +void bt_bas_bls_set_wired_external_power_source(enum bt_bas_bls_wired_power_source source); + +/** + * @brief Set the wireless external power source status. + * + * @param source The wireless external power source status to set. + */ +void bt_bas_bls_set_wireless_external_power_source(enum bt_bas_bls_wireless_power_source source); + +/** + * @brief Set the battery charge state. + * + * @param state The battery charge state to set. + */ +void bt_bas_bls_set_battery_charge_state(enum bt_bas_bls_battery_charge_state state); + +/** + * @brief Set the battery charge level. + * + * @param level The battery charge level to set. + */ +void bt_bas_bls_set_battery_charge_level(enum bt_bas_bls_battery_charge_level level); + +/** + * @brief Set the battery charge type. + * + * @param type The battery charge type to set. + */ +void bt_bas_bls_set_battery_charge_type(enum bt_bas_bls_battery_charge_type type); + +/** + * @brief Set the charging fault reason. + * + * @param reason The charging fault reason to set. + */ +void bt_bas_bls_set_charging_fault_reason(enum bt_bas_bls_charging_fault_reason reason); + +/** + * @brief Set the identifier of the battery. + * + * kconfig_dep{CONFIG_BT_BAS_BLS_IDENTIFIER_PRESENT} + * + * @param identifier Identifier to set. + */ +void bt_bas_bls_set_identifier(uint16_t identifier); + +/** + * @brief Set the service required status. + * + * kconfig_dep{CONFIG_BT_BAS_BLS_ADDITIONAL_STATUS_PRESENT} + * + * @param value Service required status to set. + */ +void bt_bas_bls_set_service_required(enum bt_bas_bls_service_required value); + +/** + * @brief Set the battery fault status. + * + * kconfig_dep{CONFIG_BT_BAS_BLS_ADDITIONAL_STATUS_PRESENT} + * + * @param value Battery fault status to set. + */ +void bt_bas_bls_set_battery_fault(enum bt_bas_bls_battery_fault value); #ifdef __cplusplus } diff --git a/samples/bluetooth/hap_ha/prj.conf b/samples/bluetooth/hap_ha/prj.conf index c492cdeb1ec..83d6f70db37 100644 --- a/samples/bluetooth/hap_ha/prj.conf +++ b/samples/bluetooth/hap_ha/prj.conf @@ -56,4 +56,9 @@ CONFIG_BT_TBS_CLIENT_GTBS=y CONFIG_BT_TBS_CLIENT_CCID=y CONFIG_BT_TBS_CLIENT_STATUS_FLAGS=y +CONFIG_BT_BAS_BLS=y +CONFIG_BT_BAS_BLS_IDENTIFIER_PRESENT=y +CONFIG_BT_BAS_BLS_BATTERY_LEVEL_PRESENT=y +CONFIG_BT_BAS_BLS_ADDITIONAL_STATUS_PRESENT=y + CONFIG_LOG=y diff --git a/subsys/bluetooth/services/CMakeLists.txt b/subsys/bluetooth/services/CMakeLists.txt index 0955f202dae..c02eb796112 100644 --- a/subsys/bluetooth/services/CMakeLists.txt +++ b/subsys/bluetooth/services/CMakeLists.txt @@ -3,12 +3,14 @@ zephyr_sources_ifdef(CONFIG_BT_DIS dis.c) -zephyr_sources_ifdef(CONFIG_BT_BAS bas.c) - zephyr_sources_ifdef(CONFIG_BT_HRS hrs.c) zephyr_sources_ifdef(CONFIG_BT_TPS tps.c) +if(CONFIG_BT_BAS) + add_subdirectory(bas) +endif() + if(CONFIG_BT_OTS OR CONFIG_BT_OTS_CLIENT) add_subdirectory(ots) endif() diff --git a/subsys/bluetooth/services/Kconfig b/subsys/bluetooth/services/Kconfig index f4a42ac37a4..0377c068c93 100644 --- a/subsys/bluetooth/services/Kconfig +++ b/subsys/bluetooth/services/Kconfig @@ -8,8 +8,6 @@ menu "GATT Services" rsource "Kconfig.dis" -rsource "Kconfig.bas" - rsource "Kconfig.hrs" rsource "Kconfig.tps" @@ -20,4 +18,6 @@ rsource "ias/Kconfig.ias" rsource "ots/Kconfig" +rsource "bas/Kconfig.bas" + endmenu diff --git a/subsys/bluetooth/services/Kconfig.bas b/subsys/bluetooth/services/Kconfig.bas deleted file mode 100644 index 2dbff5fcc33..00000000000 --- a/subsys/bluetooth/services/Kconfig.bas +++ /dev/null @@ -1,7 +0,0 @@ -# Bluetooth GATT Battery service - -# Copyright (c) 2018 Nordic Semiconductor ASA -# SPDX-License-Identifier: Apache-2.0 - -config BT_BAS - bool "GATT Battery service" diff --git a/subsys/bluetooth/services/bas.c b/subsys/bluetooth/services/bas.c deleted file mode 100644 index 1896136681c..00000000000 --- a/subsys/bluetooth/services/bas.c +++ /dev/null @@ -1,98 +0,0 @@ -/** @file - * @brief GATT Battery Service - */ - -/* - * Copyright (c) 2018 Nordic Semiconductor ASA - * Copyright (c) 2016 Intel Corporation - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#define LOG_LEVEL CONFIG_BT_BAS_LOG_LEVEL -#include -LOG_MODULE_REGISTER(bas); - -static uint8_t battery_level = 100U; - -static void blvl_ccc_cfg_changed(const struct bt_gatt_attr *attr, - uint16_t value) -{ - ARG_UNUSED(attr); - - bool notif_enabled = (value == BT_GATT_CCC_NOTIFY); - - LOG_INF("BAS Notifications %s", notif_enabled ? "enabled" : "disabled"); -} - -static ssize_t read_blvl(struct bt_conn *conn, - const struct bt_gatt_attr *attr, void *buf, - uint16_t len, uint16_t offset) -{ - uint8_t lvl8 = battery_level; - - return bt_gatt_attr_read(conn, attr, buf, len, offset, &lvl8, - sizeof(lvl8)); -} - -/* Constant values from the Assigned Numbers specification: - * https://www.bluetooth.com/wp-content/uploads/Files/Specification/Assigned_Numbers.pdf?id=89 - */ -static const struct bt_gatt_cpf level_cpf = { - .format = 0x04, /* uint8 */ - .exponent = 0x0, - .unit = 0x27AD, /* Percentage */ - .name_space = 0x01, /* Bluetooth SIG */ - .description = 0x0106, /* "main" */ -}; - -BT_GATT_SERVICE_DEFINE(bas, - BT_GATT_PRIMARY_SERVICE(BT_UUID_BAS), - BT_GATT_CHARACTERISTIC(BT_UUID_BAS_BATTERY_LEVEL, - BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, - BT_GATT_PERM_READ, read_blvl, NULL, - &battery_level), - BT_GATT_CCC(blvl_ccc_cfg_changed, - BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), - BT_GATT_CPF(&level_cpf), -); - -static int bas_init(void) -{ - - return 0; -} - -uint8_t bt_bas_get_battery_level(void) -{ - return battery_level; -} - -int bt_bas_set_battery_level(uint8_t level) -{ - int rc; - - if (level > 100U) { - return -EINVAL; - } - - battery_level = level; - - rc = bt_gatt_notify(NULL, &bas.attrs[1], &level, sizeof(level)); - - return rc == -ENOTCONN ? 0 : rc; -} - -SYS_INIT(bas_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/subsys/bluetooth/services/bas/CMakeLists.txt b/subsys/bluetooth/services/bas/CMakeLists.txt new file mode 100644 index 00000000000..ff8bdaabcb2 --- /dev/null +++ b/subsys/bluetooth/services/bas/CMakeLists.txt @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: Apache-2.0 +zephyr_sources_ifdef(CONFIG_BT_BAS bas.c) +zephyr_sources_ifdef(CONFIG_BT_BAS_BLS bas_bls.c) diff --git a/subsys/bluetooth/services/bas/Kconfig.bas b/subsys/bluetooth/services/bas/Kconfig.bas new file mode 100644 index 00000000000..5eec755ed1b --- /dev/null +++ b/subsys/bluetooth/services/bas/Kconfig.bas @@ -0,0 +1,30 @@ +# Bluetooth GATT Battery service + +# Copyright (c) 2018 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +config BT_BAS + bool "GATT Battery service" + +config BT_BAS_BLS + bool "Battery Level Status" + help + Enable this option to include Battery Level Status Characteristic. + +if BT_BAS_BLS + +config BT_BAS_BLS_IDENTIFIER_PRESENT + bool "Battery Level Identifier Present" + help + Enable this option if the Battery Level Identifier is present. + +config BT_BAS_BLS_BATTERY_LEVEL_PRESENT + bool "Battery Level Present" + help + Enable this option if the Battery Level is present. + +config BT_BAS_BLS_ADDITIONAL_STATUS_PRESENT + bool "Additional Battery Status Present" + help + Enable this option if Additional Battery Status information is present. +endif diff --git a/subsys/bluetooth/services/bas/bas.c b/subsys/bluetooth/services/bas/bas.c new file mode 100644 index 00000000000..6960107920a --- /dev/null +++ b/subsys/bluetooth/services/bas/bas.c @@ -0,0 +1,134 @@ +/** @file + * @brief GATT Battery Service + */ + +/* + * Copyright (c) 2024 Demant A/S + * Copyright (c) 2018 Nordic Semiconductor ASA + * Copyright (c) 2016 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include "bas_internal.h" + +#define LOG_LEVEL CONFIG_BT_BAS_LOG_LEVEL +#include +LOG_MODULE_REGISTER(bas, CONFIG_BT_BAS_LOG_LEVEL); + +static uint8_t battery_level = 100U; + +static void blvl_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) +{ + ARG_UNUSED(attr); + + bool notif_enabled = (value == BT_GATT_CCC_NOTIFY); + + LOG_INF("BAS Notifications %s", notif_enabled ? "enabled" : "disabled"); +} + +#if defined(CONFIG_BT_BAS_BLS) +static void blvl_status_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) +{ + ARG_UNUSED(attr); + + bool notif_enabled = (value == BT_GATT_CCC_NOTIFY); + bool ind_enabled = (value == BT_GATT_CCC_INDICATE); + + LOG_INF("BAS Notifications %s", notif_enabled ? "enabled" : "disabled"); + LOG_INF("BAS Indications %s", ind_enabled ? "enabled" : "disabled"); +} +#endif + +static ssize_t read_blvl(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + uint8_t lvl8 = battery_level; + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &lvl8, sizeof(lvl8)); +} + +/* Constant values from the Assigned Numbers specification: + * https://www.bluetooth.com/wp-content/uploads/Files/Specification/Assigned_Numbers.pdf?id=89 + */ +static const struct bt_gatt_cpf level_cpf = { + .format = 0x04, /* uint8 */ + .exponent = 0x0, + .unit = 0x27AD, /* Percentage */ + .name_space = 0x01, /* Bluetooth SIG */ + .description = 0x0106, /* "main" */ +}; + +BT_GATT_SERVICE_DEFINE( + bas, BT_GATT_PRIMARY_SERVICE(BT_UUID_BAS), + BT_GATT_CHARACTERISTIC(BT_UUID_BAS_BATTERY_LEVEL, BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, + BT_GATT_PERM_READ, read_blvl, NULL, NULL), + BT_GATT_CCC(blvl_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), + BT_GATT_CPF(&level_cpf), +#if defined(CONFIG_BT_BAS_BLS) + BT_GATT_CHARACTERISTIC(BT_UUID_BAS_BATTERY_LEVEL_STATUS, + BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY | BT_GATT_CHRC_INDICATE, + BT_GATT_PERM_READ, bt_bas_bls_read_blvl_status, NULL, NULL), + BT_GATT_CCC(blvl_status_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), +#endif +); + +static int bas_init(void) +{ + if (IS_ENABLED(CONFIG_BT_BAS_BLS)) { + /* Initialize the Battery Level Status Module */ + bt_bas_bls_init(); + if (IS_ENABLED(CONFIG_BT_BAS_BLS_IDENTIFIER_PRESENT)) { + /* Set the identifier only if BT_BAS_BLS_IDENTIFIER_PRESENT is defined */ + bt_bas_bls_set_identifier(level_cpf.description); + } + } + return 0; +} + +uint8_t bt_bas_get_battery_level(void) +{ + return battery_level; +} + +int bt_bas_set_battery_level(uint8_t level) +{ + int rc; + + if (level > 100U) { + return -EINVAL; + } + + battery_level = level; + + rc = bt_gatt_notify(NULL, &bas.attrs[1], &level, sizeof(level)); + + if (IS_ENABLED(CONFIG_BT_BAS_BLS_BATTERY_LEVEL_PRESENT)) { + bt_bas_bls_set_battery_level(level); + } + + return rc == -ENOTCONN ? 0 : rc; +} + +const struct bt_gatt_attr *bt_bas_get_bas_attr(uint16_t index) +{ + if (index < bas.attr_count) { + return &bas.attrs[index]; + } + return NULL; +} + +SYS_INIT(bas_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/subsys/bluetooth/services/bas/bas_bls.c b/subsys/bluetooth/services/bas/bas_bls.c new file mode 100644 index 00000000000..bcb2fea3619 --- /dev/null +++ b/subsys/bluetooth/services/bas/bas_bls.c @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2024 Demant A/S + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "bas_internal.h" + +#include +LOG_MODULE_DECLARE(bas, CONFIG_BT_BAS_LOG_LEVEL); + +/* The battery level status of a battery. */ +static struct bt_bas_bls bls; + +#define BT_BAS_IDX_BATT_LVL_STATUS_CHAR_VAL 6 + +/* Notify/Indicate all connections */ +static struct bt_gatt_indicate_params ind_params; + +/* + * Bitfield structure: Power State + * + * - Bits 0: Battery Present + * - Bits 1–2: Wired External Power Source Connected + * - Bits 3–4: Wireless External Power Source Connected + * - Bits 5–6: Battery Charge State + * - Bits 7–8: Battery Charge Level + * - Bits 9–11: Charging Type + * - Bits 12–14: Charging Fault Reason + * - Bit 15: RFU + * + * For detailed specification, refer to: + * https://bitbucket.org/bluetooth-SIG/public/src/main/gss/ + * org.bluetooth.characteristic.battery_level_status.yaml + */ + +#define BATTERY_SHIFT 0 +#define WIRED_POWER_SHIFT 1 +#define WIRELESS_POWER_SHIFT 3 +#define BATTERY_CHARGE_STATE_SHIFT 5 +#define BATTERY_CHARGE_LEVEL_SHIFT 7 +#define BATTERY_CHARGE_TYPE_SHIFT 9 +#define CHARGING_FAULT_SHIFT 12 + +#define BATTERY_MASK (BIT_MASK(1) << BATTERY_SHIFT) +#define WIRED_POWER_MASK (BIT_MASK(2) << WIRED_POWER_SHIFT) +#define WIRELESS_POWER_MASK (BIT_MASK(2) << WIRELESS_POWER_SHIFT) +#define BATTERY_CHARGE_STATE_MASK (BIT_MASK(2) << BATTERY_CHARGE_STATE_SHIFT) +#define BATTERY_CHARGE_LEVEL_MASK (BIT_MASK(2) << BATTERY_CHARGE_LEVEL_SHIFT) +#define BATTERY_CHARGE_TYPE_MASK (BIT_MASK(3) << BATTERY_CHARGE_TYPE_SHIFT) +#define CHARGING_FAULT_MASK (BIT_MASK(3) << CHARGING_FAULT_SHIFT) + +/* + * Bitfield structure: Additional Status + * + * - Bits 0–1: Service Required + * - Bit 2: Battery Fault + * - Bits 3–7: Reserved + */ +#define SERVICE_REQUIRED_SHIFT 0 +#define BATTERY_FAULT_SHIFT 2 + +#define SERVICE_REQUIRED_MASK (BIT_MASK(2) << SERVICE_REQUIRED_SHIFT) +#define BATTERY_FAULT_MASK (BIT_MASK(1) << BATTERY_FAULT_SHIFT) + +void bt_bas_bls_init(void) +{ + LOG_DBG("Initialise BAS Battery Level Status Module"); + + bls.flags = 0; + bls.power_state = 0; + +#if defined(CONFIG_BT_BAS_BLS_IDENTIFIER_PRESENT) + /* Set identifier flag */ + bls.flags |= BT_BAS_BLS_FLAG_IDENTIFIER_PRESENT; + bls.identifier = 0; +#endif /* CONFIG_BT_BAS_BLS_IDENTIFIER_PRESENT */ + +#if defined(CONFIG_BT_BAS_BLS_BATTERY_LEVEL_PRESENT) + /* Set battery level flag */ + bls.flags |= BT_BAS_BLS_FLAG_BATTERY_LEVEL_PRESENT; + bls.battery_level = bt_bas_get_battery_level(); +#endif /* CONFIG_BT_BAS_BLS_BATTERY_LEVEL_PRESENT */ + +#if defined(CONFIG_BT_BAS_BLS_ADDITIONAL_STATUS_PRESENT) + /* Set additional status flag */ + bls.flags |= BT_BAS_BLS_FLAG_ADDITIONAL_STATUS_PRESENT; + bls.additional_status = 0; +#endif /* CONFIG_BT_BAS_BLS_ADDITIONAL_STATUS_PRESENT */ +} + +static void indicate_cb(struct bt_conn *conn, struct bt_gatt_indicate_params *params, uint8_t err) +{ + if (err != 0) { + LOG_DBG("Indication failed with error %d\n", err); + } else { + LOG_DBG("Indication sent successfully\n"); + } +} + +static void bt_bas_bls_update_battery_level_status(void) +{ + int err; + const struct bt_gatt_attr *attr = bt_bas_get_bas_attr(BT_BAS_IDX_BATT_LVL_STATUS_CHAR_VAL); + + if (attr) { + const struct bt_bas_bls le_battery_level_status = { + .flags = bls.flags, + .power_state = sys_cpu_to_le16(bls.power_state), +#if defined(CONFIG_BT_BAS_BLS_IDENTIFIER_PRESENT) + .identifier = sys_cpu_to_le16(bls.identifier), +#endif +#if defined(CONFIG_BT_BAS_BLS_BATTERY_LEVEL_PRESENT) + .battery_level = bls.battery_level, +#endif +#if defined(CONFIG_BT_BAS_BLS_ADDITIONAL_STATUS_PRESENT) + .additional_status = bls.additional_status, +#endif + }; + + /* Notify/Indicate all connections */ + ind_params.attr = attr; + ind_params.data = &le_battery_level_status; + ind_params.len = sizeof(le_battery_level_status); + ind_params.func = indicate_cb; + err = bt_gatt_indicate(NULL, &ind_params); + if (err) { + LOG_DBG("Failed to send ntf/ind to all connections (err %d)\n", err); + } + } +} + +ssize_t bt_bas_bls_read_blvl_status(struct bt_conn *conn, const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) +{ + const struct bt_bas_bls le_battery_level_status = { + .flags = bls.flags, + .power_state = sys_cpu_to_le16(bls.power_state), +#if defined(CONFIG_BT_BAS_BLS_IDENTIFIER_PRESENT) + .identifier = sys_cpu_to_le16(bls.identifier), +#endif +#if defined(CONFIG_BT_BAS_BLS_BATTERY_LEVEL_PRESENT) + .battery_level = bls.battery_level, +#endif +#if defined(CONFIG_BT_BAS_BLS_ADDITIONAL_STATUS_PRESENT) + .additional_status = bls.additional_status, +#endif + }; + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &le_battery_level_status, + sizeof(le_battery_level_status)); +} + +void bt_bas_bls_set_battery_present(enum bt_bas_bls_battery_present present) +{ + bls.power_state &= ~BATTERY_MASK; + bls.power_state |= (present << BATTERY_SHIFT) & BATTERY_MASK; + bt_bas_bls_update_battery_level_status(); +} + +void bt_bas_bls_set_wired_external_power_source(enum bt_bas_bls_wired_power_source source) +{ + bls.power_state &= ~WIRED_POWER_MASK; + bls.power_state |= (source << WIRED_POWER_SHIFT) & WIRED_POWER_MASK; + bt_bas_bls_update_battery_level_status(); +} + +void bt_bas_bls_set_wireless_external_power_source(enum bt_bas_bls_wireless_power_source source) +{ + bls.power_state &= ~WIRELESS_POWER_MASK; + bls.power_state |= (source << WIRELESS_POWER_SHIFT) & WIRELESS_POWER_MASK; + bt_bas_bls_update_battery_level_status(); +} + +void bt_bas_bls_set_battery_charge_state(enum bt_bas_bls_battery_charge_state state) +{ + bls.power_state &= ~BATTERY_CHARGE_STATE_MASK; + bls.power_state |= (state << BATTERY_CHARGE_STATE_SHIFT) & BATTERY_CHARGE_STATE_MASK; + bt_bas_bls_update_battery_level_status(); +} + +void bt_bas_bls_set_battery_charge_level(enum bt_bas_bls_battery_charge_level level) +{ + bls.power_state &= ~BATTERY_CHARGE_LEVEL_MASK; + bls.power_state |= (level << BATTERY_CHARGE_LEVEL_SHIFT) & BATTERY_CHARGE_LEVEL_MASK; + bt_bas_bls_update_battery_level_status(); +} + +void bt_bas_bls_set_battery_charge_type(enum bt_bas_bls_battery_charge_type type) +{ + bls.power_state &= ~BATTERY_CHARGE_TYPE_MASK; + bls.power_state |= (type << BATTERY_CHARGE_TYPE_SHIFT) & BATTERY_CHARGE_TYPE_MASK; + bt_bas_bls_update_battery_level_status(); +} + +void bt_bas_bls_set_charging_fault_reason(enum bt_bas_bls_charging_fault_reason reason) +{ + bls.power_state &= ~CHARGING_FAULT_MASK; + bls.power_state |= (reason << CHARGING_FAULT_SHIFT) & CHARGING_FAULT_MASK; + bt_bas_bls_update_battery_level_status(); +} + +#if defined(CONFIG_BT_BAS_BLS_BATTERY_LEVEL_PRESENT) +void bt_bas_bls_set_battery_level(uint8_t level) +{ + bls.battery_level = level; + bt_bas_bls_update_battery_level_status(); +} +#endif /* CONFIG_BT_BAS_BLS_BATTERY_LEVEL_PRESENT */ + +#if defined(CONFIG_BT_BAS_BLS_IDENTIFIER_PRESENT) +void bt_bas_bls_set_identifier(uint16_t identifier) +{ + bls.identifier = sys_cpu_to_le16(identifier); + bt_bas_bls_update_battery_level_status(); +} +#endif /* CONFIG_BT_BAS_BLS_IDENTIFIER_PRESENT */ + +#if defined(CONFIG_BT_BAS_BLS_ADDITIONAL_STATUS_PRESENT) +void bt_bas_bls_set_service_required(enum bt_bas_bls_service_required value) +{ + bls.additional_status &= ~SERVICE_REQUIRED_MASK; + bls.additional_status |= (value << SERVICE_REQUIRED_SHIFT) & SERVICE_REQUIRED_MASK; + bt_bas_bls_update_battery_level_status(); +} + +void bt_bas_bls_set_battery_fault(enum bt_bas_bls_battery_fault value) +{ + bls.additional_status &= ~BATTERY_FAULT_MASK; + bls.additional_status |= (value << BATTERY_FAULT_SHIFT) & BATTERY_FAULT_MASK; + bt_bas_bls_update_battery_level_status(); +} +#endif /* CONFIG_BT_BAS_BLS_ADDITIONAL_STATUS_PRESENT */ diff --git a/subsys/bluetooth/services/bas/bas_internal.h b/subsys/bluetooth/services/bas/bas_internal.h new file mode 100644 index 00000000000..0dfd33aac43 --- /dev/null +++ b/subsys/bluetooth/services/bas/bas_internal.h @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2024 Demant A/S + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef BT_BAS_INTERNAL_H_ +#define BT_BAS_INTERNAL_H_ + +#include +#include +#include + +/** + * @brief Battery level status structure definition. + */ +struct bt_bas_bls { + + /** @brief Flags Field + * + * The values of this field are defined below. + * + * - bit 0: Identifier Present + * - Indicates whether the identifier field is present. + * - bit 1: Battery Level Present + * - Indicates whether the battery level field is present. + * - bit 2: Additional Status Present + * - Indicates whether the additional status field is present. + * - bit 3–7: RFU (Reserved for Future Use) + * - Reserved bits for future use; should be set to zero. + */ + uint8_t flags; + + /** @brief Power State + * + * The values of this field are defined below. + * + * - bit 0: Battery Present + * - 0 = No + * - 1 = Yes + * - bit 1–2: Wired External Power Source Connected + * - 0 = No + * - 1 = Yes + * - 2 = Unknown + * - 3 = RFU + * - bit 3–4: Wireless External Power Source Connected + * - 0 = No + * - 1 = Yes + * - 2 = Unknown + * - 3 = RFU + * - bit 5–6: Battery Charge State + * - 0 = Unknown + * - 1 = Charging + * - 2 = Discharging: Active + * - 3 = Discharging: Inactive + * - bit 7–8: Battery Charge Level + * - 0 = Unknown + * - 1 = Good + * - 2 = Low + * - 3 = Critical + * - bit 9–11: Charging Type + * - 0 = Unknown or Not Charging + * - 1 = Constant Current + * - 2 = Constant Voltage + * - 3 = Trickle + * - 4 = Float + * - 5–7 = RFU + * - bit 12–14: Charging Fault Reason + * - Bit 12: Battery + * - Bit 13: External Power source + * - Bit 14: Other + * - bit 15: RFU + */ + uint16_t power_state; + +#if defined(CONFIG_BT_BAS_BLS_IDENTIFIER_PRESENT) + /** Identifier for the battery, range 0x0000 to 0xFFFF.*/ + uint16_t identifier; +#endif /* CONFIG_BT_BAS_BLS_IDENTIFIER_PRESENT */ + +#if defined(CONFIG_BT_BAS_BLS_BATTERY_LEVEL_PRESENT) + /** Current battery level */ + uint8_t battery_level; +#endif /* CONFIG_BT_BAS_BLS_BATTERY_LEVEL_PRESENT */ + +#if defined(CONFIG_BT_BAS_BLS_ADDITIONAL_STATUS_PRESENT) + + /** @brief Additional Status + * + * The values of this field are defined below. + * + * - bit 0–1: Service Required + * - 0 = False + * - 1 = True + * - 2 = Unknown + * - 3 = RFU + * - bit 2: Battery Fault + * - 0 = False or Unknown + * - 1 = Yes + * - bit 3–7: RFU + */ + uint8_t additional_status; + +#endif /* CONFIG_BT_BAS_BLS_ADDITIONAL_STATUS_PRESENT */ + +} __packed; + +/** + * @brief Initialize the Battery Level Status Module. + * + */ +void bt_bas_bls_init(void); + +/** + * @brief Set the battery level characteristic value. + * + * @param battery_level The new battery level value in percent (0-100). + */ +void bt_bas_bls_set_battery_level(uint8_t battery_level); + +/** + * @brief Read the Battery Level Status characteristic. + * + * @param conn Pointer to the Bluetooth connection object representing the client requesting + * the characteristic. + * @param attr Pointer to the GATT attribute representing the Battery Level Status characteristic. + * @param buf Buffer to store the read value. + * @param len Length of the buffer. + * @param offset Offset within the characteristic value to start reading. + * + * @return The number of bytes read and sent to the client, or a negative error code on failure. + */ +ssize_t bt_bas_bls_read_blvl_status(struct bt_conn *conn, const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset); + +/** + * @brief Retrieve the Bluetooth GATT attribute for the BAS service by index. + * + * @param index The index of the attribute within the BAS service. + * + * @return Pointer to the Bluetooth GATT attribute if the index is valid, + * otherwise NULL if the index is out of bounds. + */ +const struct bt_gatt_attr *bt_bas_get_bas_attr(uint16_t index); + +#endif /* BT_BAS_INTERNAL_H_ */ diff --git a/tests/bsim/bluetooth/samples/battery_service/CMakeLists.txt b/tests/bsim/bluetooth/samples/battery_service/CMakeLists.txt new file mode 100644 index 00000000000..d5eda77d836 --- /dev/null +++ b/tests/bsim/bluetooth/samples/battery_service/CMakeLists.txt @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(battery_service_test) + +target_sources(app PRIVATE + src/main.c + src/central_test.c + src/peripheral_test.c +) + +# This contains a variety of helper functions that abstract away common tasks, +# like scanning, setting up a connection, querying the peer for a given +# characteristic, etc.. +add_subdirectory(${ZEPHYR_BASE}/tests/bluetooth/common/testlib testlib) +target_link_libraries(app PRIVATE testlib) + +# This contains babblesim-specific helpers, e.g. device synchronization. +add_subdirectory(${ZEPHYR_BASE}/tests/bsim/babblekit babblekit) +target_link_libraries(app PRIVATE babblekit) + +zephyr_library_include_directories(${ZEPHYR_BASE}/samples/bluetooth) + +zephyr_include_directories( + ${BSIM_COMPONENTS_PATH}/libUtilv1/src/ + ${BSIM_COMPONENTS_PATH}/libPhyComv1/src/ +) diff --git a/tests/bsim/bluetooth/samples/battery_service/prj.conf b/tests/bsim/bluetooth/samples/battery_service/prj.conf new file mode 100644 index 00000000000..e1c9de0f11b --- /dev/null +++ b/tests/bsim/bluetooth/samples/battery_service/prj.conf @@ -0,0 +1,14 @@ +CONFIG_BT=y +CONFIG_LOG=y +CONFIG_BT_CENTRAL=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_SMP=y +CONFIG_BT_BAS=y +CONFIG_BT_GATT_CLIENT=y +CONFIG_BT_DEVICE_NAME="bsim_bas" +CONFIG_BT_ATT_TX_COUNT=5 + +CONFIG_BT_BAS_BLS=y +CONFIG_BT_BAS_BLS_IDENTIFIER_PRESENT=y +CONFIG_BT_BAS_BLS_BATTERY_LEVEL_PRESENT=y +CONFIG_BT_BAS_BLS_ADDITIONAL_STATUS_PRESENT=y diff --git a/tests/bsim/bluetooth/samples/battery_service/src/central_test.c b/tests/bsim/bluetooth/samples/battery_service/src/central_test.c new file mode 100644 index 00000000000..f527a3a0e80 --- /dev/null +++ b/tests/bsim/bluetooth/samples/battery_service/src/central_test.c @@ -0,0 +1,562 @@ +/* + * Copyright (c) 2024 Demant A/S + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "bs_types.h" +#include "bs_tracing.h" +#include "time_machine.h" +#include "bstests.h" +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "testlib/conn.h" +#include "testlib/scan.h" +#include "testlib/log_utils.h" + +#include "babblekit/flags.h" +#include "babblekit/sync.h" +#include "babblekit/testcase.h" + +#include +LOG_MODULE_REGISTER(bt_bsim_bas, CONFIG_BT_BAS_LOG_LEVEL); + +static struct bt_conn *default_conn; +static bt_addr_le_t peer = {}; + +static struct bt_uuid_16 uuid = BT_UUID_INIT_16(0); +static struct bt_gatt_discover_params discover_params; + +static struct bt_gatt_subscribe_params battery_level_notify_params; +static struct bt_gatt_subscribe_params battery_level_status_sub_params; + +/* + * Battery Service test: + * We expect to find a connectable peripheral to which we will + * connect and discover Battery Service + * + * Test the Read/Notify/Indicate Characteristics of BAS + */ + +#define WAIT_TIME 10 /*seconds*/ +#define BAS_BLS_IND_RECEIVED_COUNT 20 +#define BAS_BLS_NTF_RECEIVED_COUNT 20 + +static DEFINE_FLAG(notification_count_reached); +static DEFINE_FLAG(indication_count_reached); + +extern enum bst_result_t bst_result; + +static void test_bas_central_init(void) +{ + bst_ticker_set_next_tick_absolute(WAIT_TIME * 1e6); + bst_result = In_progress; +} + +static void test_bas_central_tick(bs_time_t HW_device_time) +{ + /* + * If in WAIT_TIME seconds the testcase did not already pass + * (and finish) we consider it failed + */ + if (bst_result != Passed) { + TEST_FAIL("test_bas_central failed (not passed after %i seconds)\n", WAIT_TIME); + } +} + +/* Callback for handling Battery Level Notifications */ +static uint8_t battery_level_notify_cb(struct bt_conn *conn, + struct bt_gatt_subscribe_params *params, const void *data, + uint16_t length) +{ + if (data) { + LOG_INF("[NOTIFICATION] BAS Battery Level: %d%%", *(const uint8_t *)data); + } else { + LOG_INF("Battery Level Notifications disabled"); + } + return BT_GATT_ITER_CONTINUE; +} + +/* Callback for handling Battery Level Read Response */ +static uint8_t battery_level_read_cb(struct bt_conn *conn, uint8_t err, + struct bt_gatt_read_params *params, const void *data, + uint16_t length) +{ + TEST_ASSERT(err == 0, "Failed to read Battery Level (err %d)", err); + if (data) { + LOG_DBG("[READ] BAS Battery Level: %d%%\n", *(const uint8_t *)data); + } + + return BT_GATT_ITER_STOP; +} + +static bool parse_battery_level_status(const uint8_t *data, uint16_t length) +{ + /* Check minimum length for parsing flags and power state */ + if (length < 3) { + TEST_FAIL("Invalid data length: %d", length); + return false; + } + + /* Parse flags (first byte) */ + uint8_t flags = data[0]; + + LOG_INF("Parsed Flags: 0x%02x", flags); + + if (flags & BT_BAS_BLS_FLAG_IDENTIFIER_PRESENT) { + LOG_INF(" Identifier Present"); + } else { + LOG_INF(" Identifier Not Present"); + } + + if (flags & BT_BAS_BLS_FLAG_BATTERY_LEVEL_PRESENT) { + LOG_INF(" Battery Level Present"); + } else { + LOG_INF(" Battery Level Not Present"); + } + + if (flags & BT_BAS_BLS_FLAG_ADDITIONAL_STATUS_PRESENT) { + LOG_INF(" Additional Status Present"); + } else { + LOG_INF(" Additional Status Not Present"); + } + + /* Parse power state (next 2 bytes) */ + uint16_t power_state = sys_get_le16(&data[1]); + + LOG_INF("Parsed Power State: 0x%04x", power_state); + /* Print out each power state value */ + LOG_INF(" Battery Present: %s", (power_state & BIT(0)) ? "Yes" : "No"); + + uint8_t wired_power = (power_state >> 1) & 0x03; + + switch (wired_power) { + case 0: + LOG_INF(" Wired Power Source: No"); + break; + case 1: + LOG_INF(" Wired Power Source: Yes"); + break; + case 2: + LOG_INF(" Wired Power Source: Unknown"); + break; + default: + LOG_INF(" Wired Power Source: RFU"); + break; + } + + uint8_t wireless_power = (power_state >> 3) & 0x03; + + switch (wireless_power) { + case 0: + LOG_INF(" Wireless Power Source: No"); + break; + case 1: + LOG_INF(" Wireless Power Source: Yes"); + break; + case 2: + LOG_INF(" Wireless Power Source: Unknown"); + break; + default: + LOG_INF(" Wireless Power Source: RFU"); + break; + } + + uint8_t charge_state = (power_state >> 5) & 0x03; + + switch (charge_state) { + case 0: + LOG_INF(" Battery Charge State: Unknown"); + break; + case 1: + LOG_INF(" Battery Charge State: Charging"); + break; + case 2: + LOG_INF(" Battery Charge State: Discharging (Active)"); + break; + case 3: + LOG_INF(" Battery Charge State: Discharging (Inactive)"); + break; + } + + uint8_t charge_level = (power_state >> 7) & 0x03; + + switch (charge_level) { + case 0: + LOG_INF(" Battery Charge Level: Unknown"); + break; + case 1: + LOG_INF(" Battery Charge Level: Good"); + break; + case 2: + LOG_INF(" Battery Charge Level: Low"); + break; + case 3: + LOG_INF(" Battery Charge Level: Critical"); + break; + } + + uint8_t charging_type = (power_state >> 9) & 0x07; + + switch (charging_type) { + case 0: + LOG_INF(" Charging Type: Unknown or Not Charging"); + break; + case 1: + LOG_INF(" Charging Type: Constant Current"); + break; + case 2: + LOG_INF(" Charging Type: Constant Voltage"); + break; + case 3: + LOG_INF(" Charging Type: Trickle"); + break; + case 4: + LOG_INF(" Charging Type: Float"); + break; + default: + LOG_INF(" Charging Type: RFU"); + break; + } + + uint8_t charging_fault = (power_state >> 12) & 0x07; + + if (charging_fault) { + LOG_INF(" Charging Fault Reason: %s%s%s", + (charging_fault & BIT(0)) ? "Battery " : "", + (charging_fault & BIT(1)) ? "External Power Source " : "", + (charging_fault & BIT(2)) ? "Other " : ""); + } else { + LOG_INF(" Charging Fault Reason: None"); + } + + /* Optional: Check if identifier is present */ + if (IS_ENABLED(CONFIG_BT_BAS_BLS_IDENTIFIER_PRESENT)) { + /* Check if length is sufficient for identifier */ + if (length < 5) { + TEST_FAIL("Invalid data length for identifier"); + return false; + } + + /* Parse identifier (next 2 bytes) */ + uint16_t identifier = sys_get_le16(&data[3]); + + LOG_INF("Parsed Identifier: 0x%04x", identifier); + } + + /* Optional: Check if battery level is present */ + if (IS_ENABLED(CONFIG_BT_BAS_BLS_BATTERY_LEVEL_PRESENT)) { + /* Check if length is sufficient for battery level */ + if (length < 6) { + TEST_FAIL("Invalid data length for battery level"); + return false; + } + + /* Parse battery level (next byte) */ + uint8_t battery_level = data[5]; + + LOG_INF("Parsed Battery Level: %d%%", battery_level); + } + + /* Optional: Check if additional status is present */ + if (IS_ENABLED(CONFIG_BT_BAS_BLS_ADDITIONAL_STATUS_PRESENT)) { + /* Check if length is sufficient for additional status */ + if (length < 7) { + TEST_FAIL("Invalid data length for additional status"); + return false; + } + + /* Parse additional status (next byte) */ + uint8_t additional_status = data[6]; + + LOG_INF("Parsed Additional Status: 0x%02x", additional_status); + + /* Print out additional status values */ + uint8_t service_required = additional_status & 0x03; + + switch (service_required) { + case 0: + LOG_INF(" Service Required: False"); + break; + case 1: + LOG_INF(" Service Required: True"); + break; + case 2: + LOG_INF(" Service Required: Unknown"); + break; + default: + LOG_INF(" Service Required: RFU"); + break; + } + + bool battery_fault = (additional_status & BIT(2)) ? true : false; + + LOG_INF(" Battery Fault: %s", battery_fault ? "Yes" : "No"); + } + + return true; +} + +static unsigned char battery_level_status_indicate_cb(struct bt_conn *conn, + struct bt_gatt_subscribe_params *params, + const void *data, uint16_t length) +{ + if (!data) { + LOG_INF("bas level status indication disabled\n"); + } else { + static int ind_received; + + printk("[INDICATION] BAS Battery Level Status: "); + for (int i = 0; i < length; i++) { + printk("%02x ", ((uint8_t *)data)[i]); + } + printk("\n"); + + if (parse_battery_level_status(data, length)) { + LOG_INF("Notification parsed successfully"); + } else { + LOG_ERR("Notification parsing failed"); + } + + if (ind_received++ > BAS_BLS_IND_RECEIVED_COUNT) { + SET_FLAG(indication_count_reached); + } + } + return BT_GATT_ITER_CONTINUE; +} + +static uint8_t battery_level_status_notify_cb(struct bt_conn *conn, + struct bt_gatt_subscribe_params *params, + const void *data, uint16_t length) +{ + if (!data) { + LOG_INF("bas level status notification disabled\n"); + } else { + static int notify_count; + + printk("[NOTIFICATION] BAS Battery Level Status: "); + for (int i = 0; i < length; i++) { + printk("%02x ", ((uint8_t *)data)[i]); + } + printk("\n"); + + if (parse_battery_level_status(data, length)) { + LOG_INF("Notification parsed successfully"); + } else { + LOG_ERR("Notification parsing failed"); + } + + if (notify_count++ > BAS_BLS_NTF_RECEIVED_COUNT) { + SET_FLAG(notification_count_reached); + } + } + return BT_GATT_ITER_CONTINUE; +} + +static void read_battery_level(const struct bt_gatt_attr *attr) +{ + /* Read the battery level after subscribing */ + static struct bt_gatt_read_params read_params; + + read_params.func = battery_level_read_cb; + read_params.handle_count = 1; + read_params.single.handle = bt_gatt_attr_get_handle(attr); + read_params.single.offset = 0; + bt_gatt_read(default_conn, &read_params); +} + +static void subscribe_battery_level(const struct bt_gatt_attr *attr) +{ + int err; + + battery_level_notify_params = (struct bt_gatt_subscribe_params){ + /* In Zephyr, it is common practice for the CCC handle + * to be positioned two handles after the characteristic handle. + */ + .ccc_handle = bt_gatt_attr_get_handle(attr) + 2, + .value_handle = bt_gatt_attr_value_handle(attr), + .value = BT_GATT_CCC_NOTIFY, + .notify = battery_level_notify_cb, + }; + + err = bt_gatt_subscribe(default_conn, &battery_level_notify_params); + if (err && err != -EALREADY) { + TEST_FAIL("Subscribe failed (err %d)\n", err); + } else { + LOG_DBG("Battery level [SUBSCRIBED]\n"); + } + read_battery_level(attr); +} + +static void subscribe_battery_level_status(const struct bt_gatt_attr *attr) +{ + int err; + + if (get_device_nbr() == 1) { /* One device for Indication */ + battery_level_status_sub_params = (struct bt_gatt_subscribe_params){ + /* In Zephyr, it is common practice for the CCC handle + * to be positioned two handles after the characteristic handle. + */ + .ccc_handle = bt_gatt_attr_get_handle(attr) + 2, + .value_handle = bt_gatt_attr_value_handle(attr), + .value = BT_GATT_CCC_INDICATE, + .notify = battery_level_status_indicate_cb, + }; + } else { /* Other device for Notification */ + battery_level_status_sub_params = (struct bt_gatt_subscribe_params){ + .ccc_handle = bt_gatt_attr_get_handle(attr) + 2, + .value_handle = bt_gatt_attr_value_handle(attr), + .value = BT_GATT_CCC_NOTIFY, + .notify = battery_level_status_notify_cb, + }; + } + + err = bt_gatt_subscribe(default_conn, &battery_level_status_sub_params); + if (err && err != -EALREADY) { + TEST_FAIL("Subscribe failed (err %d)\n", err); + } else { + LOG_DBG("Battery level status [SUBSCRIBED]\n"); + } +} + +static uint8_t discover_func(struct bt_conn *conn, const struct bt_gatt_attr *attr, + struct bt_gatt_discover_params *params) +{ + int err; + + if (!attr) { + LOG_DBG("Discover complete\n"); + memset(params, 0, sizeof(*params)); + return BT_GATT_ITER_STOP; + } + + LOG_DBG("[ATTRIBUTE] handle %u\n", attr->handle); + + if (!bt_uuid_cmp(discover_params.uuid, BT_UUID_BAS)) { + LOG_DBG("Battery Service\n"); + memcpy(&uuid, BT_UUID_BAS_BATTERY_LEVEL, sizeof(uuid)); + discover_params.uuid = &uuid.uuid; + discover_params.start_handle = attr->handle + 1; + discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; + + err = bt_gatt_discover(conn, &discover_params); + if (err) { + TEST_FAIL("Discover failed (err %d)\n", err); + } + + } else if (!bt_uuid_cmp(discover_params.uuid, BT_UUID_BAS_BATTERY_LEVEL)) { + LOG_DBG("Subscribe Battery Level Char\n"); + subscribe_battery_level(attr); + + memcpy(&uuid, BT_UUID_BAS_BATTERY_LEVEL_STATUS, sizeof(uuid)); + discover_params.uuid = &uuid.uuid; + discover_params.start_handle = attr->handle + 1; + discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; + + err = bt_gatt_discover(conn, &discover_params); + if (err) { + TEST_FAIL("Discover failed (err %d)\n", err); + } + } else if (!bt_uuid_cmp(discover_params.uuid, BT_UUID_BAS_BATTERY_LEVEL_STATUS)) { + LOG_DBG("Subscribe Batterry Level Status Char\n"); + subscribe_battery_level_status(attr); + } + return BT_GATT_ITER_STOP; +} + +static void discover_bas_service(struct bt_conn *conn) +{ + int err; + + LOG_DBG("%s\n", __func__); + + memcpy(&uuid, BT_UUID_BAS, sizeof(uuid)); + discover_params.uuid = &uuid.uuid; + discover_params.func = discover_func; + discover_params.start_handle = BT_ATT_FIRST_ATTRIBUTE_HANDLE; + discover_params.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; + discover_params.type = BT_GATT_DISCOVER_PRIMARY; + err = bt_gatt_discover(conn, &discover_params); + if (err) { + TEST_FAIL("Discover failed(err %d)\n", err); + return; + } +} + +static void test_bas_central_main(void) +{ + int err; + + /* Mark test as in progress. */ + TEST_START("central"); + /* bk_sync_init only works between two devices in a simulation, with IDs 0 and 1. */ + if (get_device_nbr() == 1) { + /* Initialize device sync library */ + bk_sync_init(); + } + + err = bt_enable(NULL); + TEST_ASSERT(err == 0, "Can't enable Bluetooth (err %d)", err); + + LOG_DBG("Bluetooth initialized\n"); + + err = bt_testlib_scan_find_name(&peer, CONFIG_BT_DEVICE_NAME); + TEST_ASSERT(!err, "Failed to start scan (err %d)", err); + + /* Create a connection using that address */ + err = bt_testlib_connect(&peer, &default_conn); + TEST_ASSERT(!err, "Failed to initiate connection (err %d)", err); + + LOG_DBG("Connected"); + discover_bas_service(default_conn); + + if (get_device_nbr() == 1) { + WAIT_FOR_FLAG(indication_count_reached); + LOG_INF("Indication Count Reached!"); + } else { + WAIT_FOR_FLAG(notification_count_reached); + LOG_INF("Notification Count Reached!"); + } + + /* bk_sync_send only works between two devices in a simulation, with IDs 0 and 1. */ + if (get_device_nbr() == 1) { + bk_sync_send(); + } + + bst_result = Passed; + TEST_PASS("Central Test Passed"); +} + +static const struct bst_test_instance test_bas_central[] = { + { + .test_id = "central", + .test_descr = + "Battery Service test. It expects that a peripheral device can be found. " + "The test will pass if it can receive notifications and indications more " + "than the threshold set within 15 sec. ", + .test_pre_init_f = test_bas_central_init, + .test_tick_f = test_bas_central_tick, + .test_main_f = test_bas_central_main, + }, + BSTEST_END_MARKER, +}; + +struct bst_test_list *test_bas_central_install(struct bst_test_list *tests) +{ + tests = bst_add_tests(tests, test_bas_central); + return tests; +} diff --git a/tests/bsim/bluetooth/samples/battery_service/src/main.c b/tests/bsim/bluetooth/samples/battery_service/src/main.c new file mode 100644 index 00000000000..26af62c0614 --- /dev/null +++ b/tests/bsim/bluetooth/samples/battery_service/src/main.c @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 Demant A/S + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "bstests.h" + +extern struct bst_test_list *test_bas_central_install(struct bst_test_list *tests); +extern struct bst_test_list *test_bas_peripheral_install(struct bst_test_list *tests); + +bst_test_install_t test_installers[] = { + test_bas_central_install, + test_bas_peripheral_install, + NULL, +}; + +int main(void) +{ + bst_main(); + return 0; +} diff --git a/tests/bsim/bluetooth/samples/battery_service/src/peripheral_test.c b/tests/bsim/bluetooth/samples/battery_service/src/peripheral_test.c new file mode 100644 index 00000000000..0d7c3a3fdb4 --- /dev/null +++ b/tests/bsim/bluetooth/samples/battery_service/src/peripheral_test.c @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2024 Demant A/S + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include + +#include "bs_types.h" +#include "bs_tracing.h" +#include "time_machine.h" +#include "bstests.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "testlib/conn.h" +#include "testlib/scan.h" +#include "testlib/log_utils.h" + +#include "babblekit/flags.h" +#include "babblekit/sync.h" +#include "babblekit/testcase.h" + +#include +LOG_MODULE_DECLARE(bt_bsim_bas, CONFIG_BT_BAS_LOG_LEVEL); + +static struct bt_conn *default_conn; + +static struct k_work_delayable update_bas_char_work; + +/* + * Battery Service test: + * We expect a central to connect to us. + */ + +#define WAIT_TIME 10 /*seconds*/ + +extern enum bst_result_t bst_result; + +static void test_bas_peripheral_init(void) +{ + bst_ticker_set_next_tick_absolute(WAIT_TIME * 1e6); + bst_result = In_progress; +} + +static void test_bas_peripheral_tick(bs_time_t HW_device_time) +{ + /* + * If in WAIT_TIME seconds the testcase did not already pass + * (and finish) we consider it failed + */ + if (bst_result != Passed) { + TEST_FAIL("test_bas_peripheral failed (not passed after %i seconds)\n", WAIT_TIME); + } +} + +static const struct bt_data ad[] = { + BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), + BT_DATA_BYTES(BT_DATA_UUID16_ALL, BT_UUID_16_ENCODE(BT_UUID_BAS_VAL)), + BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, sizeof(CONFIG_BT_DEVICE_NAME) - 1), +}; + +static void connected(struct bt_conn *conn, uint8_t err) +{ + if (err) { + TEST_FAIL("Connection failed (err 0x%02x)\n", err); + } else { + default_conn = bt_conn_ref(conn); + + LOG_DBG("Peripheral Connected\n"); + } +} + +static void disconnected(struct bt_conn *conn, uint8_t reason) +{ + LOG_DBG("Peripheral %s (reason 0x%02x)\n", __func__, reason); + + if (default_conn) { + bt_conn_unref(default_conn); + default_conn = NULL; + } +} + +static struct bt_conn_cb conn_callbacks = { + .connected = connected, + .disconnected = disconnected, +}; + +static void bt_ready(void) +{ + int err; + + LOG_DBG("Peripheral Bluetooth initialized\n"); + + err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), NULL, 0); + if (err) { + TEST_FAIL("Advertising failed to start (err %d)\n", err); + return; + } + + LOG_DBG("Advertising successfully started"); +} + +static void update_bas_char(void) +{ + LOG_DBG("[PERIPHERAL] setting battery level"); + bt_bas_set_battery_level(90); + LOG_DBG("[PERIPHERAL] setting battery present"); + bt_bas_bls_set_battery_present(BT_BAS_BLS_BATTERY_PRESENT); + LOG_DBG("[PERIPHERAL] setting battery charge level"); + bt_bas_bls_set_battery_charge_level(BT_BAS_BLS_CHARGE_LEVEL_CRITICAL); + LOG_DBG("[PERIPHERAL] setting battery service required true"); + bt_bas_bls_set_service_required(BT_BAS_BLS_SERVICE_REQUIRED_TRUE); + LOG_DBG("[PERIPHERAL] setting battery service charge type "); + bt_bas_bls_set_battery_charge_type(BT_BAS_BLS_CHARGE_TYPE_FLOAT); +} + +/* Work handler function */ +void update_bas_char_work_handler(struct k_work *work) +{ + update_bas_char(); + k_work_reschedule(&update_bas_char_work, K_SECONDS(1)); +} + +static void test_bas_peripheral_main(void) +{ + int err; + + bt_conn_cb_register(&conn_callbacks); + + /* Mark test as in progress. */ + TEST_START("peripheral"); + + /* Initialize device sync library */ + bk_sync_init(); + + /* Initialize Bluetooth */ + err = bt_enable(NULL); + TEST_ASSERT(err == 0, "Can't enable Bluetooth (err %d)", err); + + LOG_DBG("Bluetooth initialized"); + + bt_ready(); + + /* Initialize the update bas char work handler */ + k_work_init_delayable(&update_bas_char_work, update_bas_char_work_handler); + + /* Schedule the update bas char work for delayed execution */ + k_work_schedule(&update_bas_char_work, K_SECONDS(1)); + + /* Main thread waits for the sync signal from other device */ + bk_sync_wait(); + + bst_result = Passed; + TEST_PASS_AND_EXIT("Peripheral Test Passed"); +} + +static const struct bst_test_instance test_bas_peripheral[] = { + { + .test_id = "peripheral", + .test_descr = "Battery Service test. It expects that a central device can be found " + "The test will pass if ind/ntf can be sent without crash. ", + .test_pre_init_f = test_bas_peripheral_init, + .test_tick_f = test_bas_peripheral_tick, + .test_main_f = test_bas_peripheral_main, + }, + BSTEST_END_MARKER, +}; + +struct bst_test_list *test_bas_peripheral_install(struct bst_test_list *tests) +{ + tests = bst_add_tests(tests, test_bas_peripheral); + return tests; +} diff --git a/tests/bsim/bluetooth/samples/battery_service/tests_scripts/bas.sh b/tests/bsim/bluetooth/samples/battery_service/tests_scripts/bas.sh new file mode 100755 index 00000000000..7a80f61a06a --- /dev/null +++ b/tests/bsim/bluetooth/samples/battery_service/tests_scripts/bas.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Copyright 2024 Demant A/S +# SPDX-License-Identifier: Apache-2.0 + +source ${ZEPHYR_BASE}/tests/bsim/sh_common.source + +# Battery service test: a central connects to a peripheral and expects a +# indication/notification of BAS chars from peripheral +simulation_id="battery_service_test" +verbosity_level=2 + +cd ${BSIM_OUT_PATH}/bin + +Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_samples_battery_service_prj_conf \ + -v=${verbosity_level} -s=${simulation_id} -d=0 \ + -testid=peripheral -rs=23 + +Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_samples_battery_service_prj_conf\ + -v=${verbosity_level} -s=${simulation_id} -d=1 \ + -testid=central -rs=6 + +Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_samples_battery_service_prj_conf\ + -v=${verbosity_level} -s=${simulation_id} -d=2 \ + -testid=central -rs=6 + +Execute ./bs_2G4_phy_v1 -v=${verbosity_level} -s=${simulation_id} \ + -D=3 -sim_length=10e6 $@ + +wait_for_background_jobs diff --git a/tests/bsim/bluetooth/samples/compile.sh b/tests/bsim/bluetooth/samples/compile.sh index 3b61bdf1779..98a6e9d4118 100755 --- a/tests/bsim/bluetooth/samples/compile.sh +++ b/tests/bsim/bluetooth/samples/compile.sh @@ -37,5 +37,10 @@ app=tests/bsim/bluetooth/samples/central_hr_peripheral_hr \ extra_conf_file=${ZEPHYR_BASE}/samples/bluetooth/central_hr/prj.conf \ conf_overlay=${ZEPHYR_BASE}/samples/bluetooth/central_hr/overlay-phy_coded.conf \ compile +if [ ${BOARD} == "nrf52_bsim" ]; then + app=tests/bsim/bluetooth/samples/battery_service \ + conf_file=prj.conf \ + compile +fi wait_for_background_jobs diff --git a/tests/bsim/bluetooth/tests.nrf5340bsim_nrf5340_cpuapp.txt b/tests/bsim/bluetooth/tests.nrf5340bsim_nrf5340_cpuapp.txt index fec0c4a6002..749a42d23d4 100644 --- a/tests/bsim/bluetooth/tests.nrf5340bsim_nrf5340_cpuapp.txt +++ b/tests/bsim/bluetooth/tests.nrf5340bsim_nrf5340_cpuapp.txt @@ -2,5 +2,5 @@ # This file is used in CI to select which tests are run tests/bsim/bluetooth/ll/conn/tests_scripts/basic_conn_encrypted_split_privacy.sh tests/bsim/bluetooth/ll/bis/tests_scripts/broadcast_iso.sh -tests/bsim/bluetooth/samples/ +tests/bsim/bluetooth/samples/central_hr_peripheral_hr/ tests/bsim/bluetooth/audio_samples/