From 3274cef849d91700905a56ec073cc2e3fb87f830 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Tue, 16 May 2023 14:46:53 -0500 Subject: [PATCH] pybricks.common: Add new BLE class. This adds a new BLE class that is used for connectionless broadcasting/ observing on hubs with built-in Bluetooth Low Energy. Also see https://github.com/pybricks/pybricks-micropython/pull/158. --- doc/common/conf.py | 1 + doc/main/hubs/cityhub.rst | 22 +++++ doc/main/hubs/essentialhub.rst | 27 ++++++ doc/main/hubs/movehub.rst | 27 ++++++ doc/main/hubs/primehub.rst | 27 ++++++ doc/main/hubs/technichub.rst | 27 ++++++ examples/pup/hub_common/ble_broadcast.py | 24 +++++ examples/pup/hub_common/ble_observe.py | 39 ++++++++ jedi/tests/test_complete_city_hub.py | 1 + jedi/tests/test_complete_essential_hub.py | 1 + jedi/tests/test_complete_move_hub.py | 1 + jedi/tests/test_complete_prime_hub.py | 1 + jedi/tests/test_complete_technic_hub.py | 1 + jedi/tests/test_get_signature.py | 42 ++++++++- src/pybricks/_common.py | 78 +++++++++++++++- src/pybricks/hubs.py | 106 ++++++++++++++++++++-- 16 files changed, 413 insertions(+), 12 deletions(-) create mode 100644 examples/pup/hub_common/ble_broadcast.py create mode 100644 examples/pup/hub_common/ble_observe.py diff --git a/doc/common/conf.py b/doc/common/conf.py index 7dc1fa9..280f7a0 100644 --- a/doc/common/conf.py +++ b/doc/common/conf.py @@ -325,6 +325,7 @@ def on_missing_reference( # warnings when Sphinx tries to cross reference units like deg/s. For # consistency, we also treat units without special characters this way. for unit in [ + "dBm", "deg", "deg/s", "deg/s²", diff --git a/doc/main/hubs/cityhub.rst b/doc/main/hubs/cityhub.rst index 1bc2b9d..ed41975 100644 --- a/doc/main/hubs/cityhub.rst +++ b/doc/main/hubs/cityhub.rst @@ -19,6 +19,17 @@ City Hub .. automethod:: pybricks.hubs::CityHub.light.animate + .. rubric:: Using connectionless Bluetooth messaging + + ``ble.broadcast()`` does not work on ``CityHub`` due to a bug in the + Bluetooth chip firmware. + + .. automethod:: pybricks.hubs::CityHub.ble.observe + + .. automethod:: pybricks.hubs::CityHub.ble.signal_strength + + .. automethod:: pybricks.hubs::CityHub.ble.version + .. rubric:: Using the battery .. automethod:: pybricks.hubs::CityHub.battery.voltage @@ -70,6 +81,17 @@ Creating light animations .. literalinclude:: ../../../examples/pup/hub_common/build/light_animate_cityhub.py + +Bluetooth examples +------------------ + +Observing data from other hubs +****************************** + +.. literalinclude:: + ../../../examples/pup/hub_common/build/ble_observe_cityhub.py + + Button and system examples ---------------------------------- diff --git a/doc/main/hubs/essentialhub.rst b/doc/main/hubs/essentialhub.rst index a24ac1d..847fdad 100644 --- a/doc/main/hubs/essentialhub.rst +++ b/doc/main/hubs/essentialhub.rst @@ -47,6 +47,16 @@ Essential Hub .. automethod:: pybricks.hubs::EssentialHub.imu.settings + .. rubric:: Using connectionless Bluetooth messaging + + .. automethod:: pybricks.hubs::EssentialHub.ble.broadcast + + .. automethod:: pybricks.hubs::EssentialHub.ble.observe + + .. automethod:: pybricks.hubs::EssentialHub.ble.signal_strength + + .. automethod:: pybricks.hubs::EssentialHub.ble.version + .. rubric:: Using the battery .. automethod:: pybricks.hubs::EssentialHub.battery.voltage @@ -136,6 +146,23 @@ Reading acceleration and angular velocity on one axis .. literalinclude:: ../../../examples/pup/hub_common/build/imu_read_scalar_essentialhub.py + +Bluetooth examples +------------------ + +Broadcasting data to other hubs +******************************* + +.. literalinclude:: + ../../../examples/pup/hub_common/build/ble_broadcast_essentialhub.py + +Observing data from other hubs +****************************** + +.. literalinclude:: + ../../../examples/pup/hub_common/build/ble_observe_essentialhub.py + + System examples ---------------------------------- diff --git a/doc/main/hubs/movehub.rst b/doc/main/hubs/movehub.rst index 2c14ad2..837c39d 100644 --- a/doc/main/hubs/movehub.rst +++ b/doc/main/hubs/movehub.rst @@ -31,6 +31,16 @@ Move Hub Changed acceleration units from m/s² to mm/s². + .. rubric:: Using connectionless Bluetooth messaging + + .. automethod:: pybricks.hubs::MoveHub.ble.broadcast + + .. automethod:: pybricks.hubs::MoveHub.ble.observe + + .. automethod:: pybricks.hubs::MoveHub.ble.signal_strength + + .. automethod:: pybricks.hubs::MoveHub.ble.version + .. rubric:: Using the battery .. automethod:: pybricks.hubs::MoveHub.battery.voltage @@ -85,6 +95,23 @@ Reading acceleration .. literalinclude:: ../../../examples/pup/hub_movehub/imu_read_acceleration.py + +Bluetooth examples +------------------ + +Broadcasting data to other hubs +******************************* + +.. literalinclude:: + ../../../examples/pup/hub_common/build/ble_broadcast_movehub.py + +Observing data from other hubs +****************************** + +.. literalinclude:: + ../../../examples/pup/hub_common/build/ble_observe_movehub.py + + Button and system examples ---------------------------------- diff --git a/doc/main/hubs/primehub.rst b/doc/main/hubs/primehub.rst index 1390f9a..c25e552 100644 --- a/doc/main/hubs/primehub.rst +++ b/doc/main/hubs/primehub.rst @@ -89,6 +89,16 @@ Prime Hub / Inventor Hub .. automethod:: pybricks.hubs::PrimeHub.speaker.play_notes + .. rubric:: Using connectionless Bluetooth messaging + + .. automethod:: pybricks.hubs::PrimeHub.ble.broadcast + + .. automethod:: pybricks.hubs::PrimeHub.ble.observe + + .. automethod:: pybricks.hubs::PrimeHub.ble.signal_strength + + .. automethod:: pybricks.hubs::PrimeHub.ble.version + .. rubric:: Using the battery .. automethod:: pybricks.hubs::PrimeHub.battery.voltage @@ -247,6 +257,23 @@ Reading acceleration and angular velocity on one axis .. literalinclude:: ../../../examples/pup/hub_common/build/imu_read_scalar_primehub.py + +Bluetooth examples +------------------ + +Broadcasting data to other hubs +******************************* + +.. literalinclude:: + ../../../examples/pup/hub_common/build/ble_broadcast_primehub.py + +Observing data from other hubs +****************************** + +.. literalinclude:: + ../../../examples/pup/hub_common/build/ble_observe_primehub.py + + System examples ---------------------------------- diff --git a/doc/main/hubs/technichub.rst b/doc/main/hubs/technichub.rst index ab6c115..03105e0 100644 --- a/doc/main/hubs/technichub.rst +++ b/doc/main/hubs/technichub.rst @@ -43,6 +43,16 @@ Technic Hub .. automethod:: pybricks.hubs::TechnicHub.imu.settings + .. rubric:: Using connectionless Bluetooth messaging + + .. automethod:: pybricks.hubs::TechnicHub.ble.broadcast + + .. automethod:: pybricks.hubs::TechnicHub.ble.observe + + .. automethod:: pybricks.hubs::TechnicHub.ble.signal_strength + + .. automethod:: pybricks.hubs::TechnicHub.ble.version + .. rubric:: Using the battery .. automethod:: pybricks.hubs::TechnicHub.battery.voltage @@ -128,6 +138,23 @@ Reading acceleration and angular velocity on one axis .. literalinclude:: ../../../examples/pup/hub_common/build/imu_read_scalar_technichub.py + +Bluetooth examples +------------------ + +Broadcasting data to other hubs +******************************* + +.. literalinclude:: + ../../../examples/pup/hub_common/build/ble_broadcast_technichub.py + +Observing data from other hubs +****************************** + +.. literalinclude:: + ../../../examples/pup/hub_common/build/ble_observe_technichub.py + + Button and system examples ---------------------------------- diff --git a/examples/pup/hub_common/ble_broadcast.py b/examples/pup/hub_common/ble_broadcast.py new file mode 100644 index 0000000..4413be6 --- /dev/null +++ b/examples/pup/hub_common/ble_broadcast.py @@ -0,0 +1,24 @@ +# ThisHub = MoveHub CityHub TechnicHub PrimeHub EssentialHub +from pybricks.hubs import ThisHub +from pybricks.pupdevices import Motor +from pybricks.parameters import Port +from pybricks.tools import wait + +# Initialize the hub. +hub = ThisHub(broadcast_channel=1) + +# Initialize the motors. +left_motor = Motor(Port.A) +right_motor = Motor(Port.B) + +while True: + # Read the motor angles to be sent to the other hub. + left_angle = left_motor.angle() + right_angle = right_motor.angle() + + # Set the broadcast data and start broadcasting if not already doing so. + hub.ble.broadcast(left_angle, right_angle) + + # Broadcasts are only sent every 100 milliseconds, so there is no reason + # to call the broadcast() method more often than that. + wait(100) diff --git a/examples/pup/hub_common/ble_observe.py b/examples/pup/hub_common/ble_observe.py new file mode 100644 index 0000000..c1f6988 --- /dev/null +++ b/examples/pup/hub_common/ble_observe.py @@ -0,0 +1,39 @@ +# ThisHub = MoveHub CityHub TechnicHub PrimeHub EssentialHub +from pybricks.hubs import ThisHub +from pybricks.pupdevices import Motor +from pybricks.parameters import Color, Port +from pybricks.tools import wait + +# Initialize the hub. +hub = ThisHub(observe_channels=[1]) + +# Initialize the motors. +left_motor = Motor(Port.A) +right_motor = Motor(Port.B) + +while True: + # Receive broadcast from the other hub. + + data = hub.ble.observe(1) + + if data is None: + # No data has been received in the last 1 second. + hub.light.on(Color.RED) + else: + # Data was received and is less that one second old. + hub.light.on(Color.GREEN) + + # *data* contains the same values in the same order + # that were passed to hub.ble.broadcast() on the + # other hub. + left_angle = data[0] + right_angle = data[1] + + # Make the motors on this hub mirror the position of the + # motors on the other hub. + left_motor.track_target(left_angle) + right_motor.track_target(right_angle) + + # Broadcasts are only sent every 100 milliseconds, so there is + # no reason to call the observe() method more often than that. + wait(100) diff --git a/jedi/tests/test_complete_city_hub.py b/jedi/tests/test_complete_city_hub.py index e9e0950..47eb45b 100644 --- a/jedi/tests/test_complete_city_hub.py +++ b/jedi/tests/test_complete_city_hub.py @@ -33,6 +33,7 @@ def test_hub_dot(): completions: list[CompletionItem] = json.loads(complete(code, 3, len(line) + 1)) assert [c["insertText"] for c in completions] == [ "battery", + "ble", "button", "light", "system", diff --git a/jedi/tests/test_complete_essential_hub.py b/jedi/tests/test_complete_essential_hub.py index 37d9d9d..544a51a 100644 --- a/jedi/tests/test_complete_essential_hub.py +++ b/jedi/tests/test_complete_essential_hub.py @@ -33,6 +33,7 @@ def test_hub_dot(): completions: list[CompletionItem] = json.loads(complete(code, 3, len(line) + 1)) assert [c["insertText"] for c in completions] == [ "battery", + "ble", "button", "charger", "imu", diff --git a/jedi/tests/test_complete_move_hub.py b/jedi/tests/test_complete_move_hub.py index c370aa5..63d86c7 100644 --- a/jedi/tests/test_complete_move_hub.py +++ b/jedi/tests/test_complete_move_hub.py @@ -33,6 +33,7 @@ def test_hub_dot(): completions: list[CompletionItem] = json.loads(complete(code, 3, len(line) + 1)) assert [c["insertText"] for c in completions] == [ "battery", + "ble", "button", "imu", "light", diff --git a/jedi/tests/test_complete_prime_hub.py b/jedi/tests/test_complete_prime_hub.py index 7f76064..65080ad 100644 --- a/jedi/tests/test_complete_prime_hub.py +++ b/jedi/tests/test_complete_prime_hub.py @@ -33,6 +33,7 @@ def test_hub_dot(): completions: list[CompletionItem] = json.loads(complete(code, 3, len(line) + 1)) assert [c["insertText"] for c in completions] == [ "battery", + "ble", "buttons", "charger", "display", diff --git a/jedi/tests/test_complete_technic_hub.py b/jedi/tests/test_complete_technic_hub.py index 2eb03ef..e9ddc9c 100644 --- a/jedi/tests/test_complete_technic_hub.py +++ b/jedi/tests/test_complete_technic_hub.py @@ -33,6 +33,7 @@ def test_hub_dot(): completions: list[CompletionItem] = json.loads(complete(code, 3, len(line) + 1)) assert [c["insertText"] for c in completions] == [ "battery", + "ble", "button", "imu", "light", diff --git a/jedi/tests/test_get_signature.py b/jedi/tests/test_get_signature.py index b3dc771..59a088d 100644 --- a/jedi/tests/test_get_signature.py +++ b/jedi/tests/test_get_signature.py @@ -78,17 +78,51 @@ def _get_constructor_signature(module: str, type: str) -> SignatureHelp: CONSTRUCTOR_PARAMS = [ - pytest.param("pybricks.hubs", "MoveHub", [[]]), - pytest.param("pybricks.hubs", "CityHub", [[]]), + pytest.param( + "pybricks.hubs", + "MoveHub", + [["broadcast_channel: int=0", "observe_channels: Sequence[int]=[]"]], + ), + pytest.param( + "pybricks.hubs", + "CityHub", + [["broadcast_channel: int=0", "observe_channels: Sequence[int]=[]"]], + ), pytest.param( "pybricks.hubs", "TechnicHub", - [["top_side: Axis=Axis.Z", "front_side: Axis=Axis.X"]], + [ + [ + "top_side: Axis=Axis.Z", + "front_side: Axis=Axis.X", + "broadcast_channel: int=0", + "observe_channels: Sequence[int]=[]", + ] + ], ), pytest.param( "pybricks.hubs", "PrimeHub", - [["top_side: Axis=Axis.Z", "front_side: Axis=Axis.X"]], + [ + [ + "top_side: Axis=Axis.Z", + "front_side: Axis=Axis.X", + "broadcast_channel: int=0", + "observe_channels: Sequence[int]=[]", + ] + ], + ), + pytest.param( + "pybricks.hubs", + "EssentialHub", + [ + [ + "top_side: Axis=Axis.Z", + "front_side: Axis=Axis.X", + "broadcast_channel: int=0", + "observe_channels: Sequence[int]=[]", + ] + ], ), pytest.param( "pybricks.pupdevices", diff --git a/src/pybricks/_common.py b/src/pybricks/_common.py index 37a9acd..e75d69f 100644 --- a/src/pybricks/_common.py +++ b/src/pybricks/_common.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MIT -# Copyright (c) 2018-2021 The Pybricks Authors +# Copyright (c) 2018-2023 The Pybricks Authors """Generic cross-platform module for typical devices like lights, displays, speakers, and batteries.""" @@ -1285,3 +1285,79 @@ class AmbientColorSensor(CommonColorSensor): Measured color. The color is described by a hue (0--359), a saturation (0--100), and a brightness value (0--100). """ + + +class BLE: + """ + Bluetooth Low Energy. + + .. versionadded:: 3.3 + """ + + def broadcast(self, *args: Union[None, bool, int, float, str, bytes]) -> None: + """broadcast(data0, data1, ...) + + Starts broadcasting the given data values. + + Each value can be any of ``int``, ``float``, ``str`, ``bytes``, + ``None``, ``True``, or ``False``. The data is broadcasted on the + *broadcast_channel* you selected when initializing the hub. + + The total data size is quite limited (26 bytes). ``None``, ``True`` and + ``False`` take 1 byte each. ``float`` takes 5 bytes. ``int`` takes 2 to + 5 bytes depending on how big the number is. ``str`` and ``bytes`` take + the number of bytes in the object plus one extra byte. + + Params: + args: Zero or more values to be broadcast. + + + + .. versionadded:: 3.3 + """ + + def observe( + self, channel: int + ) -> Optional[Tuple[Union[None, bool, int, float, str, bytes], ...]]: + """observe(channel) -> tuple | None + + Retrieves the last observed data for a given channel. + + Args: + channel (int): The channel to observe (0 to 255). + + Returns: + A tuple of the received data or ``None`` if no recent data is + available. + + .. tip:: Receiving data is more reliable when the hub is not connected + to a computer or other devices at the same time. + + .. versionadded:: 3.3 + """ + + def signal_strength(self, channel: int) -> int: + """signal_strength(channel) -> int: dBm + + Gets the average signal strength in dBm for the given channel. + + This is useful for detecting how near the broadcasting device is. A close + device may have a signal strength around -40 dBm while a far away device + might have a signal strength around -70 dBm. + + Args: + channel (int): The channel number (0 to 255). + + Returns: + The signal strength or ``-128`` if there is no recent observed data. + + .. versionadded:: 3.3 + """ + + def version(self) -> str: + """version() -> str + + Gets the firmware version from the Bluetooth chip. + + .. versionadded:: 3.3 + """ diff --git a/src/pybricks/hubs.py b/src/pybricks/hubs.py index edd1bb0..1f0208b 100644 --- a/src/pybricks/hubs.py +++ b/src/pybricks/hubs.py @@ -1,7 +1,10 @@ # SPDX-License-Identifier: MIT -# Copyright (c) 2018-2022 The Pybricks Authors +# Copyright (c) 2018-2023 The Pybricks Authors """LEGO® Programmable Hubs.""" + +from typing import Sequence + from . import _common from .ev3dev import _speaker from .media.ev3dev import Image as _Image @@ -38,6 +41,25 @@ class MoveHub: imu = _common.SimpleAccelerometer() system = _common.System() button = _common.Keypad([_Button.CENTER]) + ble = _common.BLE() + + def __init__( + self, broadcast_channel: int = 0, observe_channels: Sequence[int] = [] + ): + """MoveHub(broadcast_channel=0, observe_channels=[]) + + Arguments: + broadcast_channel: + A value from 0 to 255 indicating which channel ``hub.ble.broadcast()`` + will use. Default is channel 0. + observe_channels: + A list of channels to listen to when ``hub.ble.observe()`` is + called. Listening to more channels requires more memory. + Default is an empty list (no channels). + + .. versionchanged:: 3.3 + Added *broadcast_channel* and *observe_channels* arguments. + """ class CityHub: @@ -49,6 +71,25 @@ class CityHub: light = _common.ColorLight() system = _common.System() button = _common.Keypad([_Button.CENTER]) + ble = _common.BLE() + + def __init__( + self, broadcast_channel: int = 0, observe_channels: Sequence[int] = [] + ): + """CityHub(broadcast_channel=0, observe_channels=[]) + + Arguments: + broadcast_channel: + A value from 0 to 255 indicating which channel ``hub.ble.broadcast()`` + will use. Default is channel 0. + observe_channels: + A list of channels to listen to when ``hub.ble.observe()`` is + called. Listening to more channels requires more memory. + Default is an empty list (no channels). + + .. versionchanged:: 3.3 + Added *broadcast_channel* and *observe_channels* arguments. + """ class TechnicHub: @@ -61,9 +102,16 @@ class TechnicHub: imu = _common.IMU() system = _common.System() button = _common.Keypad([_Button.CENTER]) + ble = _common.BLE() - def __init__(self, top_side: Axis = Axis.Z, front_side: Axis = Axis.X): - """TechnicHub(top_side=Axis.Z, front_side=Axis.X) + def __init__( + self, + top_side: Axis = Axis.Z, + front_side: Axis = Axis.X, + broadcast_channel: int = 0, + observe_channels: Sequence[int] = [], + ): + """TechnicHub(top_side=Axis.Z, front_side=Axis.X, broadcast_channel=0, observe_channels=[]) Initializes the hub. Optionally, specify how the hub is :ref:`placed in your design ` by saying in which @@ -75,6 +123,16 @@ class TechnicHub: the hub. front_side (Axis): The axis that passes through the *front side* of the hub. + broadcast_channel: + A value from 0 to 255 indicating which channel ``hub.ble.broadcast()`` + will use. Default is channel 0. + observe_channels: + A list of channels to listen to when ``hub.ble.observe()`` is + called. Listening to more channels requires more memory. + Default is an empty list (no channels). + + .. versionchanged:: 3.3 + Added *broadcast_channel* and *observe_channels* arguments. """ @@ -89,9 +147,16 @@ class EssentialHub: light = _common.ColorLight() imu = _common.IMU() system = _common.System() + ble = _common.BLE() - def __init__(self, top_side=Axis.Z, front_side=Axis.X): - """__init__(top_side=Axis.Z, front_side=Axis.X) + def __init__( + self, + top_side: Axis = Axis.Z, + front_side: Axis = Axis.X, + broadcast_channel: int = 0, + observe_channels: Sequence[int] = [], + ): + """EssentialHub(top_side=Axis.Z, front_side=Axis.X, broadcast_channel=0, observe_channels=[]) Initializes the hub. Optionally, specify how the hub is :ref:`placed in your design ` by saying in which @@ -103,6 +168,16 @@ class EssentialHub: the hub. front_side (Axis): The axis that passes through the *front side* of the hub. + broadcast_channel: + A value from 0 to 255 indicating which channel ``hub.ble.broadcast()`` + will use. Default is channel 0. + observe_channels: + A list of channels to listen to when ``hub.ble.observe()`` is + called. Listening to more channels requires more memory. + Default is an empty list (no channels). + + .. versionchanged:: 3.3 + Added *broadcast_channel* and *observe_channels* arguments. """ pass @@ -127,9 +202,16 @@ class PrimeHub: speaker = _common.Speaker() imu = _common.IMU() system = _common.System() + ble = _common.BLE() - def __init__(self, top_side: Axis = Axis.Z, front_side: Axis = Axis.X): - """PrimeHub(top_side=Axis.Z, front_side=Axis.X) + def __init__( + self, + top_side: Axis = Axis.Z, + front_side: Axis = Axis.X, + broadcast_channel: int = 0, + observe_channels: Sequence[int] = [], + ): + """PrimeHub(top_side=Axis.Z, front_side=Axis.X, broadcast_channel=0, observe_channels=[]) Initializes the hub. Optionally, specify how the hub is :ref:`placed in your design ` by saying in which @@ -141,6 +223,16 @@ class PrimeHub: the hub. front_side (Axis): The axis that passes through the *front side* of the hub. + broadcast_channel: + A value from 0 to 255 indicating which channel ``hub.ble.broadcast()`` + will use. Default is channel 0. + observe_channels: + A list of channels to listen to when ``hub.ble.observe()`` is + called. Listening to more channels requires more memory. + Default is an empty list (no channels). + + .. versionchanged:: 3.3 + Added *broadcast_channel* and *observe_channels* arguments. """