drv/bluetooth: Scan at a 50% duty cycle so broadcasting works. - #500
Conversation
6f3097d to
50e1259
Compare
|
It sounds like this was quite extensively tested. Thank you! I would be happy to merge without further testing from my side.
I've been trying to find the video corresponding to this test somewhere in the issue pages but haven't found it yet. It was showing that using the Powered Up remote with one City train and broadcasting to the other was essentially instant. It would be nice to reproduce something like that as a quick extra test. Maybe it as just that fast because each hub had only one role? |
All three Bluetooth drivers scanned with the window equal to the interval, 30ms of every 30ms, so the radio was never free and a hub that broadcasts while observing could hardly transmit. Measurements in https://github.com/orgs/pybricks/discussions/2822 show a City Hub dropping from 28 packets/s to under 4 when it does both at once. Halving the window is not enough on its own: a 20ms interval with a 10ms window only reached 5.7 packets/s. Duty cycle does not set throughput by itself, because starting and ending a scan costs radio turnaround and scheduler time, and a 20ms period pays that fifty times a second. Use a 70ms interval with a 35ms window, which measured 12.3 to 13.0 packets/s. 70ms is not a multiple of the 25 to 30ms advertising interval of the faster hubs nor of the Move Hub's 100ms, so reception does not settle into a pattern of repeatedly missing a peer. Broadcasting on the BTstack hubs goes from every 100ms to every 30ms. 100ms was the Bluetooth 4.x minimum for non-connectable advertising, but the CC2564C is 5.1, where the minimum is 20ms, so the old value only limited how quickly other hubs could hear it. The Move Hub keeps 100ms because the BlueNRG-MS is a 4.1 controller and cannot go faster; it is the most limited hub, so it is allowed to be the slow one rather than holding the others back. City Hub and Technic Hub need one more setting. TI documents TGAP_CONN_SCAN_INT and _WIND as the scan parameters for the Link Layer Initiating state, in other words for connecting to a device, so we set only the general discovery pair. But GAP_DeviceDiscoveryRequest() uses the connection pair in place of the discovery pair whenever a connection exists, which is ordinary observing and not initiating at all. With a computer connected the chip therefore used TI's defaults of a 150ms window every 300ms. That is also a 50% duty cycle, so it looked harmless, and transmit throughput even appeared better when connected. Timestamping every advertisement received shows why it was not harmless. A City Hub observing a Technic Hub that only broadcasts received 4.9 packets/s while connected against 16.5 standalone, and the gaps between updates were either under 40ms or over 150ms with nothing in between: one short burst of reception every 300ms. The window never ran to completion either, because a connection event cuts the scan short and the remainder is discarded rather than resumed, leaving about 48ms of the 150ms. Transmit throughput looked good for the same reason the hub could barely hear anything. Worse, 300ms is a whole number of connection intervals for every interval a host is likely to choose, so the offset between a scan window and the next connection event never drifts and the same slice is lost every time. Apple hosts use 15ms, which would cap the effective window at a 5% duty cycle. Setting the pair to the same 70ms and 35ms brings reception while connected to 11.5 packets/s, and the typical wait for an update from 192ms down to 71ms against 64ms standalone. The worst case is unchanged at about 300ms, now an occasional stall rather than the normal cycle. A connected hub broadcasting and observing at once still transmits about 19 packets/s, so none of this was paid for out of the first fix.
58edc4d to
e814ff0
Compare
Yes, things worked really great before if one hub was only broadcasting and one hub was only observing. And broadcasting while connected worked really good before too. So that demo was doing all of the things that work really well. It was all of the other cases that didn't work so well. |
VID_20260911_185614967.mp4VID_20260911_185648552.mp4 |
|
In one of these videos, both hubs are connected to Pybricks Code and in one they are not. Can anyone tell the difference? (And yes, the remote is connected to the technic hub, which is broadcasting and the city hub is only observing.) |
# Remote control over broadcast, the sending half. Broadcasts only.
#
# This hub is connected to a Powered Up Handset and broadcasts the motor speed
# the buttons are asking for. It never observes. Run remote_motor_rx.py on a
# second hub, which drives the motor.
#
# Worth knowing what this exercises: the Handset is a real Bluetooth
# connection, so this hub is broadcasting while connected even though no
# computer is involved. That is the case the connected scan settings fix
# addresses, and pressing a button is a good way to feel the difference.
#
# Buttons:
# left +/- full speed forwards or backwards
# right +/- a third of that, for finer control
# nothing stop
from pybricks.hubs import ThisHub
from pybricks.parameters import Button, Color
from pybricks.pupdevices import Remote
from pybricks.tools import wait
CHANNEL = 1
SPEED = 900
hub = ThisHub(broadcast_channel=CHANNEL)
hub.light.on(Color.YELLOW)
remote = Remote()
hub.light.on(Color.GREEN)
last = None
while True:
pressed = remote.buttons.pressed()
if Button.LEFT_PLUS in pressed:
speed = SPEED
elif Button.LEFT_MINUS in pressed:
speed = -SPEED
elif Button.RIGHT_PLUS in pressed:
speed = SPEED // 3
elif Button.RIGHT_MINUS in pressed:
speed = -SPEED // 3
else:
speed = 0
# Only send on a change. The radio keeps advertising the last value on its
# own, so repeating it would just spend Bluetooth commands to say the same
# thing, and setting the remote's light every cycle would do the same.
if speed != last:
hub.ble.broadcast(speed)
remote.light.on(
Color.GREEN if speed > 0 else Color.RED if speed < 0 else Color.NONE
)
last = speed
wait(10)# Remote control over broadcast, the receiving half. Observes only.
#
# Drives a motor at whatever speed remote_motor_tx.py is broadcasting. This hub
# never transmits, so all of its radio time goes to listening.
#
# The status light shows what the link is doing: green while driving, yellow
# while stopped on request, red when nothing has been heard for a second.
from pybricks.hubs import ThisHub
from pybricks.parameters import Color, Port
from pybricks.pupdevices import Motor
from pybricks.tools import wait
CHANNEL = 1
hub = ThisHub(observe_channels=[CHANNEL])
motor = Motor(Port.A)
hub.light.on(Color.RED)
applied = None
while True:
speed = hub.ble.observe(CHANNEL)
if speed is None:
# observe() gives None when nothing has arrived for a second, which
# means the link is gone rather than someone asking for a stop. Coast
# instead of braking, so a moving robot is not stopped dead by a hub
# going out of range.
if applied is not None:
motor.stop()
hub.light.on(Color.RED)
applied = None
elif isinstance(speed, int) and speed != applied:
# Anyone can broadcast on this channel, so ignore anything that is not
# a speed rather than raising in the middle of a movement.
if speed == 0:
motor.brake()
hub.light.on(Color.YELLOW)
else:
motor.run(speed)
hub.light.on(Color.GREEN)
applied = speed
wait(10)Note: These are AI generated programs (because time is valuable). I think the first one is pretty good. The second one is mostly good but I would just do |
|
WOW! |
All three Bluetooth drivers scanned with the window equal to the interval, 30ms of every 30ms, so the radio was never free and a hub that broadcasts while observing could hardly transmit. Measurements in https://github.com/orgs/pybricks/discussions/2822 show a City Hub dropping from 28 packets/s to under 4 when it broadcasts and observes at the same time.
Curiously it was fine when connected to a computer. The reason is that GAP_DeviceDiscoveryRequest() picks TGAP_CONN_SCAN_INT and _WIND when a connection exists and the general discovery parameters when it does not. We only set the latter, so the chip fell back to TI's defaults of 300ms interval and 150ms window, a 50% duty cycle, whenever a computer was connected. Using a 50% duty cycle always is what this does.
The scan interval is also kept shorter than the advertising interval of a peer hub. When the two periods are close, the phase between them drifts slowly, misses come in runs and reception latency is spiky rather than merely averaging out.
Broadcasting on the BTstack hubs went from every 100ms to every 30ms. 100ms was the Bluetooth 4.x minimum for non-connectable advertising, but the CC2564C is 5.1, where the minimum is 20ms, so the old value only limited how quickly other hubs could hear it. The Move Hub keeps 100ms because BlueNRG-MS is a 4.1 controller and cannot go faster; it is the most limited hub, so it is allowed to be the slow one rather than holding the others back.
Expected reception latency is now roughly 50 to 60ms between any pair of hubs, except from the Move Hub at about 200ms.