Skip to content

Commit ec56f55

Browse files
committed
tests/bluetooth: Add a broadcast and observe at once measurement.
The Move Hub carried a note saying that broadcasting interferes with observing even when no computer is connected. These programs measure whether that is still true, in both connection states. scan_trace_both.py runs on the hub under test. Its first two windows only observe and its last two also broadcast, so the baseline and the case under test are measured back to back by one program. Measuring them in separate runs would also be comparing two different radio environments, which indoors is a real risk. scan_trace_count.py runs on a third device and counts what actually leaves the hub under test, since a hub cannot see its own advertisements. adv_rate_tx.py broadcasts and nothing else, for comparing the advertising interval between two firmware builds; it avoids trace() and bytearray so that it also runs on a build from master. radio.adv_status() reports the status of each command in the raw HCI advertising sequence. It is what identified 0x85 BLE_STATUS_INTERVAL_TOO_LARGE as the reason a fixed advertising interval is refused once the chip has an anchor period to fit, which the advertising interval range in here fixes. bytearray is enabled for every hub because trace() copies into a caller supplied writable buffer and the Move Hub's option level has no writable buffer type at all. That costs it about 1.8 KB of flash, which is why this is not something to keep. None of this is for release.
1 parent 14c41c2 commit ec56f55

6 files changed

Lines changed: 316 additions & 7 deletions

File tree

‎bricks/_common/mpconfigport.h‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@
7171
#define MICROPY_PY_ASYNC_AWAIT (1)
7272
#define MICROPY_MULTIPLE_INHERITANCE (0)
7373
#define MICROPY_PY_ARRAY (0)
74-
#define MICROPY_PY_BUILTINS_BYTEARRAY (PYBRICKS_OPT_EXTRA_LEVEL1)
74+
// TEMPORARY, FOR THE BROADCAST AND OBSERVE MEASUREMENT ONLY. hub.ble.trace()
75+
// copies into a caller supplied writable buffer, and at the Move Hub's option
76+
// level there is no writable buffer type at all.
77+
#define MICROPY_PY_BUILTINS_BYTEARRAY (1)
7578
#define MICROPY_PY_BUILTINS_MEMORYVIEW (PYBRICKS_OPT_EXTRA_LEVEL2)
7679
#define MICROPY_PY_BUILTINS_ENUMERATE (PYBRICKS_OPT_EXTRA_LEVEL1)
7780
#define MICROPY_PY_BUILTINS_FILTER (0)

‎lib/pbio/drv/bluetooth/bluetooth_stm32_bluenrg.c‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ char pbdrv_bluetooth_hub_name[16] = "Pybricks Hub";
5050

5151
static char pbdrv_bluetooth_fw_version[5]; // 0.0a
5252

53+
// TEMPORARY INSTRUMENTATION, NOT FOR RELEASE. Status of each command in the
54+
// raw HCI advertising sequence, read back with radio.adv_status().
55+
uint8_t pbdrv_bluetooth_debug_adv[3] = { 0xff, 0xff, 0xff };
56+
5357

