From 561903803bf0a6566ddcd34802d98c6aa09329d5 Mon Sep 17 00:00:00 2001 From: Sondre Pettersen Date: Fri, 17 Apr 2026 16:20:47 +0200 Subject: [PATCH 1/3] bluetooth: services: ble_hrs: fix on_connect considering central role Fix connected event handling to ignore connections where the local device have the central role. Disconnect events should only reset the ble_hrs stored conn_handle if the disconnect event carries the same conn_handle. Co-authored-by: Andreas Moltumyr Co-authored-by: Asil Zogby Signed-off-by: Sondre Pettersen --- subsys/bluetooth/services/ble_hrs/hrs.c | 9 ++- .../bluetooth/services/ble_hrs/CMakeLists.txt | 2 +- .../services/ble_hrs/src/unity_test.c | 60 ++++++++++++++++++- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/subsys/bluetooth/services/ble_hrs/hrs.c b/subsys/bluetooth/services/ble_hrs/hrs.c index 4258be544e..d4e055d138 100644 --- a/subsys/bluetooth/services/ble_hrs/hrs.c +++ b/subsys/bluetooth/services/ble_hrs/hrs.c @@ -149,13 +149,20 @@ static uint32_t body_sensor_location_char_add(struct ble_hrs *hrs, const struct static void on_connect(struct ble_hrs *hrs, const ble_gap_evt_t *gap_evt) { + if (gap_evt->params.connected.role != BLE_GAP_ROLE_PERIPH) { + return; + } + hrs->max_hrm_len = MAX_HRM_LEN_CALC(BLE_GATT_ATT_MTU_DEFAULT); hrs->conn_handle = gap_evt->conn_handle; } static void on_disconnect(struct ble_hrs *hrs, const ble_gap_evt_t *gap_evt) { - ARG_UNUSED(gap_evt); + if (gap_evt->conn_handle != hrs->conn_handle) { + return; + } + hrs->conn_handle = BLE_CONN_HANDLE_INVALID; } diff --git a/tests/unit/subsys/bluetooth/services/ble_hrs/CMakeLists.txt b/tests/unit/subsys/bluetooth/services/ble_hrs/CMakeLists.txt index 89792a793e..36cfb5b1d2 100644 --- a/tests/unit/subsys/bluetooth/services/ble_hrs/CMakeLists.txt +++ b/tests/unit/subsys/bluetooth/services/ble_hrs/CMakeLists.txt @@ -10,7 +10,7 @@ find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(unit_test_ble_hrs) -set(SOFTDEVICE_VARIANT "s115") +set(SOFTDEVICE_VARIANT "s145") set(SOFTDEVICE_INCLUDE_DIR "${ZEPHYR_NRF_BM_MODULE_DIR}/components/softdevice/nrf54l/\ ${SOFTDEVICE_VARIANT}/${SOFTDEVICE_VARIANT}_API/include") diff --git a/tests/unit/subsys/bluetooth/services/ble_hrs/src/unity_test.c b/tests/unit/subsys/bluetooth/services/ble_hrs/src/unity_test.c index 869e506ea2..00bb2c5a89 100644 --- a/tests/unit/subsys/bluetooth/services/ble_hrs/src/unity_test.c +++ b/tests/unit/subsys/bluetooth/services/ble_hrs/src/unity_test.c @@ -78,13 +78,16 @@ static uint32_t stub_sd_ble_gatts_hvx_capture(uint16_t conn_handle, return NRF_SUCCESS; } -void test_ble_hrs_on_ble_evt_disconnect(void) +void test_ble_hrs_on_ble_evt_connect_disconnect_role_periph(void) { - /* Simulate connect then disconnect, verify conn_handle resets to invalid. */ - struct ble_hrs hrs = {0}; + /* Simulate connect then disconnect with peripheral role. + * Verify that the conn_handle resets to invalid. + */ + struct ble_hrs hrs = {.conn_handle = BLE_CONN_HANDLE_INVALID}; const ble_evt_t connect_evt = { .header.evt_id = BLE_GAP_EVT_CONNECTED, .evt.gap_evt.conn_handle = TEST_CONN_HANDLE, + .evt.gap_evt.params.connected.role = BLE_GAP_ROLE_PERIPH, }; const ble_evt_t disconnect_evt = { .header.evt_id = BLE_GAP_EVT_DISCONNECTED, @@ -100,6 +103,57 @@ void test_ble_hrs_on_ble_evt_disconnect(void) TEST_ASSERT_EQUAL(BLE_CONN_HANDLE_INVALID, hrs.conn_handle); } +void test_ble_hrs_on_ble_evt_connect_disconnect_role_periph_and_central(void) +{ + /* Simulate connect then disconnect with peripheral and central role. + * Verify that the conn_handle does not change with connected events with central role. + */ + + struct ble_hrs hrs = {.conn_handle = BLE_CONN_HANDLE_INVALID}; + const ble_evt_t connect_periph_evt = { + .header.evt_id = BLE_GAP_EVT_CONNECTED, + .evt.gap_evt.conn_handle = TEST_CONN_HANDLE, + .evt.gap_evt.params.connected.role = BLE_GAP_ROLE_PERIPH, + }; + const ble_evt_t connect_central_evt = { + .header.evt_id = BLE_GAP_EVT_CONNECTED, + .evt.gap_evt.conn_handle = TEST_CONN_HANDLE + 1, + .evt.gap_evt.params.connected.role = BLE_GAP_ROLE_CENTRAL, + }; + const ble_evt_t disconnect_periph_evt = { + .header.evt_id = BLE_GAP_EVT_DISCONNECTED, + .evt.gap_evt.conn_handle = TEST_CONN_HANDLE, + }; + const ble_evt_t disconnect_central_evt = { + .header.evt_id = BLE_GAP_EVT_DISCONNECTED, + .evt.gap_evt.conn_handle = TEST_CONN_HANDLE + 1, + }; + + /* Connect with central role and expect conn_handle to stay invalid. */ + ble_hrs_on_ble_evt(&connect_central_evt, &hrs); + TEST_ASSERT_EQUAL(BLE_CONN_HANDLE_INVALID, hrs.conn_handle); + + /* Disconnect with central role and expect conn_handle to stay invalid. */ + ble_hrs_on_ble_evt(&disconnect_central_evt, &hrs); + TEST_ASSERT_EQUAL(BLE_CONN_HANDLE_INVALID, hrs.conn_handle); + + /* Connect with peripheral role so conn_handle is valid. */ + ble_hrs_on_ble_evt(&connect_periph_evt, &hrs); + TEST_ASSERT_EQUAL(TEST_CONN_HANDLE, hrs.conn_handle); + + /* Connect with central role and expect conn_handle to not change. */ + ble_hrs_on_ble_evt(&connect_central_evt, &hrs); + TEST_ASSERT_EQUAL(TEST_CONN_HANDLE, hrs.conn_handle); + + /* Disconnect with central role and expect conn_handle to not change. */ + ble_hrs_on_ble_evt(&disconnect_central_evt, &hrs); + TEST_ASSERT_EQUAL(TEST_CONN_HANDLE, hrs.conn_handle); + + /* Disconnect with peripheral role and expect conn_handle to change to invalid. */ + ble_hrs_on_ble_evt(&disconnect_periph_evt, &hrs); + TEST_ASSERT_EQUAL(BLE_CONN_HANDLE_INVALID, hrs.conn_handle); +} + void test_ble_hrs_on_ble_evt_disconnect_when_already_disconnected(void) { /* Receiving a disconnect event when not connected should be safely ignored. */ From 98414f27c9f3941ec69080a348c2ddf6f4c4465e Mon Sep 17 00:00:00 2001 From: Sondre Pettersen Date: Fri, 17 Apr 2026 16:23:19 +0200 Subject: [PATCH 2/3] bluetooth: services: ble_hrs_client: add bsl event Add body sensor location event. Signed-off-by: Sondre Pettersen Co-authored-by: Asil Zogby --- .../release_notes/release_notes_changelog.rst | 6 +- .../bm/bluetooth/services/ble_hrs_client.h | 10 +++ .../services/ble_hrs_client/ble_hrs_client.c | 67 +++++++++++++++---- .../services/ble_hrs_client/CMakeLists.txt | 2 +- .../services/ble_hrs_client/src/unity_test.c | 1 + 5 files changed, 72 insertions(+), 14 deletions(-) diff --git a/doc/nrf-bm/release_notes/release_notes_changelog.rst b/doc/nrf-bm/release_notes/release_notes_changelog.rst index 6e6ef0adff..f53c875455 100644 --- a/doc/nrf-bm/release_notes/release_notes_changelog.rst +++ b/doc/nrf-bm/release_notes/release_notes_changelog.rst @@ -84,10 +84,14 @@ Libraries Bluetooth LE Services --------------------- -* :ref:`lib_ble_scan` +* :ref:`lib_ble_scan`: * Changed :c:member:`ble_scan_filter_data.addr_filter.addr` and :c:member:`ble_scan_filter_data.name_filter.name` to ``const`` in the :c:struct:`ble_scan_filter_data` structure. +* :ref:`lib_ble_service_hrs_client`: + + * Added the :c:enumerator:`BLE_HRS_CLIENT_EVT_BSL_UPDATE` event to the :c:enum:`ble_hrs_client_evt_type` enum. + Libraries for NFC ----------------- diff --git a/include/bm/bluetooth/services/ble_hrs_client.h b/include/bm/bluetooth/services/ble_hrs_client.h index 0e82023f59..a1e5b1ed05 100644 --- a/include/bm/bluetooth/services/ble_hrs_client.h +++ b/include/bm/bluetooth/services/ble_hrs_client.h @@ -56,6 +56,10 @@ enum ble_hrs_client_evt_type { * received from the peer. */ BLE_HRS_CLIENT_EVT_HRM_NOTIFICATION, + /** Event indicating that the Body Sensor Location characteristic value was received from + * the peer. + */ + BLE_HRS_CLIENT_EVT_BSL_UPDATE, /** Error. */ BLE_HRS_CLIENT_EVT_ERROR, }; @@ -80,6 +84,8 @@ struct ble_hrs_handles { uint16_t hrm_cccd_handle; /** Handle of the Heart Rate Measurement characteristic, as provided by the SoftDevice. */ uint16_t hrm_handle; + /** Handle of the Body Sensor Location characteristic, as provided by the SoftDevice. */ + uint16_t bsl_handle; }; /** @@ -98,6 +104,10 @@ struct ble_hrs_client_evt { } discovery_complete; /** @ref BLE_HRS_CLIENT_EVT_HRM_NOTIFICATION event data. */ struct ble_hrs_measurement hrm_notification; + /** @ref BLE_HRS_CLIENT_EVT_BSL_UPDATE event data. */ + struct { + uint8_t body_sensor_location; + } bsl_update; /** @ref BLE_HRS_CLIENT_EVT_ERROR event data. */ struct { /** Error reason */ diff --git a/subsys/bluetooth/services/ble_hrs_client/ble_hrs_client.c b/subsys/bluetooth/services/ble_hrs_client/ble_hrs_client.c index d7ddb9352e..4166c99478 100644 --- a/subsys/bluetooth/services/ble_hrs_client/ble_hrs_client.c +++ b/subsys/bluetooth/services/ble_hrs_client/ble_hrs_client.c @@ -119,6 +119,7 @@ static void on_disconnected(struct ble_hrs_client *hrs_client, const ble_evt_t * hrs_client->conn_handle = BLE_CONN_HANDLE_INVALID; hrs_client->handles.hrm_cccd_handle = BLE_GATT_HANDLE_INVALID; hrs_client->handles.hrm_handle = BLE_GATT_HANDLE_INVALID; + hrs_client->handles.bsl_handle = BLE_GATT_HANDLE_INVALID; } } @@ -128,12 +129,13 @@ void ble_hrs_on_db_disc_evt(struct ble_hrs_client *hrs_client, __ASSERT(hrs_client, "HRS client instance is NULL"); __ASSERT(db_discovery_evt, "Discovery event is NULL"); + uint32_t nrf_err; const struct ble_gatt_db_char *db_char; struct ble_hrs_client_evt hrs_client_evt = { .evt_type = BLE_HRS_CLIENT_EVT_DISCOVERY_COMPLETE, .conn_handle = db_discovery_evt->conn_handle, }; - struct ble_hrs_handles *handles = &hrs_client->handles; + struct ble_hrs_handles *const evt_handles = &hrs_client_evt.discovery_complete.handles; /* Check if the Heart Rate Service was discovered. */ if (db_discovery_evt->evt_type != BLE_DB_DISCOVERY_COMPLETE || @@ -142,27 +144,40 @@ void ble_hrs_on_db_disc_evt(struct ble_hrs_client *hrs_client, return; } - /* Find the CCCD Handle of the Heart Rate Measurement characteristic. */ + /* Iterate discovered characteristics. */ for (uint32_t i = 0; i < db_discovery_evt->discovered_db->char_count; i++) { db_char = &db_discovery_evt->discovered_db->characteristics[i]; if (db_char->characteristic.uuid.uuid == BLE_UUID_HEART_RATE_MEASUREMENT_CHAR) { - /* Found Heart Rate characteristic. Store CCCD handle and break. */ - hrs_client_evt.discovery_complete.handles.hrm_cccd_handle = - db_char->cccd_handle; - hrs_client_evt.discovery_complete.handles.hrm_handle = - db_char->characteristic.handle_value; - break; + /* Found Heart Rate characteristic. Store value and CCCD handle for later + * notification setup. + */ + evt_handles->hrm_cccd_handle = db_char->cccd_handle; + evt_handles->hrm_handle = db_char->characteristic.handle_value; + + } else if (db_char->characteristic.uuid.uuid == + BLE_UUID_BODY_SENSOR_LOCATION_CHAR) { + /* Found body sensor location characteristic. Store value handle and issue + * a read request. The value will arrive asynchronously in a + * BLE_GATTC_EVT_READ_RSP event. + */ + evt_handles->bsl_handle = db_char->characteristic.handle_value; + + nrf_err = sd_ble_gattc_read(db_discovery_evt->conn_handle, + db_char->characteristic.handle_value, 0); + if (nrf_err) { + LOG_ERR("Failed to read bsl char value, nrf_err %#x", nrf_err); + } } } LOG_DBG("HRS discovered"); - /* If the instance has been assigned prior to db_discovery, assign the db_handles. */ + /* If the instance has been assigned prior to db_discovery, do not assign the handles. */ if (hrs_client->conn_handle != BLE_CONN_HANDLE_INVALID) { - if ((handles->hrm_cccd_handle == BLE_GATT_HANDLE_INVALID) && - (handles->hrm_handle == BLE_GATT_HANDLE_INVALID)) { - hrs_client->handles = hrs_client_evt.discovery_complete.handles; + if ((hrs_client->handles.hrm_cccd_handle == BLE_GATT_HANDLE_INVALID) && + (hrs_client->handles.hrm_handle == BLE_GATT_HANDLE_INVALID)) { + hrs_client->handles = *evt_handles; } } @@ -194,6 +209,31 @@ uint32_t ble_hrs_client_init(struct ble_hrs_client *hrs_client, return ble_db_discovery_service_register(hrs_client_config->db_discovery, &hrs_uuid); } +static void on_read_rsp(struct ble_hrs_client *hrs_client, const ble_evt_t *ble_evt) +{ + const ble_gattc_evt_read_rsp_t *read_rsp = &ble_evt->evt.gattc_evt.params.read_rsp; + struct ble_hrs_client_evt evt = { + .evt_type = BLE_HRS_CLIENT_EVT_BSL_UPDATE, + .conn_handle = ble_evt->evt.gattc_evt.conn_handle, + .bsl_update.body_sensor_location = ble_evt->evt.gattc_evt.params.read_rsp.data[0], + }; + + if (hrs_client->conn_handle != ble_evt->evt.gattc_evt.conn_handle) { + return; + } + + /* Check if this is a body sensor location response. */ + if (read_rsp->handle != hrs_client->handles.bsl_handle) { + return; + } + + if (read_rsp->len < 1) { + return; + } + + hrs_client->evt_handler(hrs_client, &evt); +} + void ble_hrs_client_on_ble_evt(const ble_evt_t *ble_evt, void *hrs_client) { __ASSERT(ble_evt, "BLE event is NULL"); @@ -206,6 +246,9 @@ void ble_hrs_client_on_ble_evt(const ble_evt_t *ble_evt, void *hrs_client) case BLE_GAP_EVT_DISCONNECTED: on_disconnected(hrs_client, ble_evt); break; + case BLE_GATTC_EVT_READ_RSP: + on_read_rsp(hrs_client, ble_evt); + break; default: break; } diff --git a/tests/unit/subsys/bluetooth/services/ble_hrs_client/CMakeLists.txt b/tests/unit/subsys/bluetooth/services/ble_hrs_client/CMakeLists.txt index 489e5f7c59..3ac4435452 100644 --- a/tests/unit/subsys/bluetooth/services/ble_hrs_client/CMakeLists.txt +++ b/tests/unit/subsys/bluetooth/services/ble_hrs_client/CMakeLists.txt @@ -10,7 +10,7 @@ find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(unit_test_ble_hrs_client) -set(SOFTDEVICE_VARIANT "s115") +set(SOFTDEVICE_VARIANT "s145") set(SOFTDEVICE_INCLUDE_DIR "${ZEPHYR_NRF_BM_MODULE_DIR}/components/softdevice/nrf54l/\ ${SOFTDEVICE_VARIANT}/${SOFTDEVICE_VARIANT}_API/include") diff --git a/tests/unit/subsys/bluetooth/services/ble_hrs_client/src/unity_test.c b/tests/unit/subsys/bluetooth/services/ble_hrs_client/src/unity_test.c index 5688a247ba..9d75a110f7 100644 --- a/tests/unit/subsys/bluetooth/services/ble_hrs_client/src/unity_test.c +++ b/tests/unit/subsys/bluetooth/services/ble_hrs_client/src/unity_test.c @@ -520,6 +520,7 @@ void test_ble_hrs_on_db_disc_evt_hrm_char_at_index_one(void) __cmock_ble_db_discovery_service_register_ExpectAndReturn(&db_discovery, NULL, NRF_SUCCESS); __cmock_ble_db_discovery_service_register_IgnoreArg_uuid(); + __cmock_sd_ble_gattc_read_ExpectAnyArgsAndReturn(NRF_SUCCESS); nrf_err = ble_hrs_client_init(&hrs_client, &config); TEST_ASSERT_EQUAL(NRF_SUCCESS, nrf_err); From 3bf3cce72804294b1669ec7d766160ce57df4123 Mon Sep 17 00:00:00 2001 From: Sondre Pettersen Date: Fri, 20 Mar 2026 09:43:19 +0100 Subject: [PATCH 3/3] samples: bluetooth: add ble_hrs_peripheral_central sample Add the hrs_peripheral_central sample. Co-authored-by: Andreas Moltumyr Co-authored-by: Asil Zogby Signed-off-by: Sondre Pettersen --- .../release_notes/release_notes_changelog.rst | 2 + .../ble_hrs_peripheral_central/CMakeLists.txt | 12 + .../ble_hrs_peripheral_central/Kconfig | 43 + .../ble_hrs_peripheral_central/README.rst | 135 +++ .../ble_hrs_peripheral_central/prj.conf | 67 ++ .../ble_hrs_peripheral_central/sample.yaml | 22 + .../ble_hrs_peripheral_central/src/main.c | 909 ++++++++++++++++++ 7 files changed, 1190 insertions(+) create mode 100644 samples/bluetooth/ble_hrs_peripheral_central/CMakeLists.txt create mode 100644 samples/bluetooth/ble_hrs_peripheral_central/Kconfig create mode 100644 samples/bluetooth/ble_hrs_peripheral_central/README.rst create mode 100644 samples/bluetooth/ble_hrs_peripheral_central/prj.conf create mode 100644 samples/bluetooth/ble_hrs_peripheral_central/sample.yaml create mode 100644 samples/bluetooth/ble_hrs_peripheral_central/src/main.c diff --git a/doc/nrf-bm/release_notes/release_notes_changelog.rst b/doc/nrf-bm/release_notes/release_notes_changelog.rst index f53c875455..27123b0c9c 100644 --- a/doc/nrf-bm/release_notes/release_notes_changelog.rst +++ b/doc/nrf-bm/release_notes/release_notes_changelog.rst @@ -114,6 +114,8 @@ Peripheral samples Bluetooth LE samples -------------------- +* Added the :ref:`ble_hrs_peripheral_central_sample` sample. + * Updated the following samples and applications that do not support pairing to call the :c:func:`sd_ble_gatts_sys_attr_set` function only in response to the :c:macro:`BLE_GATTS_EVT_SYS_ATTR_MISSING` event and not as a response to a :c:macro:`BLE_GAP_EVT_CONNECTED` event: * :ref:`ug_dfu_firmware_loader` (Bluetooth LE) diff --git a/samples/bluetooth/ble_hrs_peripheral_central/CMakeLists.txt b/samples/bluetooth/ble_hrs_peripheral_central/CMakeLists.txt new file mode 100644 index 0000000000..64c7774137 --- /dev/null +++ b/samples/bluetooth/ble_hrs_peripheral_central/CMakeLists.txt @@ -0,0 +1,12 @@ +# +# Copyright (c) 2026 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(ble_hrs_peripheral_central) + +target_sources(app PRIVATE src/main.c) diff --git a/samples/bluetooth/ble_hrs_peripheral_central/Kconfig b/samples/bluetooth/ble_hrs_peripheral_central/Kconfig new file mode 100644 index 0000000000..1b015d6dce --- /dev/null +++ b/samples/bluetooth/ble_hrs_peripheral_central/Kconfig @@ -0,0 +1,43 @@ +# +# Copyright (c) 2026 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menu "Bluetooth LE HRS peripheral central sample" + +config SAMPLE_BLE_DEVICE_NAME + string "Device name" + default "nRF_BM_HRS_bridge" + +config SAMPLE_USE_TARGET_PERIPHERAL_NAME + bool "Use target peripheral name" + default y + +if SAMPLE_USE_TARGET_PERIPHERAL_NAME + +config SAMPLE_TARGET_PERIPHERAL_NAME + string "Target peripheral name" + default "nRF_BM_HRS" + +endif # SAMPLE_USE_TARGET_PERIPHERAL_NAME + +config SAMPLE_USE_TARGET_PERIPHERAL_ADDR + bool "Use target peripheral address" + +if SAMPLE_USE_TARGET_PERIPHERAL_ADDR + +config SAMPLE_TARGET_PERIPHERAL_ADDR + hex "Target peripheral address" + range 0x0 0xffffffffffff + default 0xD627FDA7AE54 + +endif # SAMPLE_USE_TARGET_PERIPHERAL_ADDR + +module=SAMPLE_BLE_HRS_PERIPHERAL_CENTRAL +module-str=BLE Heart Rate Peripheral Central Service Sample +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +endmenu # "Bluetooth LE HRS peripheral central sample" + +source "Kconfig.zephyr" diff --git a/samples/bluetooth/ble_hrs_peripheral_central/README.rst b/samples/bluetooth/ble_hrs_peripheral_central/README.rst new file mode 100644 index 0000000000..16cdcf33a4 --- /dev/null +++ b/samples/bluetooth/ble_hrs_peripheral_central/README.rst @@ -0,0 +1,135 @@ +.. _ble_hrs_peripheral_central_sample: + +Bluetooth: Heart Rate Service Peripheral Central +################################################ + +.. contents:: + :local: + :depth: 2 + +The Heart Rate Service Peripheral Central sample demonstrates how you can implement the Heart Rate profile as a server and client using |BMlong|. + +Requirements +************ + +The sample supports the following development kits: + +.. tabs:: + + .. group-tab:: Simple board variants + + The following board variants do **not** have DFU capabilities: + + .. include:: /includes/supported_boards_all_non-mcuboot_variants_s145.txt + + .. group-tab:: MCUboot board variants + + The following board variants have DFU capabilities: + + .. include:: /includes/supported_boards_all_mcuboot_variants_s145.txt + +Overview +******** + +This sample acts simultaneously as both a peripheral and a central device. + +* As a peripheral it advertises with the :ref:`lib_ble_service_hrs` UUID (0x180D). + A central can connect to this device and subscribe to the Heart Rate Measurement characteristic to receive heart rate notifications. +* As a central the sample scans for other devices that advertise with the :ref:`lib_ble_service_hrs` UUID (0x180D). + When a device is found, it connects and starts service discovery. + If the heart rate service is found, it subscribes to receive heart rate notifications that will be forwarded to a connected central device. + +User interface +************** + +Button 0: + Press to disable allow list. + + When pairing with authentication, press this button to confirm the passkey shown in the COM listener and complete pairing with the other device. + +Button 1: + Press to disconnect from the connected peripheral device. + + Keep the button pressed while resetting the board to delete bonding information for all peers stored on the device. + + When pairing with authentication, press this button to reject the passkey shown in the COM listener to prevent pairing with the other device. + +Button 2: + Press to disconnect from the connected central device. + +LED 0: + Lit when the device is initialized. + +LED 1: + Lit when connected to a peripheral device. + +LED 2: + Lit when connected to a central device. + +.. _ble_hrs_peripheral_central_sample_testing: + +Building and running +******************** + +This sample can be found under :file:`samples/bluetooth/ble_hrs_peripheral_central/` in the |BMshort| folder structure. + +For details on how to create, configure, and program a sample, see :ref:`getting_started_with_the_samples`. + +Scan filtering options +====================== + +The sample always scans for devices advertising the Heart Rate Service UUID (``0x180D``). +Two optional filters can narrow this down further: + +.. list-table:: + :header-rows: 1 + + * - Kconfig option + - Matches on + * - :kconfig:option:`CONFIG_SAMPLE_USE_TARGET_PERIPHERAL_NAME` + - Advertised device name (e.g. ``"MyDeviceName"``) + * - :kconfig:option:`CONFIG_SAMPLE_USE_TARGET_PERIPHERAL_ADDR` + - Exact 48-bit Bluetooth address + +.. note:: + + Use of ``peripheral`` in the option name refers to the *remote* device being scanned for, not this device's role. + +Testing +======= + +This sample can be tested with three devices: + +* A device running this sample. +* A device running the :ref:`ble_hrs_sample` sample. +* A central device, for example, a phone or a tablet with `nRF Connect for Mobile`_ or `nRF Toolbox`_. + +Complete the following steps to test the sample: + +1. Compile and program the application. +#. Observe that the ``BLE HRS Peripheral Central sample initialized`` message is printed. +#. In the Serial Terminal, observe that the ``Advertising as nRF_BM_HRS_bridge`` message is printed. + You can configure the advertising name using the :kconfig:option:`CONFIG_SAMPLE_BLE_DEVICE_NAME` Kconfig option. + For information on how to do this, see `Configuring Kconfig`_. +#. Program the second development kit with the :ref:`ble_hrs_sample` sample. +#. Observe that the ``Scan filter match`` message is printed, followed by ``Connecting to target`` and ``Connected``, when connecting to the peripheral device. +#. Observe that the ``Heart rate service discovered`` message is printed. +#. Connect to the device from nRF Connect (the device is advertising as "nRF_BM_HRS_bridge"). +#. Observe that the ``Connecting to target`` and ``Connected`` messages are printed when connecting to the central device. +#. Observe that the device starts receiving heart rate measurement notifications and forwarding them to the central. +#. Note the address printed in the log when connecting, e.g: + + .. code-block:: console + + Connecting to target AA:BB:CC:DD:EE:FF + +#. Enable the address filter in Kconfig with: + + .. code-block:: cfg + + CONFIG_SAMPLE_USE_TARGET_PERIPHERAL_ADDR=y + CONFIG_SAMPLE_TARGET_PERIPHERAL_ADDR=0xAABBCCDDEEFF + + Rebuild and flash. Confirm the sample still connects to the same peripheral. +#. Change the address to a wrong value, rebuild and flash. Confirm the sample no + longer connects to any peripheral. diff --git a/samples/bluetooth/ble_hrs_peripheral_central/prj.conf b/samples/bluetooth/ble_hrs_peripheral_central/prj.conf new file mode 100644 index 0000000000..37842ac001 --- /dev/null +++ b/samples/bluetooth/ble_hrs_peripheral_central/prj.conf @@ -0,0 +1,67 @@ +# Logging +CONFIG_LOG=y +CONFIG_LOG_BACKEND_BM_UARTE=y + +# SoftDevice +CONFIG_SOFTDEVICE=y + +# SoftDevice handler link counts +CONFIG_NRF_SDH_BLE_TOTAL_LINK_COUNT=2 +CONFIG_NRF_SDH_BLE_CENTRAL_LINK_COUNT=1 + +# Enable RNG +CONFIG_NRF_SECURITY=y +CONFIG_MBEDTLS_PSA_CRYPTO_C=y +CONFIG_PSA_WANT_GENERATE_RANDOM=y + +# Enable Crypto functionality required by LE Secure Connections pairing (ECDH over NIST P-256) +CONFIG_PSA_WANT_ALG_ECDH=y +CONFIG_PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_GENERATE=y +CONFIG_PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_IMPORT=y +CONFIG_PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_EXPORT=y +CONFIG_PSA_WANT_ECC_SECP_R1_256=y +# PSA key storage: one slot per concurrent pairing +CONFIG_MBEDTLS_PSA_STATIC_KEY_SLOTS=y +CONFIG_MBEDTLS_PSA_KEY_SLOT_COUNT=2 + +# Button and timer +CONFIG_BM_BUTTONS=y +CONFIG_BM_GPIOTE=y +CONFIG_BM_TIMER=y + +# BLE Heart rate client +CONFIG_BLE_HRS_CLIENT=y + +# BLE Heart rate server +CONFIG_BLE_HRS=y + +# BLE BAS client +CONFIG_BLE_BAS_CLIENT=y + +# BLE BAS server +CONFIG_BLE_BAS=y + +# BLE connection parameter +CONFIG_BLE_CONN_PARAMS=y + +# BLE database discovery +CONFIG_BLE_DB_DISCOVERY=y +CONFIG_BLE_GATT_QUEUE=y + +# BLE scan +CONFIG_BLE_SCAN=y +CONFIG_BLE_SCAN_UUID_COUNT=2 +CONFIG_BLE_SCAN_ADDRESS_COUNT=1 +CONFIG_BLE_ADV_DATA=y + +# Advertising library +CONFIG_BLE_ADV=y +CONFIG_BLE_ADV_RESTART_ON_DISCONNECT=n + +# Peer manager +CONFIG_PEER_MANAGER=y +CONFIG_PM_LESC=y +CONFIG_BM_ZMS=y + +# GATT queue: one queue per concurrent connection +CONFIG_BLE_GQ_MAX_CONNECTIONS=2 diff --git a/samples/bluetooth/ble_hrs_peripheral_central/sample.yaml b/samples/bluetooth/ble_hrs_peripheral_central/sample.yaml new file mode 100644 index 0000000000..8f4088fd05 --- /dev/null +++ b/samples/bluetooth/ble_hrs_peripheral_central/sample.yaml @@ -0,0 +1,22 @@ +sample: + name: Bluetooth LE Heart Rate Peripheral Central Service Sample +tests: + sample.ble_hrs_peripheral_central: + build_only: true + integration_platforms: + - bm_nrf54l15dk/nrf54l05/cpuapp/s145_softdevice + - bm_nrf54l15dk/nrf54l05/cpuapp/s145_softdevice/mcuboot + platform_allow: + - bm_nrf54l15dk/nrf54l05/cpuapp/s145_softdevice + - bm_nrf54l15dk/nrf54l05/cpuapp/s145_softdevice/mcuboot + - bm_nrf54l15dk/nrf54l10/cpuapp/s145_softdevice + - bm_nrf54l15dk/nrf54l10/cpuapp/s145_softdevice/mcuboot + - bm_nrf54l15dk/nrf54l15/cpuapp/s145_softdevice + - bm_nrf54l15dk/nrf54l15/cpuapp/s145_softdevice/mcuboot + - bm_nrf54lm20dk/nrf54lm20a/cpuapp/s145_softdevice + - bm_nrf54lm20dk/nrf54lm20a/cpuapp/s145_softdevice/mcuboot + - bm_nrf54ls05dk/nrf54ls05b/cpuapp/s145_softdevice + - bm_nrf54ls05dk/nrf54ls05b/cpuapp/s145_softdevice/mcuboot + - bm_nrf54lv10dk/nrf54lv10a/cpuapp/s145_softdevice + - bm_nrf54lv10dk/nrf54lv10a/cpuapp/s145_softdevice/mcuboot + tags: ci_build diff --git a/samples/bluetooth/ble_hrs_peripheral_central/src/main.c b/samples/bluetooth/ble_hrs_peripheral_central/src/main.c new file mode 100644 index 0000000000..4c735c6bf2 --- /dev/null +++ b/samples/bluetooth/ble_hrs_peripheral_central/src/main.c @@ -0,0 +1,909 @@ +/* + * Copyright (c) 2014-2026 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +LOG_MODULE_REGISTER(sample, CONFIG_SAMPLE_BLE_HRS_PERIPHERAL_CENTRAL_LOG_LEVEL); + +/* Advertising instance. */ +BLE_ADV_DEF(ble_adv); +/* Battery service instance. */ +BLE_BAS_DEF(ble_bas); +/* Battery service client instance. */ +BLE_BAS_CLIENT_DEF(ble_bas_client); +/* Database discovery instance. */ +BLE_DB_DISCOVERY_DEF(ble_db_disc); +/* GATT queue instance. */ +BLE_GQ_DEF(ble_gq); +/* Heart rate service instance. */ +BLE_HRS_DEF(ble_hrs); +/* Heart rate service client instance. */ +BLE_HRS_CLIENT_DEF(ble_hrs_client); +/* Scanning instance. */ +BLE_SCAN_DEF(ble_scan); + +/* Current connection handle for peer that acts as a central. */ +static uint16_t conn_handle_central = BLE_CONN_HANDLE_INVALID; +/* Current connection handle for peer that acts as a peripheral. */ +static uint16_t conn_handle_peripheral = BLE_CONN_HANDLE_INVALID; +/* True if allow list has been temporarily disabled. */ +static bool allow_list_disabled; +/* Authentication key request queue. */ +static uint16_t auth_key_requests[CONFIG_NRF_SDH_BLE_TOTAL_LINK_COUNT]; +/* Number of authentication key requests in queue. */ +static uint16_t auth_key_requests_num; + +#if defined(CONFIG_SAMPLE_USE_TARGET_PERIPHERAL_ADDR) +static const uint8_t target_periph_addr[BLE_GAP_ADDR_LEN] = { + (CONFIG_SAMPLE_TARGET_PERIPHERAL_ADDR) & 0xff, + (CONFIG_SAMPLE_TARGET_PERIPHERAL_ADDR >> 8) & 0xff, + (CONFIG_SAMPLE_TARGET_PERIPHERAL_ADDR >> 16) & 0xff, + (CONFIG_SAMPLE_TARGET_PERIPHERAL_ADDR >> 24) & 0xff, + (CONFIG_SAMPLE_TARGET_PERIPHERAL_ADDR >> 32) & 0xff, + (CONFIG_SAMPLE_TARGET_PERIPHERAL_ADDR >> 40) & 0xff, +}; +#endif /* CONFIG_SAMPLE_USE_TARGET_PERIPHERAL_ADDR */ + +static void delete_bonds(void) +{ + uint32_t nrf_err; + + LOG_INF("Erase bonds!"); + + nrf_err = pm_peers_delete(); + if (nrf_err) { + LOG_ERR("Failed to delete bonds, nrf_error %#x", nrf_err); + } +} + +static void peer_list_get(uint16_t *peers, uint32_t *size) +{ + uint16_t peer_id; + uint32_t peers_to_copy; + + peers_to_copy = (*size < BLE_GAP_WHITELIST_ADDR_MAX_COUNT) + ? *size + : BLE_GAP_WHITELIST_ADDR_MAX_COUNT; + + peer_id = pm_next_peer_id_get(PM_PEER_ID_INVALID); + *size = 0; + + while ((peer_id != PM_PEER_ID_INVALID) && (peers_to_copy--)) { + peers[(*size)++] = peer_id; + peer_id = pm_next_peer_id_get(peer_id); + } +} + +static uint32_t allow_list_load(void) +{ + uint32_t nrf_err; + uint16_t peers[BLE_GAP_WHITELIST_ADDR_MAX_COUNT]; + uint32_t peer_cnt; + + memset(peers, PM_PEER_ID_INVALID, sizeof(peers)); + peer_cnt = BLE_GAP_WHITELIST_ADDR_MAX_COUNT; + + peer_list_get(peers, &peer_cnt); + + nrf_err = pm_allow_list_set(peers, peer_cnt); + if (nrf_err) { + return nrf_err; + } + + nrf_err = pm_device_identities_list_set(peers, peer_cnt); + if (nrf_err != NRF_ERROR_NOT_SUPPORTED) { + return nrf_err; + } + + return NRF_SUCCESS; +} + +static uint32_t on_allow_list_req(void) +{ + uint32_t nrf_err; + + ble_gap_addr_t allow_list_addrs[BLE_GAP_WHITELIST_ADDR_MAX_COUNT]; + ble_gap_irk_t allow_list_irks[BLE_GAP_WHITELIST_ADDR_MAX_COUNT]; + uint32_t addr_cnt = BLE_GAP_WHITELIST_ADDR_MAX_COUNT; + uint32_t irk_cnt = BLE_GAP_WHITELIST_ADDR_MAX_COUNT; + + nrf_err = allow_list_load(); + if (nrf_err) { + LOG_ERR("Failed to delete bonds, nrf_error %#x", nrf_err); + return nrf_err; + } + + nrf_err = pm_allow_list_get(allow_list_addrs, &addr_cnt, allow_list_irks, &irk_cnt); + if (nrf_err) { + return nrf_err; + } + + if (((addr_cnt == 0) && (irk_cnt == 0)) || (allow_list_disabled)) { + /* Don't use allow list. */ + nrf_err = ble_scan_params_set(&ble_scan, NULL); + if (nrf_err) { + return nrf_err; + } + } + + return NRF_SUCCESS; +} + +static void scan_start(void) +{ + uint32_t nrf_err; + + nrf_err = ble_scan_start(&ble_scan); + if (nrf_err) { + LOG_ERR("Failed to start scanning, nrf_error %#x", nrf_err); + } else { + LOG_INF("Scanning"); + } +} + +static void advertising_start(void) +{ + uint32_t nrf_err; + + nrf_err = ble_adv_start(&ble_adv, BLE_ADV_MODE_FAST); + if (nrf_err) { + LOG_ERR("Failed to start advertising, nrf_error %#x", nrf_err); + } else { + LOG_INF("Advertising as %s", CONFIG_SAMPLE_BLE_DEVICE_NAME); + } +} + +static void allow_list_disable(void) +{ + if (!allow_list_disabled) { + LOG_INF("allow list temporarily disabled."); + allow_list_disabled = true; + scan_start(); + } +} + +static void on_ble_evt(const ble_evt_t *ble_evt, void *ctx) +{ + uint32_t nrf_err; + const ble_gap_evt_t *const gap_evt = &ble_evt->evt.gap_evt; + + switch (ble_evt->header.evt_id) { + case BLE_GAP_EVT_CONNECTED: + LOG_INF("Connected"); + if (gap_evt->params.connected.role == BLE_GAP_ROLE_CENTRAL) { + conn_handle_peripheral = gap_evt->conn_handle; + nrf_gpio_pin_write(BOARD_PIN_LED_1, BOARD_LED_ACTIVE_STATE); + + nrf_err = ble_db_discovery_start(&ble_db_disc, + ble_evt->evt.gap_evt.conn_handle); + if (nrf_err) { + LOG_ERR("db discovery start failed, nrf_error %#x", nrf_err); + } + } else if (gap_evt->params.connected.role == BLE_GAP_ROLE_PERIPH) { + conn_handle_central = gap_evt->conn_handle; + nrf_gpio_pin_write(BOARD_PIN_LED_2, BOARD_LED_ACTIVE_STATE); + } + break; + + case BLE_GAP_EVT_DISCONNECTED: + LOG_INF("Disconnected conn_handle %#x, reason %#x", + gap_evt->conn_handle, gap_evt->params.disconnected.reason); + + if (gap_evt->conn_handle == conn_handle_peripheral) { + conn_handle_peripheral = BLE_CONN_HANDLE_INVALID; + nrf_gpio_pin_write(BOARD_PIN_LED_1, !BOARD_LED_ACTIVE_STATE); + scan_start(); + } else if (gap_evt->conn_handle == conn_handle_central) { + conn_handle_central = BLE_CONN_HANDLE_INVALID; + nrf_gpio_pin_write(BOARD_PIN_LED_2, !BOARD_LED_ACTIVE_STATE); + advertising_start(); + } + break; + + case BLE_GAP_EVT_PASSKEY_DISPLAY: + LOG_INF("Passkey: %.*s", BLE_GAP_PASSKEY_LEN, + gap_evt->params.passkey_display.passkey); + if (gap_evt->params.passkey_display.match_request) { + LOG_INF("Pairing request, press button 0 to accept or button 1 to reject."); + if (auth_key_requests_num < ARRAY_SIZE(auth_key_requests)) { + auth_key_requests[auth_key_requests_num++] = gap_evt->conn_handle; + } + } + break; + + case BLE_GAP_EVT_TIMEOUT: + if (gap_evt->params.timeout.src == BLE_GAP_TIMEOUT_SRC_CONN) { + LOG_INF("Connection Request timed out"); + } + break; + + case BLE_GATTC_EVT_TIMEOUT: + LOG_INF("GATT Client Timeout."); + nrf_err = sd_ble_gap_disconnect(ble_evt->evt.gattc_evt.conn_handle, + BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION); + if (nrf_err) { + LOG_ERR("Failed to disconnect, nrf_error %#x", nrf_err); + } + break; + + case BLE_GATTS_EVT_TIMEOUT: + LOG_INF("GATT Server Timeout."); + nrf_err = sd_ble_gap_disconnect(ble_evt->evt.gatts_evt.conn_handle, + BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION); + if (nrf_err) { + LOG_ERR("Failed to disconnect, nrf_error %#x", nrf_err); + } + break; + + default: + break; + } +} +NRF_SDH_BLE_OBSERVER(sdh_ble, on_ble_evt, NULL, USER_LOW); + +static void db_disc_evt_handler(struct ble_db_discovery *db_discovery, + struct ble_db_discovery_evt *evt) +{ + ble_hrs_on_db_disc_evt(&ble_hrs_client, evt); + ble_bas_on_db_disc_evt(&ble_bas_client, evt); +} + +static void num_comp_reply(uint16_t conn_handle, bool accept) +{ + uint32_t nrf_err; + uint8_t key_type; + + if (accept) { + LOG_INF("Numeric Match. Conn handle %d", conn_handle); + key_type = BLE_GAP_AUTH_KEY_TYPE_PASSKEY; + } else { + LOG_INF("Numeric REJECT. Conn handle %d", conn_handle); + key_type = BLE_GAP_AUTH_KEY_TYPE_NONE; + } + + nrf_err = sd_ble_gap_auth_key_reply(conn_handle, key_type, NULL); + if (nrf_err) { + LOG_ERR("Failed to reply auth request, nrf_error %#x", nrf_err); + } +} + +static void button_handler(uint8_t pin, uint8_t action) +{ + uint32_t nrf_err; + uint16_t conn_handle_disconnect = BLE_CONN_HANDLE_INVALID; + + if (action != BM_BUTTONS_PRESS) { + return; + } + + /* Handle pairing match request yes/no. */ + if ((auth_key_requests_num > 0) && (pin == BOARD_PIN_BTN_0 || pin == BOARD_PIN_BTN_1)) { + num_comp_reply(auth_key_requests[0], (pin == BOARD_PIN_BTN_0)); + + /* Move to next auth key request in the array (if any). */ + auth_key_requests_num = MAX(0, auth_key_requests_num - 1); + memmove(&auth_key_requests[0], &auth_key_requests[1], auth_key_requests_num); + return; + } + + /* Handle regular button functionality. */ + switch (pin) { + case BOARD_PIN_BTN_0: + LOG_INF("Button allow list off"); + allow_list_disable(); + break; + case BOARD_PIN_BTN_1: + if (conn_handle_peripheral != BLE_CONN_HANDLE_INVALID) { + LOG_INF("Button disconnect peripheral"); + conn_handle_disconnect = conn_handle_peripheral; + } + break; + case BOARD_PIN_BTN_2: + if (conn_handle_central != BLE_CONN_HANDLE_INVALID) { + LOG_INF("Button disconnect central"); + conn_handle_disconnect = conn_handle_central; + } + break; + default: + break; + } + + if (conn_handle_disconnect != BLE_CONN_HANDLE_INVALID) { + nrf_err = sd_ble_gap_disconnect(conn_handle_disconnect, + BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION); + if (nrf_err) { + LOG_ERR("ble gap disconnect failed, nrf_error %#x", nrf_err); + } + } +} + +static void pm_evt_handler(const struct pm_evt *evt) +{ + pm_handler_on_pm_evt(evt); + pm_handler_disconnect_on_sec_failure(evt); + pm_handler_flash_clean(evt); + + switch (evt->evt_id) { + case PM_EVT_PEERS_DELETE_SUCCEEDED: + scan_start(); + advertising_start(); + break; + + default: + break; + } +} + +static void ble_adv_evt_handler(struct ble_adv *adv, const struct ble_adv_evt *adv_evt) +{ + switch (adv_evt->evt_type) { + case BLE_ADV_EVT_ERROR: + LOG_ERR("Advertising error %#x", adv_evt->error.reason); + break; + default: + break; + } +} + +static void hrs_client_evt_handler(struct ble_hrs_client *hrs, const struct ble_hrs_client_evt *evt) +{ + + uint32_t nrf_err; + + switch (evt->evt_type) { + case BLE_HRS_CLIENT_EVT_DISCOVERY_COMPLETE: + LOG_INF("Heart rate service discovered"); + + nrf_err = ble_hrs_client_handles_assign(hrs, evt->conn_handle, + &evt->discovery_complete.handles); + if (nrf_err) { + LOG_ERR("ble_hrs_client_handles_assign failed, nrf_error %#x", nrf_err); + } + + /* Heart rate service discovered. Enable notification of Heart Rate Measurement. */ + nrf_err = ble_hrs_client_hrm_notif_enable(hrs); + if (nrf_err) { + LOG_ERR("ble_hrs_client_hrm_notif_enable failed, nrf_error %#x", nrf_err); + } + + break; + + case BLE_HRS_CLIENT_EVT_HRM_NOTIFICATION: + LOG_INF("Heart Rate = %d.", evt->hrm_notification.hr_value); + if (evt->hrm_notification.rr_intervals_cnt != 0) { + uint32_t rr_avg = 0; + + for (uint32_t i = 0; i < evt->hrm_notification.rr_intervals_cnt; i++) { + rr_avg += evt->hrm_notification.rr_intervals[i]; + + nrf_err = ble_hrs_rr_interval_add( + &ble_hrs, evt->hrm_notification.rr_intervals[i]); + if (nrf_err) { + LOG_INF("Failed to add RR interval, nrf_error %#x", + nrf_err); + } + } + rr_avg = rr_avg / evt->hrm_notification.rr_intervals_cnt; + LOG_INF("rr_interval (avg) = %d.", rr_avg); + } + nrf_err = ble_hrs_heart_rate_measurement_send(&ble_hrs, + evt->hrm_notification.hr_value); + if (nrf_err) { + /* Ignore if not connected, or CCCD not written/configured by peer. */ + if (nrf_err != BLE_ERROR_INVALID_CONN_HANDLE && + nrf_err != NRF_ERROR_INVALID_STATE && + nrf_err != BLE_ERROR_GATTS_SYS_ATTR_MISSING) { + LOG_ERR("Failed to send heart rate measurement, nrf_error %#x", + nrf_err); + } + } + break; + case BLE_HRS_CLIENT_EVT_BSL_UPDATE: + ble_hrs_body_sensor_location_set(&ble_hrs, evt->bsl_update.body_sensor_location); + break; + + default: + LOG_WRN("Unhandled hrs event %d", evt->evt_type); + break; + } +} + +static void hrs_evt_handler(struct ble_hrs *hrs, const struct ble_hrs_evt *evt) +{ + switch (evt->evt_type) { + case BLE_HRS_EVT_NOTIFICATION_ENABLED: + LOG_INF("HRM notifications enabled for connection %#x", evt->conn_handle); + break; + case BLE_HRS_EVT_NOTIFICATION_DISABLED: + LOG_INF("HRM notifications disabled for connection %#x", evt->conn_handle); + break; + case BLE_HRS_EVT_ERROR: + LOG_ERR("HRS error event, nrf_error %#x", evt->error.reason); + break; + default: + break; + } +} + +static void bas_client_evt_handler(struct ble_bas_client *bas, const struct ble_bas_client_evt *evt) +{ + uint32_t nrf_err; + + switch (evt->evt_type) { + case BLE_BAS_CLIENT_EVT_DISCOVERY_COMPLETE: + LOG_INF("Battery service discovered"); + nrf_err = ble_bas_client_handles_assign(bas, evt->conn_handle, + &evt->discovery_complete.handles); + if (nrf_err) { + LOG_ERR("ble_bas_client_handles_assign failed, nrf_error %#x", nrf_err); + } + + /* Battery service discovered. Enable notification of battery level. */ + nrf_err = ble_bas_client_bl_notif_enable(bas); + if (nrf_err) { + LOG_ERR("ble_bas_client_bl_notif_enable failed, nrf_error %#x", nrf_err); + } + break; + case BLE_BAS_CLIENT_EVT_BATT_NOTIFICATION: + LOG_INF("Battery Level = %d", evt->battery.level); + + nrf_err = ble_bas_battery_level_update(&ble_bas, conn_handle_central, + evt->battery.level); + if (nrf_err) { + /* Ignore if not connected, or CCCD not written/configured by peer. */ + if (nrf_err != BLE_ERROR_INVALID_CONN_HANDLE && + nrf_err != NRF_ERROR_INVALID_STATE && + nrf_err != BLE_ERROR_GATTS_SYS_ATTR_MISSING) { + LOG_ERR("Failed to send heart rate measurement, nrf_error %#x", + nrf_err); + } + } + break; + default: + LOG_WRN("Unhandled ble event %d", evt->evt_type); + break; + } +} + +static void bas_evt_handler(struct ble_bas *bas, const struct ble_bas_evt *evt) +{ + switch (evt->evt_type) { + case BLE_BAS_EVT_NOTIFICATION_ENABLED: + LOG_INF("Battery notifications enabled for connection %#x", evt->conn_handle); + break; + case BLE_BAS_EVT_NOTIFICATION_DISABLED: + LOG_INF("Battery notifications disabled for connection %#x", evt->conn_handle); + break; + case BLE_BAS_EVT_ERROR: + LOG_ERR("BAS error event, nrf_error %#x", evt->error.reason); + break; + default: + break; + } +} + +static void scan_evt_handler(const struct ble_scan_evt *scan_evt) +{ + switch (scan_evt->evt_type) { + case BLE_SCAN_EVT_NOT_FOUND: + /* Ignore. */ + break; + + case BLE_SCAN_EVT_ALLOW_LIST_REQUEST: + on_allow_list_req(); + allow_list_disabled = false; + LOG_INF("allow list request"); + break; + + case BLE_SCAN_EVT_CONNECTING_ERROR: + LOG_ERR("Failed to connect, nrf_error %#x", scan_evt->connecting_err.reason); + break; + + case BLE_SCAN_EVT_SCAN_TIMEOUT: + LOG_INF("Scan timed out"); + scan_start(); + break; + + case BLE_SCAN_EVT_FILTER_MATCH: + LOG_INF("Scan filter match"); + break; + + case BLE_SCAN_EVT_ALLOW_LIST_ADV_REPORT: + LOG_INF("allow list advertise report"); + break; + + case BLE_SCAN_EVT_CONNECTED: { + const ble_gap_addr_t *const peer_addr = &scan_evt->connected.connected->peer_addr; + + LOG_INF("Connecting to target %02X:%02X:%02X:%02X:%02X:%02X", + peer_addr->addr[5], peer_addr->addr[4], peer_addr->addr[3], + peer_addr->addr[2], peer_addr->addr[1], peer_addr->addr[0]); + } break; + + default: + LOG_WRN("Unhandled scan event %d", scan_evt->evt_type); + break; + } +} + +static void conn_params_evt_handler(const struct ble_conn_params_evt *evt) +{ + ble_hrs_conn_params_evt(&ble_hrs, evt); +} + +static int buttons_leds_init(void) +{ + int err; + static struct bm_buttons_config btn_cfg[] = { + { + .pin_number = BOARD_PIN_BTN_0, + .active_state = BM_BUTTONS_ACTIVE_LOW, + .pull_config = BM_BUTTONS_PIN_PULLUP, + .handler = button_handler, + }, + { + .pin_number = BOARD_PIN_BTN_1, + .active_state = BM_BUTTONS_ACTIVE_LOW, + .pull_config = BM_BUTTONS_PIN_PULLUP, + .handler = button_handler, + }, + { + .pin_number = BOARD_PIN_BTN_2, + .active_state = BM_BUTTONS_ACTIVE_LOW, + .pull_config = BM_BUTTONS_PIN_PULLUP, + .handler = button_handler, + }, + }; + + err = bm_buttons_init(btn_cfg, ARRAY_SIZE(btn_cfg), BM_BUTTONS_DETECTION_DELAY_MIN_US); + if (err) { + LOG_ERR("Failed to initialize buttons, err %d", err); + return err; + } + + err = bm_buttons_enable(); + if (err) { + LOG_ERR("Failed to enable buttons, err %d", err); + return err; + } + + nrf_gpio_cfg_output(BOARD_PIN_LED_0); + nrf_gpio_cfg_output(BOARD_PIN_LED_1); + nrf_gpio_cfg_output(BOARD_PIN_LED_2); + nrf_gpio_pin_write(BOARD_PIN_LED_0, !BOARD_LED_ACTIVE_STATE); + nrf_gpio_pin_write(BOARD_PIN_LED_1, !BOARD_LED_ACTIVE_STATE); + nrf_gpio_pin_write(BOARD_PIN_LED_2, !BOARD_LED_ACTIVE_STATE); + + return 0; +} + +static uint32_t peer_manager_init(void) +{ + uint32_t nrf_err; + ble_gap_sec_params_t sec_param = { + .bond = 1, + .mitm = 0, + .lesc = 1, + .keypress = 0, + .io_caps = BLE_GAP_IO_CAPS_DISPLAY_YESNO, + .oob = 0, + .min_key_size = 7, + .max_key_size = 16, + .kdist_own.enc = 1, + .kdist_own.id = 1, + .kdist_peer.enc = 1, + .kdist_peer.id = 1, + }; + + nrf_err = pm_init(); + if (nrf_err) { + LOG_ERR("PM init failed, nrf_error %#x", nrf_err); + return nrf_err; + } + + nrf_err = pm_sec_params_set(&sec_param); + if (nrf_err) { + LOG_ERR("Failed to set PM sec params, nrf_error %#x", nrf_err); + return nrf_err; + } + + nrf_err = pm_register(pm_evt_handler); + if (nrf_err) { + LOG_ERR("PM register failed, nrf_error %#x", nrf_err); + return nrf_err; + } + + return NRF_SUCCESS; +} + +static uint32_t db_discovery_init(void) +{ + struct ble_db_discovery_config db_cfg = { + .evt_handler = db_disc_evt_handler, + .gatt_queue = &ble_gq, + }; + + return ble_db_discovery_init(&ble_db_disc, &db_cfg); +} + +static uint32_t hrs_client_init(void) +{ + struct ble_hrs_client_config hrs_client_cfg = { + .evt_handler = hrs_client_evt_handler, + .gatt_queue = &ble_gq, + .db_discovery = &ble_db_disc, + }; + + return ble_hrs_client_init(&ble_hrs_client, &hrs_client_cfg); +} + +static uint32_t hrs_init(void) +{ + uint8_t body_sensor_location = BLE_HRS_BODY_SENSOR_LOCATION_CHEST; + struct ble_hrs_config hrs_cfg = { + .evt_handler = hrs_evt_handler, + .is_sensor_contact_supported = true, + .body_sensor_location = &body_sensor_location, + .sec_mode = BLE_HRS_CONFIG_SEC_MODE_DEFAULT, + }; + + return ble_hrs_init(&ble_hrs, &hrs_cfg); +} + +static uint32_t bas_client_init(void) +{ + struct ble_bas_client_config bas_client_cfg = { + .evt_handler = bas_client_evt_handler, + .gatt_queue = &ble_gq, + .db_discovery = &ble_db_disc, + }; + + return ble_bas_client_init(&ble_bas_client, &bas_client_cfg); +} + +static uint32_t bas_init(void) +{ + struct ble_bas_config bas_cfg = { + .evt_handler = bas_evt_handler, + .can_notify = true, + .battery_level = 0, + .sec_mode = BLE_BAS_CONFIG_SEC_MODE_DEFAULT, + }; + + return ble_bas_init(&ble_bas, &bas_cfg); +} + +static uint32_t scan_init(void) +{ + uint32_t nrf_err; + struct ble_scan_config scan_cfg = { + .scan_params = { + .active = 0x01, + .interval = BLE_GAP_SCAN_INTERVAL_US_MIN * 6, + .window = BLE_GAP_SCAN_WINDOW_US_MIN * 6, + .filter_policy = BLE_GAP_SCAN_FP_ACCEPT_ALL, + .timeout = BLE_GAP_SCAN_TIMEOUT_UNLIMITED, + .scan_phys = BLE_GAP_PHY_AUTO, + }, + .conn_params = BLE_SCAN_CONN_PARAMS_DEFAULT, + .connect_if_match = true, + .conn_cfg_tag = CONFIG_NRF_SDH_BLE_CONN_TAG, + .evt_handler = scan_evt_handler, + }; + struct ble_scan_filter_data filter_data = { + .uuid_filter.uuid = { + .uuid = BLE_UUID_HEART_RATE_SERVICE, + .type = BLE_UUID_TYPE_BLE, + }, + }; + uint8_t filter_mode_mask = BLE_SCAN_UUID_FILTER; + + nrf_err = ble_scan_init(&ble_scan, &scan_cfg); + if (nrf_err) { + LOG_ERR("ble_scan_init failed, nrf_error %#x", nrf_err); + return nrf_err; + } + + nrf_err = ble_scan_filter_add(&ble_scan, BLE_SCAN_UUID_FILTER, &filter_data); + if (nrf_err) { + LOG_ERR("ble_scan_filter_add uuid failed, nrf_error %#x", nrf_err); + return nrf_err; + } + +#if defined(CONFIG_SAMPLE_USE_TARGET_PERIPHERAL_NAME) + filter_data.name_filter.name = CONFIG_SAMPLE_TARGET_PERIPHERAL_NAME; + filter_mode_mask |= BLE_SCAN_NAME_FILTER; + + nrf_err = ble_scan_filter_add(&ble_scan, BLE_SCAN_NAME_FILTER, &filter_data); + if (nrf_err) { + LOG_ERR("ble_scan_filter_add name failed, nrf_error %#x", nrf_err); + return nrf_err; + } +#endif /* CONFIG_SAMPLE_USE_TARGET_PERIPHERAL_NAME */ + +#if defined(CONFIG_SAMPLE_USE_TARGET_PERIPHERAL_ADDR) + filter_data.addr_filter.addr = target_periph_addr; + filter_mode_mask |= BLE_SCAN_ADDR_FILTER; + + nrf_err = ble_scan_filter_add(&ble_scan, BLE_SCAN_ADDR_FILTER, &filter_data); + if (nrf_err) { + LOG_ERR("ble_scan_filter_add address failed, nrf_error %#x", nrf_err); + return nrf_err; + } +#endif /* CONFIG_SAMPLE_USE_TARGET_PERIPHERAL_ADDR */ + + nrf_err = ble_scan_filters_enable(&ble_scan, filter_mode_mask, true); + if (nrf_err) { + LOG_ERR("Failed to enable scan filters, nrf_error %#x", nrf_err); + return nrf_err; + } + + return NRF_SUCCESS; +} + +static uint32_t adv_init(void) +{ + ble_uuid_t adv_uuid_list[] = { + {.uuid = BLE_UUID_HEART_RATE_SERVICE, .type = BLE_UUID_TYPE_BLE}, + }; + struct ble_adv_config ble_adv_cfg = { + .conn_cfg_tag = CONFIG_NRF_SDH_BLE_CONN_TAG, + .evt_handler = ble_adv_evt_handler, + .adv_data = { + .name_type = BLE_ADV_DATA_FULL_NAME, + .flags = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE, + }, + .sr_data.uuid_lists.complete = { + .len = ARRAY_SIZE(adv_uuid_list), + .uuid = &adv_uuid_list[0], + }, + }; + + return ble_adv_init(&ble_adv, &ble_adv_cfg); +} + +int main(void) +{ + int err; + uint32_t nrf_err; + ble_gap_conn_sec_mode_t device_name_write_sec; + + LOG_INF("BLE HRS Peripheral Central sample started"); + + err = buttons_leds_init(); + if (err) { + goto idle; + } + + const bool erase_bonds = bm_buttons_is_pressed(BOARD_PIN_BTN_1); + + err = nrf_sdh_enable_request(); + if (err) { + LOG_ERR("Failed to enable SoftDevice, err %d", err); + goto idle; + } + + LOG_INF("SoftDevice enabled"); + + err = nrf_sdh_ble_enable(CONFIG_NRF_SDH_BLE_CONN_TAG); + if (err) { + LOG_ERR("Failed to enable BLE, err %d", err); + goto idle; + } + + LOG_INF("Bluetooth enabled"); + + BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS(&device_name_write_sec); + nrf_err = sd_ble_gap_device_name_set(&device_name_write_sec, CONFIG_SAMPLE_BLE_DEVICE_NAME, + strlen(CONFIG_SAMPLE_BLE_DEVICE_NAME)); + if (nrf_err) { + LOG_ERR("Failed to set device name, nrf_error %#x", nrf_err); + goto idle; + } + + nrf_err = ble_conn_params_evt_handler_set(conn_params_evt_handler); + if (nrf_err) { + LOG_ERR("Failed to setup conn params event handler, nrf_error %#x", nrf_err); + goto idle; + } + + nrf_err = peer_manager_init(); + if (nrf_err) { + LOG_ERR("Failed to initialize peer manager, nrf_error %#x", nrf_err); + goto idle; + } + + nrf_err = db_discovery_init(); + if (nrf_err) { + LOG_ERR("Failed to initialize db discovery, nrf_error %#x", nrf_err); + goto idle; + } + + nrf_err = hrs_client_init(); + if (nrf_err) { + LOG_ERR("Failed to initialize HRS client, nrf_error %#x", nrf_err); + goto idle; + } + + nrf_err = hrs_init(); + if (nrf_err) { + LOG_ERR("Failed to initialize HRS, nrf_error %#x", nrf_err); + goto idle; + } + + nrf_err = bas_client_init(); + if (nrf_err) { + LOG_ERR("Failed to initialize BAS client, nrf_error %#x", nrf_err); + goto idle; + } + + nrf_err = bas_init(); + if (nrf_err) { + LOG_ERR("Failed to initialize BAS, nrf_error %#x", nrf_err); + goto idle; + } + + nrf_err = adv_init(); + if (nrf_err) { + LOG_ERR("Failed to initialize advertising, nrf_error %#x", nrf_err); + goto idle; + } + + nrf_err = scan_init(); + if (nrf_err) { + LOG_ERR("Failed to initialize scan library, nrf_error %#x", nrf_err); + goto idle; + } + + nrf_gpio_pin_write(BOARD_PIN_LED_0, BOARD_LED_ACTIVE_STATE); + LOG_INF("BLE HRS Peripheral Central sample initialized"); + + if (erase_bonds) { + delete_bonds(); + /* Scan and advertising will be started on a PM_EVT_PEERS_DELETE_SUCCEEDED event. */ + } else { + scan_start(); + advertising_start(); + } + +idle: + while (true) { +#if defined(CONFIG_PM_LESC) + (void)nrf_ble_lesc_request_handler(); +#endif + log_flush(); + + k_cpu_idle(); + } +}