|
| 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) |
0 commit comments