Bluetooth: L2CAP: Add re-assembly stress test

Add a test that verifies no resources are leaked when re-assembling L2CAP
PDUs over an unreliable channel (ie. lots of disconnects).

Signed-off-by: Jonathan Rico <jonathan.rico@nordicsemi.no>
This commit is contained in:
Jonathan Rico 2024-08-07 14:47:08 +02:00 committed by Carles Cufí
commit e489ec2b95
12 changed files with 1045 additions and 0 deletions

View file

@ -20,6 +20,8 @@ app=tests/bsim/bluetooth/host/l2cap/stress conf_file=prj_nofrag.conf compile
app=tests/bsim/bluetooth/host/l2cap/stress conf_file=prj_syswq.conf compile
app=tests/bsim/bluetooth/host/l2cap/split/dut compile
app=tests/bsim/bluetooth/host/l2cap/split/tester compile
app=tests/bsim/bluetooth/host/l2cap/reassembly/dut compile
app=tests/bsim/bluetooth/host/l2cap/reassembly/peer compile
app=tests/bsim/bluetooth/host/l2cap/ecred/dut compile
app=tests/bsim/bluetooth/host/l2cap/ecred/peer compile
app=tests/bsim/bluetooth/host/l2cap/credits compile

View file

@ -0,0 +1,15 @@
/*
* Copyright (c) 2024 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef ZEPHYR_TESTS_BSIM_BLUETOOTH_HOST_L2CAP_REASSEMBLY_SRC_DATA_H_
#define ZEPHYR_TESTS_BSIM_BLUETOOTH_HOST_L2CAP_REASSEMBLY_SRC_DATA_H_
#define GATT_HANDLE 0x1337
#define PEER_NAME "peer"
#define TEST_ITERATIONS 20
#define NOTIFICATION_PAYLOAD "the !ification"
#endif /* ZEPHYR_TESTS_BSIM_BLUETOOTH_HOST_L2CAP_REASSEMBLY_SRC_DATA_H_ */

View file

@ -0,0 +1,24 @@
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(reassembly)
add_subdirectory(${ZEPHYR_BASE}/tests/bluetooth/common/testlib testlib)
target_link_libraries(app PRIVATE testlib)
add_subdirectory(${ZEPHYR_BASE}/tests/bsim/babblekit babblekit)
target_link_libraries(app PRIVATE babblekit)
zephyr_include_directories(
../
${BSIM_COMPONENTS_PATH}/libUtilv1/src/
${BSIM_COMPONENTS_PATH}/libPhyComv1/src/
)
target_sources(app PRIVATE
src/main.c
src/dut.c
)

View file

@ -0,0 +1,32 @@
CONFIG_BT=y
CONFIG_BT_DEVICE_NAME="reassembly"
CONFIG_BT_CENTRAL=y
# Dependency of testlib/adv and testlib/scan.
CONFIG_BT_EXT_ADV=y
CONFIG_BT_GATT_CLIENT=y
CONFIG_ASSERT=y
CONFIG_LOG=y
CONFIG_LOG_RUNTIME_FILTERING=y
CONFIG_THREAD_NAME=y
CONFIG_LOG_THREAD_ID_PREFIX=y
CONFIG_ASSERT_ON_ERRORS=y
CONFIG_ARCH_POSIX_TRAP_ON_FATAL=y
# Disable auto-initiated procedures so they don't
# mess with the test's execution.
CONFIG_BT_AUTO_PHY_UPDATE=n
CONFIG_BT_AUTO_DATA_LEN_UPDATE=n
CONFIG_BT_GAP_AUTO_UPDATE_CONN_PARAMS=n
# Since the test's purpose is to test for leaks in the ACL
# RX buffer pool, it is a good idea to constrain said buffer
# pool.
CONFIG_BT_MAX_CONN=1
CONFIG_BT_BUF_ACL_RX_COUNT=6
CONFIG_BT_BUF_EVT_RX_COUNT=6

View file

