From f433a4c9a50a02678e7f6a88c14d6246bf84bc7f Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 10 Nov 2019 19:26:34 -0600 Subject: [PATCH 1/4] Remove Adafruit library bleak now has macOS support, so we don't need the separate Adafriut library for macOS. --- .gitignore | 2 - README.rst | 10 +-- bricknil/ble_queue.py | 147 +++++++++++------------------------- bricknil/bleak_interface.py | 2 +- bricknil/bricknil.py | 36 +++------ bricknil/const.py | 5 -- bricknil/hub.py | 1 - requirements-dev.txt | 1 + requirements.txt | 2 +- setup.py | 3 +- test/test_hub.py | 81 +++++++------------- tox.ini | 3 +- 12 files changed, 88 insertions(+), 205 deletions(-) diff --git a/.gitignore b/.gitignore index 0e31c17..499d597 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,6 @@ htmlcov/ # pycharm .idea/* .cache/* -# adafruit checkout -Adafruit_Python_BluefruitLE/* # C extensions *.so diff --git a/README.rst b/README.rst index e381bbe..5962512 100644 --- a/README.rst +++ b/README.rst @@ -90,8 +90,7 @@ Features * Allows expressive concurrent programming using async/await syntax * The current implementation uses the async library Curio_ by David Beazley * Cross-platform - * Uses the Adafruit Bluefruit BluetoothLE library for Mac OS X - * Uses the Bleak Bluetooth library for Linux and Win10; also tested on Raspberry Pi. + * Uses the Bleak Bluetooth Low Energy library .. _Curio: http://curio.readthedocs.io @@ -503,8 +502,7 @@ Bluetooth event processing. .. figure:: images/run_loops.svg :align: center - BrickNil running inside Curio's event loop, which in turn is run by the - Adafruit_BluefruitLE library run loop + BrickNil running inside Curio's event loop I'd much have preferred to have the Bluetooth library be implemented via an async library like Curio, asyncio, or Trio, but I wasn't able to find any such @@ -614,11 +612,9 @@ Credits This project is also greatly indebted to the following persons, as well as their open-sourced libraries, portions of which have been incorporated into BrickNil under the terms of their respective licenses: -* **Tony DiCola** for his Adafruit_Python_BluefruitLE_ library that provides the BluetoothLE communication stack on Mac OS X -* :gh_user:`Henrik Blidh ` for his Bleak_ library that provided a pure python way to communicate with BluetoothLE over DBus on Linux. +* :gh_user:`Henrik Blidh ` for his Bleak_ library that provided a cross-platform, pure python way to communicate with BluetoothLE. .. _Bleak: https://github.com/hbldh/bleak -.. _Adafruit_Python_BluefruitLE: https://github.com/adafruit/Adafruit_Python_BluefruitLE Disclaimer diff --git a/bricknil/ble_queue.py b/bricknil/ble_queue.py index b32179a..1d8c32b 100644 --- a/bricknil/ble_queue.py +++ b/bricknil/ble_queue.py @@ -12,24 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Singleton interface to the Adafruit Bluetooth library""" -import Adafruit_BluefruitLE from curio import Queue, sleep, CancelledError import sys, functools, uuid from .sensor import Button # Hack! only to get the button sensor_id for the fake attach message from .process import Process from .message_dispatch import MessageDispatch -from .const import USE_BLEAK # Need a class to represent the bluetooth adapter provided -# by adafruit that receives messages class BLEventQ(Process): """All bluetooth comms go through this object Provides interfaces to connect to a device/hub, send_messages to, - and receive_messages from. Also abstracts away the underlying bluetooth library - that depends on the OS (Adafruit_Bluefruit for Mac, and Bleak for Linux/Win10) + and receive_messages from. All requests to send messages to the BLE device must be inserted into the :class:`bricknil.BLEventQ.q` Queue object. @@ -40,18 +35,9 @@ def __init__(self, ble): super().__init__('BLE Event Q') self.ble = ble self.q = Queue() - if USE_BLEAK: - self.message('using bleak') - self.adapter = None - # User needs to make sure adapter is powered up and on - # sudo hciconfig hci0 up - else: - self.message('Clearing BLE cache data') - self.ble.clear_cached_data() - self.adapter = self.ble.get_default_adapter() - self.message(f'Found adapter {self.adapter.name}') - self.message(f'Powering up adapter {self.adapter.name}') - self.adapter.power_on() + self.adapter = None + # User needs to make sure adapter is powered up and on + # sudo hciconfig hci0 up self.hubs = {} async def run(self): @@ -63,29 +49,22 @@ async def run(self): self.message_debug(f'Got msg: {msg_type} = {msg_val}') await self.send_message(hub.tx, msg_val) except CancelledError: - self.message(f'Terminating and disconnecxting') - if USE_BLEAK: - await self.ble.in_queue.put( 'quit' ) - else: - self.device.disconnect() + self.message(f'Terminating and disconnecting') + await self.ble.in_queue.put( 'quit' ) async def send_message(self, characteristic, msg): """Prepends a byte with the length of the msg and writes it to the characteristic Arguments: - characteristic : An object from bluefruit, or if using Bleak, - a tuple (device, uuid : str) + characteristic : A tuple (device, uuid : str) msg (bytearray) : Message with header """ # Message needs to have length prepended length = len(msg)+1 values = bytearray([length]+msg) - if USE_BLEAK: - device, char_uuid = characteristic - await self.ble.in_queue.put( ('tx', (device, char_uuid, values)) ) - else: - characteristic.write_value(values) + device, char_uuid = characteristic + await self.ble.in_queue.put( ('tx', (device, char_uuid, values)) ) async def get_messages(self, hub): """Instance a Message object to parse incoming messages and setup @@ -102,17 +81,9 @@ def bleak_received(sender, data): self.message_debug(f'Bleak Raw data received: {data}') msg = msg_parser.parse(data) self.message_debug('{0} Received: {1}'.format(hub.name, msg)) - def received(data): - self.message_debug(f'Adafruit_Bluefruit Raw data received: {data}') - msg = msg_parser.parse(data) - self.message_debug('{0} Received: {1}'.format(hub.name, msg)) - if USE_BLEAK: - device, char_uuid = hub.tx - await self.ble.in_queue.put( ('notify', (device, char_uuid, bleak_received) )) - else: - # Adafruit library does not callback with the sender, only the data - hub.tx.start_notify(received) + device, char_uuid = hub.tx + await self.ble.in_queue.put( ('notify', (device, char_uuid, bleak_received) )) def _check_devices_for(self, devices, name, manufacturer_id, address): @@ -157,50 +128,30 @@ async def _ble_connect(self, uart_uuid, ble_name, ble_manufacturer_id, ble_id=No self.message_info(f'Looking for first matching hub') # Start discovery - if not USE_BLEAK: - self.adapter.start_scan() - - try: - found = False - while not found and timeout > 0: - if USE_BLEAK: - await self.ble.in_queue.put('discover') # Tell bleak to start discovery - devices = await self.ble.out_queue.get() # Wait for discovered devices - await self.ble.out_queue.task_done() - # Filter out no-matching uuid - devices = [d for d in devices if str(uart_uuid) in d.uuids] - # NOw, extract the manufacturer_id - for device in devices: - assert len(device.manufacturer_data) == 1 - data = next(iter(device.manufacturer_data.values())) # Get the one and only key - device.manufacturer_id = data[1] - else: - devices = self.ble.find_devices(service_uuids=[uart_uuid]) - for device in devices: - self.message_info(f'advertised: {device.advertised}') - if len(device.advertised) > 4: - device.manufacturer_id = device.advertised[4] - else: - device.manufacturer_id = None - # Remap device.id to device.address to be consistent with bleak - device.address = device.id - - device = self._check_devices_for(devices, ble_name, ble_manufacturer_id, ble_id) - if device: - self.device = device - found = True - else: - self.message(f'Rescanning for {uart_uuid} ({timeout} tries left)') - timeout -= 1 - self.device = None - await sleep(1) - if self.device is None: - raise RuntimeError('Failed to find UART device!') - except: - raise - finally: - if not USE_BLEAK: - self.adapter.stop_scan() + found = False + while not found and timeout > 0: + await self.ble.in_queue.put('discover') # Tell bleak to start discovery + devices = await self.ble.out_queue.get() # Wait for discovered devices + await self.ble.out_queue.task_done() + # Filter out no-matching uuid + devices = [d for d in devices if str(uart_uuid) in d.metadata['uuids']] + # Now, extract the manufacturer_id + for device in devices: + assert len(device.metadata['manufacturer_data']) == 1 + data = next(iter(device.metadata['manufacturer_data'].values())) # Get the one and only key + device.manufacturer_id = data[1] + + device = self._check_devices_for(devices, ble_name, ble_manufacturer_id, ble_id) + if device: + self.device = device + found = True + else: + self.message(f'Rescanning for {uart_uuid} ({timeout} tries left)') + timeout -= 1 + self.device = None + await sleep(1) + if self.device is None: + raise RuntimeError('Failed to find UART device!') async def connect(self, hub): @@ -229,25 +180,15 @@ async def connect(self, hub): self.message(f"found device {self.device.name}") - if USE_BLEAK: - await self.ble.in_queue.put( ('connect', self.device.address) ) - device = await self.ble.out_queue.get() - await self.ble.out_queue.task_done() - hub.ble_id = self.device.address - self.message_info(f'Device advertised: {device.characteristics}') - hub.tx = (device, hub.char_uuid) # Need to store device because the char is not an object in Bleak, unlike Bluefruit library - # Hack to fix device name on Windows - if self.device.name == "Unknown" and hasattr(device._requester, 'Name'): - self.device.name = device._requester.Name - else: - self.device.connect() - hub.ble_id = self.device.id - # discover services - self.device.discover([hub.uart_uuid], [hub.char_uuid]) - uart = self.device.find_service(hub.uart_uuid) - hub.tx = uart.find_characteristic(hub.char_uuid) # same for rx - self.message_info(f'Device advertised {self.device.advertised}') - + await self.ble.in_queue.put( ('connect', self.device.address) ) + device = await self.ble.out_queue.get() + await self.ble.out_queue.task_done() + hub.ble_id = self.device.address + self.message_info(f'Device advertised: {device.services.characteristics}') + hub.tx = (device, hub.char_uuid) + # Hack to fix device name on Windows + if self.device.name == "Unknown" and hasattr(device._requester, 'Name'): + self.device.name = device._requester.Name self.message_info(f"Connected to device {self.device.name}:{hub.ble_id}") self.hubs[hub.ble_id] = hub diff --git a/bricknil/bleak_interface.py b/bricknil/bleak_interface.py index ebab0f4..85a1bcd 100644 --- a/bricknil/bleak_interface.py +++ b/bricknil/bleak_interface.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Interface to the BLEAK library in Linux for BLE calls +"""Interface to the BLEAK library for BLE calls """ import curio, asyncio, threading, logging diff --git a/bricknil/bricknil.py b/bricknil/bricknil.py index 459c536..ac7357f 100644 --- a/bricknil/bricknil.py +++ b/bricknil/bricknil.py @@ -23,7 +23,6 @@ import logging import pprint from curio import run, spawn, sleep, Queue, tcp_server -import Adafruit_BluefruitLE from functools import partial, wraps import uuid @@ -31,11 +30,8 @@ from .process import Process from .ble_queue import BLEventQ from .hub import PoweredUpHub, BoostHub, Hub -from .const import USE_BLEAK from .sockets import bricknil_socket_server - -#if USE_BLEAK: - #from .bleak_interface import Bleak +from .bleak_interface import Bleak import threading @@ -184,7 +180,7 @@ def _curio_event_run(ble, system): communcation thread loop. Args: - ble : The Adafruit_BluefruitLE interface object + ble : The interface object system : Coroutine that the user provided to instantate their system """ @@ -197,27 +193,15 @@ def start(user_system_setup_func): #pragma: no cover Just pass in the async co-routine that instantiates all your hubs, and this function will take care of the rest. This includes: - - Initializing the Adafruit bluetooth interface object + - Initializing the bluetooth interface object - Starting a run loop inside this bluetooth interface for executing the Curio event loop - Starting up the user async co-routines inside the Curio event loop """ - - if USE_BLEAK: - from .bleak_interface import Bleak - ble = Bleak() - # Run curio in a thread - curry_curio_event_run = partial(_curio_event_run, ble=ble, system=user_system_setup_func) - t = threading.Thread(target=curry_curio_event_run) - t.start() - print('started thread for curio') - ble.run() - else: - ble = Adafruit_BluefruitLE.get_provider() - ble.initialize() - # run_mainloop_with call does not accept function args. So let's curry - # the my_run with the ble arg as curry_my_run - curry_curio_event_run = partial(_curio_event_run, ble=ble, system=user_system_setup_func) - - ble.run_mainloop_with(curry_curio_event_run) - + ble = Bleak() + # Run curio in a thread + curry_curio_event_run = partial(_curio_event_run, ble=ble, system=user_system_setup_func) + t = threading.Thread(target=curry_curio_event_run) + t.start() + print('started thread for curio') + ble.run() diff --git a/bricknil/const.py b/bricknil/const.py index 261a99b..ef913d2 100644 --- a/bricknil/const.py +++ b/bricknil/const.py @@ -17,11 +17,6 @@ from enum import Enum import platform -if platform.system() == "Darwin": - USE_BLEAK = False -else: - USE_BLEAK = True - class Color(Enum): """11 colors""" black = 0 diff --git a/bricknil/hub.py b/bricknil/hub.py index 8c8b367..33ebeed 100644 --- a/bricknil/hub.py +++ b/bricknil/hub.py @@ -19,7 +19,6 @@ from curio import sleep, UniversalQueue, CancelledError from .process import Process from .sensor.peripheral import Peripheral # for type check -from .const import USE_BLEAK from .sockets import WebMessage class UnknownPeripheralMessage(Exception): pass diff --git a/requirements-dev.txt b/requirements-dev.txt index 0f2aa18..3cb99a5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,7 @@ ansible; sys_platform != "win32" pyobjc; sys_platform == "darwin" curio +bleak fabric sphinx sphinx-readable-theme diff --git a/requirements.txt b/requirements.txt index 289c604..21de995 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ pyyaml curio -bluebrick-Adafruit_BluefruitLE >= 0.9.12 +bleak diff --git a/setup.py b/setup.py index 411f139..6e5d749 100644 --- a/setup.py +++ b/setup.py @@ -79,8 +79,7 @@ def read(*filenames, **kwargs): zip_safe = True, include_package_data = True, packages = packages, - install_requires = required + ['pyobjc ; sys.platform == "darwin"', - 'bricknil-bleak ; sys.platform != "darwin"'], + install_requires = required, dependency_links = dependency_links, entry_points = { 'console_scripts': [ diff --git a/test/test_hub.py b/test/test_hub.py index 6098336..5049900 100644 --- a/test/test_hub.py +++ b/test/test_hub.py @@ -106,14 +106,32 @@ def test_run_hub(self, data): hub = TestHub('test_hub') # Start the hub - #kernel.run(self._emit_control(TestHub)) - with patch('Adafruit_BluefruitLE.get_provider') as ble,\ - patch('bricknil.ble_queue.USE_BLEAK', False) as use_bleak: - ble.return_value = MockBLE(hub) - sensor_obj = getattr(hub, sensor_name) - sensor_obj.send_message = Mock(side_effect=coroutine(lambda x,y: "the awaitable should return this")) - kernel.run(self._emit_control, data, hub, stop_evt, ble(), sensor_obj) - #start(system) + sys.modules['bleak'] = MockBleak(hub) + sensor_obj = getattr(hub, sensor_name) + sensor_obj.send_message = Mock(side_effect=coroutine(lambda x,y: "the awaitable should return this")) + from bricknil.bleak_interface import Bleak + ble = Bleak() + # Run curio in a thread + async def dummy(): pass + + async def start_curio(): + system = await spawn(bricknil.bricknil._run_all(ble, dummy)) + while len(ble.devices) < 1 or not ble.devices[0].notify: + await sleep(0.01) + await stop_evt.set() + print("sending quit") + await ble.in_queue.put( ('quit', '')) + #await system.join() + print('system joined') + + def start_thread(): + kernel.run(start_curio) + + t = threading.Thread(target=start_thread) + t.start() + print('started thread for curio') + ble.run() + t.join() async def _wait_send_message(self, mock_call, msg): print("in mock") @@ -165,53 +183,6 @@ async def dummy(): await hub_stop_evt.set() await system.join() - @given(data = st.data()) - def test_run_hub_with_bleak(self, data): - - Hub.hubs = [] - sensor_name = 'sensor' - sensor = data.draw(st.sampled_from(self.sensor_list)) - capabilities = self._draw_capabilities(data, sensor) - - hub_type = data.draw(st.sampled_from(self.hub_list)) - TestHub, stop_evt = self._get_hub_class(hub_type, sensor, sensor_name, capabilities) - hub = TestHub('test_hub') - - async def dummy(): - pass - # Start the hub - #MockBleak = MagicMock() - sys.modules['bleak'] = MockBleak(hub) - with patch('bricknil.bricknil.USE_BLEAK', True), \ - patch('bricknil.ble_queue.USE_BLEAK', True) as use_bleak: - sensor_obj = getattr(hub, sensor_name) - sensor_obj.send_message = Mock(side_effect=coroutine(lambda x,y: "the awaitable should return this")) - from bricknil.bleak_interface import Bleak - ble = Bleak() - # Run curio in a thread - async def dummy(): pass - - async def start_curio(): - system = await spawn(bricknil.bricknil._run_all(ble, dummy)) - while len(ble.devices) < 1 or not ble.devices[0].notify: - await sleep(0.01) - await stop_evt.set() - print("sending quit") - await ble.in_queue.put( ('quit', '')) - #await system.join() - print('system joined') - - def start_thread(): - kernel.run(start_curio) - - t = threading.Thread(target=start_thread) - t.start() - print('started thread for curio') - ble.run() - t.join() - - - class MockBleak(MagicMock): def __init__(self, hub): diff --git a/tox.ini b/tox.ini index 8fd7009..424b5f3 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,5 @@ deps= coverage curio pyyaml - bluebrick-Adafruit_BluefruitLE - bricknil-bleak + bleak commands=pytest From b9e5e370f61ad40b46ddcc4fde86c27c5f2dbb7c Mon Sep 17 00:00:00 2001 From: Jan Vrany Date: Mon, 6 Jan 2020 22:40:39 +0000 Subject: [PATCH 2/4] Use (standard) `asyncio` instead of `curio` This allows to run BLEAK interface in the same loop as hubs, which makes it simpler. --- README.rst | 29 ++-------- bricknil/ble_queue.py | 12 ++-- bricknil/bleak_interface.py | 32 +++++------ bricknil/bricknil.py | 94 +++++++++++++++++-------------- bricknil/hub.py | 35 ++++++------ bricknil/message_dispatch.py | 18 +++--- bricknil/sensor/light.py | 10 ++-- bricknil/sensor/motor.py | 48 ++++++++-------- bricknil/sensor/peripheral.py | 42 +++++++------- bricknil/sensor/sensor.py | 72 +++++++++++------------ bricknil/sensor/sound.py | 12 ++-- bricknil/sockets.py | 20 +++---- docs/images/run_loops.svg | 3 - examples/boost_motor_position.py | 2 +- examples/boost_wedo_sensors.py | 2 +- examples/duplo_train.py | 2 +- examples/list_ports_boost_hub.py | 2 +- examples/list_ports_duplo_hub.py | 2 +- examples/list_ports_pup_hub.py | 2 +- examples/list_ports_pup_remote.py | 2 +- examples/technic_4x4.py | 2 +- examples/train_all.py | 2 +- examples/train_color.py | 2 +- examples/train_distance_sensor.py | 2 +- examples/train_iv.py | 2 +- examples/train_light.py | 2 +- examples/train_ramp.py | 2 +- examples/vernie_remote.py | 2 +- requirements-dev.txt | 1 - requirements.txt | 1 - test/test_hub.py | 2 +- test/test_peripheral.py | 2 +- tox.ini | 1 - 33 files changed, 219 insertions(+), 245 deletions(-) delete mode 100644 docs/images/run_loops.svg diff --git a/README.rst b/README.rst index 5962512..3e5a9e0 100644 --- a/README.rst +++ b/README.rst @@ -32,7 +32,7 @@ An example BrickNil program for controlling the Train motor speed is shown below .. code-block:: python - from curio import sleep + from asyncio import sleep from bricknil import attach, start from bricknil.hub import PoweredUpHub from bricknil.sensor import TrainMotor @@ -93,7 +93,6 @@ Features * Uses the Bleak Bluetooth Low Energy library -.. _Curio: http://curio.readthedocs.io .. _EuroBricks: https://www.eurobricks.com/forum/index.php?/forums/topic/162288-powered-up-a-tear-down/ .. _Powered-Up: https://github.com/nathankellenicki/node-poweredup .. _Bleak: https://github.com/hbldh/bleak @@ -134,7 +133,7 @@ numbers are reverse speeds): .. code-block:: python - from curio import sleep + from asyncio import sleep from bricknil.hub import PoweredUpHub from bricknil.sensor import TrainMotor @@ -488,28 +487,8 @@ BrickNil Architecture This section documents the internal architecture of BrickNil and how all the components communicate with each other. -Run loops ---------- -There are actually two threads of execution in the current system architecture. -The main Bluetooth radio communication loop is provided by the BluetoothLE -library, which manages everything in the background and can callback directly -into user code. In parallel with this, inside this library, a separate -execution loop is running the Curio event library, which provides the async -event loop that executes our user code. Thus, we need to be careful about -maintaining thread safety between the Curio async event loop and the background -Bluetooth event processing. - -.. figure:: images/run_loops.svg - :align: center - - BrickNil running inside Curio's event loop - -I'd much have preferred to have the Bluetooth library be implemented via an -async library like Curio, asyncio, or Trio, but I wasn't able to find any such -library. This admitted kludge of nested run loops was the only way I could get everything -working. - - +Both, the Bluetooth handling and the user-defined code runs in a signle asyncio +loop. Installation ############ diff --git a/bricknil/ble_queue.py b/bricknil/ble_queue.py index 1d8c32b..20680ac 100644 --- a/bricknil/ble_queue.py +++ b/bricknil/ble_queue.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from curio import Queue, sleep, CancelledError +from asyncio import Queue, sleep, CancelledError import sys, functools, uuid from .sensor import Button # Hack! only to get the button sensor_id for the fake attach message @@ -45,7 +45,7 @@ async def run(self): while True: msg = await self.q.get() msg_type, hub, msg_val = msg - await self.q.task_done() + #await self.q.task_done() self.message_debug(f'Got msg: {msg_type} = {msg_val}') await self.send_message(hub.tx, msg_val) except CancelledError: @@ -128,11 +128,11 @@ async def _ble_connect(self, uart_uuid, ble_name, ble_manufacturer_id, ble_id=No self.message_info(f'Looking for first matching hub') # Start discovery + found = False while not found and timeout > 0: await self.ble.in_queue.put('discover') # Tell bleak to start discovery - devices = await self.ble.out_queue.get() # Wait for discovered devices - await self.ble.out_queue.task_done() + devices = await self.ble.out_queue.get() # Wai# await self.ble.out_queue.task_done() # Filter out no-matching uuid devices = [d for d in devices if str(uart_uuid) in d.metadata['uuids']] # Now, extract the manufacturer_id @@ -164,7 +164,7 @@ async def connect(self, hub): different OS and libraries """ - # Connect the messaging queue for communication between self and the hub + # Connect the messaging queue for communication between self and the h hub.message_queue = self.q self.message(f'Starting scan for UART {hub.uart_uuid}') @@ -182,7 +182,7 @@ async def connect(self, hub): await self.ble.in_queue.put( ('connect', self.device.address) ) device = await self.ble.out_queue.get() - await self.ble.out_queue.task_done() + hub.ble_id = self.device.address self.message_info(f'Device advertised: {device.services.characteristics}') hub.tx = (device, hub.char_uuid) diff --git a/bricknil/bleak_interface.py b/bricknil/bleak_interface.py index 85a1bcd..e54b39a 100644 --- a/bricknil/bleak_interface.py +++ b/bricknil/bleak_interface.py @@ -1,11 +1,11 @@ -# Copyright 2019 Virantha N. Ekanayake -# +# Copyright 2019 Virantha N. Ekanayake +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,15 +15,15 @@ """Interface to the BLEAK library for BLE calls """ -import curio, asyncio, threading, logging +import asyncio, threading, logging import bleak #from bleak import BleakClient class Bleak: """Interface class between curio loop and asyncio loop running bleak - - This class is basically just a queue interface. It has two queue, + + This class is basically just a queue interface. It has two queue, one for incoming messages `in_queue` and one for outgoing messages `out_queue`. A loop running in asyncio's event_loop waits for messages on the `in_queue`. @@ -38,35 +38,29 @@ class Bleak: def __init__(self): # Need to start an event loop - self.in_queue = curio.UniversalQueue() # Incoming message queue - self.out_queue = curio.UniversalQueue() # Outgoing message queue + self.in_queue = asyncio.Queue() # Incoming message queue + self.out_queue = asyncio.Queue() # Outgoing message queue self.devices = [] #self.loop = threading.Thread(target=self.run, daemon=True) #self.loop.start() - def run(self): - #self.loop = asyncio.new_event_loop() - #asyncio.set_event_loop(self.loop) - self.loop = asyncio.get_event_loop() - self.loop.run_until_complete(self.asyncio_loop()) - async def asyncio_loop(self): + # Wait for messages on in_queue done = False while not done: msg = await self.in_queue.get() if isinstance(msg, tuple): msg, val = msg - await self.in_queue.task_done() if msg == 'discover': print('Awaiting on bleak discover') - devices = await bleak.discover(timeout=1, loop=self.loop) + devices = await bleak.discover(timeout=1) print('Done Awaiting on bleak discover') await self.out_queue.put(devices) elif msg == 'connect': - device = bleak.BleakClient(address=val, loop=self.loop) + device = bleak.BleakClient(address=val) self.devices.append(device) await device.connect() await self.out_queue.put(device) @@ -88,4 +82,4 @@ async def asyncio_loop(self): - + diff --git a/bricknil/bricknil.py b/bricknil/bricknil.py index ac7357f..d0ef9f9 100644 --- a/bricknil/bricknil.py +++ b/bricknil/bricknil.py @@ -1,11 +1,11 @@ -# Copyright 2019 Virantha N. Ekanayake -# +# Copyright 2019 Virantha N. Ekanayake +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -13,16 +13,17 @@ # limitations under the License. """Utility functions to attach sensors/motors and start the whole event loop - + #. The decorator :class:`attach` to specify peripherals that - connect to a hub (which enables sensing and motor control functions), + connect to a hub (which enables sensing and motor control functions), #. The function :func:`start` that starts running the BLE communication queue, and all the hubs, in the event-loop system """ import logging import pprint -from curio import run, spawn, sleep, Queue, tcp_server +from asyncio import run, sleep, Queue, get_event_loop +from asyncio import create_task as spawn from functools import partial, wraps import uuid @@ -40,19 +41,19 @@ class attach: """ Class-decorator to attach peripherals onto a Hub - Injects sub-classes of `Peripheral` as instance variables on a Hub + Injects sub-classes of `Peripheral` as instance variables on a Hub such as the PoweredUp Hub, akin to "attaching" a physical sensor or motor onto the Hub. - Before you attach a peripheral with sensing capabilities, + Before you attach a peripheral with sensing capabilities, you need to ensure your `Peripheral` sub-class has the matching - call-back method 'peripheralname_change'. + call-back method 'peripheralname_change'. Examples:: - @attach(PeripheralType, - name="instance name", - port='port', + @attach(PeripheralType, + name="instance name", + port='port', capabilities=[]) Warnings: @@ -75,9 +76,9 @@ def __call__ (self, cls): going into that __init__ call. Inside that wrapper, we do the following: - + # Instance the peripheral that was decorated with the saved **kwargs - # Check that any `sense_*` capabiilities in the peripheral have an + # Check that any `sense_*` capabiilities in the peripheral have an appropriate handler method in the hub class being decorated. # Instance the Hub # Set the peripheral instance as an instance variable on the hub via the @@ -106,7 +107,7 @@ def wrapper_f(*args, **kwargs): async def _run_all(ble, system): - """Curio run loop + """Curio run loop """ print('inside curio run loop') # Instantiate the Bluetooth LE handler/queue @@ -114,9 +115,9 @@ async def _run_all(ble, system): # The web client out_going queue web_out_queue = Queue() # Instantiate socket listener - #task_socket = await spawn(socket_server, web_out_queue, ('',25000)) - task_tcp = await spawn(bricknil_socket_server, web_out_queue, ('',25000)) - await task_tcp.join() + # task_socket = await spawn(socket_server, web_out_queue, ('',25000)) + # task_tcp = await spawn(bricknil_socket_server, web_out_queue, ('',25000)) + # await task_tcp.join() # Call the user's system routine to instantiate the processes await system() @@ -125,18 +126,17 @@ async def _run_all(ble, system): hub_peripheral_listen_tasks = [] # Need to cancel these at the end # Run the bluetooth listen queue - task_ble_q = await spawn(ble_q.run()) + task_ble_q = spawn(ble_q.run()) # Connect all the hubs first before enabling any of them for hub in Hub.hubs: hub.web_queue_out = web_out_queue - task_connect = await spawn(ble_q.connect(hub)) - await task_connect.join() - + task_connect = spawn(ble_q.connect(hub)) + await task_connect for hub in Hub.hubs: # Start the peripheral listening loop in each hub - task_listen = await spawn(hub.peripheral_message_loop()) + task_listen = spawn(hub.peripheral_message_loop()) hub_peripheral_listen_tasks.append(task_listen) # Need to wait here until all the ports are set @@ -152,30 +152,30 @@ async def _run_all(ble, system): await sleep(1) # Start each hub - task_run = await spawn(hub.run()) + task_run = spawn(hub.run()) hub_tasks.append(task_run) - + await task_run # Now wait for the tasks to finish ble_q.message_info(f'Waiting for hubs to end') for task in hub_tasks: - await task.join() + await task ble_q.message_info(f'Hubs end') for task in hub_peripheral_listen_tasks: - await task.cancel() - await task_ble_q.cancel() + task.cancel() + task_ble_q.cancel() # Print out the port information in debug mode for hub in Hub.hubs: if hub.query_port_info: hub.message_info(pprint.pformat(hub.port_info)) - -def _curio_event_run(ble, system): - """ One line function to start the Curio Event loop, + +async def _curio_event_run(ble, system): + """ One line function to start the Curio Event loop, starting all the hubs with the message queue to the bluetooth communcation thread loop. @@ -184,7 +184,7 @@ def _curio_event_run(ble, system): system : Coroutine that the user provided to instantate their system """ - run(_run_all(ble, system), with_monitor=False) + await _run_all(ble, system) def start(user_system_setup_func): #pragma: no cover """ @@ -195,13 +195,23 @@ def start(user_system_setup_func): #pragma: no cover - Initializing the bluetooth interface object - Starting a run loop inside this bluetooth interface for executing the - Curio event loop - - Starting up the user async co-routines inside the Curio event loop + asyncio event loop + - Starting up the user async co-routines inside the asyncio event loop """ - ble = Bleak() - # Run curio in a thread - curry_curio_event_run = partial(_curio_event_run, ble=ble, system=user_system_setup_func) - t = threading.Thread(target=curry_curio_event_run) - t.start() - print('started thread for curio') - ble.run() + async def main(): + ble = Bleak() + + async def user(): + print('Started bricknil main task') + await _curio_event_run(ble, user_system_setup_func) + async def btle(): + print('Started BLE command task') + await ble.asyncio_loop() + user_t = spawn(user()) + btle_t = spawn(btle()) + await user_t + await btle_t + loop = get_event_loop() + loop.run_until_complete(main()) + + diff --git a/bricknil/hub.py b/bricknil/hub.py index 33ebeed..be1ea64 100644 --- a/bricknil/hub.py +++ b/bricknil/hub.py @@ -1,11 +1,11 @@ -# Copyright 2019 Virantha N. Ekanayake -# +# Copyright 2019 Virantha N. Ekanayake +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,7 +16,7 @@ """ import uuid -from curio import sleep, UniversalQueue, CancelledError +from asyncio import sleep, Queue, CancelledError from .process import Process from .sensor.peripheral import Peripheral # for type check from .sockets import WebMessage @@ -34,10 +34,10 @@ class Hub(Process): ble_id (str) : BluetoothLE network(MAC) adddress to connect to (None if you want to connect to the first matching hub) Attributes: - + hubs (list [`Hub`]) : Class attr to keep track of all Hub (and subclasses) instances - message_queue (`curio.Queue`) : Outgoing message queue to :class:`bricknil.ble_queue.BLEventQ` - peripheral_queue (`curio.UniversalQueue`) : Incoming messages from :class:`bricknil.ble_queue.BLEventQ` + message_queue (`asyncio.Queue`) : Outgoing message queue to :class:`bricknil.ble_queue.BLEventQ` + peripheral_queue (`asyncio.Queue`) : Incoming messages from :class:`bricknil.ble_queue.BLEventQ` uart_uuid (`uuid.UUID`) : UUID broadcast by LEGO UARTs char_uuid (`uuid.UUID`) : Lego uses only one service characteristic for communicating with the UART services tx : Service characteristic for tx/rx messages that's set by :func:`bricknil.ble_queue.BLEventQ.connect` @@ -60,7 +60,7 @@ def __init__(self, name, query_port_info=False, ble_id=None): self.peripherals = {} # attach_sensor method will add sensors to this self.port_to_peripheral = {} # Quick mapping from a port number to a peripheral object # Only gets populated once the peripheral attaches itself physically - self.peripheral_queue = UniversalQueue() # Incoming messages from peripherals + self.peripheral_queue = Queue() # Incoming messages from peripherals # Keep track of port info as we get messages from the hub ('update_port' messages) self.port_info = {} @@ -92,7 +92,7 @@ async def send_message(self, msg_name, msg_bytes, peripheral=None): async def peripheral_message_loop(self): """The main loop that receives messages from the :class:`bricknil.messages.Message` parser. - Waits for messages on a UniversalQueue and dispatches to the appropriate peripheral handler. + Waits for messages on a Queue and dispatches to the appropriate peripheral handler. """ try: self.message_debug(f'starting peripheral message loop') @@ -103,7 +103,6 @@ async def peripheral_message_loop(self): while True: msg = await self.peripheral_queue.get() msg, data = msg - await self.peripheral_queue.task_done() if msg == 'value_change': port, msg_bytes = data peripheral = self.port_to_peripheral[port] @@ -134,14 +133,14 @@ async def peripheral_message_loop(self): await self._get_port_info(port, msg) else: raise UnknownPeripheralMessage - + except CancelledError: self.message(f'Terminating peripheral') async def connect_peripheral_to_port(self, device_name, port): """Set the port number of the newly attached peripheral - + When the hub gets an Attached I/O message on a new port with the device_name, this method is called to find the peripheral it should set this port to. If the user has manually specified a port, then this function just validates that @@ -159,7 +158,7 @@ async def connect_peripheral_to_port(self, device_name, port): else: raise DifferentPeripheralOnPortError - # This port has not been reserved for a specific peripheral, so let's just + # This port has not been reserved for a specific peripheral, so let's just # search for the first peripheral with a matching name and attach this port to it for peripheral_name, peripheral in self.peripherals.items(): if peripheral.sensor_name == device_name and peripheral.port == None: @@ -185,7 +184,7 @@ def attach_sensor(self, sensor: Peripheral): async def _get_port_info(self, port, msg): """Utility function to query information on available ports and modes from a hub. - + """ if msg == 'port_detected': # Request mode info @@ -208,14 +207,14 @@ async def _get_port_info(self, port, msg): 'PCT Range': 0x02, 'SI Range':0x03, 'Symbol':0x04, 'MAPPING': 0x05, } - # Send a message to requeust each type of info + # Send a message to requeust each type of info for k,v in info_types.items(): b = [0x00, 0x22, port, mode, v] await self.send_message(f'req info({k}) on mode {mode} {port}', b) class PoweredUpHub(Hub): - """PoweredUp Hub class + """PoweredUp Hub class """ def __init__(self, name, query_port_info=False, ble_id=None): super().__init__(name, query_port_info, ble_id) @@ -223,7 +222,7 @@ def __init__(self, name, query_port_info=False, ble_id=None): self.manufacturer_id = 65 class PoweredUpRemote(Hub): - """PoweredUp Remote class + """PoweredUp Remote class """ def __init__(self, name, query_port_info=False, ble_id=None): super().__init__(name, query_port_info, ble_id) diff --git a/bricknil/message_dispatch.py b/bricknil/message_dispatch.py index 0e46b30..cea75c8 100644 --- a/bricknil/message_dispatch.py +++ b/bricknil/message_dispatch.py @@ -1,7 +1,7 @@ """Parse incoming BLE Lego messages from hubs Each hub has one of these objects to control access to the underlying BLE library notification thread. -Communication back into the hub (running in python async-land) is through a :class:`curio.UniversalQueue` +Communication back into the hub is through a :class:`asyncio.Queue` object. Todo: @@ -17,7 +17,7 @@ class MessageDispatch: """Parse messages (bytearray) Once the :meth:`parse` method is called, the message header will be parsed, and based on the msg_type - byte, the processing of the message body will be dispatched to the `parse` method of the matching Message body parser. + byte, the processing of the message body will be dispatched to the `parse` method of the matching Message body parser. Message body parsers are subclasses of :class:`bricknil.messages.Message`, and will call back to the `message*` methods below. This object will then send a message to the connected :class:`bricknil.hub.Hub` object. @@ -33,7 +33,7 @@ def __init__(self, hub): """ self.hub = hub self.port_info = {} - + def parse(self, msg:bytearray): """Parse the header of the message and dispatch message body processing @@ -64,25 +64,25 @@ def _parse_msg_bytes(self, msg_bytes): def message_update_value_to_peripheral(self, port, value): """Called whenever a peripheral on the hub reports a change in its sensed value """ - self.hub.peripheral_queue.put( ('value_change', (port, value)) ) + self.hub.peripheral_queue.put_nowait( ('value_change', (port, value)) ) def message_port_info_to_peripheral(self, port, message): """Called whenever a peripheral needs to update its meta-data """ - self.hub.peripheral_queue.put( ('update_port', (port, self.port_info[port])) ) - self.hub.peripheral_queue.put( (message, port) ) + self.hub.peripheral_queue.put_nowait( ('update_port', (port, self.port_info[port])) ) + self.hub.peripheral_queue.put_nowait( (message, port) ) def message_attach_to_hub(self, device_name, port): """Called whenever a peripheral is attached to the hub """ # Now, we should activate updates from this sensor - self.hub.peripheral_queue.put( ('attach', (port, device_name)) ) + self.hub.peripheral_queue.put_nowait( ('attach', (port, device_name)) ) # Send a message to update the information on this port - self.hub.peripheral_queue.put( ('update_port', (port, self.port_info[port])) ) + self.hub.peripheral_queue.put_nowait( ('update_port', (port, self.port_info[port])) ) # Send a message saying this port is detected, in case the hub # wants to query for more properties. (Since an attach message # doesn't do anything if the user hasn't @attach'ed a peripheral to it) - self.hub.peripheral_queue.put( ('port_detected', port)) + self.hub.peripheral_queue.put_nowait( ('port_detected', port)) diff --git a/bricknil/sensor/light.py b/bricknil/sensor/light.py index 799b45c..341c8f0 100644 --- a/bricknil/sensor/light.py +++ b/bricknil/sensor/light.py @@ -1,11 +1,11 @@ -# Copyright 2019 Virantha N. Ekanayake -# +# Copyright 2019 Virantha N. Ekanayake +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -13,7 +13,7 @@ # limitations under the License. """All LED/light output devices""" -from curio import sleep, current_task, spawn # Needed for motor speed ramp +from asyncio import sleep, current_task, create_task as spawn # Needed for motor speed ramp from enum import Enum from struct import pack diff --git a/bricknil/sensor/motor.py b/bricknil/sensor/motor.py index c52a922..efa1e0e 100644 --- a/bricknil/sensor/motor.py +++ b/bricknil/sensor/motor.py @@ -1,11 +1,11 @@ -# Copyright 2019 Virantha N. Ekanayake -# +# Copyright 2019 Virantha N. Ekanayake +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -13,7 +13,7 @@ # limitations under the License. """All motor related peripherals including base motor classes""" -from curio import sleep, current_task, spawn # Needed for motor speed ramp +from asyncio import sleep, current_task, create_task as spawn # Needed for motor speed ramp from enum import Enum from struct import pack @@ -32,7 +32,7 @@ def __init__(self, name, port=None, capabilities=[]): async def set_speed(self, speed): """ Validate and set the train speed - If there is an in-progress ramp, and this command is not part of that ramp, + If there is an in-progress ramp, and this command is not part of that ramp, then cancel that in-progress ramp first, before issuing this set_speed command. Args: @@ -44,11 +44,11 @@ async def set_speed(self, speed): self.speed = speed self.message_info(f'Setting speed to {speed}') await self.set_output(0, self._convert_speed_to_val(speed)) - + async def _cancel_existing_differet_ramp(self): """Cancel the existing speed ramp if it was from a different task - Remember that speed ramps must be a task with daemon=True, so there is no + Remember that speed ramps must be a task with daemon=True, so there is no one awaiting its future. """ # Check if there's a ramp task in progress @@ -56,7 +56,7 @@ async def _cancel_existing_differet_ramp(self): # Check if it's this current task or not current = await current_task() if current != self.ramp_in_progress_task: - # We're trying to set the speed + # We're trying to set the speed # outside a previously in-progress ramp, so cancel the previous ramp await self.ramp_in_progress_task.cancel() self.ramp_in_progress_task = None @@ -67,7 +67,7 @@ async def ramp_speed(self, target_speed, ramp_time_ms): """Ramp the speed by 10 units in the time given in milliseconds """ - TIME_STEP_MS = 100 + TIME_STEP_MS = 100 await self._cancel_existing_differet_ramp() assert ramp_time_ms > 100, f'Ramp speed time must be greater than 100ms ({ramp_time_ms}ms used)' @@ -85,8 +85,8 @@ async def _ramp_speed(): while current_step < number_of_steps: next_speed = int(start_speed + current_step*speed_step) self.message_debug(f'Setting next_speed: {next_speed}') - current_step +=1 - if current_step == number_of_steps: + current_step +=1 + if current_step == number_of_steps: next_speed = target_speed await self.set_speed(next_speed) await sleep(TIME_STEP_MS/1000) @@ -100,7 +100,7 @@ class TachoMotor(Motor): capability = Enum("capability", {"sense_speed":1, "sense_pos":2}) - datasets = { + datasets = { capability.sense_speed: (1, 1), capability.sense_pos: (1, 4), } @@ -115,7 +115,7 @@ async def set_pos(self, pos, speed=50, max_power=50): """Set the absolute position of the motor Everytime the hub is powered up, the zero-angle reference will be reset to the - motor's current position. When you issue this command, the motor will rotate to + motor's current position. When you issue this command, the motor will rotate to the position given in degrees. The sign of the pos tells you which direction to rotate: (1) a positive number will rotate clockwise as looking from end of shaft towards the motor, (2) a negative number will rotate counter-clockwise @@ -132,7 +132,7 @@ async def set_pos(self, pos, speed=50, max_power=50): speed (int) : Absolute value from 0-100 max_power (int): Max percentage power that will be applied (0-100%) - Notes: + Notes: Use command GotoAbsolutePosition * 0x00 = hub id @@ -168,7 +168,7 @@ async def rotate(self, degrees, speed, max_power=50): speed (int) : -100 to 100 max_power (int): Max percentage power that will be applied (0-100%) - Notes: + Notes: Use command StartSpeedForDegrees * 0x00 = hub id @@ -195,7 +195,7 @@ async def ramp_speed2(self, target_speed, ramp_time_ms): # pragma: no cover """ # Set acceleration profile delta_speed = target_speed - self.speed - zero_100_ramp_time_ms = int(ramp_time_ms/delta_speed * 100.0) + zero_100_ramp_time_ms = int(ramp_time_ms/delta_speed * 100.0) zero_100_ramp_time_ms = zero_100_ramp_time_ms % 10000 # limit time to 10s hi = (zero_100_ramp_time_ms >> 8) & 255 @@ -225,7 +225,7 @@ class InternalMotor(TachoMotor): # Any speed command will cause both motors to rotate at the same speed @attach(InternalMotor, name='motors', port=InternalMotor.Port.AB) - # Report back when motor speed changes. You must have a motor_change method defined + # Report back when motor speed changes. You must have a motor_change method defined @attach(InternalMotor, name='motor', port=InternalMotor.Port.A, capabilities=['sense_speed']) # Only report back when speed change exceeds 5 units @@ -256,8 +256,8 @@ def __init__(self, name, port=None, capabilities=[]): port = port_map[port.value] self.speed = 0 super().__init__(name, port, capabilities) - - + + class ExternalMotor(TachoMotor): """ Access the stand-alone Boost motors @@ -271,7 +271,7 @@ class ExternalMotor(TachoMotor): # Basic connection to the motor on Port A @attach(ExternalMotor, name='motor') - # Report back when motor speed changes. You must have a motor_change method defined + # Report back when motor speed changes. You must have a motor_change method defined @attach(ExternalMotor, name='motor', capabilities=['sense_speed']) # Only report back when speed change exceeds 5 units, and position changes (degrees) @@ -306,7 +306,7 @@ class CPlusLargeMotor(TachoMotor): # Basic connection to the motor on Port A @attach(CPlusLargeMotor, name='motor') - # Report back when motor speed changes. You must have a motor_change method defined + # Report back when motor speed changes. You must have a motor_change method defined @attach(CPlusLargeMotor, name='motor', capabilities=['sense_speed']) # Only report back when speed change exceeds 5 units, and position changes (degrees) @@ -341,7 +341,7 @@ class CPlusXLMotor(TachoMotor): # Basic connection to the motor on Port A @attach(CPlusXLMotor, name='motor') - # Report back when motor speed changes. You must have a motor_change method defined + # Report back when motor speed changes. You must have a motor_change method defined @attach(CPlusXLMotor, name='motor', capabilities=['sense_speed']) # Only report back when speed change exceeds 5 units, and position changes (degrees) @@ -413,7 +413,7 @@ class WedoMotor(Motor): class DuploTrainMotor(Motor): """Train Motor on Duplo Trains - Make sure that the train is sitting on the ground (the front wheels need to keep rotating) in + Make sure that the train is sitting on the ground (the front wheels need to keep rotating) in order to keep the train motor powered. If you pick up the train, the motor will stop operating withina few seconds. diff --git a/bricknil/sensor/peripheral.py b/bricknil/sensor/peripheral.py index 9ba0dc5..bf9f258 100644 --- a/bricknil/sensor/peripheral.py +++ b/bricknil/sensor/peripheral.py @@ -1,11 +1,11 @@ -# Copyright 2019 Virantha N. Ekanayake -# +# Copyright 2019 Virantha N. Ekanayake +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,7 +20,7 @@ from collections import namedtuple from ..process import Process -from curio import sleep, spawn, current_task +from asyncio import sleep, current_task, create_task as spawn from ..const import DEVICES @@ -42,15 +42,15 @@ class Peripheral(Process): * For each mode/capability, send a byte like the following: * Upper 4-bits is mode number * Lower 4-bits is the dataset number - * For example, for getting RGB values, it's mode 6, and we want all three datasets - (for each color), so we'd add three bytes [0x60, 0x61, 0x62]. + * For example, for getting RGB values, it's mode 6, and we want all three datasets + (for each color), so we'd add three bytes [0x60, 0x61, 0x62]. If you just wanted the Red value, you just append [0x60] * Send a [0x42, port, 0x03] message to unlock the port * Now, when the sensor sends back values, it uses 0x46 messages with the following byte sequence: * Port id * 16-bit entry where the true bits mark which mode has values included in this message (So 0x00 0x05 means values from Modes 2 and 0) - * Then the set of values from the sensor, which are ordered by Mode number + * Then the set of values from the sensor, which are ordered by Mode number (so the sensor reading from mode 0 would come before the reading from mode 2) * Each set of values includes however many bytes are needed to represent each dataset (for example, up to 3 for RGB colors), and the byte-width of each value (4 bytes for a 32-bit int) @@ -63,20 +63,20 @@ class Peripheral(Process): capabilities : can be input in the following formats (where the number in the tuple can be a threshold to trigger updates) - * ['sense_color', 'sense_distannce'] + * ['sense_color', 'sense_distannce'] * [capability.sense_color, capability.sense_distance] * [('sense_color', 1), ('sense_distance', 2)] name (str) : Human readable name port (int) : Port to connect to (otherwise will connect to first matching peripheral with defined sensor_id) - + Attributes: port (int) : Physical port on the hub this Peripheral attaches to sensor_name (str) : Name coming out of `const.DEVICES` value (dict) : Sensor readings get dumped into this dict message_handler (func) : Outgoing message queue to `BLEventQ` that's set by the Hub when an attach message is seen - capabilites (list [ `capability` ]) : Support capabilities + capabilites (list [ `capability` ]) : Support capabilities thresholds (list [ int ]) : Integer list of thresholds for updates for each of the sensing capabilities """ @@ -96,7 +96,7 @@ def _get_validated_capabilities(self, caps): """Convert capabilities in different formats (string, tuple, etc) Returns: - + validated_caps, thresholds (list[`capability`], list[int]): list of capabilities and list of associated thresholds """ validated_caps = [] @@ -144,7 +144,7 @@ def _convert_bytes(self, msg_bytes:bytearray, byte_count): async def _parse_combined_sensor_values(self, msg: bytearray): """ Byte sequence is as follows: - # uint16 where each set bit indicates data value from that mode is present + # uint16 where each set bit indicates data value from that mode is present (e.g. 0x00 0x05 means Mode 2 and Mode 0 data is present # The data from the lowest Mode number comes first in the subsequent bytes # Each Mode has a number of datasets associated with it (RGB for example is 3 datasets), and @@ -158,7 +158,7 @@ async def _parse_combined_sensor_values(self, msg: bytearray): Side-effects: self.value - + """ msg.pop(0) # Remove the leading 0 (since we never have more than 7 datasets even with all the combo modes activated # The next byte is a bit mask of the mode/dataset entries present in this value @@ -200,7 +200,7 @@ def _convert_speed_to_val(self, speed): """ if speed == 127: return 127 if speed > 100: speed = 100 - if speed < 0: + if speed < 0: # Now, truncate to 8-bits speed = speed & 255 # Or I guess I could do 256-abs(s) return speed @@ -208,7 +208,7 @@ def _convert_speed_to_val(self, speed): async def set_output(self, mode, value): """Don't change this unless you're changing the way you do a Port Output command - + Outputs the following sequence to the sensor * 0x00 = hub id from common header * 0x81 = Port Output Command @@ -261,15 +261,15 @@ async def activate_updates(self): this call from :func:`bricknil.hub.Hub.peripheral_message_loop` See class description for explanation on how Combined Mode updates are done. - + Returns: None """ - + assert self.port is not None, f"Cannot activate updates on sensor before it's been attached to {self.name}!" - - if len(self.capabilities) == 0: + + if len(self.capabilities) == 0: # Nothing to do since no capabilities defined return @@ -280,7 +280,7 @@ async def activate_updates(self): if len(self.capabilities)==1: # Just a normal single sensor mode = self.capabilities[0].value b = [0x00, 0x41, self.port, mode, self.thresholds[0], 0, 0, 0, 1] - await self.send_message(f'Activate SENSOR: port {self.port}', b) + await self.send_message(f'Activate SENSOR: port {self.port}', b) else: # Combo mode. Need to make sure only allowed combinations are preset # Lock sensor diff --git a/bricknil/sensor/sensor.py b/bricknil/sensor/sensor.py index 76f825e..b93dd83 100644 --- a/bricknil/sensor/sensor.py +++ b/bricknil/sensor/sensor.py @@ -1,11 +1,11 @@ -# Copyright 2019 Virantha N. Ekanayake -# +# Copyright 2019 Virantha N. Ekanayake +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -14,7 +14,7 @@ """Actual sensor and motor peripheral definitions from Boost and PoweredUp """ -from curio import sleep, current_task, spawn # Needed for motor speed ramp +from asyncio import sleep, current_task, create_task as spawn # Needed for motor speed ramp from enum import Enum, IntEnum from struct import pack @@ -34,7 +34,7 @@ class VisionSensor(Peripheral): - *sense_ambient*: Distance under one inch (so inverse of the preceeding) - *sense_rgb*: R, G, B values (3 sets of uint16) - Any combination of sense_color, sense_distance, sense_count, sense_reflectivity, + Any combination of sense_color, sense_distance, sense_count, sense_reflectivity, and sense_rgb is supported. Examples:: @@ -51,11 +51,11 @@ class VisionSensor(Peripheral): @attach(VisionSensor, name='vision', capabilities=[('sense_color', 1), ('sense_rgb', 5)]) The values returned by the sensor will always be available in the instance variable - `self.value`. For example, when the `sense_color` and `sense_rgb` capabilities are + `self.value`. For example, when the `sense_color` and `sense_rgb` capabilities are enabled, the following values will be stored and updated:: self.value = { VisionSensor.capability.sense_color: uint8, - VisionSensor.capability.sense_rgb: + VisionSensor.capability.sense_rgb: [ uint16, uint16, uint16 ] } @@ -75,7 +75,7 @@ class VisionSensor(Peripheral): """ _sensor_id = 0x0025 - capability = Enum("capability", + capability = Enum("capability", [('sense_color', 0), ('sense_distance', 1), ('sense_count', 2), @@ -102,7 +102,7 @@ class VisionSensor(Peripheral): class InternalTiltSensor(Peripheral): """ Access the internal tilt sensor in the Boost Move Hub. - + The various modes are: - **sense_angle** - X, Y angles. Both are 0 if hub is lying flat with button up @@ -137,12 +137,12 @@ class InternalTiltSensor(Peripheral): will be stored and updated:: self.value = { InternalTiltSensor.capability.sense_angle: [uint8, uint8], - InternalTiltSensor.capability.sense_orientation: + InternalTiltSensor.capability.sense_orientation: Enum(InternalTiltSensor.orientation) } """ _sensor_id = 0x0028 - capability = Enum("capability", + capability = Enum("capability", [('sense_angle', 0), ('sense_tilt', 1), ('sense_orientation', 2), @@ -152,7 +152,7 @@ class InternalTiltSensor(Peripheral): datasets = { capability.sense_angle: (2, 1), capability.sense_tilt: (1, 1), - capability.sense_orientation: (1, 1), + capability.sense_orientation: (1, 1), capability.sense_impact: (1, 4), capability.sense_acceleration_3_axis: (3, 1), } @@ -164,10 +164,10 @@ class InternalTiltSensor(Peripheral): capability.sense_acceleration_3_axis, ] - orientation = Enum('orientation', + orientation = Enum('orientation', { 'up': 0, - 'right': 1, - 'left': 2, + 'right': 1, + 'left': 2, 'far_side':3, 'near_side':4, 'down':5, @@ -189,7 +189,7 @@ async def update_value(self, msg_bytes): class ExternalMotionSensor(Peripheral): """Access the external motion sensor (IR) provided in the Wedo sets - Measures distance to object, or if an object is moving (distance varying). + Measures distance to object, or if an object is moving (distance varying). - **sense_distance** - distance in inches from 0-10 - **sense_count** - Increments every time it detects motion (32-bit value) @@ -206,12 +206,12 @@ class ExternalMotionSensor(Peripheral): @attach(ExternalMotionSensor, name='motion_sensor', capabilities=['sense_count']) """ _sensor_id = 0x0023 - capability = Enum("capability", + capability = Enum("capability", [('sense_distance', 0), ('sense_count', 1), ]) - datasets = { capability.sense_distance: (1, 1), + datasets = { capability.sense_distance: (1, 1), capability.sense_count: (1, 4), } allowed_combo = [ ] @@ -234,22 +234,22 @@ class ExternalTiltSensor(Peripheral): """ _sensor_id = 0x0022 - capability = Enum("capability", + capability = Enum("capability", [('sense_angle', 0), ('sense_orientation', 1), ('sense_impact', 2), ]) - datasets = { capability.sense_angle: (2, 1), + datasets = { capability.sense_angle: (2, 1), capability.sense_orientation: (1, 1), capability.sense_impact: (3, 1), } allowed_combo = [ ] - orientation = Enum('orientation', + orientation = Enum('orientation', { 'up': 0, - 'right': 7, - 'left': 5, + 'right': 7, + 'left': 5, 'far_side':3, 'near_side':9, }) @@ -286,7 +286,7 @@ class RemoteButtons(Peripheral): There are actually a few different modes that the hardware supports, but we are only going to use one of them called 'KEYSD' (see the notes in the documentation on the raw values reported by the hub). This mode makes the remote send three values back - in a list. To access each button state, there are three helper methods provided + in a list. To access each button state, there are three helper methods provided (see below) Examples:: @@ -335,7 +335,7 @@ def red_pressed(self): class Button(Peripheral): """ Register to be notified of button presses on the Hub (Boost or PoweredUp) - This is actually a slight hack, since the Hub button is not a peripheral that is + This is actually a slight hack, since the Hub button is not a peripheral that is attached like other sensors in the Lego protocol. Instead, the buttons are accessed through Hub property messages. We abstract away these special messages to make the button appear to be like any other peripheral sensor. @@ -373,7 +373,7 @@ async def activate_updates(self): self.value[cap] = [None]*self.datasets[cap][0] b = [0x00, 0x01, 0x02, 0x02] # Button reports from "Hub Properties Message Type" - await self.send_message(f'Activate button reports: port {self.port}', b) + await self.send_message(f'Activate button reports: port {self.port}', b) class DuploVisionSensor(Peripheral): @@ -384,7 +384,7 @@ class DuploVisionSensor(Peripheral): - *sense_reflectivity*: Under distances of one inch, the inverse of the distance - *sense_rgb*: R, G, B values (3 sets of uint16) - Any combination of sense_color, sense_ctag, sense_reflectivity, + Any combination of sense_color, sense_ctag, sense_reflectivity, and sense_rgb is supported. Examples:: @@ -401,11 +401,11 @@ class DuploVisionSensor(Peripheral): @attach(DuploVisionSensor, name='vision', capabilities=[('sense_color', 1), ('sense_rgb', 5)]) The values returned by the sensor will always be available in the instance variable - `self.value`. For example, when the `sense_color` and `sense_rgb` capabilities are + `self.value`. For example, when the `sense_color` and `sense_rgb` capabilities are enabled, the following values will be stored and updated:: self.value = { DuploVisionSensor.capability.sense_color: uint8, - DuploVisionSensor.capability.sense_rgb: + DuploVisionSensor.capability.sense_rgb: [ uint16, uint16, uint16 ] } @@ -418,7 +418,7 @@ class DuploVisionSensor(Peripheral): - 3 = RGB I """ _sensor_id = 0x002B - capability = Enum("capability", + capability = Enum("capability", [('sense_color', 0), ('sense_ctag', 1), ('sense_reflectivity', 2), @@ -455,7 +455,7 @@ class VoltageSensor(Peripheral): capability = Enum("capability", {'sense_s': 0, 'sense_l': 1}) datasets = {capability.sense_s: (1, 2), # 2-bytes (16-bit) - capability.sense_l: (1, 2), + capability.sense_l: (1, 2), } allowed_combo = [ ] @@ -477,7 +477,7 @@ class CurrentSensor(Peripheral): capability = Enum("capability", {'sense_s': 0, 'sense_l': 1}) datasets = {capability.sense_s: (1, 2), # 2-bytes (16-bit) - capability.sense_l: (1, 2), + capability.sense_l: (1, 2), } allowed_combo = [ ] @@ -489,7 +489,7 @@ class DuploSpeedSensor(Peripheral): - *sense_speed*: Returns the speed of the front wheels - *sense_count*: Keeps count of the number of revolutions the front wheels have spun - Either or both can be enabled for measurement. + Either or both can be enabled for measurement. Examples:: @@ -503,7 +503,7 @@ class DuploSpeedSensor(Peripheral): current speed by:: speed = self.speed_sensor.value - + For the second example, the two values will be in a dict:: speed = self.speed_sensor.value[DuploSpeedSensor.sense_speed] @@ -511,7 +511,7 @@ class DuploSpeedSensor(Peripheral): """ _sensor_id = 0x002C - capability = Enum("capability", + capability = Enum("capability", [('sense_speed', 0), ('sense_count', 1), ]) diff --git a/bricknil/sensor/sound.py b/bricknil/sensor/sound.py index 2320cdc..ed276e8 100644 --- a/bricknil/sensor/sound.py +++ b/bricknil/sensor/sound.py @@ -1,12 +1,12 @@ -# Copyright 2019 Virantha N. Ekanayake -# +# Copyright 2019 Virantha N. Ekanayake +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -14,7 +14,7 @@ # limitations under the License. """All sound related output devices """ -from curio import sleep, current_task, spawn # Needed for motor speed ramp +from asyncio import sleep, current_task, create_task as spawn # Needed for motor speed ramp from enum import Enum, IntEnum from struct import pack @@ -31,7 +31,7 @@ class DuploSpeaker(Peripheral): @attach(DuploSpeaker, name='speaker') ... await self.speaker.play_sound(DuploSpeaker.sounds.brake) - + Notes: Uses Mode 1 to play the presets diff --git a/bricknil/sockets.py b/bricknil/sockets.py index f051840..b03c3ca 100644 --- a/bricknil/sockets.py +++ b/bricknil/sockets.py @@ -1,12 +1,12 @@ -# Copyright 2019 Virantha N. Ekanayake -# +# Copyright 2019 Virantha N. Ekanayake +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,7 +16,7 @@ import logging import json -from curio import run, spawn, sleep, Queue, tcp_server +from asyncio import run, create_task as spawn, sleep, Queue logger = logging.getLogger(str(__name__)) #async def socket_server(web_out_queue, address): @@ -42,15 +42,13 @@ async def web_client_connected(client, addr): wc = WebClient(client, addr, web_out_queue) await wc.run() - task = await spawn(tcp_server, '', 25000, web_client_connected, daemon=True) - class WebClient: #pragma: no cover """ Represents a client that has connected to BrickNil's server - Each client has a connection to the global BrickNil `curio.Queue` + Each client has a connection to the global BrickNil `asyncio.Queue` that handles broadcast messages about peripherals. Peripherals - insert the messages into the queue, and clients can read from + insert the messages into the queue, and clients can read from it (hence why it's called in_queue in this class). """ def __init__(self, client, addr, in_queue): @@ -59,7 +57,7 @@ def __init__(self, client, addr, in_queue): self.client = client self.addr = addr logger.info(f'Web client {client} connected from {addr}') - + async def run(self): @@ -77,7 +75,7 @@ class WebMessage: def __init__(self, hub): self.hub = hub - + async def send(self, peripheral, msg): obj = { 'hub': self.hub.name, 'peripheral_type': peripheral.__class__.__name__, diff --git a/docs/images/run_loops.svg b/docs/images/run_loops.svg deleted file mode 100644 index 6418322..0000000 --- a/docs/images/run_loops.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - Produced by OmniGraffle 6.6.1 2019-02-21 03:41:57 +0000Canvas 1Layer 1Bluetooth Library Run LoopCurio Async Event LoopBluebrick diff --git a/examples/boost_motor_position.py b/examples/boost_motor_position.py index dc11dc0..ba8e96b 100644 --- a/examples/boost_motor_position.py +++ b/examples/boost_motor_position.py @@ -1,6 +1,6 @@ import logging -from curio import sleep, Queue +from asyncio import sleep, Queue from bricknil import attach, start from bricknil.hub import PoweredUpRemote, BoostHub from bricknil.sensor import InternalMotor, RemoteButtons, LED, Button, ExternalMotor diff --git a/examples/boost_wedo_sensors.py b/examples/boost_wedo_sensors.py index 281d2be..fc00902 100644 --- a/examples/boost_wedo_sensors.py +++ b/examples/boost_wedo_sensors.py @@ -1,6 +1,6 @@ import logging, struct -from curio import sleep, Queue +from asyncio import sleep, Queue from bricknil import attach, start from bricknil.hub import BoostHub from bricknil.sensor import LED, ExternalMotionSensor, ExternalTiltSensor, WedoMotor diff --git a/examples/duplo_train.py b/examples/duplo_train.py index e715790..9c89293 100644 --- a/examples/duplo_train.py +++ b/examples/duplo_train.py @@ -1,6 +1,6 @@ from itertools import cycle -from curio import sleep +from asyncio import sleep from bricknil import attach, start from bricknil.hub import DuploTrainHub from bricknil.sensor import DuploTrainMotor, DuploSpeedSensor, LED, DuploVisionSensor, DuploSpeaker, Button, VoltageSensor diff --git a/examples/list_ports_boost_hub.py b/examples/list_ports_boost_hub.py index 668f57a..e08adb5 100644 --- a/examples/list_ports_boost_hub.py +++ b/examples/list_ports_boost_hub.py @@ -1,6 +1,6 @@ import logging from itertools import cycle -from curio import sleep +from asyncio import sleep from bricknil import attach, start from bricknil.hub import BoostHub from bricknil.sensor import TrainMotor, VisionSensor, Button, LED, InternalTiltSensor, InternalMotor diff --git a/examples/list_ports_duplo_hub.py b/examples/list_ports_duplo_hub.py index 9a85c41..8e114af 100644 --- a/examples/list_ports_duplo_hub.py +++ b/examples/list_ports_duplo_hub.py @@ -1,5 +1,5 @@ from itertools import cycle -from curio import sleep +from asyncio import sleep from bricknil import attach, start from bricknil.hub import DuploTrainHub from bricknil.sensor import DuploTrainMotor diff --git a/examples/list_ports_pup_hub.py b/examples/list_ports_pup_hub.py index 5b656cc..a78ee24 100644 --- a/examples/list_ports_pup_hub.py +++ b/examples/list_ports_pup_hub.py @@ -1,5 +1,5 @@ from itertools import cycle -from curio import sleep +from asyncio import sleep from bricknil import attach, start from bricknil.hub import PoweredUpHub from bricknil.sensor import TrainMotor, VisionSensor, Button, LED, InternalTiltSensor, InternalMotor diff --git a/examples/list_ports_pup_remote.py b/examples/list_ports_pup_remote.py index 6b92219..e4cc09f 100644 --- a/examples/list_ports_pup_remote.py +++ b/examples/list_ports_pup_remote.py @@ -1,6 +1,6 @@ import logging from itertools import cycle -from curio import sleep +from asyncio import sleep from bricknil import attach, start from bricknil.hub import PoweredUpRemote from bricknil.sensor import TrainMotor, VisionSensor, Button, LED, InternalTiltSensor, InternalMotor, RemoteButtons diff --git a/examples/technic_4x4.py b/examples/technic_4x4.py index b81ff97..453e161 100755 --- a/examples/technic_4x4.py +++ b/examples/technic_4x4.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import logging -from curio import sleep +from asyncio import sleep from bricknil import attach, start from bricknil.hub import CPlusHub from bricknil.sensor.motor import CPlusXLMotor diff --git a/examples/train_all.py b/examples/train_all.py index 73e3727..3af66c2 100644 --- a/examples/train_all.py +++ b/examples/train_all.py @@ -1,6 +1,6 @@ import logging from itertools import cycle -from curio import sleep +from asyncio import sleep from bricknil import attach, start from bricknil.hub import PoweredUpHub from bricknil.sensor import TrainMotor, VisionSensor, Button, LED diff --git a/examples/train_color.py b/examples/train_color.py index 5bbf895..c957147 100644 --- a/examples/train_color.py +++ b/examples/train_color.py @@ -1,6 +1,6 @@ import logging from itertools import cycle -from curio import sleep +from asyncio import sleep from bricknil import attach, start from bricknil.hub import PoweredUpHub from bricknil.sensor import TrainMotor, VisionSensor, Button, LED diff --git a/examples/train_distance_sensor.py b/examples/train_distance_sensor.py index 49b58cf..7bc7151 100644 --- a/examples/train_distance_sensor.py +++ b/examples/train_distance_sensor.py @@ -1,5 +1,5 @@ import logging -from curio import sleep +from asyncio import sleep from bricknil import attach, start from bricknil.hub import PoweredUpHub from bricknil.sensor import TrainMotor, VisionSensor diff --git a/examples/train_iv.py b/examples/train_iv.py index de43dc7..18aa164 100644 --- a/examples/train_iv.py +++ b/examples/train_iv.py @@ -1,6 +1,6 @@ import logging from itertools import cycle -from curio import sleep +from asyncio import sleep from bricknil import attach, start from bricknil.hub import PoweredUpHub from bricknil.sensor import TrainMotor, VisionSensor, Button, LED, VoltageSensor, CurrentSensor diff --git a/examples/train_light.py b/examples/train_light.py index 5386765..16b4591 100644 --- a/examples/train_light.py +++ b/examples/train_light.py @@ -1,5 +1,5 @@ import logging -from curio import sleep +from asyncio import sleep from bricknil import attach, start from bricknil.hub import PoweredUpHub from bricknil.sensor import Light diff --git a/examples/train_ramp.py b/examples/train_ramp.py index d47ebc6..e62cfa6 100644 --- a/examples/train_ramp.py +++ b/examples/train_ramp.py @@ -1,6 +1,6 @@ import logging -from curio import sleep +from asyncio import sleep from bricknil import attach, start from bricknil.hub import PoweredUpHub from bricknil.sensor import TrainMotor diff --git a/examples/vernie_remote.py b/examples/vernie_remote.py index 8b7aac3..e2344ac 100644 --- a/examples/vernie_remote.py +++ b/examples/vernie_remote.py @@ -1,6 +1,6 @@ import logging -from curio import sleep, Queue +from asyncio import sleep, Queue from bricknil import attach, start from bricknil.hub import PoweredUpRemote, BoostHub from bricknil.sensor import InternalMotor, RemoteButtons, LED, Button diff --git a/requirements-dev.txt b/requirements-dev.txt index 3cb99a5..1831cb8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,5 @@ ansible; sys_platform != "win32" pyobjc; sys_platform == "darwin" -curio bleak fabric sphinx diff --git a/requirements.txt b/requirements.txt index 21de995..bdd60a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ pyyaml -curio bleak diff --git a/test/test_hub.py b/test/test_hub.py index 5049900..947aa88 100644 --- a/test/test_hub.py +++ b/test/test_hub.py @@ -3,7 +3,7 @@ from functools import partial import logging, threading from asyncio import coroutine -from curio import kernel, sleep, spawn, Event +from asyncio import kernel, sleep, spawn, Event import time from mock import Mock diff --git a/test/test_peripheral.py b/test/test_peripheral.py index 0df8d3b..7f1e80a 100644 --- a/test/test_peripheral.py +++ b/test/test_peripheral.py @@ -2,7 +2,7 @@ import os, struct, copy import logging from asyncio import coroutine -from curio import kernel, sleep +from asyncio import kernel, sleep from mock import Mock from mock import patch, call diff --git a/tox.ini b/tox.ini index 424b5f3..fb25d61 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,6 @@ deps= hypothesis mock coverage - curio pyyaml bleak commands=pytest From dd752e486be2b7561897052d18f1a6669da172d1 Mon Sep 17 00:00:00 2001 From: Jan Vrany Date: Tue, 7 Jan 2020 14:31:10 +0000 Subject: [PATCH 3/4] Remove BLEAK interface class `Bleak` Since both the hub and BLEAK is running within the same asyncio loop, we can call BLEAK APIs directly within hub control flow. --- bricknil/ble_queue.py | 56 ++++++++++++------------ bricknil/bleak_interface.py | 85 ------------------------------------- bricknil/bricknil.py | 49 ++++----------------- 3 files changed, 37 insertions(+), 153 deletions(-) delete mode 100644 bricknil/bleak_interface.py diff --git a/bricknil/ble_queue.py b/bricknil/ble_queue.py index 20680ac..a46a692 100644 --- a/bricknil/ble_queue.py +++ b/bricknil/ble_queue.py @@ -1,11 +1,11 @@ -# Copyright 2019 Virantha N. Ekanayake -# +# Copyright 2019 Virantha N. Ekanayake +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -13,7 +13,7 @@ # limitations under the License. from asyncio import Queue, sleep, CancelledError -import sys, functools, uuid +import sys, functools, uuid, bleak from .sensor import Button # Hack! only to get the button sensor_id for the fake attach message from .process import Process @@ -31,14 +31,14 @@ class BLEventQ(Process): """ - def __init__(self, ble): + def __init__(self): super().__init__('BLE Event Q') - self.ble = ble self.q = Queue() self.adapter = None # User needs to make sure adapter is powered up and on # sudo hciconfig hci0 up self.hubs = {} + self.devices = [] async def run(self): try: @@ -49,8 +49,14 @@ async def run(self): self.message_debug(f'Got msg: {msg_type} = {msg_val}') await self.send_message(hub.tx, msg_val) except CancelledError: + await self.disconnect() + + async def disconnect(self): + if len(self.devices) > 0: self.message(f'Terminating and disconnecting') - await self.ble.in_queue.put( 'quit' ) + for device in self.devices: + await device.disconnect() + self.devices = [] async def send_message(self, characteristic, msg): """Prepends a byte with the length of the msg and writes it to @@ -64,7 +70,7 @@ async def send_message(self, characteristic, msg): length = len(msg)+1 values = bytearray([length]+msg) device, char_uuid = characteristic - await self.ble.in_queue.put( ('tx', (device, char_uuid, values)) ) + await device.write_gatt_char(char_uuid, values) async def get_messages(self, hub): """Instance a Message object to parse incoming messages and setup @@ -83,14 +89,14 @@ def bleak_received(sender, data): self.message_debug('{0} Received: {1}'.format(hub.name, msg)) device, char_uuid = hub.tx - await self.ble.in_queue.put( ('notify', (device, char_uuid, bleak_received) )) + await device.start_notify(char_uuid, bleak_received) def _check_devices_for(self, devices, name, manufacturer_id, address): """Check if any of the devices match what we're looking for - + First, check to make sure the manufacturer_id matches. If the - manufacturer_id is not present in the BLE advertised data from the + manufacturer_id is not present in the BLE advertised data from the device, then fall back to the name (although this is unreliable because the name on the device can be changed by the user through the LEGO apps). @@ -131,8 +137,9 @@ async def _ble_connect(self, uart_uuid, ble_name, ble_manufacturer_id, ble_id=No found = False while not found and timeout > 0: - await self.ble.in_queue.put('discover') # Tell bleak to start discovery - devices = await self.ble.out_queue.get() # Wai# await self.ble.out_queue.task_done() + print('Awaiting on bleak discover') + devices = await bleak.discover(timeout=1) + print('Done Awaiting on bleak discover') # Filter out no-matching uuid devices = [d for d in devices if str(uart_uuid) in d.metadata['uuids']] # Now, extract the manufacturer_id @@ -155,16 +162,7 @@ async def _ble_connect(self, uart_uuid, ble_name, ble_manufacturer_id, ble_id=No async def connect(self, hub): - """ - We probably need a different ble_queue type per operating system, - and try to abstract away some of these hacks. - - Todo: - * This needs to be cleaned up to get rid of all the hacks for - different OS and libraries - - """ - # Connect the messaging queue for communication between self and the h + # Connect the messaging queue for communication between self and the hub hub.message_queue = self.q self.message(f'Starting scan for UART {hub.uart_uuid}') @@ -172,7 +170,7 @@ async def connect(self, hub): try: ble_id = uuid.UUID(hub.ble_id) if hub.ble_id else None except ValueError: - # In case the user passed in a + # In case the user passed in a self.message_info(f"ble_id {hub.ble_id} is not a parseable UUID, so assuming it's a BLE network addresss") ble_id = hub.ble_id @@ -180,9 +178,11 @@ async def connect(self, hub): self.message(f"found device {self.device.name}") - await self.ble.in_queue.put( ('connect', self.device.address) ) - device = await self.ble.out_queue.get() - + + device = bleak.BleakClient(address=self.device.address) + self.devices.append(device) + await device.connect() + hub.ble_id = self.device.address self.message_info(f'Device advertised: {device.services.characteristics}') hub.tx = (device, hub.char_uuid) diff --git a/bricknil/bleak_interface.py b/bricknil/bleak_interface.py deleted file mode 100644 index e54b39a..0000000 --- a/bricknil/bleak_interface.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright 2019 Virantha N. Ekanayake -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Interface to the BLEAK library for BLE calls - -""" -import asyncio, threading, logging - -import bleak -#from bleak import BleakClient - -class Bleak: - """Interface class between curio loop and asyncio loop running bleak - - This class is basically just a queue interface. It has two queue, - one for incoming messages `in_queue` and one for outgoing messages `out_queue`. - - A loop running in asyncio's event_loop waits for messages on the `in_queue`. - - The `out_queue` is used to respond to "discover" and "connect" messages with the - list of discovered devices and a connected device respectively. All messages - incoming from a device are relayed directly to a call back function, and does - not go through either of these queues. - - - """ - - def __init__(self): - # Need to start an event loop - self.in_queue = asyncio.Queue() # Incoming message queue - self.out_queue = asyncio.Queue() # Outgoing message queue - - self.devices = [] - #self.loop = threading.Thread(target=self.run, daemon=True) - #self.loop.start() - - async def asyncio_loop(self): - - - # Wait for messages on in_queue - done = False - while not done: - msg = await self.in_queue.get() - if isinstance(msg, tuple): - msg, val = msg - if msg == 'discover': - print('Awaiting on bleak discover') - devices = await bleak.discover(timeout=1) - print('Done Awaiting on bleak discover') - await self.out_queue.put(devices) - elif msg == 'connect': - device = bleak.BleakClient(address=val) - self.devices.append(device) - await device.connect() - await self.out_queue.put(device) - elif msg == 'tx': - device, char_uuid, msg_bytes = val - await device.write_gatt_char(char_uuid, msg_bytes) - elif msg == 'notify': - device, char_uuid, msg_handler = val - await device.start_notify(char_uuid, msg_handler) - elif msg =='quit': - print("quitting") - logging.info('quitting') - for device in self.devices: - await device.disconnect() - done = True - print("quit") - else: - print(f'Unknown message to Bleak: {msg}') - - - - diff --git a/bricknil/bricknil.py b/bricknil/bricknil.py index d0ef9f9..8945b1c 100644 --- a/bricknil/bricknil.py +++ b/bricknil/bricknil.py @@ -32,7 +32,6 @@ from .ble_queue import BLEventQ from .hub import PoweredUpHub, BoostHub, Hub from .sockets import bricknil_socket_server -from .bleak_interface import Bleak import threading @@ -102,16 +101,16 @@ def wrapper_f(*args, **kwargs): return o return wrapper_f +async def main(system): + """ + Entry-point coroutine that handles everything. This is to be run run + in bricknil's main loop. - - - -async def _run_all(ble, system): - """Curio run loop + You normally don't need to use this directly, instead use start() """ - print('inside curio run loop') + # Instantiate the Bluetooth LE handler/queue - ble_q = BLEventQ(ble) + ble_q = BLEventQ() # The web client out_going queue web_out_queue = Queue() # Instantiate socket listener @@ -154,7 +153,6 @@ async def _run_all(ble, system): # Start each hub task_run = spawn(hub.run()) hub_tasks.append(task_run) - await task_run # Now wait for the tasks to finish ble_q.message_info(f'Waiting for hubs to end') @@ -162,7 +160,7 @@ async def _run_all(ble, system): for task in hub_tasks: await task ble_q.message_info(f'Hubs end') - + await ble_q.disconnect() for task in hub_peripheral_listen_tasks: task.cancel() task_ble_q.cancel() @@ -172,20 +170,6 @@ async def _run_all(ble, system): if hub.query_port_info: hub.message_info(pprint.pformat(hub.port_info)) - - -async def _curio_event_run(ble, system): - """ One line function to start the Curio Event loop, - starting all the hubs with the message queue to the bluetooth - communcation thread loop. - - Args: - ble : The interface object - system : Coroutine that the user provided to instantate their system - - """ - await _run_all(ble, system) - def start(user_system_setup_func): #pragma: no cover """ Main entry point into running everything. @@ -194,24 +178,9 @@ def start(user_system_setup_func): #pragma: no cover function will take care of the rest. This includes: - Initializing the bluetooth interface object - - Starting a run loop inside this bluetooth interface for executing the - asyncio event loop - Starting up the user async co-routines inside the asyncio event loop """ - async def main(): - ble = Bleak() - - async def user(): - print('Started bricknil main task') - await _curio_event_run(ble, user_system_setup_func) - async def btle(): - print('Started BLE command task') - await ble.asyncio_loop() - user_t = spawn(user()) - btle_t = spawn(btle()) - await user_t - await btle_t loop = get_event_loop() - loop.run_until_complete(main()) + loop.run_until_complete(main(user_system_setup_func)) From 6ed897e5c3a5e299074b8142e2691b71df39983b Mon Sep 17 00:00:00 2001 From: Jan Vrany Date: Sat, 11 Jan 2020 23:14:58 +0000 Subject: [PATCH 4/4] Remove unnecessary queue for sending messages to the hub Since both user-code and Bluetooth handling code runs in the same asyncio loop, we can communicate directly and not through the queue. --- bricknil/ble_queue.py | 14 +------- bricknil/bricknil.py | 4 --- bricknil/hub.py | 78 ++++++++++++++++++++++--------------------- 3 files changed, 41 insertions(+), 55 deletions(-) diff --git a/bricknil/ble_queue.py b/bricknil/ble_queue.py index a46a692..4967124 100644 --- a/bricknil/ble_queue.py +++ b/bricknil/ble_queue.py @@ -33,24 +33,12 @@ class BLEventQ(Process): def __init__(self): super().__init__('BLE Event Q') - self.q = Queue() self.adapter = None # User needs to make sure adapter is powered up and on # sudo hciconfig hci0 up self.hubs = {} self.devices = [] - async def run(self): - try: - while True: - msg = await self.q.get() - msg_type, hub, msg_val = msg - #await self.q.task_done() - self.message_debug(f'Got msg: {msg_type} = {msg_val}') - await self.send_message(hub.tx, msg_val) - except CancelledError: - await self.disconnect() - async def disconnect(self): if len(self.devices) > 0: self.message(f'Terminating and disconnecting') @@ -163,7 +151,7 @@ async def _ble_connect(self, uart_uuid, ble_name, ble_manufacturer_id, ble_id=No async def connect(self, hub): # Connect the messaging queue for communication between self and the hub - hub.message_queue = self.q + hub.ble_handler = self self.message(f'Starting scan for UART {hub.uart_uuid}') # HACK diff --git a/bricknil/bricknil.py b/bricknil/bricknil.py index 8945b1c..c8f196b 100644 --- a/bricknil/bricknil.py +++ b/bricknil/bricknil.py @@ -124,9 +124,6 @@ async def main(system): hub_tasks = [] hub_peripheral_listen_tasks = [] # Need to cancel these at the end - # Run the bluetooth listen queue - task_ble_q = spawn(ble_q.run()) - # Connect all the hubs first before enabling any of them for hub in Hub.hubs: hub.web_queue_out = web_out_queue @@ -163,7 +160,6 @@ async def main(system): await ble_q.disconnect() for task in hub_peripheral_listen_tasks: task.cancel() - task_ble_q.cancel() # Print out the port information in debug mode for hub in Hub.hubs: diff --git a/bricknil/hub.py b/bricknil/hub.py index be1ea64..ae4544f 100644 --- a/bricknil/hub.py +++ b/bricknil/hub.py @@ -36,7 +36,7 @@ class Hub(Process): Attributes: hubs (list [`Hub`]) : Class attr to keep track of all Hub (and subclasses) instances - message_queue (`asyncio.Queue`) : Outgoing message queue to :class:`bricknil.ble_queue.BLEventQ` + ble_handler (`BLEventQ`) : Hub's Bluetooth LE handling object peripheral_queue (`asyncio.Queue`) : Incoming messages from :class:`bricknil.ble_queue.BLEventQ` uart_uuid (`uuid.UUID`) : UUID broadcast by LEGO UARTs char_uuid (`uuid.UUID`) : Lego uses only one service characteristic for communicating with the UART services @@ -52,8 +52,8 @@ class Hub(Process): def __init__(self, name, query_port_info=False, ble_id=None): super().__init__(name) self.ble_id = ble_id + self.ble_handler = None self.query_port_info = query_port_info - self.message_queue = None self.uart_uuid = uuid.UUID('00001623-1212-efde-1623-785feabcd123') self.char_uuid = uuid.UUID('00001624-1212-efde-1623-785feabcd123') self.tx = None @@ -76,19 +76,52 @@ def __init__(self, name, query_port_info=False, ble_id=None): async def send_message(self, msg_name, msg_bytes, peripheral=None): - """Insert a message to the hub into the queue(:func:`bricknil.hub.Hub.message_queue`) connected to our BLE - interface + """Send a message (command) to the hub. """ - while not self.tx: # Need to make sure we have a handle to the uart await sleep(1) - await self.message_queue.put((msg_name, self, msg_bytes)) + await self.ble_handler.send_message(self.tx, msg_bytes) if self.web_queue_out and peripheral: cls_name = peripheral.__class__.__name__ await self.web_message.send(peripheral, msg_name) #await self.web_queue_out.put( f'{self.name}|{cls_name}|{peripheral.name}|{peripheral.port}|{msg_name}\r\n'.encode('utf-8') ) + async def recv_message(self, msg, data): + """Receive and process message (notification) from the hub. + + """ + if msg == 'value_change': + port, msg_bytes = data + peripheral = self.port_to_peripheral[port] + await peripheral.update_value(msg_bytes) + self.message_debug(f'peripheral msg: {peripheral} {msg}') + if self.web_queue_out: + cls_name = peripheral.__class__.__name__ + if len(peripheral.capabilities) > 0: + for cap in peripheral.value: + await self.web_message.send(peripheral, f'value change mode: {cap.value} = {peripheral.value[cap]}') + #await self.web_queue_out.put( f'{self.name}|{cls_name}|{peripheral.name}|{peripheral.port}|value change mode: {cap.value} = {peripheral.value[cap]}\r\n'.encode('utf-8') ) + handler_name = f'{peripheral.name}_change' + handler = getattr(self, handler_name) + await handler() + elif msg == 'attach': + port, device_name = data + peripheral = await self.connect_peripheral_to_port(device_name, port) + if peripheral: + self.message_debug(f'peripheral msg: {peripheral} {msg}') + peripheral.message_handler = self.send_message + await peripheral.activate_updates() + elif msg == 'update_port': + port, info = data + self.port_info[port] = info + elif msg.startswith('port'): + port = data + if self.query_port_info: + await self._get_port_info(port, msg) + else: + raise UnknownPeripheralMessage + async def peripheral_message_loop(self): """The main loop that receives messages from the :class:`bricknil.messages.Message` parser. @@ -103,38 +136,7 @@ async def peripheral_message_loop(self): while True: msg = await self.peripheral_queue.get() msg, data = msg - if msg == 'value_change': - port, msg_bytes = data - peripheral = self.port_to_peripheral[port] - await peripheral.update_value(msg_bytes) - self.message_debug(f'peripheral msg: {peripheral} {msg}') - if self.web_queue_out: - cls_name = peripheral.__class__.__name__ - if len(peripheral.capabilities) > 0: - for cap in peripheral.value: - await self.web_message.send(peripheral, f'value change mode: {cap.value} = {peripheral.value[cap]}') - #await self.web_queue_out.put( f'{self.name}|{cls_name}|{peripheral.name}|{peripheral.port}|value change mode: {cap.value} = {peripheral.value[cap]}\r\n'.encode('utf-8') ) - handler_name = f'{peripheral.name}_change' - handler = getattr(self, handler_name) - await handler() - elif msg == 'attach': - port, device_name = data - peripheral = await self.connect_peripheral_to_port(device_name, port) - if peripheral: - self.message_debug(f'peripheral msg: {peripheral} {msg}') - peripheral.message_handler = self.send_message - await peripheral.activate_updates() - elif msg == 'update_port': - port, info = data - self.port_info[port] = info - elif msg.startswith('port'): - port = data - if self.query_port_info: - await self._get_port_info(port, msg) - else: - raise UnknownPeripheralMessage - - + await self.recv_message(msg, data) except CancelledError: self.message(f'Terminating peripheral')