5458
// bluetooth address is set at factory at this address
5559
#define FLASH_BD_ADDR ((const uint8_t *)0x08004ffa)
@@ -631,9 +635,13 @@ pbio_error_t pbdrv_bluetooth_start_broadcasting_func(pbio_os_state_t *state, voi
631635
PBIO_OS_AWAIT_WHILE(state, write_xfer_size);
632636
{
633637
static const uint8_t params[] = {
634-
// 100ms, the fastest this controller allows.
635-
0xa0, 0x00, // Advertising_Interval_Min
636-
0xa0, 0x00, // Advertising_Interval_Max
638+
// 100ms is the fastest this controller allows. The range has
639+
// to be wide, because the chip fits a new activity to the
640+
// anchor period it is already running for the connection and
641+
// any scanning, and rejects an interval that is not a multiple
642+
// of it.
643+
0xa0, 0x00, // Advertising_Interval_Min, 100ms
644+
0x40, 0x01, // Advertising_Interval_Max, 200ms
637645
ADV_NONCONN_IND,
638646
STATIC_RANDOM_ADDR,
639647
0, // Peer_Address_Type
@@ -644,7 +652,7 @@ pbio_error_t pbdrv_bluetooth_start_broadcasting_func(pbio_os_state_t *state, voi
644652
hci_send_le_command(OCF_LE_SET_ADV_PARAMETERS, params, sizeof(params));
645653
}
646654
PBIO_OS_AWAIT_UNTIL(state, hci_command_complete);
647-
status = hci_le_command_end();
655+
pbdrv_bluetooth_debug_adv[0] = status = hci_le_command_end();
648656

649657
if (status != BLE_STATUS_SUCCESS) {
650658
return ble_error_to_pbio_error(status);
@@ -655,7 +663,7 @@ pbio_error_t pbdrv_bluetooth_start_broadcasting_func(pbio_os_state_t *state, voi
655663
PBIO_OS_AWAIT_WHILE(state, write_xfer_size);
656664
hci_le_set_advertising_data_begin(pbdrv_bluetooth_broadcast_data_size, pbdrv_bluetooth_broadcast_data);
657665
PBIO_OS_AWAIT_UNTIL(state, hci_command_complete);
658-
status = hci_le_set_advertising_data_end();
666+
pbdrv_bluetooth_debug_adv[1] = status = hci_le_set_advertising_data_end();
659667

660668
if (status != BLE_STATUS_SUCCESS) {
661669
return ble_error_to_pbio_error(status);
@@ -672,7 +680,7 @@ pbio_error_t pbdrv_bluetooth_start_broadcasting_func(pbio_os_state_t *state, voi
672680
hci_send_le_command(OCF_LE_SET_ADVERTISE_ENABLE, &enable, sizeof(enable));
673681
}
674682
PBIO_OS_AWAIT_UNTIL(state, hci_command_complete);
675-
status = hci_le_command_end();
683+
pbdrv_bluetooth_debug_adv[2] = status = hci_le_command_end();
676684

677685
if (status == BLE_STATUS_SUCCESS) {
678686
pbdrv_bluetooth_advertising_state = PBDRV_BLUETOOTH_ADVERTISING_STATE_BROADCASTING;

‎pybricks/messaging/pb_type_ble_radio.c‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,30 @@ static mp_obj_t pb_module_ble_trace(mp_obj_t self_in, mp_obj_t buf_in) {
614614
return mp_obj_new_tuple(2, items);
615615
}
616616
static MP_DEFINE_CONST_FUN_OBJ_2(pb_module_ble_trace_obj, pb_module_ble_trace);
617+
618+
#if PBDRV_CONFIG_BLUETOOTH_STM32_BLUENRG
619+
/**
620+
* TEMPORARY INSTRUMENTATION, NOT FOR RELEASE.
621+
*
622+
* Move Hub only: the other drivers do not use raw link layer commands to
623+
* advertise and so have nothing to report here.
624+
*
625+
* @param [in] self_in The BLE MicroPython object instance.
626+
* @returns A tuple of the status of the set advertising
627+
* parameters, set advertising data and set advertise
628+
* enable commands, or 255 if not attempted yet.
629+
*/
630+
static mp_obj_t pb_module_ble_adv_status(mp_obj_t self_in) {
631+
extern uint8_t pbdrv_bluetooth_debug_adv[3];
632+
mp_obj_t items[3] = {
633+
MP_OBJ_NEW_SMALL_INT(pbdrv_bluetooth_debug_adv[0]),
634+
MP_OBJ_NEW_SMALL_INT(pbdrv_bluetooth_debug_adv[1]),
635+
MP_OBJ_NEW_SMALL_INT(pbdrv_bluetooth_debug_adv[2]),
636+
};
637+
return mp_obj_new_tuple(3, items);
638+
}
639+
static MP_DEFINE_CONST_FUN_OBJ_1(pb_module_ble_adv_status_obj, pb_module_ble_adv_status);
640+
#endif // PBDRV_CONFIG_BLUETOOTH_STM32_BLUENRG
617641
#endif // PYBRICKS_BLE_TRACE
618642

619643
mp_obj_t pb_module_ble_data_close(mp_obj_t self_in) {
@@ -631,6 +655,9 @@ static const mp_rom_map_elem_t common_BLE_locals_dict_table[] = {
631655
{ MP_ROM_QSTR(MP_QSTR_version), MP_ROM_PTR(&pb_module_ble_version_obj) },
632656
#if PYBRICKS_BLE_TRACE
633657
{ MP_ROM_QSTR(MP_QSTR_trace), MP_ROM_PTR(&pb_module_ble_trace_obj) },
658+
#if PBDRV_CONFIG_BLUETOOTH_STM32_BLUENRG
659+
{ MP_ROM_QSTR(MP_QSTR_adv_status), MP_ROM_PTR(&pb_module_ble_adv_status_obj) },
660+
#endif
634661
#endif
635662
};
636663
static MP_DEFINE_CONST_DICT(common_BLE_locals_dict, common_BLE_locals_dict_table);

‎tests/bluetooth/adv_rate_tx.py‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Advertising interval A/B, transmitter. Broadcast only, nothing else.
2+
#
3+
# The advertising interval used to be whatever the GAP layer picked, and is now
4+
# ours to choose, so this checks that the new choice is not slower than the old
5+
# one. It broadcasts and never observes, so the interval is the only thing that
6+
# decides how often a value goes out.
7+
#
8+
# Run it standalone on the hub under test, once per firmware being compared,
9+
# with scan_trace_count.py counting on a spare hub. The spare hub's own
10+
# reception is the same in both runs, so the two counts can be compared even
11+
# though neither is the transmit rate itself.
12+
#
13+
# Works on released firmware: no instrumentation and no bytearray, so it runs
14+
# on a Move Hub built from master as well as from the branch under test.
15+
16+
from pybricks.hubs import ThisHub
17+
from pybricks.messaging import BLERadio
18+
from pybricks.parameters import Color
19+
from pybricks.tools import StopWatch, wait
20+
21+
# Matches TX_CHANNEL in scan_trace_count.py.
22+
CHANNEL = 2
23+
24+
# Faster than any advertising interval the chip can use, so the value that goes
25+
# out is always a fresh one and the count is limited by the interval alone.
26+
TX_PERIOD_MS = 50
27+
28+
hub = ThisHub()
29+
radio = BLERadio(broadcast_channel=CHANNEL)
30+
hub.light.on(Color.BLUE)
31+
32+
watch = StopWatch()
33+
counter = 0
34+
next_ms = 0
35+
36+
while True:
37+
radio.broadcast(counter)
38+
counter = (counter + 1) % 32768
39+
40+
# Absolute schedule, so the rate does not drift with the time broadcast()
41+
# itself takes.
42+
next_ms += TX_PERIOD_MS
43+
delay = next_ms - watch.time()
44+
if delay > 0:
45+
wait(delay)

‎tests/bluetooth/scan_trace_both.py‎

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Broadcast and observe at once, on the hub under test.
2+
#
3+
# This is the case the Move Hub comment claims does not work: a hub that
4+
# transmits and receives broadcasts at the same time. It measures the
5+
# receiving half directly, with radio.trace(). The transmitting half is
6+
# measured by scan_trace_count.py on a third device, since a hub cannot see
7+
# its own advertisements.
8+
#
9+
# The first two windows only observe and the last two also broadcast, so the
10+
# baseline and the case under test are measured back to back by one program.
11+
# Nothing else changes between them, which is the whole point: comparing
12+
# against a separate observe-only run would also be comparing two different
13+
# radio environments.
14+
#
15+
# Run this twice, with no edits in between:
16+
#
17+
# Run 1 (connected) Start it from Pybricks Code and leave the hub
18+
# connected for the whole run. Results are printed.
19+
# Run 2 (standalone) Download it, disconnect, then start it with the hub
20+
# button. Results are shown as a status light colour
21+
# and afterwards broadcast for scan_trace_report.py.
22+
#
23+
# Hub B runs scan_trace_tx.py standalone throughout, on RX_CHANNEL. Its own
24+
# rate never changes, so every difference seen here belongs to this hub.
25+
26+
from pybricks.hubs import ThisHub
27+
from pybricks.messaging import BLERadio
28+
from pybricks.parameters import Color
29+
from pybricks.tools import StopWatch, wait
30+
31+
# The reference transmitter, hub B.
32+
RX_CHANNEL = 1
33+
34+
# Our own broadcasts, counted by scan_trace_count.py. The results are sent on
35+
# the same channel once measuring is over, which is what scan_trace_report.py
36+
# already listens on.
37+
TX_CHANNEL = 2
38+
39+
WINDOWS = 4
40+
BROADCAST_FROM = 2
41+
42+
WINDOW_MS = 15000
43+
SHOW_MS = 3000
44+
45+
# Slower than the 100ms this hub advertises at, so every value goes out at
46+
# least once and a receiver that misses nothing sees 1000 / TX_PERIOD_MS
47+
# distinct values per second.
48+
TX_PERIOD_MS = 50
49+
50+
# Must match BLE_TRACE_SIZE in pb_type_ble_radio.c, times four bytes a record.
51+
# Allocated once, because trace() is called inside the measurement window and
52+
# a garbage collection there would distort the gaps it is reporting.
53+
BUF = bytearray(128 * 4)
54+
55+
# Upper edges of the gap histogram buckets, in units of 100 us, matching
56+
# scan_trace_rx.py so the two can be compared.
57+
EDGES = (150, 250, 400, 600, 900, 1500, 3000)
58+
59+
60+
def bucket(rate_x10):
61+
if rate_x10 >= 100:
62+
return Color.GREEN
63+
if rate_x10 >= 50:
64+
return Color.CYAN
65+
if rate_x10 >= 20:
66+
return Color.ORANGE
67+
return Color.RED
68+
69+
70+
hub = ThisHub()
71+
radio = BLERadio(broadcast_channel=TX_CHANNEL, observe_channels=[RX_CHANNEL])
72+
73+
# Discard whatever accumulated before the program was ready, including the
74+
# bogus first gap that is measured from the epoch.
75+
radio.trace(BUF)
76+
77+
watch = StopWatch()
78+
results = []
79+
counter = 0
80+
81+
for index in range(WINDOWS):
82+
broadcasting = index >= BROADCAST_FROM
83+
hub.light.on(Color.MAGENTA if broadcasting else Color.YELLOW)
84+
85+
hist = [0] * (len(EDGES) + 1)
86+
ours = 0
87+
total = 0
88+
max_gap = 0
89+
dropped = 0
90+
91+
# Gaps have to be accumulated across advertisements from other devices.
92+
# Binning the raw gap would measure how often the radio heard anything at
93+
# all, which depends on whatever else is transmitting nearby, rather than
94+
# how long this hub waited for an update from the hub under test.
95+
gap = 0
96+
97+
radio.trace(BUF)
98+
watch.reset()
99+
next_ms = 0
100+
101+
while watch.time() < WINDOW_MS:
102+
if broadcasting:
103+
# TEMPORARY: report which link layer command the chip refused
104+
# rather than stopping on the exception it raises.
105+
try:
106+
radio.broadcast(counter)
107+
except Exception:
108+
print("adv", radio.adv_status())
109+
raise
110+
counter = (counter + 1) % 32768
111+
112+
# Absolute schedule, so the rate does not drift with the time
113+
# broadcast() itself takes.
114+
next_ms += TX_PERIOD_MS
115+
delay = next_ms - watch.time()
116+
if delay > 0:
117+
wait(delay)
118+
119+
lost, size = radio.trace(BUF)
120+
dropped += lost
121+
122+
for i in range(0, size, 4):
123+
delta = BUF[i] | (BUF[i + 1] << 8)
124+
channel = BUF[i + 3]
125+
126+
total += 1
127+
gap += delta
128+
129+
if channel != RX_CHANNEL:
130+
continue
131+
132+
ours += 1
133+
if gap > max_gap:
134+
max_gap = gap
135+
136+
slot = len(EDGES)
137+
for edge in range(len(EDGES)):
138+
if gap < EDGES[edge]:
139+
slot = edge
140+
break
141+
hist[slot] += 1
142+
gap = 0
143+
144+
elapsed = watch.time()
145+
146+
rate_ours = ours * 10000 // elapsed
147+
rate_all = total * 10000 // elapsed
148+
149+
results.append(
150+
(
151+
rate_ours,
152+
max_gap // 10,
153+
dropped,
154+
bytes([h if h < 255 else 255 for h in hist]),
155+
)
156+
)
157+
158+
# Visible when standalone. Printing is deliberately left until the window
159+
# is closed, so the stdio traffic it generates in the connected run cannot
160+
# disturb the measurement.
161+
hub.light.on(bucket(rate_ours))
162+
print(index, broadcasting, rate_ours, rate_all, max_gap // 10, dropped, hist)
163+
wait(SHOW_MS)
164+
165+
# Broadcast the results forever so scan_trace_report.py can collect them after
166+
# a standalone run. Measurement is over, so transmitting now is harmless.
167+
hub.light.on(Color.WHITE)
168+
while True:
169+
for index in range(len(results)):
170+
rate_ours, max_gap, dropped, hist = results[index]
171+
radio.broadcast((index, rate_ours, max_gap, hist))
172+
wait(700)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# The transmitting half of the broadcast and observe test, on a third device.
2+
#
3+
# A hub cannot see its own advertisements, so this counts how many of the hub
4+
# under test's broadcasts actually reach anyone. It reports distinct values per
5+
# second, which is a lower bound on the transmit rate: the hub under test steps
6+
# its counter every TX_PERIOD_MS there, so a device that misses nothing sees
7+
# 1000 / TX_PERIOD_MS of them.
8+
#
9+
# Run it from Pybricks Code on a spare hub and start it before the hub under
10+
# test, then read the rate during each of that hub's windows. The first two
11+
# windows are silent by design, so the interesting number is the difference
12+
# between the last two and what a hub that only broadcasts manages.
13+
#
14+
# This hub does nothing but observe, so it does not compete for the hub under
15+
# test's radio. It is a passive listener and does not transmit at all.
16+
17+
from pybricks.hubs import ThisHub
18+
from pybricks.messaging import BLERadio
19+
from pybricks.parameters import Color
20+
from pybricks.tools import StopWatch, wait
21+
22+
# The hub under test's broadcasts, matching TX_CHANNEL in scan_trace_both.py.
23+
TX_CHANNEL = 2
24+
25+
REPORT_MS = 5000
26+
POLL_MS = 10
27+
28+
hub = ThisHub()
29+
radio = BLERadio(observe_channels=[TX_CHANNEL])
30+
hub.light.on(Color.CYAN)
31+
32+
report = StopWatch()
33+
seen = 0
34+
last = None
35+
36+
print("rate*10 distinct elapsed_ms")
37+
38+
while True:
39+
data = radio.observe(TX_CHANNEL)
40+
41+
# Only the plain counter is of interest. Once measuring is over the hub
42+
# under test sends its results as a tuple on the same channel, which is
43+
# where the rate drops to nothing and the run is done.
44+
if isinstance(data, int) and data != last:
45+
last = data
46+
seen += 1
47+
48+
wait(POLL_MS)
49+
50+
if report.time() >= REPORT_MS:
51+
elapsed = report.time()
52+
print(seen * 10000 // elapsed, seen, elapsed)
53+
seen = 0
54+
report.reset()

0 commit comments

Comments
 (0)