@ -0,0 +1,162 @@
/*
* Copyright (c) 2024 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/kernel.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/logging/log.h>
#include "testlib/att_read.h"
#include "testlib/att_write.h"
#include "testlib/conn.h"
#include "testlib/scan.h"
#include "testlib/log_utils.h"
#include "babblekit/flags.h"
#include "babblekit/testcase.h"
/* local includes */
#include "data.h"
LOG_MODULE_REGISTER(dut, LOG_LEVEL_DBG);
extern unsigned long runtime_log_level;
static DEFINE_FLAG(got_notification);
static uint8_t received_notification(struct bt_conn *conn,
struct bt_gatt_subscribe_params *params,
const void *data,
uint16_t length)
{
if (length) {
size_t expected_length = sizeof(NOTIFICATION_PAYLOAD);
bool payload_is_correct = 0 == memcmp(data, NOTIFICATION_PAYLOAD, length);
LOG_INF("Received notification");
LOG_HEXDUMP_DBG(data, length, "payload");
TEST_ASSERT(params->value_handle == GATT_HANDLE,
"Wrong handle used: expect 0x%x got 0x%x",
GATT_HANDLE, params->value_handle);
TEST_ASSERT(length == expected_length,
"Length is incorrect: expect %d got %d",
expected_length, length);
TEST_ASSERT(payload_is_correct, "Notification contents mismatch");
SET_FLAG(got_notification);
}
return BT_GATT_ITER_CONTINUE;
}
/* Subscription parameters have the same lifetime as a subscription.
* That is the backing struct should stay valid until a call to
* `bt_gatt_unsubscribe()` is made. Hence the `static`.
*/
static struct bt_gatt_subscribe_params sub_params;
/* Link `cb` to notifications received from `peer` for `handle`. Using
* `bt_gatt_resubscribe()` doesn't send anything on-air and just does the
* linking in the host.
*/
static void fake_subscribe(bt_addr_le_t *peer,
uint16_t handle,
bt_gatt_notify_func_t cb)
{
int err;
/* Subscribe to notifications */
sub_params.notify = cb;
sub_params.value = BT_GATT_CCC_NOTIFY;
sub_params.value_handle = handle;
/* Doesn't matter for re-subscribe. */
sub_params.ccc_handle = handle + 2;
err = bt_gatt_resubscribe(0, peer, &sub_params);
TEST_ASSERT(!err, "Subscribe failed (err %d)", err);
}
static void run_test_iteration(bt_addr_le_t *peer)
{
int err;
struct bt_conn *conn = NULL;
/* Create a connection using that address */
err = bt_testlib_connect(peer, &conn);
TEST_ASSERT(!err, "Failed to initiate connection (err %d)", err);
LOG_DBG("Connected");
LOG_DBG("Subscribe to test characteristic: handle 0x%04x", GATT_HANDLE);
fake_subscribe(peer, GATT_HANDLE, received_notification);
WAIT_FOR_FLAG(got_notification);
LOG_DBG("Wait for disconnection from peer");
bt_testlib_wait_disconnected(conn);
bt_testlib_conn_unref(&conn);
}
void entrypoint_dut(void)
{
/* Test purpose:
*
* Verifies that the Host does not leak resources related to
* reassembling L2CAP PDUs when operating over an unreliable connection.
*
* Two devices:
* - `peer`: sends long GATT notifications
* - `dut`: receives long notifications from `peer`
*
* To do this, we configure the devices that ensures L2CAP PDUs are
* fragmented on-air over a long period. That mostly means smallest data
* length possible combined with a long connection interval.
*
* We try to disconnect when a PDU is mid-reassembly. This is slightly
* tricky to ensure: we rely that the implementation of the controller
* will forward PDU fragments as soon as they are received on-air.
*
* Procedure (loop 20x):
* - [dut] establish connection to `peer`
* - [peer] send notification #1
* - [dut] wait until notification #1 received
*
* - [peer] send 2 out of 3 frags of notification #2
* - [peer] disconnect
* - [dut] wait for disconnection
*
* [verdict]
* - dut receives notification #1 for all iterations
*/
int err;
bt_addr_le_t peer = {};
/* Mark test as in progress. */
TEST_START("dut");
/* Set the log level given by the `log_level` CLI argument */
bt_testlib_log_level_set("dut", runtime_log_level);
/* Initialize Bluetooth */
err = bt_enable(NULL);
TEST_ASSERT(err == 0, "Can't enable Bluetooth (err %d)", err);
LOG_DBG("Bluetooth initialized");
/* Find the address of the peer, using its advertised name */
err = bt_testlib_scan_find_name(&peer, "peer");
TEST_ASSERT(!err, "Failed to start scan (err %d)", err);
for (size_t i = 0; i < TEST_ITERATIONS; i++) {
LOG_INF("## Iteration %d", i);
run_test_iteration(&peer);
}
TEST_PASS("dut");
}

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2024 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/kernel.h>
#include "bs_tracing.h"
#include "bstests.h"
#include "babblekit/testcase.h"
#include "testlib/log_utils.h"
extern void entrypoint_dut(void);
extern enum bst_result_t bst_result;
unsigned long runtime_log_level = LOG_LEVEL_INF;
static void test_args(int argc, char *argv[])
{
size_t argn = 0;
const char *arg = argv[argn];
if (strcmp(arg, "log_level") == 0) {
runtime_log_level = strtoul(argv[++argn], NULL, 10);
if (runtime_log_level >= LOG_LEVEL_NONE &&
runtime_log_level <= LOG_LEVEL_DBG){
TEST_PRINT("Runtime log level configuration: %d", runtime_log_level);
} else {
TEST_FAIL("Invalid arguments to set log level: %d", runtime_log_level);
}
} else {
TEST_PRINT("Default runtime log level configuration: INFO");
}
}
static void test_end_cb(void)
{
if (bst_result != Passed) {
TEST_PRINT("Test has not passed.");
}
}
static const struct bst_test_instance entrypoints[] = {
{
.test_id = "dut",
.test_delete_f = test_end_cb,
.test_main_f = entrypoint_dut,
.test_args_f = test_args,
},
BSTEST_END_MARKER,
};
static struct bst_test_list *install(struct bst_test_list *tests)
{
return bst_add_tests(tests, entrypoints);
};
bst_test_install_t test_installers[] = {install, NULL};
int main(void)
{
bst_main();
return 0;
}

