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:
parent
f078d188ae
commit
dc1c388fe8
@ -27,7 +27,7 @@ addons:
|
||||
- python3-dbus
|
||||
- python3-gi
|
||||
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
|
||||
|
@ -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 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 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.
|
||||
|
||||
@ -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 `GattoolConnection()` - if you use GattTool Backend on Linux (`pygatt` 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).
|
||||
|
||||
All the functions above have optional arguments to specify adapter name and MoveHub mac address. Please look function source code for details.
|
||||
|
@ -29,9 +29,15 @@ def get_connection_gattlib(controller='hci0', hub_mac=None):
|
||||
|
||||
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):
|
||||
fns = [
|
||||
get_connection_bluepy,
|
||||
get_connection_bluegiga,
|
||||
get_connection_gatt,
|
||||
get_connection_gattool,
|
||||
|
129
pylgbst/comms/cbluepy.py
Normal file
129
pylgbst/comms/cbluepy.py
Normal 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
|
||||
|
1
setup.py
1
setup.py
@ -11,5 +11,6 @@ setup(name='pylgbst',
|
||||
'gatt': ["gatt"],
|
||||
'gattlib': ["gattlib"],
|
||||
'pygatt': ["pygatt"],
|
||||
'bluepy': ["bluepy"],
|
||||
}
|
||||
)
|
||||
|
61
tests/test_cbluepy.py
Normal file
61
tests/test_cbluepy.py
Normal 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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user