-
-
Notifications
You must be signed in to change notification settings - Fork 61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
BLE Broadcast v2 #158
BLE Broadcast v2 #158
Conversation
Here are the programs I was using during testing: from pybricks.hubs import ThisHub
from pybricks.pupdevices import Motor
from pybricks.tools import wait
from pybricks.parameters import Port, Color
hub = ThisHub(observe_channels=[1])
motor = Motor(Port.A)
print("radio fw", hub.ble.version())
hub.light.on(Color.GREEN)
prev = None
while True:
data = hub.ble.observe(1)
if data is not None:
target = data[0]
motor.track_target(target)
if target != prev:
print(*data)
prev = target
else:
motor.track_target(0)
wait(10) from pybricks.hubs import ThisHub
from pybricks.pupdevices import Remote, Motor
from pybricks.tools import wait
from pybricks.parameters import Color, Button, Port
hub = ThisHub(broadcast_channel=1)
motor = Motor(Port.A)
print("radio fw", hub.ble.version())
hub.light.on(Color.YELLOW)
remote = Remote()
hub.light.on(Color.GREEN)
remote.light.on(Color.GREEN)
def button_oneshot(button: Button):
prev = False
oneshot = None
while True:
new = yield oneshot
if new is None:
continue
now = button in new
if now == prev:
oneshot = None
else:
oneshot = prev = now
def make_button_oneshot(button):
gen = button_oneshot(button)
gen.send(None)
return gen
left_plus_oneshot = make_button_oneshot(Button.LEFT_PLUS)
left_minus_oneshot = make_button_oneshot(Button.LEFT_MINUS)
right_plus_oneshot = make_button_oneshot(Button.RIGHT_PLUS)
right_minus_oneshot = make_button_oneshot(Button.RIGHT_MINUS)
local_target = 0
remote_target = 0
while True:
motor.track_target(local_target)
pressed = remote.buttons.pressed()
if left_plus_oneshot.send(pressed):
local_target += 45
if left_minus_oneshot.send(pressed):
local_target -= 45
if right_plus_oneshot.send(pressed):
remote_target += 45
hub.ble.broadcast(remote_target)
if right_minus_oneshot.send(pressed):
remote_target -= 45
hub.ble.broadcast(remote_target)
wait(10) This uses the LEGO Powered Up remote to control 2 motors, one connected to each of two different hubs. The left side of the controller controls the motor on the hub the remote is connected to and the right side of the controller controls the motor connected to the secondary hub. |
For higher-level documentation, we should also explain proper usage of this. The main point to get across is that this type of communication is best suited to sending output state rather than commands/events or input state. (The technical reason being this is unreliable (delivery is not guaranteed), low-bandwidth communication.) This is why in my test program, it is sending the target motor angle rather than button press events or button state. |
Nice work! Would it be better if the received data is packed the same as the sent data? In this example the sender has: hub.ble.broadcast(target) but the receiver has: target = data[0] # Why is unpacking needed here, but packing not needed on sender? It would be nice if packing and unpacking was the same, or even absent altogether for single-valued data. Originally posted by @laurensvalk in a02c18c#r108901780
👍 |
ee4345c
to
400bebf
Compare
updated. (builds now require Pybricks Beta >= v2.2.0-beta.3 released 2023-04-24) |
38727c3
to
e50a5fa
Compare
If it's the same in a technical sense, it would be nice to pick the simplest for users (which might not be the I've added a proposal to get it up the same level as #80. So allowing int/float/bool/str or a tuple thereof. It is quite a small change, without adding any extra encoding fields. I've also added a 1 second timeout so that you can easily detect that the sender has stopped. Do we want a way to stop broadcasting? Since Personally, I would perhaps drop |
Thank you for the suggestion. However, I am really very happy with the simplicity of the the way I had implemented it. I disagree that
If we find there is demand for this, we can always add it later. I think we should just add a new method for this if we need it rather than trying to put two separate functions into a single method. |
d8ea6f6
to
e50a5fa
Compare
Having implemented the MicroPython module side of #80 originally, so was I 😄 I've moved the explorations to a separate branch and restored this one. It still doesn't seem that intuitive to me to treat data on the receiving end different (two unpacking) from the sending end (no packings). Even if intellisense says it is the same, I usually look at it in terms of the resulting code. I'll explore a bit further on this one, including some suggestions for rssi.
Right, in this variant the one rule is: object goes in, same object comes out, instead of having to remember to unpack kwargs with |
This is good by the way --- heated API debates remind me of 2018, and we've made some pretty good stuff since then 😄 |
5dc72d5
to
c0f581a
Compare
Updated with some suggestions from Laurens. Known issue: On Technic hub, if observing is started before creating a |
Nitpick, this gets somewhat confusing: hub.ble.broadcast(None) data = hub.ble.observe(0)
if data is None:
# It's not none? 🤔 I think we could perhaps do without 8f7ba06. Since the empty set is used for no data, users can still do |
Idea: How about merging it up to 20ba223 already and use a follow up PR for the next round of improvements like:
This would be a convenient way to bring it into day-to-day testing and the next beta round. On the topic of City/Move Hub, even if we decide to have only scanning, we could make it work intuitive enough in the docs and autocomplete similar to the IMU being a superset (inherited) of the simpler Accelerometer of the Move Hub: Only the available methods are be documented/autocompleted. |
Simpler example where motor on Hub 1 mirrors motor that is manually turned on Hub 2. Hub 1: from pybricks.hubs import ThisHub
from pybricks.pupdevices import Motor
from pybricks.tools import wait
from pybricks.parameters import Port, Color
hub = ThisHub(observe_channels=[2])
motor = Motor(Port.A)
print("radio fw", hub.ble.version())
prev = None
while True:
data = hub.ble.observe(2)
if data is None:
hub.light.on(Color.RED)
motor.track_target(0)
else:
hub.light.on(Color.GREEN)
target = data[0]
motor.track_target(target)
if target != prev:
print(target)
prev = target
# broadcasts cannot be sent/received faster than once every 100ms
wait(100) Hub 2: from pybricks.hubs import ThisHub
from pybricks.pupdevices import Motor
from pybricks.tools import wait
from pybricks.parameters import Port, Color
hub = ThisHub(broadcast=2)
motor = Motor(Port.A)
print("radio fw", hub.ble.version())
prev = None
while True:
target = motor.angle()
hub.ble.broadcast(target)
if target != prev:
print(target)
prev = target
# broadcasts cannot be sent/received faster than once every 100ms
wait(100) |
For what it's worth, I'm fine keeping this in also after thinking about it some more. So, if there are no technical hurdles that could make other Bluetooth developments more complicated, I vote to merge.
Maybe we could start by having broadcast disabled on these? |
This adds a new BLE class that is used for connectionless broadcasting/ observing on hubs with built-in Bluetooth Low Energy. Also see pybricks/pybricks-micropython#158.
This adds a new BLE class that is used for connectionless broadcasting/ observing on hubs with built-in Bluetooth Low Energy. Also see pybricks/pybricks-micropython#158.
This adds a new BLE class that is used for connectionless broadcasting/ observing on hubs with built-in Bluetooth Low Energy. Also see pybricks/pybricks-micropython#158.
This adds a new BLE class that is used for connectionless broadcasting/ observing on hubs with built-in Bluetooth Low Energy. Also see pybricks/pybricks-micropython#158.
This adds a new function to the Bluetooth drivers to get the firmware version from of the Bluetooth chip.
This adds a new ble module for Bluetooth Low Energy functions and adds it to supported hubs. To start, this contains one function to get the firmware version from the Bluetooth chip. Additional functions will be added later.
This adds new Bluetooth driver APIs to enable broadcasting and observing. Technically speaking, we aren't using the BLE Broadcaster Mode or Observer Mode since those are not available on all of the Bluetooth chips. However, the end result is essentially the same - the same data is going over the air. There appears to be a firmware bug in the Bluetooth chip on the city hub that prevents broadcasting non-connectable advertisements from being transmitted over the air even though the commands succeed without error, so this is noted in the code comments.
This adds new methods to broadcast and observe advertising data using Bluetooth Low Energy. The communication protocol uses the LEGO manufacturer-specific data similar to the experimental protocol from the official Robot Inventor firmware. Multiple Python objects are serialized using a compressed format to squeeze as many objects into the 31 byte advertising data as possible. Since the BLE object is initialized on the hub object, the initialization parameters are added to the Hub constructors. Each hub can only broadcast on a single "channel" and can receive on multiple channels.
This provides a way for the receiver to know that the sender has stopped.
Now that we have an explicit set of channels to observe, it makes sense to start observing when the class is initialized, instead of starting to scan all channels when calling observe on channel X.
Now that the RSSI is filtered, it no longer belongs to a particular observe event, so there is no benefit of returning it from the same method. This simplifies the observe method to deal only with data, so it is a bit closer to what the broadcast is doing.
This modifies ble.observe() to return None if no data has been observed yet or there has been a time out since the last received data. This way there is an unambiguous way to check for valid data.
If observing has already started when connecting to another Bluetooth device, we need to pause observing (which is passive scanning) so that we can do active scanning to find the device we want to connect to. Then we need to restore passive scanning for observing once active scanning is complete. This is done on CC2540 and BTStack. On BlueNRG, the chip can't handle observing while being connected to another device so we just raise an error instead.
This adds a new BLE class that is used for connectionless broadcasting/ observing on hubs with built-in Bluetooth Low Energy. Also see pybricks/pybricks-micropython#158.
I had a change to play with my Johnny 5 robot and using the broadcast feature. It all started when I wanted to test the robot without first connecting to the PC (Windows 10) The robot did nothing. I thought maybe it was due to the print statements in the code. So, I commented them out and the robot does not do anything. If I uncomment the statements the robot moves its arms. I then got a message that there was a new beta.pybricks.com so I restarted both browser tabs. No change. I then also thought there might be a new beta firmware version. I downloaded the pybricks-code-2.2.0-beta.3.zip file. When I add that to the beta.pybricks.com for firmware using the advanced feature I get this error message: Something else I have not figured out, but that is minor, is how to find out what position the arms are in when the robot starts up? I am using the Lego Spike/Inventor motors for the arms. ''' hub = InventorHub(broadcast_channel=1, observe_channels=[1, 2]) hub.light.on(Color.YELLOW) while True:
Drive Hub (lower hub)from pybricks.hubs import InventorHub hub = InventorHub(broadcast_channel=2, observe_channels=[1]) right_arm_target_angle = 0 hub.light.on(Color.GREEN) while True:
''' |
This is not a firmware file which is why it fails to flash to the hub. The current beta has support for BLE broadcasting so you no longer need a special firmware.zip file, just flash using https://beta.pybricks.com. In the future, if you need support the best place to ask is https://github.com/orgs/pybricks/discussions. |
I tested the new example programs included in the documentation of hub.ble.broadcast / observe functions included in latest 2.2.0-beta.4 release. I'm really happy to see it finally implemented, thank you for that! |
Thanks @vardayzsolt! Let's follow up in pybricks/support#1086 |
This is an alternate attempt at hub to hub communication using BLE advertising data similar #80. (Thanks again @NStrijbosch and @laurensvalk for all of the work done there.)
Differences from #80
Limitations
OSError
withEPERM
.Python API
This adds a new
ble
attribute to theHub
object with the following methods and extends theHub
constructor with additional arguments.Ideas for changes to current implementation
Known issue: RSSI is initially 0 instead of -127.RSSI should change to -127 after some timeout of not receiving new advertisement data. How long should this be? 1 second, 10 seconds?last_observe_channel
should be changedobserve_channels
and take an iterable of channels. The range of allowable channel values for both broadcast and observe can then be expanded to 0 to 255 instead of 0 to 15. This would be nice to make it easier to avoid using the same channels as your neighbor.Furthermore, BLE scanning could be enabled at construction time rather than the first timehub.ble.observer()
is called.