View file

@ -0,0 +1,21 @@
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(l2cap_reassembly_peer)
add_subdirectory(${ZEPHYR_BASE}/tests/bsim/babblekit babblekit)
target_link_libraries(app PRIVATE babblekit)
target_sources(app PRIVATE
src/main.c
src/peer.c
)
zephyr_include_directories(
../
${ZEPHYR_BASE}/subsys/bluetooth/common/
${BSIM_COMPONENTS_PATH}/libUtilv1/src/
${BSIM_COMPONENTS_PATH}/libPhyComv1/src/
)

View file

@ -0,0 +1,19 @@
CONFIG_LOG=y
CONFIG_ASSERT=y
CONFIG_BT=y
CONFIG_BT_HCI_RAW=y
CONFIG_BT_MAX_CONN=1
CONFIG_BT_BUF_CMD_TX_COUNT=10
CONFIG_BT_BUF_ACL_TX_COUNT=20
CONFIG_BT_BUF_ACL_RX_SIZE=255
CONFIG_BT_BUF_CMD_TX_SIZE=255
CONFIG_BT_BUF_EVT_DISCARDABLE_SIZE=255
# Allow whole L2CAP PDUs to fit on-air
CONFIG_BT_BUF_ACL_TX_SIZE=251
CONFIG_BT_BUF_ACL_RX_SIZE=251
CONFIG_BT_DATA_LEN_UPDATE=y
CONFIG_BT_CTLR_DATA_LENGTH_MAX=251

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2024 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/kernel.h>
#include "babblekit/testcase.h"
#include "bs_tracing.h"
#include "bstests.h"
extern void entrypoint_peer(void);
extern enum bst_result_t bst_result;
static void test_end_cb(void)
{
if (bst_result != Passed) {
TEST_PRINT("Test has not passed.");
}
}
static const struct bst_test_instance entrypoints[] = {
{
.test_id = "peer",
.test_delete_f = test_end_cb,
.test_main_f = entrypoint_peer,
},
BSTEST_END_MARKER,
};
static struct bst_test_list *install(struct bst_test_list *tests)
{
return bst_add_tests(tests, entrypoints);
};
bst_test_install_t test_installers[] = {install, NULL};
int main(void)
{
bst_main();
return 0;
}

View file

