1
0
mirror of https://github.com/undera/pylgbst.git synced 2020-11-18 19:37:26 -08:00

Experiment with Bleak changes (#55)

* Introduce driver that works with Bleak, enables to use BLE devices in windows without a need of external BLE dongle.

* Fix issues in auto review.

* Add method description and UT.

* Fix docstring to comply with pep257

* Experiment

* Make test only work in 3.7+

* Fix versions

* One more try

* Kick it

* Kick

* cmon

* Dummm

* yeah yeah

* Add

* Fix version

Co-authored-by: mgr <tomekmgr@gmail.com>
This commit is contained in:
Andrey Pokhilko 2020-06-27 13:45:53 +03:00 committed by GitHub
parent ba7594a081
commit 3c2f0b493b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 290 additions and 14 deletions

View File

@ -1,14 +1,8 @@
sudo: false
language: python
virtualenv:
system_site_packages: true
matrix:
include:
- os: linux
python: 2.7
- os: linux
python: 3.5
python:
- 3.6
- 3.8
addons:
apt:
@ -21,16 +15,17 @@ addons:
- libdbus-1-dev
- libdbus-glib-1-dev
- libgirepository-1.0-1
- libgirepository1.0-dev
- python-dbus
- python-gi
- python3-dbus
- python3-gi
- bluez
install:
- wget https://github.com/labapart/gattlib/releases/download/dev/gattlib_dbus_0.2-dev_x86_64.deb
- sudo dpkg -i gattlib_dbus_0.2-dev_x86_64.deb
- pip install codecov codacy-coverage nose-exclude pygatt gatt pexpect bluepy
- pip install codecov codacy-coverage nose-exclude pygatt gatt pexpect bluepy bleak packaging dbus-python pygobject
- pip install --upgrade attrs
env:
- READTHEDOCS=True
script:
- coverage run --omit="examples/*" --source=. -m nose tests -v --exclude-dir=examples

View File

@ -37,6 +37,20 @@ def get_connection_bluepy(controller='hci0', hub_mac=None):
return BluepyConnection(controller).connect(hub_mac)
def get_connection_bleak(controller='hci0', hub_mac=None):
"""
Return connection based with Bleak API as an endpoint.
:param controller: Not used, kept for compatibility with others.
:param hub_mac: Optional Lego HUB MAC to connect to.
:return: Driver object.
"""
del controller # to prevent code analysis warning
from pylgbst.comms.cbleak import BleakDriver
return BleakDriver(hub_mac)
def get_connection_auto(controller='hci0', hub_mac=None):
fns = [
get_connection_bluepy,
@ -44,6 +58,7 @@ def get_connection_auto(controller='hci0', hub_mac=None):
get_connection_gatt,
get_connection_gattool,
get_connection_gattlib,
get_connection_bleak,
]
conn = None

204
pylgbst/comms/cbleak.py Normal file
View File

@ -0,0 +1,204 @@
import asyncio
import logging
import queue
import threading
import time
from bleak import BleakClient, discover
from pylgbst.comms import Connection, MOVE_HUB_HW_UUID_CHAR
log = logging.getLogger('comms-bleak')
# Queues to handle request / responses. Acts as a buffer between API and async BLE driver
resp_queue = queue.Queue()
req_queue = queue.Queue()
class BleakDriver(object):
"""Driver that provides interface between API and Bleak."""
def __init__(self, hub_mac=None):
"""
Initialize new object of Bleak Driver class.
:param hub_mac: Optional Lego HUB MAC to connect to.
"""
self.hub_mac = hub_mac
self._handler = None
self._abort = False
self._connection_thread = None
self._processing_thread = None
def set_notify_handler(self, handler):
"""
Set handler function used to communicate with an API.
:param handler: Handler function called by driver when received data
:return: None
"""
self._handler = handler
def enable_notifications(self):
"""
Enable notifications, in our cases starts communication threads.
We cannot do this earlier, because API need to fist set notification handler.
:return: None
"""
self._connection_thread = threading.Thread(target=lambda: asyncio.run(self._bleak_thread()))
self._connection_thread.daemon = True
self._connection_thread.start()
self._processing_thread = threading.Thread(target=self._processing)
self._processing_thread.daemon = True
self._processing_thread.start()
async def _bleak_thread(self):
bleak = BleakConnection()
await bleak.connect(self.hub_mac)
await bleak.set_notify_handler(self._safe_handler)
# After connecting, need to send any data or hub will drop the connection,
# below command is Advertising name request update
await bleak.write_char(MOVE_HUB_HW_UUID_CHAR, bytearray([0x05, 0x00, 0x01, 0x01, 0x05]))
while not self._abort:
await asyncio.sleep(0.1)
if req_queue.qsize() != 0:
data = req_queue.get()
await bleak.write(data[0], data[1])
@staticmethod
def _safe_handler(handler, data):
resp_queue.put((handler, data))
def _processing(self):
while not self._abort:
if resp_queue.qsize() != 0:
msg = resp_queue.get()
self._handler(msg[0], msg[1])
time.sleep(0.1)
def write(self, handle, data):
"""
Send data to given handle number.
:param handle: Handle number that will be translated into characteristic uuid
:param data: data to send
:raises ConnectionError" When internal threads are not working
:return: None
"""
if not self._connection_thread.is_alive() or not self._processing_thread.is_alive():
raise ConnectionError('Something went wrong, communication threads not functioning.')
req_queue.put((handle, data))
def disconnect(self):
"""
Disconnect and stops communication threads.
:return: None
"""
self._abort = True
def is_alive(self):
"""
Indicate whether driver is functioning or not.
:return: True if driver is functioning; False otherwise.
"""
if self._connection_thread is not None and self._processing_thread is not None:
return self._connection_thread.is_alive() and self._processing_thread.is_alive()
else:
return False
class BleakConnection(Connection):
"""Bleak driver for communicating with BLE device."""
def __init__(self):
"""Initialize new instance of BleakConnection class."""
Connection.__init__(self)
self.loop = asyncio.get_event_loop()
self._device = None
self._client = None
logging.getLogger('bleak.backends.dotnet.client').setLevel(logging.getLogger().level)
async def connect(self, hub_mac=None):
"""
Connect to device.
:param hub_mac: Optional Lego HUB MAC to connect to.
:raises ConnectionError: When cannot connect to given MAC or name matching fails.
:return: None
"""
log.info("Discovering devices... Press Green button on lego MoveHub")
devices = await discover()
log.debug("Devices: %s", devices)
for dev in devices:
log.debug(dev)
address = dev.address
name = dev.name
if self._is_device_matched(address, name, hub_mac):
log.info('Device matched')
self._device = dev
break
if not self._device:
raise ConnectionError('Device not found.')
self._client = BleakClient(self._device.address, self.loop)
status = await self._client.connect()
log.debug('Connection status: {status}'.format(status=status))
async def write(self, handle, data):
"""
Send data to given handle number.
If handle cannot be found in service description, hardcoded LEGO uuid will be used.
:param handle: Handle number that will be translated into characteristic uuid
:param data: data to send
:return: None
"""
log.debug('Request: {handle} {payload}'.format(handle=handle, payload=[hex(x) for x in data]))
desc = self._client.services.get_descriptor(handle)
if desc is None:
# dedicated handle not found, try to send by using LEGO Move Hub default characteristic
await self._client.write_gatt_char(MOVE_HUB_HW_UUID_CHAR, data)
else:
await self._client.write_gatt_char(desc.characteristic_uuid, data)
async def write_char(self, characteristic_uuid, data):
"""
Send data to given handle number.
:param characteristic_uuid: Characteristic uuid used to send data
:param data: data to send
:return: None
"""
await self._client.write_gatt_char(characteristic_uuid, data)
async def set_notify_handler(self, handler):
"""
Set notification handler.
:param handler: Handle function to be called when receive any data.
:return: None
"""
def c(handle, data):
log.debug('Response: {handle} {payload}'.format(handle=handle, payload=[hex(x) for x in data]))
handler(handle, data)
await self._client.start_notify(MOVE_HUB_HW_UUID_CHAR, c)
def is_alive(self):
"""
To keep compatibility with the driver interface.
This method does nothing.
:return: None.
"""
pass

View File

@ -19,5 +19,6 @@ setup(
"gattlib": ["gattlib"],
"pygatt": ["pygatt", "pexpect"],
"bluepy": ["bluepy"],
"bleak": ["bleak"],
},
)

