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

Bluepy comm (#18)

* Added support for bluepy communication backend.

* Added bluepy information into the readme.

* Added tests, fixed dependency specs in setup.py.

* Fixed dep in travis.

* Removed unused import. Added ability to fail the application on dispatcher thread error.

* Fixed bluepy test to be more appropriate.

* Properly handle hub mac if set.
This commit is contained in:
Vasily Loginov 2019-01-28 20:13:07 +01:00 committed by Andrey Pokhilko
parent f078d188ae
commit dc1c388fe8
6 changed files with 200 additions and 1 deletions

View File

@ -27,7 +27,7 @@ addons:
- python3-dbus - python3-dbus
- python3-gi - python3-gi
install: install:
- pip install codecov nose-exclude gattlib pygatt gatt pexpect - pip install codecov nose-exclude gattlib pygatt gatt pexpect bluepy
script: coverage run --source=. `which nosetests` tests --nocapture --exclude-dir=examples script: coverage run --source=. `which nosetests` tests --nocapture --exclude-dir=examples

View File

@ -257,6 +257,7 @@ You have following options to install as Bluetooth backend:
- `pip install pygatt` - [pygatt](https://github.com/peplin/pygatt) lib, works on both Windows and Linux - `pip install pygatt` - [pygatt](https://github.com/peplin/pygatt) lib, works on both Windows and Linux
- `pip install gatt` - [gatt](https://github.com/getsenic/gatt-python) lib, supports Linux, does not work on Windows - `pip install gatt` - [gatt](https://github.com/getsenic/gatt-python) lib, supports Linux, does not work on Windows
- `pip install gattlib` - [gattlib](https://bitbucket.org/OscarAcena/pygattlib) - supports Linux, does not work on Windows, requires `sudo` - `pip install gattlib` - [gattlib](https://bitbucket.org/OscarAcena/pygattlib) - supports Linux, does not work on Windows, requires `sudo`
- `pip install bluepy` - [bluepy](https://github.com/IanHarvey/bluepy) lib, supports Linux, including Raspbian, which allows connection to the hub from the Raspberry PI
Running on Windows requires [Bluegiga BLED112 Bluetooth Smart Dongle](https://www.silabs.com/products/wireless/bluetooth/bluetooth-low-energy-modules/bled112-bluetooth-smart-dongle) hardware piece, because no other hardware currently works on Windows with Python+BLE. Running on Windows requires [Bluegiga BLED112 Bluetooth Smart Dongle](https://www.silabs.com/products/wireless/bluetooth/bluetooth-low-energy-modules/bled112-bluetooth-smart-dongle) hardware piece, because no other hardware currently works on Windows with Python+BLE.
@ -272,6 +273,7 @@ There is optional parameter for `MoveHub` class constructor, accepting instance
- use `GattConnection()` - if you use Gatt Backend on Linux (`gatt` library prerequisite) - use `GattConnection()` - if you use Gatt Backend on Linux (`gatt` library prerequisite)
- use `GattoolConnection()` - if you use GattTool Backend on Linux (`pygatt` library prerequisite) - use `GattoolConnection()` - if you use GattTool Backend on Linux (`pygatt` library prerequisite)
- use `GattLibConnection()` - if you use GattLib Backend on Linux (`gattlib` library prerequisite) - use `GattLibConnection()` - if you use GattLib Backend on Linux (`gattlib` library prerequisite)
- use `BluepyConnection()` - if you use Bluepy backend on Linux/Raspbian (`bluepy` library prerequisite)
- pass instance of `DebugServerConnection` if you are using [Debug Server](#debug-server) (more details below). - pass instance of `DebugServerConnection` if you are using [Debug Server](#debug-server) (more details below).
All the functions above have optional arguments to specify adapter name and MoveHub mac address. Please look function source code for details. All the functions above have optional arguments to specify adapter name and MoveHub mac address. Please look function source code for details.

View File

@ -29,9 +29,15 @@ def get_connection_gattlib(controller='hci0', hub_mac=None):
return GattLibConnection(controller).connect(hub_mac) return GattLibConnection(controller).connect(hub_mac)
def get_connection_bluepy(controller='hci0', hub_mac=None):
from pylgbst.comms.cbluepy import BluepyConnection
return BluepyConnection(controller).connect(hub_mac)
def get_connection_auto(controller='hci0', hub_mac=None): def get_connection_auto(controller='hci0', hub_mac=None):
fns = [ fns = [
get_connection_bluepy,
get_connection_bluegiga, get_connection_bluegiga,
get_connection_gatt, get_connection_gatt,
get_connection_gattool, get_connection_gattool,

129
pylgbst/comms/cbluepy.py Normal file
View File

@ -0,0 +1,129 @@
import re
import logging
from threading import Thread, Event
import time
from contextlib import contextmanager
from enum import Enum
from bluepy import btle
from pylgbst.comms import Connection, LEGO_MOVE_HUB
from pylgbst.constants import MOVE_HUB_HW_UUID_CHAR
from pylgbst.utilities import str2hex, queue
log = logging.getLogger('comms-bluepy')
COMPLETE_LOCAL_NAME_ADTYPE = 9
PROPAGATE_DISPATCHER_EXCEPTION = False
def _get_iface_number(controller):
"""bluepy uses iface numbers instead of full names."""
if not controller:
return None
m = re.search(r'hci(\d+)$', controller)
if not m:
raise ValueError('Cannot find iface number in {}.'.format(controller))
return int(m.group(1))
class BluepyDelegate(btle.DefaultDelegate):
def __init__(self, handler):
btle.DefaultDelegate.__init__(self)
self._handler = handler
def handleNotification(self, cHandle, data):
log.debug('Incoming notification')
self._handler(cHandle, data)
# We need a separate thread to wait for notifications,
# but calling peripheral's methods from different threads creates issues,
# so we will wrap all the calls into a thread
class BluepyThreadedPeripheral(object):
def __init__(self, addr, addrType, controller):
self._call_queue = queue.Queue()
self._addr = addr
self._addrType = addrType
self._iface_number = _get_iface_number(controller)
self._disconnect_event = Event()
self._dispatcher_thread = Thread(target=self._dispatch_calls)
self._dispatcher_thread.setDaemon(True)
self._dispatcher_thread.setName("Bluepy call dispatcher")
self._dispatcher_thread.start()
def _dispatch_calls(self):
self._peripheral = btle.Peripheral(self._addr, self._addrType, self._iface_number)
try:
while not self._disconnect_event.is_set():
try:
try:
method = self._call_queue.get(False)
method()
except queue.Empty:
pass
self._peripheral.waitForNotifications(1.)
except Exception as ex:
log.exception('Exception in call dispatcher thread', exc_info=ex)
if PROPAGATE_DISPATCHER_EXCEPTION:
log.error("Terminating dispatcher thread.")
raise
finally:
self._peripheral.disconnect()
def write(self, handle, data):
self._call_queue.put(lambda: self._peripheral.writeCharacteristic(handle, data))
def set_notify_handler(self, handler):
delegate = BluepyDelegate(handler)
self._call_queue.put(lambda: self._peripheral.withDelegate(delegate))
def disconnect(self):
self._disconnect_event.set()
class BluepyConnection(Connection):
def __init__(self, controller='hci0'):
Connection.__init__(self)
self._peripheral = None # :type BluepyThreadedPeripheral
self._controller = controller
def connect(self, hub_mac=None):
log.debug("Trying to connect client to MoveHub with MAC: %s", hub_mac)
scanner = btle.Scanner()
while not self._peripheral:
log.info("Discovering devices...")
scanner.scan(1)
devices = scanner.getDevices()
for dev in devices:
address = dev.addr
addressType = dev.addrType
name = dev.getValueText(COMPLETE_LOCAL_NAME_ADTYPE)
log.debug("Found dev, name: {}, address: {}".format(name, address))
if (not hub_mac and name == LEGO_MOVE_HUB) or hub_mac == address:
logging.info("Found %s at %s", name, address)
self._peripheral = BluepyThreadedPeripheral(address, addressType, self._controller)
break
return self
def disconnect(self):
self._peripheral.disconnect()
def write(self, handle, data):
log.debug("Writing to handle %s: %s", handle, str2hex(data))
self._peripheral.write(handle, data)
def set_notify_handler(self, handler):
self._peripheral.set_notify_handler(handler)
def is_alive(self):
return True

View File

@ -11,5 +11,6 @@ setup(name='pylgbst',
'gatt': ["gatt"], 'gatt': ["gatt"],
'gattlib': ["gattlib"], 'gattlib': ["gattlib"],
'pygatt': ["pygatt"], 'pygatt': ["pygatt"],
'bluepy': ["bluepy"],
} }
) )

61
tests/test_cbluepy.py Normal file
View File

@ -0,0 +1,61 @@
import unittest
import time
import pylgbst.comms.cbluepy as bp_backend
class PeripheralMock(object):
def __init__(self, addr, addrType, ifaceNumber):
pass
def waitForNotifications(self, timeout):
pass
def writeCharacteristic(self, handle, data):
pass
def withDelegate(self, delegate):
pass
def disconnect(self):
pass
bp_backend.PROPAGATE_DISPATCHER_EXCEPTION = True
bp_backend.btle.Peripheral = lambda *args, **kwargs: PeripheralMock(*args, **kwargs)
class BluepyTestCase(unittest.TestCase):
def test_get_iface_number(self):
self.assertEqual(bp_backend._get_iface_number('hci0'), 0)
self.assertEqual(bp_backend._get_iface_number('hci10'), 10)
try:
bp_backend._get_iface_number('ads12')
self.fail('Missing exception for incorrect value')
except ValueError:
pass
def test_delegate(self):
def _handler(handle, data):
_handler.called = True
delegate = bp_backend.BluepyDelegate(_handler)
delegate.handleNotification(123, 'qwe')
self.assertEqual(_handler.called, True)
def test_threaded_peripheral(self):
tp = bp_backend.BluepyThreadedPeripheral('address', 'addrType', 'hci0')
self.assertEqual(tp._addr, 'address')
self.assertEqual(tp._addrType, 'addrType')
self.assertEqual(tp._iface_number, 0)
self.assertNotEqual(tp._dispatcher_thread, None)
# Schedule some methods to async queue and give them some time to resolve
tp.set_notify_handler(lambda: '')
tp.write(123, 'qwe')
tp._dispatcher_thread.join(1)
self.assertEqual(tp._dispatcher_thread.is_alive(), True)
tp.disconnect()
tp._dispatcher_thread.join(2)
self.assertEqual(tp._dispatcher_thread.is_alive(), False)