@ -0,0 +1,613 @@
/*
* Copyright (c) 2024 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/kernel.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/__assert.h>
#include <zephyr/net/buf.h>
#include <zephyr/bluetooth/buf.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/hci.h>
#include <zephyr/bluetooth/hci_raw.h>
#include "common/bt_str.h"
#include "host/conn_internal.h"
#include "host/l2cap_internal.h"
#include "babblekit/flags.h"
#include "babblekit/testcase.h"
#include "data.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(bt_tinyhost, LOG_LEVEL_INF);
#define BT_ATT_OP_MTU_REQ 0x02
#define BT_ATT_OP_MTU_RSP 0x03
#define BT_ATT_OP_WRITE_REQ 0x12
#define BT_ATT_OP_WRITE_RSP 0x13
#define BT_ATT_OP_NOTIFY 0x1b
#define BT_ATT_OP_INDICATE 0x1d
#define BT_ATT_OP_CONFIRM 0x1e
#define BT_ATT_OP_WRITE_CMD 0x52
#define BT_L2CAP_CID_ATT 0x0004
#define LAST_SUPPORTED_ATT_OPCODE 0x20
DEFINE_FLAG(is_connected);
static K_FIFO_DEFINE(rx_queue);
#define CMD_BUF_SIZE MAX(BT_BUF_EVT_RX_SIZE, BT_BUF_CMD_TX_SIZE)
NET_BUF_POOL_FIXED_DEFINE(hci_cmd_pool, CONFIG_BT_BUF_CMD_TX_COUNT,
CMD_BUF_SIZE, 8, NULL);
#define MAX_CMD_COUNT 1
static K_SEM_DEFINE(cmd_sem, MAX_CMD_COUNT, MAX_CMD_COUNT);
static struct k_sem acl_pkts;
static uint16_t conn_handle;
static volatile uint16_t active_opcode = 0xFFFF;
static struct net_buf *cmd_rsp;
struct net_buf *bt_hci_cmd_create(uint16_t opcode, uint8_t param_len)
{
struct bt_hci_cmd_hdr *hdr;
struct net_buf *buf;
LOG_DBG("opcode 0x%04x param_len %u", opcode, param_len);
buf = net_buf_alloc(&hci_cmd_pool, K_FOREVER);
TEST_ASSERT(buf, "failed allocation");
LOG_DBG("buf %p", buf);
net_buf_reserve(buf, BT_BUF_RESERVE);
bt_buf_set_type(buf, BT_BUF_CMD);
hdr = net_buf_add(buf, sizeof(*hdr));
hdr->opcode = sys_cpu_to_le16(opcode);
hdr->param_len = param_len;
return buf;
}
static void handle_cmd_complete(struct net_buf *buf)
{
struct bt_hci_evt_hdr *hdr;
uint8_t status, ncmd;
uint16_t opcode;
struct net_buf_simple_state state;
net_buf_simple_save(&buf->b, &state);
hdr = net_buf_pull_mem(buf, sizeof(*hdr));
if (hdr->evt == BT_HCI_EVT_CMD_COMPLETE) {
struct bt_hci_evt_cmd_complete *evt;
evt = net_buf_pull_mem(buf, sizeof(*evt));
status = 0;
ncmd = evt->ncmd;
opcode = sys_le16_to_cpu(evt->opcode);
} else if (hdr->evt == BT_HCI_EVT_CMD_STATUS) {
struct bt_hci_evt_cmd_status *evt;
evt = net_buf_pull_mem(buf, sizeof(*evt));
status = buf->data[0];
ncmd = evt->ncmd;
opcode = sys_le16_to_cpu(evt->opcode);
} else {
TEST_FAIL("unhandled event 0x%x", hdr->evt);
}
LOG_DBG("opcode 0x%04x status %x", opcode, status);
TEST_ASSERT(status == 0x00, "cmd 0x%x status: 0x%x", opcode, status);
TEST_ASSERT(active_opcode == opcode, "unexpected opcode %x != %x", active_opcode, opcode);
active_opcode = 0xFFFF;
cmd_rsp = net_buf_ref(buf);
net_buf_simple_restore(&buf->b, &state);
if (ncmd) {
k_sem_give(&cmd_sem);
}
}
static void handle_meta_event(struct net_buf *buf)
{
uint8_t code = buf->data[2];
switch (code) {
case BT_HCI_EVT_LE_ENH_CONN_COMPLETE:
case BT_HCI_EVT_LE_ENH_CONN_COMPLETE_V2:
conn_handle = sys_get_le16(&buf->data[4]);
LOG_DBG("connected: handle: %d", conn_handle);
SET_FLAG(is_connected);
break;
case BT_HCI_EVT_LE_CHAN_SEL_ALGO:
/* do nothing */
break;
default:
LOG_ERR("unhandled meta event %x", code);
LOG_HEXDUMP_ERR(buf->data, buf->len, "HCI META EVT");
}
}
static void handle_ncp(struct net_buf *buf)
{
struct bt_hci_evt_num_completed_packets *evt;
struct bt_hci_evt_hdr *hdr;
uint16_t handle, count;
hdr = net_buf_pull_mem(buf, sizeof(*hdr));
evt = (void *)buf->data;
handle = sys_le16_to_cpu(evt->h[0].handle);
count = sys_le16_to_cpu(evt->h[0].count);
LOG_DBG("sent %d packets", count);
while (count--) {
k_sem_give(&acl_pkts);
}
}
struct net_buf *alloc_l2cap_pdu(void);
static void send_l2cap_packet(struct net_buf *buf, uint16_t cid);
static void handle_att(struct net_buf *buf)
{
uint8_t op = net_buf_pull_u8(buf);
switch (op) {
case BT_ATT_OP_NOTIFY:
LOG_INF("got ATT notification");
return;
case BT_ATT_OP_WRITE_RSP:
LOG_INF("got ATT write RSP");
return;
case BT_ATT_OP_MTU_RSP:
LOG_INF("got ATT MTU RSP");
return;
default:
LOG_HEXDUMP_ERR(buf->data, buf->len, "payload");
TEST_FAIL("unhandled opcode %x\n", op);
return;
}
}
static void handle_l2cap(struct net_buf *buf)
{
struct bt_l2cap_hdr *hdr;
uint16_t cid;
hdr = net_buf_pull_mem(buf, sizeof(*hdr));
cid = sys_le16_to_cpu(hdr->cid);
LOG_DBG("Packet for CID %u len %u", cid, buf->len);
LOG_HEXDUMP_DBG(buf->data, buf->len, "l2cap");
/* Make sure we don't have to recombine packets */
TEST_ASSERT(buf->len == hdr->len, "buflen = %d != hdrlen %d",
buf->len, hdr->len);
TEST_ASSERT(cid == BT_L2CAP_CID_ATT, "We only support (U)ATT");
/* (U)ATT PDU */
handle_att(buf);
}
static void handle_acl(struct net_buf *buf)
{
struct bt_hci_acl_hdr *hdr;
uint16_t len, handle;
uint8_t flags;
hdr = net_buf_pull_mem(buf, sizeof(*hdr));
len = sys_le16_to_cpu(hdr->len);
handle = sys_le16_to_cpu(hdr->handle);
flags = bt_acl_flags(handle);
handle = bt_acl_handle(handle);
TEST_ASSERT(flags == BT_ACL_START,
"Fragmentation not supported");
LOG_DBG("ACL: conn %d len %d flags %d", handle, len, flags);
LOG_HEXDUMP_DBG(buf->data, buf->len, "HCI ACL");
handle_l2cap(buf);
}
static void recv(struct net_buf *buf)
{
LOG_HEXDUMP_DBG(buf->data, buf->len, "HCI RX");
uint8_t code = buf->data[0];
if (bt_buf_get_type(buf) == BT_BUF_EVT) {
switch (code) {
case BT_HCI_EVT_CMD_COMPLETE:
case BT_HCI_EVT_CMD_STATUS:
handle_cmd_complete(buf);
break;
case BT_HCI_EVT_LE_META_EVENT:
handle_meta_event(buf);
break;
case BT_HCI_EVT_DISCONN_COMPLETE:
UNSET_FLAG(is_connected);
break;
case BT_HCI_EVT_NUM_COMPLETED_PACKETS:
handle_ncp(buf);
break;
default:
LOG_ERR("unhandled msg %x", code);
LOG_HEXDUMP_ERR(buf->data, buf->len, "HCI EVT");
}
/* handlers should take a ref if they want to access the buffer
* later
*/
net_buf_unref(buf);
return;
}
if (bt_buf_get_type(buf) == BT_BUF_ACL_IN) {
handle_acl(buf);
net_buf_unref(buf);
return;
}
LOG_ERR("HCI RX (not data or event)");
net_buf_unref(buf);
}
static void send_cmd(uint16_t opcode, struct net_buf *cmd, struct net_buf **rsp)
{
LOG_DBG("opcode %x", opcode);
if (!cmd) {
cmd = bt_hci_cmd_create(opcode, 0);
}
k_sem_take(&cmd_sem, K_FOREVER);
TEST_ASSERT(active_opcode == 0xFFFF, "");
__ASSERT_NO_MSG(opcode);
active_opcode = opcode;
LOG_HEXDUMP_DBG(cmd->data, cmd->len, "HCI TX");
bt_send(cmd);
/* Wait until the command completes:
*
* Use `cmd_sem` as a signal that we are able to send another command,
* which means that the current command (for which we took cmd_sem
* above) likely has gotten a response.
*
* We don't actually want to send anything more, so when we got that
* signal (ie the thread is un-suspended), then we release the sem
* immediately.
*/
BUILD_ASSERT(MAX_CMD_COUNT == 1, "Logic depends on only 1 cmd at a time");
k_sem_take(&cmd_sem, K_FOREVER);
k_sem_give(&cmd_sem);
net_buf_unref(cmd);
/* return response. it's okay if cmd_rsp gets overwritten, since the app
* gets the ref to the underlying buffer when this fn returns.
*/
if (rsp) {
*rsp = cmd_rsp;
} else {
net_buf_unref(cmd_rsp);
cmd_rsp = NULL;
}
}
static K_THREAD_STACK_DEFINE(rx_thread_stack, 1024);
static struct k_thread rx_thread_data;
static void rx_thread(void *p1, void *p2, void *p3)
{
LOG_DBG("start HCI rx");
while (true) {
struct net_buf *buf;
/* Wait until a buffer is available */
buf = net_buf_get(&rx_queue, K_FOREVER);
recv(buf);
}
}
static void le_read_buffer_size_complete(struct net_buf *rsp)
{
struct bt_hci_rp_le_read_buffer_size *rp = (void *)rsp->data;
LOG_DBG("status 0x%02x", rp->status);
LOG_DBG("max len %d max num %d", rp->le_max_len, rp->le_max_num);
k_sem_init(&acl_pkts, rp->le_max_num, rp->le_max_num);
net_buf_unref(rsp);
}
static void set_event_mask(uint16_t opcode)
{
struct bt_hci_cp_set_event_mask *cp_mask;
struct net_buf *buf;
uint64_t mask = 0U;
/* The two commands have the same length/params */
buf = bt_hci_cmd_create(opcode, sizeof(*cp_mask));
TEST_ASSERT(buf, "");
/* Forward all events */
cp_mask = net_buf_add(buf, sizeof(*cp_mask));
mask = UINT64_MAX;
sys_put_le64(mask, cp_mask->events);
send_cmd(opcode, buf, NULL);
}
static void set_random_address(void)
{
struct net_buf *buf;
bt_addr_le_t addr = {BT_ADDR_LE_RANDOM, {{0x0A, 0x89, 0x67, 0x45, 0x23, 0xC1}}};
LOG_DBG("%s", bt_addr_str(&addr.a));
buf = bt_hci_cmd_create(BT_HCI_OP_LE_SET_RANDOM_ADDRESS, sizeof(addr.a));
TEST_ASSERT(buf, "");
net_buf_add_mem(buf, &addr.a, sizeof(addr.a));
send_cmd(BT_HCI_OP_LE_SET_RANDOM_ADDRESS, buf, NULL);
}
static void start_adv(uint16_t interval, const char *name, size_t name_len)
{
struct bt_hci_cp_le_set_adv_data data;
struct bt_hci_cp_le_set_adv_param set_param;
struct net_buf *buf;
/* name_len should also not include the \0 */
__ASSERT(name_len < (31 - 2), "name_len should be < 30");
(void)memset(&data, 0, sizeof(data));
data.len = name_len + 2;
data.data[0] = name_len + 1;
data.data[1] = BT_DATA_NAME_COMPLETE;
memcpy(&data.data[2], name, name_len);
buf = bt_hci_cmd_create(BT_HCI_OP_LE_SET_ADV_DATA, sizeof(data));
__ASSERT_NO_MSG(buf);
net_buf_add_mem(buf, &data, sizeof(data));
send_cmd(BT_HCI_OP_LE_SET_ADV_DATA, buf, NULL);
(void)memset(&set_param, 0, sizeof(set_param));
set_param.min_interval = sys_cpu_to_le16(interval);
set_param.max_interval = sys_cpu_to_le16(interval);
set_param.channel_map = 0x07;
set_param.filter_policy = BT_LE_ADV_FP_NO_FILTER;
set_param.type = BT_HCI_ADV_IND;
set_param.own_addr_type = 0x01; /* random */
buf = bt_hci_cmd_create(BT_HCI_OP_LE_SET_ADV_PARAM, sizeof(set_param));
__ASSERT_NO_MSG(buf);
net_buf_add_mem(buf, &set_param, sizeof(set_param));
send_cmd(BT_HCI_OP_LE_SET_ADV_PARAM, buf, NULL);
buf = bt_hci_cmd_create(BT_HCI_OP_LE_SET_ADV_ENABLE, 1);
__ASSERT_NO_MSG(buf);
net_buf_add_u8(buf, BT_HCI_LE_ADV_ENABLE);
send_cmd(BT_HCI_OP_LE_SET_ADV_ENABLE, buf, NULL);
}
static void disconnect(void)
{
struct net_buf *buf;
struct bt_hci_cp_disconnect *disconn;
uint8_t reason = BT_HCI_ERR_REMOTE_USER_TERM_CONN;
uint16_t handle = conn_handle;
LOG_INF("Disconnecting");
buf = bt_hci_cmd_create(BT_HCI_OP_DISCONNECT, sizeof(*disconn));
TEST_ASSERT(buf);
disconn = net_buf_add(buf, sizeof(*disconn));
disconn->handle = sys_cpu_to_le16(handle);
disconn->reason = reason;
send_cmd(BT_HCI_OP_DISCONNECT, buf, NULL);
WAIT_FOR_FLAG_UNSET(is_connected);
LOG_INF("Disconnected");
}
NET_BUF_POOL_DEFINE(acl_tx_pool, 5, BT_L2CAP_BUF_SIZE(200), 8, NULL);
struct net_buf *alloc_l2cap_pdu(void)
{
struct net_buf *buf;
uint16_t reserve;
buf = net_buf_alloc(&acl_tx_pool, K_FOREVER);
TEST_ASSERT(buf, "failed ACL allocation");
reserve = sizeof(struct bt_l2cap_hdr);
reserve += sizeof(struct bt_hci_acl_hdr) + BT_BUF_RESERVE;
net_buf_reserve(buf, reserve);
return buf;
}
static int send_acl(struct net_buf *buf, uint8_t flags)
{
struct bt_hci_acl_hdr *hdr;
hdr = net_buf_push(buf, sizeof(*hdr));
hdr->handle = sys_cpu_to_le16(bt_acl_handle_pack(conn_handle, flags));
hdr->len = sys_cpu_to_le16(buf->len - sizeof(*hdr));
bt_buf_set_type(buf, BT_BUF_ACL_OUT);
k_sem_take(&acl_pkts, K_FOREVER);
return bt_send(buf);
}
static void push_l2cap_pdu_header(struct net_buf *dst, uint16_t len, uint16_t cid)
{
struct bt_l2cap_hdr *hdr;
hdr = net_buf_push(dst, sizeof(*hdr));
hdr->len = sys_cpu_to_le16(len);
hdr->cid = sys_cpu_to_le16(cid);
}
static void send_l2cap_packet(struct net_buf *buf, uint16_t cid)
{
push_l2cap_pdu_header(buf, buf->len, cid);
send_acl(buf, BT_ACL_START_NO_FLUSH);
}
static void prepare_controller(void)
{
/* Initialize controller */
struct net_buf *rsp;
send_cmd(BT_HCI_OP_RESET, NULL, NULL);
send_cmd(BT_HCI_OP_LE_READ_BUFFER_SIZE, NULL, &rsp);
le_read_buffer_size_complete(rsp);
set_event_mask(BT_HCI_OP_SET_EVENT_MASK);
set_event_mask(BT_HCI_OP_LE_SET_EVENT_MASK);
set_random_address();
}
static void init_tinyhost(void)
{
bt_enable_raw(&rx_queue);
/* Start the RX thread */
k_thread_create(&rx_thread_data, rx_thread_stack,
K_THREAD_STACK_SIZEOF(rx_thread_stack), rx_thread,
NULL, NULL, NULL, K_PRIO_PREEMPT(0), 0, K_NO_WAIT);
k_thread_name_set(&rx_thread_data, "HCI RX");
k_thread_priority_set(k_current_get(), K_PRIO_PREEMPT(0));
prepare_controller();
}
static void gatt_notify(void)
{
static uint8_t data[] = NOTIFICATION_PAYLOAD;
uint16_t handle = GATT_HANDLE;
struct net_buf *buf = alloc_l2cap_pdu();
net_buf_add_u8(buf, BT_ATT_OP_NOTIFY);
net_buf_add_le16(buf, handle);
net_buf_add_mem(buf, data, sizeof(data));
LOG_INF("Sending complete notification");
send_l2cap_packet(buf, BT_L2CAP_CID_ATT);
}
/* Send all but the last fragment of a notification */
static void gatt_notify_without_last_fragment(void)
{
static uint8_t data[] = NOTIFICATION_PAYLOAD;
uint16_t handle = GATT_HANDLE;
struct net_buf *att_packet = alloc_l2cap_pdu();
/* Prepare (G)ATT notification packet */
net_buf_add_u8(att_packet, BT_ATT_OP_NOTIFY);
net_buf_add_le16(att_packet, handle);
net_buf_add_mem(att_packet, data, sizeof(data));
size_t on_air_size = 5;
uint8_t flags = BT_ACL_START_NO_FLUSH;
LOG_INF("Sending partial notification");
for (size_t i = 0; att_packet->len > on_air_size; i++) {
struct net_buf *buf = alloc_l2cap_pdu();
__ASSERT_NO_MSG(buf);
/* This is the size of the ACL payload. I.e. not including the HCI header. */
size_t frag_len = MIN(att_packet->len, on_air_size);
if (i == 0) {
/* Only first fragment should have L2CAP PDU header */
push_l2cap_pdu_header(buf, att_packet->len, BT_L2CAP_CID_ATT);
frag_len -= BT_L2CAP_HDR_SIZE;
}
/* copy data into ACL frag */
net_buf_add_mem(buf,
net_buf_pull_mem(att_packet, frag_len),
frag_len);
LOG_DBG("send ACL frag %d (%d bytes, remaining %d)", i, buf->len, att_packet->len);
LOG_HEXDUMP_DBG(buf->data, buf->len, "ACL Fragment");
send_acl(buf, flags);
flags = BT_ACL_CONT;
}
net_buf_unref(att_packet);
/* Hey! You didn't send the last frag, no fair!
* - The DUT (probably)
*/
LOG_INF("Partial notification sent");
}
static void run_test_iteration(void)
{
LOG_INF("advertise");
/* Start advertising & wait for a connection */
start_adv(40, PEER_NAME, sizeof(PEER_NAME) - 1);
WAIT_FOR_FLAG(is_connected);
LOG_INF("connected");
/* Generous time allotment for dut to fake-subscribe */
k_sleep(K_MSEC(100));
gatt_notify();
gatt_notify_without_last_fragment();
disconnect();
}
void entrypoint_peer(void)
{
init_tinyhost();
LOG_INF("##################### START TEST #####################");
for (size_t i = 0; i < TEST_ITERATIONS; i++) {
LOG_INF("## Iteration %d", i);
run_test_iteration();
}
TEST_PASS_AND_EXIT("Peer (tester) done\n");
}