61
tests/test_cbleak.py Normal file
View File

@ -0,0 +1,61 @@
import sys
import time
import unittest
import bleak
from packaging import version
import pylgbst
import pylgbst.comms.cbleak as cbleak
bleak.BleakClient = object()
bleak.discover = object()
last_response = None
lt37 = version.parse(sys.version.split(' ')[0]) < version.parse("3.7")
class BleakDriverTest(unittest.TestCase):
def test_driver_creation(self):
connection = pylgbst.get_connection_bleak()
self.assertIsInstance(connection, cbleak.BleakDriver)
self.assertFalse(connection.is_alive(), 'Checking that factory returns not started driver')
@unittest.skipIf(lt37, "Python version is too low")
def test_communication(self):
driver = cbleak.BleakDriver()
async def fake_thread():
print('Fake thread initialized')
while not driver._abort:
time.sleep(0.1)
if cbleak.req_queue.qsize() != 0:
print('Received data, sending back')
data = cbleak.req_queue.get()
cbleak.resp_queue.put(data)
driver._bleak_thread = fake_thread
driver.set_notify_handler(BleakDriverTest.validation_handler)
driver.enable_notifications()
time.sleep(0.5) # time for driver initialization
self.assertTrue(driver.is_alive(), 'Checking that driver starts')
handle = 0x32
data = [0xD, 0xE, 0xA, 0xD, 0xB, 0xE, 0xE, 0xF]
driver.write(handle, data)
time.sleep(0.5) # processing time
self.assertEqual(handle, last_response[0], 'Verifying response handle')
self.assertEqual(data, last_response[1], 'Verifying response data')
driver.disconnect()
time.sleep(0.5) # processing time
self.assertFalse(driver.is_alive())
@staticmethod
def validation_handler(handle, data):
global last_response
last_response = (handle, data)
if __name__ == '__main__':
unittest.main()