Skip to content

Commit 0cad952

Browse files
carrefinhoclaude
andcommitted
test(ctlr): bsim M->N transition + conn-update-while-subrated scenarios
Add conn_subrate_transitions: the peripheral negotiates factor 4, the central re-negotiates to factor 8 (a true M->N transition, central- initiated 5.1.19), and the central verifies both sides re-skip at 8 via timed reads. It then changes the connection interval while subrated and verifies subrating resets to factor 1 (low read latency) and the link survives -- exercising the steady-state gate (both present every event during the procedure) and the zmkfirmware#51 latency_upd fix. No controller changes: the gate already handles M->N transitions and the conn-update instant; this validates that behavior. central.c / peripheral.c refactored to share connect/probe helpers across the two scenarios; new run script + CI step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 31c3801 commit 0cad952

5 files changed

Lines changed: 250 additions & 73 deletions

File tree

.github/workflows/subrating-test.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,10 @@ jobs:
120120
app=tests/bsim/bluetooth/ll/conn_subrate compile
121121
wait_for_background_jobs
122122
123-
- name: Run subrating bsim scenario
123+
- name: Run subrating bsim scenarios
124124
shell: bash
125125
run: |
126126
export ZEPHYR_BASE="${PWD}"
127127
export EXECUTE_TIMEOUT=120
128128
tests/bsim/bluetooth/ll/conn_subrate/tests_scripts/conn_subrate.sh
129+
tests/bsim/bluetooth/ll/conn_subrate/tests_scripts/conn_subrate_transitions.sh

tests/bsim/bluetooth/ll/conn_subrate/src/central.c

Lines changed: 172 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@
33
*
44
* SPDX-License-Identifier: Apache-2.0
55
*
6-
* Subrating central: connect to the peripheral at a fixed interval, let the
7-
* peripheral negotiate subrating (factor > 1), then probe the link with timed
8-
* GATT reads while the peripheral is idle. Because a subrated peripheral only
9-
* listens on subrated events, a read issued while it sleeps is answered only
10-
* after it next wakes, so the observed read latency reveals the skip cadence:
11-
* latencies far above one connection interval prove the peripheral skipped
12-
* events. The central itself (not yet subrating-aware) is present on every
13-
* event, so this isolates the peripheral skipping (Phase 2).
6+
* Subrating central. Two scenarios:
7+
* "central" - let the peripheral negotiate subrating (factor > 1)
8+
* and verify via timed GATT reads that the peripheral
9+
* skips connection events (Phase 2 / 3a).
10+
* "central_transitions" - peripheral negotiates factor M, central re-negotiates
11+
* to factor N (M->N transition), verify both re-skip at
12+
* N; then change the connection interval while subrated
13+
* and verify subrating resets to factor 1 (gate handles
14+
* the update, no crash, both present every event).
15+
*
16+
* A subrated peer only listens on subrated events, so a read issued while it
17+
* sleeps is answered only after the next subrated event: large read latency
18+
* proves skipping, small latency proves no skipping.
1419
*/
1520
#include <zephyr/kernel.h>
1621

@@ -52,6 +57,7 @@ static struct bt_conn *default_conn;
5257
static struct bt_gatt_read_params read_params;
5358

5459
static volatile uint16_t subrate_factor;
60+
static volatile uint16_t conn_interval;
5561
static volatile int read_err;
5662
static K_SEM_DEFINE(read_done, 0, 1);
5763

@@ -81,6 +87,14 @@ static void subrate_changed(struct bt_conn *conn,
8187
subrate_factor = params->factor;
8288
}
8389

