Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ htmlcov/
# pycharm
.idea/*
.cache/*
# adafruit checkout
Adafruit_Python_BluefruitLE/*
# C extensions
*.so

Expand Down
37 changes: 6 additions & 31 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -90,11 +90,9 @@ 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
.. _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
Expand Down Expand Up @@ -135,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

Expand Down Expand Up @@ -489,29 +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, which in turn is run by the
Adafruit_BluefruitLE library run 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
############
Expand Down Expand Up @@ -614,11 +591,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 <hbldh>` for his Bleak_ library that provided a pure python way to communicate with BluetoothLE over DBus on Linux.
* :gh_user:`Henrik Blidh <hbldh>` 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
Expand Down
193 changes: 61 additions & 132 deletions bricknil/ble_queue.py
Original file line number Diff line number Diff line change
@@ -1,91 +1,64 @@
# 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.
# 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 asyncio import Queue, sleep, CancelledError
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
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.

"""

def __init__(self, ble):
def __init__(self):
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 = {}
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:
self.message(f'Terminating and disconnecxting')
if USE_BLEAK:
await self.ble.in_queue.put( 'quit' )
else:
self.device.disconnect()
async def disconnect(self):
if len(self.devices) > 0:
self.message(f'Terminating and disconnecting')
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
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 device.write_gatt_char(char_uuid, values)

async def get_messages(self, hub):
"""Instance a Message object to parse incoming messages and setup
Expand All @@ -102,24 +75,16 @@ 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 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).

Expand Down Expand Up @@ -157,97 +122,61 @@ 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:
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
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):
"""
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 hub
hub.message_queue = self.q
hub.ble_handler = self
self.message(f'Starting scan for UART {hub.uart_uuid}')

# HACK
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

await self._ble_connect(hub.uart_uuid, hub.ble_name, hub.manufacturer_id, ble_id)

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}')

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)
# 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
Expand Down
Loading