From a823bbf7c1434464b3e26b196c534bf743e9ebf6 Mon Sep 17 00:00:00 2001 From: Vinayak Kariappa Chettimada Date: Fri, 5 Jun 2026 21:50:22 +0200 Subject: [PATCH] samples: Bluetooth: Central plus Peripheral with notifications Add three new Bluetooth samples: - peripheral_gatt_notify: Peripheral that sends GATT notifications - central_notify_receive: Central that receives GATT notifications - central_peripheral_notify_send_receive: Central plus Peripheral Add bsim CI test tests/bsim/bluetooth/host/multirole_gatt_relay to verify the 3-device notification relay topology under simulation. Assisted-by: GitHub CoPilot: Claude Opus 4.6 Signed-off-by: Vinayak Kariappa Chettimada --- .../central_notify_receive/CMakeLists.txt | 8 + .../bluetooth/central_notify_receive/Kconfig | 4 + .../central_notify_receive/README.rst | 34 ++ .../bluetooth/central_notify_receive/prj.conf | 12 + .../central_notify_receive/sample.yaml | 12 + .../src/central_notify_receive.c | 306 +++++++++++ .../central_notify_receive/src/main.c | 15 + .../CMakeLists.txt | 8 + .../Kconfig | 4 + .../README.rst | 38 ++ .../prj.conf | 15 + .../sample.yaml | 12 + .../src/central_peripheral_notify.c | 479 ++++++++++++++++++ .../src/main.c | 15 + .../peripheral_gatt_notify/CMakeLists.txt | 8 + .../bluetooth/peripheral_gatt_notify/Kconfig | 4 + .../peripheral_gatt_notify/README.rst | 34 ++ .../bluetooth/peripheral_gatt_notify/prj.conf | 12 + .../peripheral_gatt_notify/sample.yaml | 12 + .../peripheral_gatt_notify/src/main.c | 15 + .../src/peripheral_gatt_notify.c | 241 +++++++++ .../host/multirole_gatt_relay/CMakeLists.txt | 18 + .../host/multirole_gatt_relay/Kconfig | 4 + .../host/multirole_gatt_relay/prj.conf | 41 ++ .../host/multirole_gatt_relay/src/main.c | 157 ++++++ .../host/multirole_gatt_relay/testcase.yaml | 10 + .../tests_scripts/multirole_gatt_relay.sh | 28 + 27 files changed, 1546 insertions(+) create mode 100644 samples/bluetooth/central_notify_receive/CMakeLists.txt create mode 100644 samples/bluetooth/central_notify_receive/Kconfig create mode 100644 samples/bluetooth/central_notify_receive/README.rst create mode 100644 samples/bluetooth/central_notify_receive/prj.conf create mode 100644 samples/bluetooth/central_notify_receive/sample.yaml create mode 100644 samples/bluetooth/central_notify_receive/src/central_notify_receive.c create mode 100644 samples/bluetooth/central_notify_receive/src/main.c create mode 100644 samples/bluetooth/central_peripheral_notify_send_receive/CMakeLists.txt create mode 100644 samples/bluetooth/central_peripheral_notify_send_receive/Kconfig create mode 100644 samples/bluetooth/central_peripheral_notify_send_receive/README.rst create mode 100644 samples/bluetooth/central_peripheral_notify_send_receive/prj.conf create mode 100644 samples/bluetooth/central_peripheral_notify_send_receive/sample.yaml create mode 100644 samples/bluetooth/central_peripheral_notify_send_receive/src/central_peripheral_notify.c create mode 100644 samples/bluetooth/central_peripheral_notify_send_receive/src/main.c create mode 100644 samples/bluetooth/peripheral_gatt_notify/CMakeLists.txt create mode 100644 samples/bluetooth/peripheral_gatt_notify/Kconfig create mode 100644 samples/bluetooth/peripheral_gatt_notify/README.rst create mode 100644 samples/bluetooth/peripheral_gatt_notify/prj.conf create mode 100644 samples/bluetooth/peripheral_gatt_notify/sample.yaml create mode 100644 samples/bluetooth/peripheral_gatt_notify/src/main.c create mode 100644 samples/bluetooth/peripheral_gatt_notify/src/peripheral_gatt_notify.c create mode 100644 tests/bsim/bluetooth/host/multirole_gatt_relay/CMakeLists.txt create mode 100644 tests/bsim/bluetooth/host/multirole_gatt_relay/Kconfig create mode 100644 tests/bsim/bluetooth/host/multirole_gatt_relay/prj.conf create mode 100644 tests/bsim/bluetooth/host/multirole_gatt_relay/src/main.c create mode 100644 tests/bsim/bluetooth/host/multirole_gatt_relay/testcase.yaml create mode 100755 tests/bsim/bluetooth/host/multirole_gatt_relay/tests_scripts/multirole_gatt_relay.sh diff --git a/samples/bluetooth/central_notify_receive/CMakeLists.txt b/samples/bluetooth/central_notify_receive/CMakeLists.txt new file mode 100644 index 000000000000..f0216483855a --- /dev/null +++ b/samples/bluetooth/central_notify_receive/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(central_notify_receive) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/samples/bluetooth/central_notify_receive/Kconfig b/samples/bluetooth/central_notify_receive/Kconfig new file mode 100644 index 000000000000..1b33e6224dd4 --- /dev/null +++ b/samples/bluetooth/central_notify_receive/Kconfig @@ -0,0 +1,4 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +source "Kconfig.zephyr" diff --git a/samples/bluetooth/central_notify_receive/README.rst b/samples/bluetooth/central_notify_receive/README.rst new file mode 100644 index 000000000000..67abb26b7df0 --- /dev/null +++ b/samples/bluetooth/central_notify_receive/README.rst @@ -0,0 +1,34 @@ +.. zephyr:code-sample:: ble_central_notify_receive + :name: Central / Notify Receive + :relevant-api: bluetooth + + Scan for a Bluetooth LE peripheral and receive GATT notifications. + +Overview +******** + +This sample acts as a Bluetooth LE central that scans for a peripheral +advertising the custom notify service, connects to it, discovers the notify +characteristic, subscribes to notifications, and measures the received +notification throughput. Throughput metrics are printed to the console at a +configurable interval. + +This sample is designed to work with the +:zephyr:code-sample:`ble_central_peripheral_notify_send_receive` sample which +acts as a multirole device that sends notifications to this central. + +Requirements +************ + +* A board with Bluetooth LE support + +Building and Running +******************** + +Build and flash the sample as follows, replacing ```` with your target board: + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/central_notify_receive + :board: + :goals: build flash + :compact: diff --git a/samples/bluetooth/central_notify_receive/prj.conf b/samples/bluetooth/central_notify_receive/prj.conf new file mode 100644 index 000000000000..1f8045a54315 --- /dev/null +++ b/samples/bluetooth/central_notify_receive/prj.conf @@ -0,0 +1,12 @@ +CONFIG_BT=y +CONFIG_BT_CENTRAL=y +CONFIG_BT_GATT_CLIENT=y + +CONFIG_BT_BUF_ACL_RX_SIZE=255 +CONFIG_BT_BUF_ACL_TX_SIZE=251 +CONFIG_BT_BUF_CMD_TX_SIZE=255 +CONFIG_BT_BUF_EVT_DISCARDABLE_SIZE=255 + +CONFIG_BT_L2CAP_TX_MTU=247 + +CONFIG_LOG=y diff --git a/samples/bluetooth/central_notify_receive/sample.yaml b/samples/bluetooth/central_notify_receive/sample.yaml new file mode 100644 index 000000000000..fd34dc567afa --- /dev/null +++ b/samples/bluetooth/central_notify_receive/sample.yaml @@ -0,0 +1,12 @@ +sample: + name: Bluetooth Central Notify Receive +tests: + sample.bluetooth.central_notify_receive: + harness: bluetooth + platform_allow: + - qemu_cortex_m3 + - qemu_x86 + - nrf52_bsim + tags: bluetooth + integration_platforms: + - qemu_cortex_m3 diff --git a/samples/bluetooth/central_notify_receive/src/central_notify_receive.c b/samples/bluetooth/central_notify_receive/src/central_notify_receive.c new file mode 100644 index 000000000000..3424b70a4c52 --- /dev/null +++ b/samples/bluetooth/central_notify_receive/src/central_notify_receive.c @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include +#include +#include + +/* Interval between printing throughput metrics */ +#define METRICS_INTERVAL 1U /* seconds */ + +/* Custom service UUID: 12345678-1234-5678-1234-56789abcdef0 + * This is used by peripheral_gatt_notify and for central role of multirole. + */ +#define BT_UUID_NOTIFY_SERVICE_VAL \ + BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef0) +#define BT_UUID_NOTIFY_SERVICE BT_UUID_DECLARE_128(BT_UUID_NOTIFY_SERVICE_VAL) + +/* Custom characteristic UUID: 12345678-1234-5678-1234-56789abcdef1 */ +#define BT_UUID_NOTIFY_CHAR_VAL \ + BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef1) +#define BT_UUID_NOTIFY_CHAR BT_UUID_DECLARE_128(BT_UUID_NOTIFY_CHAR_VAL) + +/* Custom service UUID for multirole's peripheral role: 12345678-1234-5678-1234-56789abcdef2 + * This is what central_notify_receive discovers when connecting to the multirole device. + */ +#define BT_UUID_MULTIROLE_NOTIFY_CHAR_VAL \ + BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef3) +#define BT_UUID_MULTIROLE_NOTIFY_CHAR \ + BT_UUID_DECLARE_128(BT_UUID_MULTIROLE_NOTIFY_CHAR_VAL) + +static struct bt_conn *central_conn_connected; +static struct bt_gatt_subscribe_params central_subscribe_params; +static bool central_subscribed; +static bool central_role_active; +static void (*central_restart_scan_func)(void); + +/* Throughput metrics */ +static uint32_t central_notify_count; +static uint32_t central_notify_len; +static uint32_t central_notify_rate; +uint32_t central_last_notify_rate; +uint32_t *central_notify_countdown; + +static uint8_t central_notify_cb(struct bt_conn *conn, + struct bt_gatt_subscribe_params *params, + const void *data, uint16_t length) +{ + static uint32_t cycle_stamp; + uint64_t delta; + + if (!data) { + printk("[Central] Notification subscription ended\n"); + params->value_handle = 0U; + central_subscribed = false; + return BT_GATT_ITER_STOP; + } + + delta = k_cycle_get_32() - cycle_stamp; + delta = k_cyc_to_ns_floor64(delta); + + if (delta == 0) { + return BT_GATT_ITER_CONTINUE; + } + + if (delta > (METRICS_INTERVAL * NSEC_PER_SEC)) { + printk("[Central] Notify RX: count= %u, len= %u, rate= %u bps.\n", + central_notify_count, central_notify_len, central_notify_rate); + + central_last_notify_rate = central_notify_rate; + + central_notify_count = 0U; + central_notify_len = 0U; + central_notify_rate = 0U; + cycle_stamp = k_cycle_get_32(); + } else { + central_notify_count++; + central_notify_len += length; + central_notify_rate = ((uint64_t)central_notify_len << 3) * + (METRICS_INTERVAL * NSEC_PER_SEC) / delta; + } + + if (central_notify_countdown && *central_notify_countdown) { + (*central_notify_countdown)--; + } + + return BT_GATT_ITER_CONTINUE; +} + +static uint8_t central_discover_cb(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + struct bt_gatt_discover_params *params) +{ + int err; + + if (!attr) { + printk("[Central] Discover complete\n"); + return BT_GATT_ITER_STOP; + } + + printk("[Central] Discovered attr handle %u\n", attr->handle); + + /* Subscribe to notifications on the CCC (handle after char value) */ + central_subscribe_params.notify = central_notify_cb; + central_subscribe_params.value = BT_GATT_CCC_NOTIFY; + central_subscribe_params.value_handle = attr->handle + 1; + central_subscribe_params.ccc_handle = attr->handle + 2; + + err = bt_gatt_subscribe(conn, ¢ral_subscribe_params); + if (err && err != -EALREADY) { + printk("[Central] Subscribe failed (err %d)\n", err); + } else { + printk("[Central] Subscribed to notifications\n"); + central_subscribed = true; + } + + return BT_GATT_ITER_STOP; +} + +static struct bt_gatt_discover_params central_discover_params; +static struct bt_uuid_128 discover_uuid = BT_UUID_INIT_128(0); + +static void central_discover_and_subscribe(struct bt_conn *conn) +{ + int err; + + printk("[Central] Discover ...\n"); + memcpy(&discover_uuid, BT_UUID_MULTIROLE_NOTIFY_CHAR, sizeof(discover_uuid)); + central_discover_params.uuid = &discover_uuid.uuid; + central_discover_params.func = central_discover_cb; + central_discover_params.start_handle = BT_ATT_FIRST_ATTRIBUTE_HANDLE; + central_discover_params.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; + central_discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; + + err = bt_gatt_discover(conn, ¢ral_discover_params); + if (err) { + printk("[Central] Discover failed (err %d)\n", err); + } +} + +static void central_device_found(const bt_addr_le_t *addr, int8_t rssi, uint8_t type, + struct net_buf_simple *ad) +{ + struct bt_conn *conn; + int err; + + if (type != BT_GAP_ADV_TYPE_ADV_IND && + type != BT_GAP_ADV_TYPE_ADV_DIRECT_IND) { + return; + } + + /* Connect only to devices in close proximity */ + if (rssi < -50) { + return; + } + + err = bt_le_scan_stop(); + if (err) { + printk("[Central] Stop LE scan failed (err %d)\n", err); + return; + } + + err = bt_conn_le_create(addr, BT_CONN_LE_CREATE_CONN, + BT_LE_CONN_PARAM_DEFAULT, &conn); + if (err) { + printk("[Central] Create conn failed (err %d)\n", err); + central_restart_scan_func(); + } else { + bt_conn_unref(conn); + } +} + +static void central_start_scan(void) +{ + int err; + + err = bt_le_scan_start(BT_LE_SCAN_ACTIVE, central_device_found); + if (err) { + printk("[Central] Scanning failed to start (err %d)\n", err); + return; + } + + printk("[Central] Scanning successfully started\n"); +} + +static void central_mtu_exchange_cb(struct bt_conn *conn, uint8_t err, + struct bt_gatt_exchange_params *params) +{ + printk("[Central] MTU exchange %s (%u)\n", + err == 0U ? "successful" : "failed", + bt_gatt_get_mtu(conn)); + + /* After MTU exchange, discover and subscribe */ + central_discover_and_subscribe(conn); +} + +static struct bt_gatt_exchange_params central_mtu_exchange_params; + +static void central_mtu_updated(struct bt_conn *conn, uint16_t tx, uint16_t rx) +{ + printk("[Central] Updated MTU: TX: %d RX: %d bytes\n", tx, rx); +} + +static struct bt_gatt_cb central_gatt_callbacks = { + .att_mtu_updated = central_mtu_updated, +}; + +static void central_connected(struct bt_conn *conn, uint8_t conn_err) +{ + if (!central_role_active) { + return; + } + + if (conn_err) { + printk("[Central] Failed to connect (0x%02x)\n", conn_err); + central_restart_scan_func(); + return; + } + + printk("[Central] Connected: %s\n", bt_conn_dst_str(conn)); + central_conn_connected = bt_conn_ref(conn); + + /* Exchange MTU first, then discover */ + central_mtu_exchange_params.func = central_mtu_exchange_cb; + int err = bt_gatt_exchange_mtu(conn, ¢ral_mtu_exchange_params); + + if (err) { + printk("[Central] MTU exchange failed (err %d)\n", err); + /* Try discovery anyway */ + central_discover_and_subscribe(conn); + } +} + +static void central_disconnected(struct bt_conn *conn, uint8_t reason) +{ + if (conn != central_conn_connected) { + return; + } + + printk("[Central] Disconnected: %s (reason 0x%02x)\n", bt_conn_dst_str(conn), reason); + + if (central_conn_connected) { + bt_conn_unref(central_conn_connected); + central_conn_connected = NULL; + } + + central_subscribed = false; + + central_restart_scan_func(); +} + +static void central_le_param_updated(struct bt_conn *conn, uint16_t interval, + uint16_t latency, uint16_t timeout) +{ + printk("[Central] Conn params updated: int 0x%04x lat %u to %u\n", interval, + latency, timeout); +} + +BT_CONN_CB_DEFINE(central_recv_conn_cb) = { + .connected = central_connected, + .disconnected = central_disconnected, + .le_param_updated = central_le_param_updated, +}; + +uint32_t central_notify_receive(uint32_t count) +{ + int err; + + err = bt_enable(NULL); + if (err) { + printk("[Central] Bluetooth init failed (err %d)\n", err); + return 0U; + } + printk("[Central] Bluetooth initialized\n"); + + bt_gatt_cb_register(¢ral_gatt_callbacks); + + central_role_active = true; + central_conn_connected = NULL; + central_last_notify_rate = 0U; + central_notify_countdown = &count; + + if (count != 0U) { + printk("[Central] GATT Notify receive countdown %u.\n", count); + } else { + printk("[Central] GATT Notify receive forever.\n"); + } + + central_restart_scan_func = central_start_scan; + central_start_scan(); + + while (true) { + if (count && !*central_notify_countdown) { + break; + } + k_sleep(K_SECONDS(1)); + } + + return central_last_notify_rate; +} diff --git a/samples/bluetooth/central_notify_receive/src/main.c b/samples/bluetooth/central_notify_receive/src/main.c new file mode 100644 index 000000000000..fe547a41369a --- /dev/null +++ b/samples/bluetooth/central_notify_receive/src/main.c @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +extern uint32_t central_notify_receive(uint32_t count); + +int main(void) +{ + (void)central_notify_receive(0U); + return 0; +} diff --git a/samples/bluetooth/central_peripheral_notify_send_receive/CMakeLists.txt b/samples/bluetooth/central_peripheral_notify_send_receive/CMakeLists.txt new file mode 100644 index 000000000000..87b24fb52ef4 --- /dev/null +++ b/samples/bluetooth/central_peripheral_notify_send_receive/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(central_peripheral_notify_send_receive) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/samples/bluetooth/central_peripheral_notify_send_receive/Kconfig b/samples/bluetooth/central_peripheral_notify_send_receive/Kconfig new file mode 100644 index 000000000000..1b33e6224dd4 --- /dev/null +++ b/samples/bluetooth/central_peripheral_notify_send_receive/Kconfig @@ -0,0 +1,4 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +source "Kconfig.zephyr" diff --git a/samples/bluetooth/central_peripheral_notify_send_receive/README.rst b/samples/bluetooth/central_peripheral_notify_send_receive/README.rst new file mode 100644 index 000000000000..25acb67950ad --- /dev/null +++ b/samples/bluetooth/central_peripheral_notify_send_receive/README.rst @@ -0,0 +1,38 @@ +.. zephyr:code-sample:: ble_central_peripheral_notify_send_receive + :name: Central Peripheral / Notify Send Receive + :relevant-api: bluetooth + + Multirole device that receives notifications as central and sends notifications as peripheral. + +Overview +******** + +This sample acts as a Bluetooth LE multirole device supporting simultaneous +central and peripheral roles. As a central, it connects to a peripheral running +the :zephyr:code-sample:`ble_peripheral_gatt_notify` sample and receives GATT +notifications. As a peripheral, it advertises its own notify service and sends +GATT notifications to a connected central running the +:zephyr:code-sample:`ble_central_notify_receive` sample. + +This creates a relay-like topology: + +* **Device 1** (Central) ← notifications ← **Device 2** (Multirole) ← notifications ← **Device 3** (Peripheral) + +Throughput metrics are printed to the console at a configurable interval for +both the received and sent notification streams. + +Requirements +************ + +* A board with Bluetooth LE support + +Building and Running +******************** + +Build and flash the sample as follows, replacing ```` with your target board: + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/central_peripheral_notify_send_receive + :board: + :goals: build flash + :compact: diff --git a/samples/bluetooth/central_peripheral_notify_send_receive/prj.conf b/samples/bluetooth/central_peripheral_notify_send_receive/prj.conf new file mode 100644 index 000000000000..bf51d4ba3ec3 --- /dev/null +++ b/samples/bluetooth/central_peripheral_notify_send_receive/prj.conf @@ -0,0 +1,15 @@ +CONFIG_BT=y +CONFIG_BT_CENTRAL=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_GATT_CLIENT=y +CONFIG_BT_DEVICE_NAME="Zephyr Multirole Notify" +CONFIG_BT_MAX_CONN=2 + +CONFIG_BT_BUF_ACL_RX_SIZE=255 +CONFIG_BT_BUF_ACL_TX_SIZE=251 +CONFIG_BT_BUF_CMD_TX_SIZE=255 +CONFIG_BT_BUF_EVT_DISCARDABLE_SIZE=255 + +CONFIG_BT_L2CAP_TX_MTU=247 + +CONFIG_LOG=y diff --git a/samples/bluetooth/central_peripheral_notify_send_receive/sample.yaml b/samples/bluetooth/central_peripheral_notify_send_receive/sample.yaml new file mode 100644 index 000000000000..d633c757cec9 --- /dev/null +++ b/samples/bluetooth/central_peripheral_notify_send_receive/sample.yaml @@ -0,0 +1,12 @@ +sample: + name: Bluetooth Central Peripheral Notify Send Receive +tests: + sample.bluetooth.central_peripheral_notify_send_receive: + harness: bluetooth + platform_allow: + - qemu_cortex_m3 + - qemu_x86 + - nrf52_bsim + tags: bluetooth + integration_platforms: + - qemu_cortex_m3 diff --git a/samples/bluetooth/central_peripheral_notify_send_receive/src/central_peripheral_notify.c b/samples/bluetooth/central_peripheral_notify_send_receive/src/central_peripheral_notify.c new file mode 100644 index 000000000000..d92d4e3ad028 --- /dev/null +++ b/samples/bluetooth/central_peripheral_notify_send_receive/src/central_peripheral_notify.c @@ -0,0 +1,479 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include +#include +#include + +/* Interval between printing throughput metrics */ +#define METRICS_INTERVAL 1U /* seconds */ + +/* Custom service UUID: 12345678-1234-5678-1234-56789abcdef0 */ +#define BT_UUID_NOTIFY_SERVICE_VAL \ + BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef0) +#define BT_UUID_NOTIFY_SERVICE BT_UUID_DECLARE_128(BT_UUID_NOTIFY_SERVICE_VAL) + +/* Custom characteristic UUID in remote peripheral role: 12345678-1234-5678-1234-56789abcdef1 */ +#define BT_UUID_NOTIFY_CHAR_VAL \ + BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef1) +#define BT_UUID_NOTIFY_CHAR BT_UUID_DECLARE_128(BT_UUID_NOTIFY_CHAR_VAL) + +/* Custom service UUID for multirole's peripheral role: 12345678-1234-5678-1234-56789abcdef2 */ +#define BT_UUID_MULTIROLE_NOTIFY_SERVICE_VAL \ + BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef2) +#define BT_UUID_MULTIROLE_NOTIFY_SERVICE \ + BT_UUID_DECLARE_128(BT_UUID_MULTIROLE_NOTIFY_SERVICE_VAL) + +/* Custom characteristic UUID for multirole's peripheral role: + * 12345678-1234-5678-1234-56789abcdef3 + */ +#define BT_UUID_MULTIROLE_NOTIFY_CHAR_VAL \ + BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef3) +#define BT_UUID_MULTIROLE_NOTIFY_CHAR \ + BT_UUID_DECLARE_128(BT_UUID_MULTIROLE_NOTIFY_CHAR_VAL) + +/* ===================== Peripheral role (sends notifications to Device 1) ====================== */ + +static struct bt_conn *peripheral_conn; +static bool peripheral_notify_enabled; +static struct bt_conn *central_conn; +static bool central_subscribed; +static bool multirole_active; + +/* Throughput metrics for sent notifications */ +static uint32_t tx_notify_count; +static uint32_t tx_notify_len; +static uint32_t tx_notify_rate; +uint32_t multirole_last_tx_notify_rate; + +static void multirole_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) +{ + peripheral_notify_enabled = (value == BT_GATT_CCC_NOTIFY); + printk("[Multirole] [Peripheral] Notifications %s\n", + peripheral_notify_enabled ? "enabled" : "disabled"); +} + +BT_GATT_SERVICE_DEFINE(multirole_notify_svc, + BT_GATT_PRIMARY_SERVICE(BT_UUID_MULTIROLE_NOTIFY_SERVICE), + BT_GATT_CHARACTERISTIC(BT_UUID_MULTIROLE_NOTIFY_CHAR, + BT_GATT_CHRC_NOTIFY, + BT_GATT_PERM_NONE, + NULL, NULL, NULL), + BT_GATT_CCC(multirole_ccc_cfg_changed, + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), +); + +static const struct bt_data multirole_ad[] = { + BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), + BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, + sizeof(CONFIG_BT_DEVICE_NAME) - 1), +}; + +static int multirole_send_notification(struct bt_conn *conn) +{ + static uint8_t data[BT_ATT_MAX_ATTRIBUTE_LEN]; + uint16_t data_len; + int err; + + data_len = bt_gatt_get_mtu(conn) - 3; + if (data_len > BT_ATT_MAX_ATTRIBUTE_LEN) { + data_len = BT_ATT_MAX_ATTRIBUTE_LEN; + } + + err = bt_gatt_notify(conn, &multirole_notify_svc.attrs[1], data, data_len); + if (err == -ENOMEM) { + return err; + } + + if (err) { + printk("[Multirole] [Peripheral] Notify failed (err %d)\n", err); + return err; + } + + return data_len; +} + +static void update_tx_metrics(uint16_t len) +{ + static uint32_t cycle_stamp; + uint64_t delta; + + delta = k_cycle_get_32() - cycle_stamp; + delta = k_cyc_to_ns_floor64(delta); + + if (delta == 0) { + return; + } + + if (delta > (METRICS_INTERVAL * NSEC_PER_SEC)) { + printk("[Multirole] [Peripheral] Notify TX: count= %u, len= %u, rate= %u bps.\n", + tx_notify_count, tx_notify_len, tx_notify_rate); + + multirole_last_tx_notify_rate = tx_notify_rate; + + tx_notify_count = 0U; + tx_notify_len = 0U; + tx_notify_rate = 0U; + cycle_stamp = k_cycle_get_32(); + } else { + tx_notify_count++; + tx_notify_len += len; + tx_notify_rate = ((uint64_t)tx_notify_len << 3) * + (METRICS_INTERVAL * NSEC_PER_SEC) / delta; + } +} + +/* ===================== Central role (receives notifications from Device 3) ==================== */ + +static struct bt_gatt_subscribe_params subscribe_params; + +/* Throughput metrics for received notifications */ +static uint32_t rx_notify_count; +static uint32_t rx_notify_len; +static uint32_t rx_notify_rate; +uint32_t multirole_last_rx_notify_rate; +uint32_t *multirole_notify_countdown; + +static uint8_t notify_rx_cb(struct bt_conn *conn, + struct bt_gatt_subscribe_params *params, + const void *data, uint16_t length) +{ + static uint32_t cycle_stamp; + uint64_t delta; + + if (!data) { + printk("[Multirole] [Central] Notification subscription ended\n"); + params->value_handle = 0U; + central_subscribed = false; + return BT_GATT_ITER_STOP; + } + + delta = k_cycle_get_32() - cycle_stamp; + delta = k_cyc_to_ns_floor64(delta); + + if (delta == 0) { + return BT_GATT_ITER_CONTINUE; + } + + if (delta > (METRICS_INTERVAL * NSEC_PER_SEC)) { + printk("[Multirole] [Central] Notify RX: count= %u, len= %u, rate= %u bps.\n", + rx_notify_count, rx_notify_len, rx_notify_rate); + + multirole_last_rx_notify_rate = rx_notify_rate; + + rx_notify_count = 0U; + rx_notify_len = 0U; + rx_notify_rate = 0U; + cycle_stamp = k_cycle_get_32(); + } else { + rx_notify_count++; + rx_notify_len += length; + rx_notify_rate = ((uint64_t)rx_notify_len << 3) * + (METRICS_INTERVAL * NSEC_PER_SEC) / delta; + } + + if (multirole_notify_countdown && *multirole_notify_countdown) { + (*multirole_notify_countdown)--; + } + + return BT_GATT_ITER_CONTINUE; +} + +static uint8_t discover_cb(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + struct bt_gatt_discover_params *params) +{ + int err; + + if (!attr) { + printk("[Multirole] [Central] Discover complete\n"); + return BT_GATT_ITER_STOP; + } + + printk("[Multirole] [Central] Discovered attr handle %u\n", attr->handle); + + subscribe_params.notify = notify_rx_cb; + subscribe_params.value = BT_GATT_CCC_NOTIFY; + subscribe_params.value_handle = attr->handle + 1; + subscribe_params.ccc_handle = attr->handle + 2; + + err = bt_gatt_subscribe(conn, &subscribe_params); + if (err && err != -EALREADY) { + printk("[Multirole] [Central] Subscribe failed (err %d)\n", err); + } else { + printk("[Multirole] [Central] Subscribed to notifications\n"); + central_subscribed = true; + } + + return BT_GATT_ITER_STOP; +} + +static struct bt_gatt_discover_params discover_params; +static struct bt_uuid_128 discover_uuid = BT_UUID_INIT_128(0); + +static void discover_and_subscribe(struct bt_conn *conn) +{ + int err; + + printk("[Multirole] [Central] Discover ...\n"); + memcpy(&discover_uuid, BT_UUID_NOTIFY_CHAR, sizeof(discover_uuid)); + discover_params.uuid = &discover_uuid.uuid; + discover_params.func = discover_cb; + discover_params.start_handle = BT_ATT_FIRST_ATTRIBUTE_HANDLE; + discover_params.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; + discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; + + err = bt_gatt_discover(conn, &discover_params); + if (err) { + printk("[Multirole] [Central] Discover failed (err %d)\n", err); + } +} + +/* ===================== Scanning (central role) ===================== */ + +static void multirole_start_scan(void); + +static void multirole_device_found(const bt_addr_le_t *addr, int8_t rssi, uint8_t type, + struct net_buf_simple *ad) +{ + struct bt_conn *conn; + int err; + + if (type != BT_GAP_ADV_TYPE_ADV_IND && + type != BT_GAP_ADV_TYPE_ADV_DIRECT_IND) { + return; + } + + /* Connect only to devices in close proximity */ + if (rssi < -50) { + return; + } + + err = bt_le_scan_stop(); + if (err) { + printk("[Multirole] [Central] Stop LE scan failed (err %d)\n", err); + return; + } + + err = bt_conn_le_create(addr, BT_CONN_LE_CREATE_CONN, + BT_LE_CONN_PARAM_DEFAULT, &conn); + if (err) { + printk("[Multirole] [Central] Create conn failed (err %d)\n", err); + multirole_start_scan(); + } else { + bt_conn_unref(conn); + } +} + +static void multirole_start_scan(void) +{ + int err; + + err = bt_le_scan_start(BT_LE_SCAN_ACTIVE, multirole_device_found); + if (err) { + printk("[Multirole] [Central] Scanning failed to start (err %d)\n", err); + return; + } + + printk("[Multirole] [Central] Scanning successfully started\n"); +} + +/* ===================== Connection callbacks ===================== */ + +static void multirole_mtu_exchange_cb(struct bt_conn *conn, uint8_t err, + struct bt_gatt_exchange_params *params) +{ + printk("[Multirole] MTU exchange %s (%u)\n", + err == 0U ? "successful" : "failed", + bt_gatt_get_mtu(conn)); + + /* If this is the central connection, discover and subscribe */ + if (conn == central_conn) { + discover_and_subscribe(conn); + } +} + +static struct bt_gatt_exchange_params multirole_mtu_exchange_params_central; +static struct bt_gatt_exchange_params multirole_mtu_exchange_params_peripheral; + +static void multirole_mtu_updated(struct bt_conn *conn, uint16_t tx, uint16_t rx) +{ + printk("[Multirole] Updated MTU: TX: %d RX: %d bytes\n", tx, rx); +} + +static struct bt_gatt_cb multirole_gatt_callbacks = { + .att_mtu_updated = multirole_mtu_updated, +}; + +static void multirole_connected(struct bt_conn *conn, uint8_t conn_err) +{ + struct bt_conn_info conn_info; + int err; + + if (!multirole_active) { + return; + } + + if (conn_err) { + printk("[Multirole] Failed to connect (0x%02x)\n", conn_err); + return; + } + + err = bt_conn_get_info(conn, &conn_info); + if (err) { + printk("[Multirole] Failed to get connection info (%d)\n", err); + return; + } + + printk("[Multirole] Connected: %s role %u\n", bt_conn_dst_str(conn), conn_info.role); + + if (conn_info.role == BT_CONN_ROLE_CENTRAL) { + /* We connected as central to Device 3 (peripheral_gatt_notify) */ + central_conn = bt_conn_ref(conn); + + multirole_mtu_exchange_params_central.func = multirole_mtu_exchange_cb; + err = bt_gatt_exchange_mtu(conn, &multirole_mtu_exchange_params_central); + if (err) { + printk("[Multirole] [Central] MTU exchange failed (err %d)\n", err); + discover_and_subscribe(conn); + } + } else { + /* Device 1 connected to us as peripheral */ + peripheral_conn = bt_conn_ref(conn); + + multirole_mtu_exchange_params_peripheral.func = multirole_mtu_exchange_cb; + (void)bt_gatt_exchange_mtu(conn, &multirole_mtu_exchange_params_peripheral); + } +} + +static void multirole_disconnected(struct bt_conn *conn, uint8_t reason) +{ + struct bt_conn_info conn_info; + int err; + + if (conn != central_conn && conn != peripheral_conn) { + return; + } + + err = bt_conn_get_info(conn, &conn_info); + if (err) { + printk("[Multirole] Failed to get connection info (%d)\n", err); + return; + } + + printk("[Multirole] Disconnected: %s role %u (reason 0x%02x)\n", + bt_conn_dst_str(conn), conn_info.role, reason); + + if (conn == central_conn) { + bt_conn_unref(central_conn); + central_conn = NULL; + central_subscribed = false; + /* Restart scanning to reconnect to Device 3 */ + multirole_start_scan(); + } else if (conn == peripheral_conn) { + bt_conn_unref(peripheral_conn); + peripheral_conn = NULL; + peripheral_notify_enabled = false; + /* Restart advertising for Device 1 */ + err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, multirole_ad, + ARRAY_SIZE(multirole_ad), NULL, 0); + if (err) { + printk("[Multirole] [Peripheral] Advertising failed to restart (err %d)\n", + err); + } + } +} + +static void multirole_le_param_updated(struct bt_conn *conn, uint16_t interval, + uint16_t latency, uint16_t timeout) +{ + printk("[Multirole] Conn params updated: int 0x%04x lat %u to %u\n", interval, + latency, timeout); +} + +BT_CONN_CB_DEFINE(multirole_conn_cb) = { + .connected = multirole_connected, + .disconnected = multirole_disconnected, + .le_param_updated = multirole_le_param_updated, +}; + +/* ===================== Main entry point ===================== */ + +uint32_t central_peripheral_notify(uint32_t count) +{ + int err; + + err = bt_enable(NULL); + if (err) { + printk("[Multirole] Bluetooth init failed (err %d)\n", err); + return 0U; + } + printk("[Multirole] Bluetooth initialized\n"); + + bt_gatt_cb_register(&multirole_gatt_callbacks); + + multirole_active = true; + central_conn = NULL; + peripheral_conn = NULL; + peripheral_notify_enabled = false; + central_subscribed = false; + multirole_last_tx_notify_rate = 0U; + multirole_last_rx_notify_rate = 0U; + multirole_notify_countdown = &count; + + /* Start advertising (peripheral role - for Device 1 to connect) */ + err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, multirole_ad, + ARRAY_SIZE(multirole_ad), NULL, 0); + if (err) { + printk("[Multirole] Advertising failed to start (err %d)\n", err); + return 0U; + } + printk("[Multirole] [Peripheral] Advertising successfully started\n"); + + /* Start scanning (central role - to connect to Device 3) */ + multirole_start_scan(); + + if (count != 0U) { + printk("[Multirole] Notify relay countdown %u.\n", count); + } else { + printk("[Multirole] Notify relay forever.\n"); + } + + /* Main loop: send notifications to Device 1 as fast as possible */ + while (true) { + struct bt_conn *conn = NULL; + + if (peripheral_conn && peripheral_notify_enabled) { + conn = bt_conn_ref(peripheral_conn); + } + + if (conn) { + int ret = multirole_send_notification(conn); + + bt_conn_unref(conn); + + if (ret > 0) { + update_tx_metrics((uint16_t)ret); + + if (count && !*multirole_notify_countdown) { + break; + } + } else if (ret == -ENOMEM) { + k_yield(); + } + + k_yield(); + } else { + k_sleep(K_SECONDS(1)); + } + } + + return multirole_last_tx_notify_rate; +} diff --git a/samples/bluetooth/central_peripheral_notify_send_receive/src/main.c b/samples/bluetooth/central_peripheral_notify_send_receive/src/main.c new file mode 100644 index 000000000000..1361c180ef90 --- /dev/null +++ b/samples/bluetooth/central_peripheral_notify_send_receive/src/main.c @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +extern uint32_t central_peripheral_notify(uint32_t count); + +int main(void) +{ + (void)central_peripheral_notify(0U); + return 0; +} diff --git a/samples/bluetooth/peripheral_gatt_notify/CMakeLists.txt b/samples/bluetooth/peripheral_gatt_notify/CMakeLists.txt new file mode 100644 index 000000000000..083545563885 --- /dev/null +++ b/samples/bluetooth/peripheral_gatt_notify/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(peripheral_gatt_notify) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/samples/bluetooth/peripheral_gatt_notify/Kconfig b/samples/bluetooth/peripheral_gatt_notify/Kconfig new file mode 100644 index 000000000000..1b33e6224dd4 --- /dev/null +++ b/samples/bluetooth/peripheral_gatt_notify/Kconfig @@ -0,0 +1,4 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +source "Kconfig.zephyr" diff --git a/samples/bluetooth/peripheral_gatt_notify/README.rst b/samples/bluetooth/peripheral_gatt_notify/README.rst new file mode 100644 index 000000000000..ac390aa80226 --- /dev/null +++ b/samples/bluetooth/peripheral_gatt_notify/README.rst @@ -0,0 +1,34 @@ +.. zephyr:code-sample:: ble_peripheral_gatt_notify + :name: Peripheral / GATT Notify + :relevant-api: bluetooth + + Advertise and send GATT notifications as fast as possible to a connected central. + +Overview +******** + +This sample acts as a Bluetooth LE peripheral that advertises, accepts a +connection from a central device, and then sends GATT notifications as fast as +possible. Throughput metrics are printed to the console at a configurable +interval. + +This sample is designed to work together with the +:zephyr:code-sample:`ble_central_notify_receive` sample or the +:zephyr:code-sample:`ble_central_peripheral_notify_send_receive` sample acting +as the central. + +Requirements +************ + +* A board with Bluetooth LE support + +Building and Running +******************** + +Build and flash the sample as follows, replacing ```` with your target board: + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/peripheral_gatt_notify + :board: + :goals: build flash + :compact: diff --git a/samples/bluetooth/peripheral_gatt_notify/prj.conf b/samples/bluetooth/peripheral_gatt_notify/prj.conf new file mode 100644 index 000000000000..e633306b8c7e --- /dev/null +++ b/samples/bluetooth/peripheral_gatt_notify/prj.conf @@ -0,0 +1,12 @@ +CONFIG_BT=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_DEVICE_NAME="Zephyr GATT Notify" + +CONFIG_BT_BUF_ACL_RX_SIZE=255 +CONFIG_BT_BUF_ACL_TX_SIZE=251 +CONFIG_BT_BUF_CMD_TX_SIZE=255 +CONFIG_BT_BUF_EVT_DISCARDABLE_SIZE=255 + +CONFIG_BT_L2CAP_TX_MTU=247 + +CONFIG_LOG=y diff --git a/samples/bluetooth/peripheral_gatt_notify/sample.yaml b/samples/bluetooth/peripheral_gatt_notify/sample.yaml new file mode 100644 index 000000000000..b641b583238b --- /dev/null +++ b/samples/bluetooth/peripheral_gatt_notify/sample.yaml @@ -0,0 +1,12 @@ +sample: + name: Bluetooth Peripheral GATT Notify +tests: + sample.bluetooth.peripheral_gatt_notify: + harness: bluetooth + platform_allow: + - qemu_cortex_m3 + - qemu_x86 + - nrf52_bsim + tags: bluetooth + integration_platforms: + - qemu_cortex_m3 diff --git a/samples/bluetooth/peripheral_gatt_notify/src/main.c b/samples/bluetooth/peripheral_gatt_notify/src/main.c new file mode 100644 index 000000000000..92627f33e08f --- /dev/null +++ b/samples/bluetooth/peripheral_gatt_notify/src/main.c @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +extern uint32_t peripheral_gatt_notify(uint32_t count); + +int main(void) +{ + (void)peripheral_gatt_notify(0U); + return 0; +} diff --git a/samples/bluetooth/peripheral_gatt_notify/src/peripheral_gatt_notify.c b/samples/bluetooth/peripheral_gatt_notify/src/peripheral_gatt_notify.c new file mode 100644 index 000000000000..d625922101df --- /dev/null +++ b/samples/bluetooth/peripheral_gatt_notify/src/peripheral_gatt_notify.c @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include +#include +#include + +/* Interval between printing throughput metrics */ +#define METRICS_INTERVAL 1U /* seconds */ + +/* Custom service UUID: 12345678-1234-5678-1234-56789abcdef0 */ +#define BT_UUID_NOTIFY_SERVICE_VAL \ + BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef0) +#define BT_UUID_NOTIFY_SERVICE BT_UUID_DECLARE_128(BT_UUID_NOTIFY_SERVICE_VAL) + +/* Custom characteristic UUID: 12345678-1234-5678-1234-56789abcdef1 */ +#define BT_UUID_NOTIFY_CHAR_VAL \ + BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef1) +#define BT_UUID_NOTIFY_CHAR BT_UUID_DECLARE_128(BT_UUID_NOTIFY_CHAR_VAL) + +static struct bt_conn *periph_conn_connected; +static bool periph_notify_enabled; +static bool periph_role_active; + +/* Throughput metrics */ +static uint32_t periph_notify_count; +static uint32_t periph_notify_len; +static uint32_t periph_notify_rate; +uint32_t periph_last_notify_rate; + +static void periph_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) +{ + periph_notify_enabled = (value == BT_GATT_CCC_NOTIFY); + printk("[Peripheral] Notifications %s\n", periph_notify_enabled ? "enabled" : "disabled"); +} + +BT_GATT_SERVICE_DEFINE(periph_notify_svc, + BT_GATT_PRIMARY_SERVICE(BT_UUID_NOTIFY_SERVICE), + BT_GATT_CHARACTERISTIC(BT_UUID_NOTIFY_CHAR, + BT_GATT_CHRC_NOTIFY, + BT_GATT_PERM_NONE, + NULL, NULL, NULL), + BT_GATT_CCC(periph_ccc_cfg_changed, + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), +); + +static const struct bt_data periph_ad[] = { + BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), + BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, + sizeof(CONFIG_BT_DEVICE_NAME) - 1), +}; + +static void periph_mtu_updated(struct bt_conn *conn, uint16_t tx, uint16_t rx) +{ + printk("[Peripheral] Updated MTU: TX: %d RX: %d bytes\n", tx, rx); +} + +static struct bt_gatt_cb periph_gatt_callbacks = { + .att_mtu_updated = periph_mtu_updated, +}; + +static void periph_connected(struct bt_conn *conn, uint8_t conn_err) +{ + if (!periph_role_active) { + return; + } + + if (conn_err) { + printk("[Peripheral] Failed to connect (0x%02x)\n", conn_err); + return; + } + + printk("[Peripheral] Connected: %s\n", bt_conn_dst_str(conn)); + periph_conn_connected = bt_conn_ref(conn); +} + +static void periph_disconnected(struct bt_conn *conn, uint8_t reason) +{ + if (conn != periph_conn_connected) { + return; + } + + printk("[Peripheral] Disconnected: %s (reason 0x%02x)\n", bt_conn_dst_str(conn), reason); + + if (periph_conn_connected) { + bt_conn_unref(periph_conn_connected); + periph_conn_connected = NULL; + } + + periph_notify_enabled = false; + + /* Restart advertising */ + int err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, periph_ad, ARRAY_SIZE(periph_ad), + NULL, 0); + + if (err) { + printk("[Peripheral] Advertising failed to restart (err %d)\n", err); + } +} + +static void periph_le_param_updated(struct bt_conn *conn, uint16_t interval, + uint16_t latency, uint16_t timeout) +{ + printk("[Peripheral] Conn params updated: int 0x%04x lat %u to %u\n", interval, + latency, timeout); +} + +BT_CONN_CB_DEFINE(periph_notify_conn_cb) = { + .connected = periph_connected, + .disconnected = periph_disconnected, + .le_param_updated = periph_le_param_updated, +}; + +static int periph_send_notification(struct bt_conn *conn) +{ + static uint8_t data[BT_ATT_MAX_ATTRIBUTE_LEN]; + uint16_t data_len; + int err; + + data_len = bt_gatt_get_mtu(conn) - 3; + if (data_len > BT_ATT_MAX_ATTRIBUTE_LEN) { + data_len = BT_ATT_MAX_ATTRIBUTE_LEN; + } + + /* Use the second attribute (the characteristic value) in the service */ + err = bt_gatt_notify(conn, &periph_notify_svc.attrs[1], data, data_len); + if (err == -ENOMEM) { + /* No buffers available, not an error - just back off */ + return err; + } + + if (err) { + printk("[Peripheral] Notify failed (err %d)\n", err); + return err; + } + + return data_len; +} + +static void periph_update_metrics(uint16_t len) +{ + static uint32_t cycle_stamp; + uint64_t delta; + + delta = k_cycle_get_32() - cycle_stamp; + delta = k_cyc_to_ns_floor64(delta); + + if (delta == 0) { + return; + } + + if (delta > (METRICS_INTERVAL * NSEC_PER_SEC)) { + printk("[Peripheral] Notify: count= %u, len= %u, rate= %u bps.\n", + periph_notify_count, periph_notify_len, periph_notify_rate); + + periph_last_notify_rate = periph_notify_rate; + + periph_notify_count = 0U; + periph_notify_len = 0U; + periph_notify_rate = 0U; + cycle_stamp = k_cycle_get_32(); + } else { + periph_notify_count++; + periph_notify_len += len; + periph_notify_rate = ((uint64_t)periph_notify_len << 3) * + (METRICS_INTERVAL * NSEC_PER_SEC) / delta; + } +} + +uint32_t peripheral_gatt_notify(uint32_t count) +{ + int err; + + err = bt_enable(NULL); + if (err) { + printk("[Peripheral] Bluetooth init failed (err %d)\n", err); + return 0U; + } + printk("[Peripheral] Bluetooth initialized\n"); + + bt_gatt_cb_register(&periph_gatt_callbacks); + + err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, periph_ad, ARRAY_SIZE(periph_ad), NULL, 0); + if (err) { + printk("[Peripheral] Advertising failed to start (err %d)\n", err); + return 0U; + } + printk("[Peripheral] Advertising successfully started\n"); + + periph_role_active = true; + periph_conn_connected = NULL; + periph_notify_enabled = false; + periph_last_notify_rate = 0U; + + if (count != 0U) { + printk("[Peripheral] GATT Notify countdown %u on connection.\n", count); + } else { + printk("[Peripheral] GATT Notify forever on connection.\n"); + } + + while (true) { + struct bt_conn *conn = NULL; + + if (periph_conn_connected && periph_notify_enabled) { + conn = bt_conn_ref(periph_conn_connected); + } + + if (conn) { + int ret = periph_send_notification(conn); + + bt_conn_unref(conn); + + if (ret > 0) { + periph_update_metrics((uint16_t)ret); + + if (count) { + count--; + if (!count) { + break; + } + } + } else if (ret == -ENOMEM) { + /* Wait briefly for buffers to free up */ + k_yield(); + } + + k_yield(); + } else { + k_sleep(K_SECONDS(1)); + } + } + + return periph_last_notify_rate; +} diff --git a/tests/bsim/bluetooth/host/multirole_gatt_relay/CMakeLists.txt b/tests/bsim/bluetooth/host/multirole_gatt_relay/CMakeLists.txt new file mode 100644 index 000000000000..ebe21c0272b7 --- /dev/null +++ b/tests/bsim/bluetooth/host/multirole_gatt_relay/CMakeLists.txt @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(bsim_test_multirole_gatt_relay) + +target_sources(app PRIVATE + src/main.c + ${ZEPHYR_BASE}/samples/bluetooth/peripheral_gatt_notify/src/peripheral_gatt_notify.c + ${ZEPHYR_BASE}/samples/bluetooth/central_notify_receive/src/central_notify_receive.c + ${ZEPHYR_BASE}/samples/bluetooth/central_peripheral_notify_send_receive/src/central_peripheral_notify.c +) + +zephyr_include_directories( + ${BSIM_COMPONENTS_PATH}/libUtilv1/src/ + ${BSIM_COMPONENTS_PATH}/libPhyComv1/src/ +) diff --git a/tests/bsim/bluetooth/host/multirole_gatt_relay/Kconfig b/tests/bsim/bluetooth/host/multirole_gatt_relay/Kconfig new file mode 100644 index 000000000000..1b33e6224dd4 --- /dev/null +++ b/tests/bsim/bluetooth/host/multirole_gatt_relay/Kconfig @@ -0,0 +1,4 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +source "Kconfig.zephyr" diff --git a/tests/bsim/bluetooth/host/multirole_gatt_relay/prj.conf b/tests/bsim/bluetooth/host/multirole_gatt_relay/prj.conf new file mode 100644 index 000000000000..d9565ab64fd5 --- /dev/null +++ b/tests/bsim/bluetooth/host/multirole_gatt_relay/prj.conf @@ -0,0 +1,41 @@ +CONFIG_BT=y +CONFIG_BT_CENTRAL=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_GATT_CLIENT=y +CONFIG_BT_DEVICE_NAME="Zephyr Multirole Notify" +CONFIG_BT_MAX_CONN=2 + +# Maximum L2CAP size for one LL PDU size +CONFIG_BT_L2CAP_TX_MTU=247 + +# Auto initiated Data Length update on connection established +CONFIG_BT_AUTO_DATA_LEN_UPDATE=y +# Enable user initiated Data Length update support +CONFIG_BT_USER_DATA_LEN_UPDATE=y + +# BT HCI Command Buffers +CONFIG_BT_BUF_CMD_TX_COUNT=4 +CONFIG_BT_BUF_CMD_TX_SIZE=255 + +# BT HCI Event Buffers +CONFIG_BT_BUF_EVT_RX_COUNT=7 +CONFIG_BT_BUF_EVT_RX_SIZE=255 + +# BT HCI Discardable Event Buffers +CONFIG_BT_BUF_EVT_DISCARDABLE_COUNT=3 +CONFIG_BT_BUF_EVT_DISCARDABLE_SIZE=255 + +# BT HCI ACL TX Data Buffers +CONFIG_BT_BUF_ACL_TX_COUNT=6 +CONFIG_BT_BUF_ACL_TX_SIZE=251 + +# BT HCI ACL RX Data Buffers +CONFIG_BT_BUF_ACL_RX_COUNT_EXTRA=1 +CONFIG_BT_BUF_ACL_RX_SIZE=255 + +# Maximum LL ACL PDU length +CONFIG_BT_CTLR_DATA_LENGTH_MAX=251 + +CONFIG_CLOCK_CONTROL_NRF_K32SRC_100PPM=y + +CONFIG_LOG=y diff --git a/tests/bsim/bluetooth/host/multirole_gatt_relay/src/main.c b/tests/bsim/bluetooth/host/multirole_gatt_relay/src/main.c new file mode 100644 index 000000000000..36c9cab5ebc9 --- /dev/null +++ b/tests/bsim/bluetooth/host/multirole_gatt_relay/src/main.c @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include + +#include +#include + +#include "bs_types.h" +#include "bs_tracing.h" +#include "time_machine.h" +#include "bstests.h" + +/* + * Test topology: + * Device 0: Central (central_notify_receive) - receives notifications from Device 1 + * Device 1: Multirole (central_peripheral_notify) - peripheral to Dev 0, central to Dev 2 + * Device 2: Peripheral (peripheral_gatt_notify) - sends notifications to Device 1 + * + * Notification flow: Device 2 -> Device 1 -> Device 0 + */ + +/* Number of notifications to receive/send before declaring the test passed. + * This needs to be large enough to ensure notifications flow for at least + * one metrics interval (1 second) on each link. + */ +#define COUNT_PERIPHERAL 5000U +#define COUNT_MULTIROLE 5000U +#define COUNT_CENTRAL 5000U + +extern uint32_t peripheral_gatt_notify(uint32_t count); +extern uint32_t central_peripheral_notify(uint32_t count); +extern uint32_t central_notify_receive(uint32_t count); + +#define FAIL(...) \ + do { \ + bst_result = Failed; \ + bs_trace_error_time_line(__VA_ARGS__); \ + } while (0) + +#define PASS(...) \ + do { \ + bst_result = Passed; \ + bs_trace_info_time(1, __VA_ARGS__); \ + } while (0) + +extern enum bst_result_t bst_result; + +static void test_central_main(void) +{ + uint32_t rate; + + rate = central_notify_receive(COUNT_CENTRAL); + + printk("%s: Notify RX Rate = %u bps\n", __func__, rate); + if (rate > 0U) { + PASS("Central tests passed\n"); + } else { + FAIL("Central tests failed\n"); + } + + /* Give extra time for other devices to finish */ + k_sleep(K_SECONDS(1)); + + bs_trace_silent_exit(0); +} + +static void test_multirole_main(void) +{ + uint32_t rate; + + rate = central_peripheral_notify(COUNT_MULTIROLE); + + printk("%s: Notify TX Rate = %u bps\n", __func__, rate); + if (rate > 0U) { + PASS("Multirole tests passed\n"); + } else { + FAIL("Multirole tests failed\n"); + } + + /* Give extra time for other devices to finish */ + k_sleep(K_SECONDS(1)); + + bs_trace_silent_exit(0); +} + +static void test_peripheral_main(void) +{ + uint32_t rate; + + rate = peripheral_gatt_notify(COUNT_PERIPHERAL); + + printk("%s: Notify TX Rate = %u bps\n", __func__, rate); + if (rate > 0U) { + PASS("Peripheral tests passed\n"); + } else { + FAIL("Peripheral tests failed\n"); + } +} + +static void test_init(void) +{ + bst_ticker_set_next_tick_absolute(600e6); + bst_result = In_progress; +} + +static void test_tick(bs_time_t HW_device_time) +{ + bst_result = Failed; + bs_trace_error_line("Test multirole GATT relay timed out.\n"); +} + +static const struct bst_test_instance test_def[] = { + { + .test_id = "central", + .test_descr = "Central Notify Receive", + .test_pre_init_f = test_init, + .test_tick_f = test_tick, + .test_main_f = test_central_main, + }, + { + .test_id = "multirole", + .test_descr = "Multirole Notify Send Receive", + .test_pre_init_f = test_init, + .test_tick_f = test_tick, + .test_main_f = test_multirole_main, + }, + { + .test_id = "peripheral", + .test_descr = "Peripheral GATT Notify", + .test_pre_init_f = test_init, + .test_tick_f = test_tick, + .test_main_f = test_peripheral_main, + }, + BSTEST_END_MARKER +}; + +struct bst_test_list *test_multirole_gatt_relay_install(struct bst_test_list *tests) +{ + return bst_add_tests(tests, test_def); +} + +bst_test_install_t test_installers[] = { + test_multirole_gatt_relay_install, + NULL +}; + +int main(void) +{ + bst_main(); + return 0; +} diff --git a/tests/bsim/bluetooth/host/multirole_gatt_relay/testcase.yaml b/tests/bsim/bluetooth/host/multirole_gatt_relay/testcase.yaml new file mode 100644 index 000000000000..a3030fe8093a --- /dev/null +++ b/tests/bsim/bluetooth/host/multirole_gatt_relay/testcase.yaml @@ -0,0 +1,10 @@ +tests: + bsim.bluetooth.host.multirole_gatt_relay: + build_only: true + tags: + - bluetooth + platform_allow: + - nrf52_bsim/native + harness: bsim + harness_config: + bsim_exe_name: tests_bsim_bluetooth_host_multirole_gatt_relay_prj_conf diff --git a/tests/bsim/bluetooth/host/multirole_gatt_relay/tests_scripts/multirole_gatt_relay.sh b/tests/bsim/bluetooth/host/multirole_gatt_relay/tests_scripts/multirole_gatt_relay.sh new file mode 100755 index 000000000000..83acbf8f96f3 --- /dev/null +++ b/tests/bsim/bluetooth/host/multirole_gatt_relay/tests_scripts/multirole_gatt_relay.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Copyright 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +source ${ZEPHYR_BASE}/tests/bsim/sh_common.source + +simulation_id="${BOARD_TS}_host-multirole-gatt-relay" +verbosity_level=2 +EXECUTE_TIMEOUT=900 + +cd ${BSIM_OUT_PATH}/bin + +Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_host_multirole_gatt_relay_prj_conf \ + -v=${verbosity_level} -s=${simulation_id} -d=0 \ + -testid=central + +Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_host_multirole_gatt_relay_prj_conf \ + -v=${verbosity_level} -s=${simulation_id} -d=1 -xo_drift=+100e-6 \ + -testid=multirole + +Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_host_multirole_gatt_relay_prj_conf \ + -v=${verbosity_level} -s=${simulation_id} -d=2 \ + -testid=peripheral + +Execute ./bs_2G4_phy_v1 -v=${verbosity_level} -s=${simulation_id} \ + -D=3 -sim_length=600e6 $@ -argschannel -at=40 + +wait_for_background_jobs