View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Copyright 2024 Nordic Semiconductor ASA
# SPDX-License-Identifier: Apache-2.0
set -eu
: "${ZEPHYR_BASE:?ZEPHYR_BASE must be defined}"
INCR_BUILD=1
source ${ZEPHYR_BASE}/tests/bsim/compile.source
app="$(guess_test_relpath)/dut" compile
app="$(guess_test_relpath)/peer" compile
wait_for_background_jobs

View file

@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Copyright (c) 2024 Nordic Semiconductor
# SPDX-License-Identifier: Apache-2.0
set -eu
source ${ZEPHYR_BASE}/tests/bsim/sh_common.source
test_name="$(guess_test_long_name)"
simulation_id=${test_name}
verbosity_level=2
# Ten-second (maximum) sim time.
# The test will exit simulation as soon as it has passed.
SIM_LEN_US=$((10 * 1000 * 1000))
dut_exe="${BSIM_OUT_PATH}/bin/bs_${BOARD_TS}_${test_name}_dut_prj_conf"
peer_exe="${BSIM_OUT_PATH}/bin/bs_${BOARD_TS}_${test_name}_peer_prj_conf"
cd ${BSIM_OUT_PATH}/bin
Execute ./bs_2G4_phy_v1 -v=${verbosity_level} -s=${simulation_id} -D=2 -sim_length=${SIM_LEN_US} $@
Execute "${peer_exe}" -v=${verbosity_level} -s=${simulation_id} \
-d=1 -rs=69 -testid=peer
Execute "${dut_exe}" -v=${verbosity_level} -s=${simulation_id} \
-d=0 -rs=420 -testid=dut -argstest log_level 3
wait_for_background_jobs