90+
static void le_param_updated(struct bt_conn *conn, uint16_t interval,
91+
uint16_t latency, uint16_t timeout)
92+
{
93+
printk("Central conn params updated: interval %u latency %u timeout %u\n",
94+
interval, latency, timeout);
95+
conn_interval = interval;
96+
}
97+
8498
static void connected(struct bt_conn *conn, uint8_t conn_err)
8599
{
86100
if (conn_err) {
@@ -105,6 +119,7 @@ static struct bt_conn_cb conn_callbacks = {
105119
.connected = connected,
106120
.disconnected = disconnected,
107121
.subrate_changed = subrate_changed,
122+
.le_param_updated = le_param_updated,
108123
};
109124

110125
static void device_found(const bt_addr_le_t *addr, int8_t rssi, uint8_t type,
@@ -179,7 +194,30 @@ static int64_t probe_read_latency(void)
179194
return k_uptime_get() - t0;
180195
}
181196

182-
static void test_central_main(void)
197+
/* Run `count` probes spaced by READ_GAP_MS and return the worst-case latency,
198+
* or -1 on failure. The gap exceeds the skip period so the peripheral has gone
199+
* back to sleep before each probe.
200+
*/
201+
static int64_t probe_max_latency(int count)
202+
{
203+
int64_t max_latency = 0;
204+
205+
for (int i = 0; i < count; i++) {
206+
int64_t latency = probe_read_latency();
207+
208+
if (latency < 0) {
209+
return -1;
210+
}
211+
212+
printk("Central read %d latency %lld ms\n", i, latency);
213+
max_latency = MAX(max_latency, latency);
214+
k_sleep(K_MSEC(READ_GAP_MS));
215+
}
216+
217+
return max_latency;
218+
}
219+
220+
static int central_start(void)
183221
{
184222
struct bt_conn_le_subrate_param defaults = {
185223
.subrate_min = 1U,
@@ -188,81 +226,80 @@ static void test_central_main(void)
188226
.continuation_number = 0U,
189227
.supervision_timeout = CONN_TIMEOUT_UNITS,
190228
};
191-
struct bt_conn_info info;
192-
int64_t max_latency = 0;
193-
uint32_t interval_ms;
194-
uint32_t threshold_ms;
195229
int err;
196230

197231
bt_conn_cb_register(&conn_callbacks);
198232

199233
err = bt_enable(NULL);
200234
if (err) {
201235
FAIL("Bluetooth init failed (err %d)\n", err);
202-
return;
236+
return err;
203237
}
204238

205239
printk("Central Bluetooth initialized\n");
206240

207-
/* Allow the peripheral's request to be granted with a large factor. */
241+
/* Allow the peripheral's requests to be granted with a large factor. */
208242
err = bt_conn_le_subrate_set_defaults(&defaults);
209243
if (err) {
210244
FAIL("Set default subrate failed (err %d)\n", err);
211-
return;
245+
return err;
212246
}
213247

214248
err = bt_le_scan_start(BT_LE_SCAN_ACTIVE, device_found);
215249
if (err) {
216250
FAIL("Scanning failed to start (err %d)\n", err);
217-
return;
251+
return err;
218252
}
219253

220-
/* Wait until the link is up and subrating has been negotiated (factor>1). */
221-
while (!default_conn || subrate_factor < 2U) {
222-
k_sleep(K_MSEC(100));
254+
return 0;
255+
}
223256

224-
if (bst_result == Failed) {
225-
return;
226-
}
227-
}
257+
/* Wait for a condition with the WAIT_TIME tick as the backstop, bailing on
258+
* an already-failed result.
259+
*/
260+
#define WAIT_FOR(_cond) \
261+
do { \
262+
while (!(_cond)) { \
263+
k_sleep(K_MSEC(100)); \
264+
if (bst_result == Failed) { \
265+
return; \
266+
} \
267+
} \
268+
} while (0)
228269

229-
err = bt_conn_get_info(default_conn, &info);
230-
if (err) {
231-
FAIL("Central conn info failed (err %d)\n", err);
270+
static uint32_t interval_to_ms(uint16_t units)
271+
{
272+
return (units * 5U) / 4U; /* 1.25 ms units -> ms */
273+
}
274+
275+
static void test_central_main(void)
276+
{
277+
int64_t max_latency;
278+
uint32_t interval_ms;
279+
uint32_t threshold_ms;
280+
281+
if (central_start()) {
232282
return;
233283
}
234-
interval_ms = (info.le.interval * 5U) / 4U; /* 1.25 ms units -> ms */
235284

236-
/* A subrated, idle peripheral listens once every (factor * interval); a
237-
* probe issued while it sleeps waits up to that long. Require the worst
238-
* observed latency to clear half the skip period - unreachable unless
239-
* the peripheral actually skipped events.
240-
*/
241-
threshold_ms = (subrate_factor * interval_ms) / 2U;
285+
/* Wait for the link and a negotiated factor > 1. */
286+
WAIT_FOR(default_conn && subrate_factor >= 2U);
242287

288+
interval_ms = interval_to_ms(CONN_INTERVAL_UNITS);
289+
threshold_ms = (subrate_factor * interval_ms) / 2U;
243290
printk("Central probing: factor %u, interval %u ms, threshold %u ms\n",
244291
subrate_factor, interval_ms, threshold_ms);
245292

246-
for (int i = 0; i < NUM_READS; i++) {
247-
int64_t latency = probe_read_latency();
248-
249-
if (latency < 0) {
250-
return; /* FAIL already set */
251-
}
252-
253-
printk("Central read %d latency %lld ms\n", i, latency);
254-
max_latency = MAX(max_latency, latency);
255-
256-
/* Let the peripheral go back to sleep before the next probe. */
257-
k_sleep(K_MSEC(READ_GAP_MS));
293+
max_latency = probe_max_latency(NUM_READS);
294+
if (max_latency < 0) {
295+
return;
258296
}
259297

260298
printk("Central max read latency %lld ms (threshold %u ms)\n",
261299
max_latency, threshold_ms);
262-
263300
if (max_latency < threshold_ms) {
264-
FAIL("Peripheral did not skip events: max read latency %lld ms "
265-
"below threshold %u ms\n", max_latency, threshold_ms);
301+
FAIL("Peripheral did not skip events: max latency %lld ms < %u ms\n",
302+
max_latency, threshold_ms);
266303
return;
267304
}
268305

@@ -272,6 +309,82 @@ static void test_central_main(void)
272309
bs_trace_silent_exit(0);
273310
}
274311

312+
static void test_central_main_transitions(void)
313+
{
314+
struct bt_conn_le_subrate_param to_n = {
315+
.subrate_min = 1U,
316+
.subrate_max = SUBRATE_MTON_N,
317+
.max_latency = 0U,
318+
.continuation_number = 0U,
319+
.supervision_timeout = CONN_TIMEOUT_UNITS,
320+
};
321+
struct bt_le_conn_param *upd;
322+
uint32_t interval_ms;
323+
int64_t max_latency;
324+
int err;
325+
326+
if (central_start()) {
327+
return;
328+
}
329+
330+
/* Peripheral negotiates factor M first. */
331+
WAIT_FOR(default_conn && subrate_factor == SUBRATE_MTON_M);
332+
printk("Central: peripheral negotiated M=%u\n", subrate_factor);
333+
334+
/* M->N: Central re-negotiates to factor N (central-initiated, 5.1.19). */
335+
err = bt_conn_le_subrate_request(default_conn, &to_n);
336+
if (err) {
337+
FAIL("Central M->N subrate request failed (err %d)\n", err);
338+
return;
339+
}
340+
WAIT_FOR(subrate_factor == SUBRATE_MTON_N);
341+
printk("Central: transitioned to N=%u\n", subrate_factor);
342+
343+
/* Both should now skip at factor N. */
344+
interval_ms = interval_to_ms(CONN_INTERVAL_UNITS);
345+
max_latency = probe_max_latency(NUM_READS);
346+
if (max_latency < 0) {
347+
return;
348+
}
349+
printk("Central post-M->N max latency %lld ms\n", max_latency);
350+
if (max_latency < (SUBRATE_MTON_N * interval_ms) / 2U) {
351+
FAIL("No skip after M->N: max latency %lld ms\n", max_latency);
352+
return;
353+
}
354+
355+
/* conn-update while subrated: change the interval, which resets subrating
356+
* to factor 1. Validates that the procedure completes (gate keeps both
357+
* present every event, #51 prevents the latency_upd crash) and that the
358+
* link survives.
359+
*/
360+
conn_interval = 0U;
361+
upd = BT_LE_CONN_PARAM(CONN_UPDATE_INTERVAL_UNITS, CONN_UPDATE_INTERVAL_UNITS,
362+
0, CONN_TIMEOUT_UNITS);
363+
err = bt_conn_le_param_update(default_conn, upd);
364+
if (err) {
365+
FAIL("Central conn param update failed (err %d)\n", err);
366+
return;
367+
}
368+
WAIT_FOR(conn_interval == CONN_UPDATE_INTERVAL_UNITS);
369+
printk("Central: interval updated to %u units while subrated\n", conn_interval);
370+
371+
/* Subrating must have reset to factor 1 -> no more skipping -> low latency. */
372+
max_latency = probe_max_latency(NUM_READS / 2);
373+
if (max_latency < 0) {
374+
return;
375+
}
376+
printk("Central post-update max latency %lld ms\n", max_latency);
377+
if (max_latency >= (SUBRATE_MTON_N * interval_to_ms(CONN_UPDATE_INTERVAL_UNITS)) / 2U) {
378+
FAIL("Subrating not reset after interval change: max latency %lld ms\n",
379+
max_latency);
380+
return;
381+
}
382+
383+
(void)bt_conn_disconnect(default_conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
384+
PASS("Central M->N + conn-update validated\n");
385+
bs_trace_silent_exit(0);
386+
}
387+
275388
static void test_central_init(void)
276389
{
277390
bst_ticker_set_next_tick_absolute(WAIT_TIME * 1e6);
@@ -288,13 +401,20 @@ static void test_central_tick(bs_time_t HW_device_time)
288401
static const struct bst_test_instance test_central[] = {
289402
{
290403
.test_id = "central",
291-
.test_descr = "Subrating central: connects, lets the peripheral "
292-
"negotiate subrating, and verifies via timed reads "
293-
"that the peripheral skips connection events.",
404+
.test_descr = "Central: peripheral negotiates subrating; verify the "
405+
"peripheral skips connection events via timed reads.",
294406
.test_pre_init_f = test_central_init,
295407
.test_tick_f = test_central_tick,
296408
.test_main_f = test_central_main,
297409
},
410+
{
411+
.test_id = "central_transitions",
412+
.test_descr = "Central: M->N subrate transition then a conn-param "
413+
"interval change while subrated (resets to factor 1).",
414+
.test_pre_init_f = test_central_init,
415+
.test_tick_f = test_central_tick,
416+
.test_main_f = test_central_main_transitions,
417+
},
298418
BSTEST_END_MARKER,
299419
};
300420

tests/bsim/bluetooth/ll/conn_subrate/src/conn_subrate.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@
2727
/* Central acceptable subrate defaults (must allow the request above). */
2828
#define SUBRATE_ACC_MAX 8U
2929

30+
/* M->N transition test: the Peripheral first negotiates SUBRATE_MTON_M, then the
31+
* Central re-negotiates to SUBRATE_MTON_N (both > 1 -> a true M->N transition).
32+
*/
33+
#define SUBRATE_MTON_M 4U
34+
#define SUBRATE_MTON_N 8U
35+
36+
/* conn-update test: the Central changes the interval while subrated, which must
37+
* reset subrating to factor 1. A different value from CONN_INTERVAL_UNITS so the
38+
* interval actually changes.
39+
*/
40+
#define CONN_UPDATE_INTERVAL_UNITS 36U /* 45 ms */
41+
3042
/* Time the peripheral waits after connecting before requesting subrating, to
3143
* let feature exchange and the central's discovery complete first.
3244
*/

0 commit comments

Comments
 (0)