Skip to content

Commit

Permalink
Merge pull request #28 from stratosinc/bluetooth-adapter2
Browse files Browse the repository at this point in the history
Refactor API to make tracking backend connectivity and device connectively easier
  • Loading branch information
peplin committed Oct 8, 2015
2 parents 8dfd55e + f787538 commit 929c868
Show file tree
Hide file tree
Showing 24 changed files with 1,213 additions and 1,188 deletions.
82 changes: 28 additions & 54 deletions README.mkd
Original file line number Diff line number Diff line change
Expand Up @@ -6,70 +6,23 @@ behavior.

pygatt provides a Pythonic API by wrapping two different backends:

* BlueZ's `gatttool` command-line utility.
* Bluegiga's BGAPI, compatble with dongles like the BLED112.

Requires Python 2.7.
* BlueZ (requires Linux), using the `gatttool` command-line utility.
* Bluegiga's BGAPI, compatible with USB adapters like the BLED112.

## Motivation

Despite the popularilty of BLE, we have yet to find a good programming interface
Despite the popularity of BLE, we have yet to find a good programming interface
for it on desktop computers. Since most peripherals are designed to work with
smartphones, this space is neglected. One interactive interface, BlueZ's
`gatttool`, is functional but difficult to use programatically. BlueZ itself
obviously works, but the interface leaves something to be desired only
works in Linux.

pygatt consists of a front end that provies an API and two interchangable
backends that implement the Bluetooth communication using differently. The
backend can either use gatttool/BlueZ or a Bluegiga BGAPI compatible inteface.
gatttool/BlueZ is Linux only whereas the BGAPI is cross platform.

### Front end

The front end class pygatt.pygatt.BluetoothLEDevice represents a remote BLE
device. The API provides the methods -- connect, char_read, char_write,
subscribe -- that you need to interact with the device. When a BluetoothLEDevice
object is created, there is an optional argument "backend" that allows for
selection of the backend used. The default is gattool but the BGAPI can be
used by setting the optional bled112 arguement to an instance of a
BGAPIBackend. Note that there are optional arguments for some of the methods
of BluetoothLEDevice that must be specified when using one backend and not the
other.

### BGAPI Backend

This backend uses a minimalist implementation of Bluegiga's BGAPI to execute the
Bluetooth communication. In this case, the class used by BluetoothLEDevice is
pygatt.backends.BGAPIBackend.

BGAPIBackend in turn uses pygatt.backends.bgapi.bglib to communicate with the
BLED112 dongle. BGLib's job is to construct comands for the dongle and parse the
bytes received from the dongle into packets. BGAPIBackend's job is to manage
the timing of commands, handling of the data, and keep track of the state of the
dongle and connection to the remote device.

#### Dependencies

* The BGAPI backend should work on Linux, Windows, and Mac OS and has no other
external dependencies.
## Requirements

### GATTTool Backend

This backend uses gatttool/BlueZ on Linux to execute the Bluetooth
communication. In this case, the class used by BluetoothLEDevice is
pygatt.gatttool_classes.GATTToolBackend. GATTToolBackend uses the python module
pexpect to execute gatttool commannds as if a user were entering them on the
comand line.

#### Dependencies

* Currently the gatttool backend is currently only tested under Linux as it
requires `gatttool` which is included with BlueZ which is a Linux library.
* BlueZ >= 5.5
* Tested on 5.18 and 5.21
* Ubuntu is stuck on BlueZ 4.x and does not work - you need to build BlueZ
from source.
* Python 2.7
* BlueZ 5.5 or greater (with gatttool) - required for the gatttool backend only.
* Tested on 5.18, 5.21 and 5.35.

## Installation

Expand All @@ -83,6 +36,27 @@ backend, install the optional dependencies with:

$ pip install pygatt[GATTTOOL]

## Example Use

The primary API for users of this library is provided by
`pygatt.backends.BLEBackend` and `pygatt.BLEDevice`. After initializing an
instance of the preferred backend (available implementations are found in
`pygatt.backends`, use the `BLEBackend.connect` method to connect to a device
and get an instance of `BLEDevice.`

```python
import pygatt.backends

# The BGAPI backend will attemt to auto-discover the serial device name of the
# attached BGAPI-compatible USB adapter.
adapter = pygatt.backends.BGAPIBackend()
device = adapter.connect('01:23:45:67:89:ab')
value = device.char_read("a1e8f5b1-696b-4e4c-87c6-69dfe0b0093b")
```

Note that not all backends support connecting to more than 1 device at at time,
so calling `BLEBackend.connect` again may terminate existing connections.

## Authors

- Jeff Rowberg @jrowberg https://github.com/jrowberg/bglib
Expand Down
5 changes: 3 additions & 2 deletions pygatt/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .exceptions import BluetoothLEError # noqa
from .classes import BluetoothLEDevice # noqa
from .exceptions import BLEError # noqa
from .device import BLEDevice # noqa
from .backends import BGAPIBackend, GATTToolBackend # noqa
1 change: 1 addition & 0 deletions pygatt/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .backend import BLEBackend, Characteristic # noqa
from .bgapi.bgapi import BGAPIBackend # noqa
from .gatttool.gatttool import GATTToolBackend # noqa
150 changes: 46 additions & 104 deletions pygatt/backends/backend.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import threading
import logging

from collections import defaultdict
from binascii import hexlify

log = logging.getLogger(__name__)

DEFAULT_CONNECT_TIMEOUT_S = 5.0


class BLEBackend(object):
"""Abstract base class representing a Bluetooth adapter backend. """
"""Abstract base class representing a Bluetooth adapter backend. See the
`pygatt.backends` module for available implementations.
"""

def start(self):
"""Initialize and resource required to run the backend, e.g. background
threads, USB device connections, etc.
"""
raise NotImplementedError()

def __init__(self):
self._callbacks = defaultdict(set)
self._subscribed_handlers = {}
def stop(self):
"""Stop and free any resources required while the backend is running.
"""
raise NotImplementedError()

def supports_unbonded(self):
"""Return True if the backend supports unbonded communication - this is
Expand All @@ -21,103 +28,18 @@ def supports_unbonded(self):
"""
return True

def bond(self):
raise NotImplementedError()

def connect(self, address, **kwargs):
raise NotImplementedError()

def char_read_uuid(self, uuid):
raise NotImplementedError()

def char_write(self, handle, value, wait_for_response=False):
raise NotImplementedError()

def char_write_uuid(self, uuid, value, wait_for_response=False):
log.info("char_write %s", uuid)
handle = self.get_handle(uuid)
self.char_write(handle, value, wait_for_response=wait_for_response)

def encrypt(self):
raise NotImplementedError()

def get_rssi(self):
raise NotImplementedError()

def start(self):
self._lock = threading.Lock()

def stop(self):
raise NotImplementedError()

def disconnect(self):
def connect(self, address, timeout=DEFAULT_CONNECT_TIMEOUT_S, **kwargs):
"""Return a BLEDevice for the connection if connected, otherwise raise
an exception.
"""
raise NotImplementedError()

def subscribe(self, uuid, callback=None, indication=False):
def scan(self, *args, **kwargs):
"""
Enables subscription to a Characteristic with ability to call callback.
Performs a BLE scan.
uuid -- UUID as a string of the characteristic to subscribe.
callback -- function to be called when a notification/indication is
received on this characteristic.
indication -- use indications (requires application ACK) rather than
notifications (does not requrie application ACK).
"""
log.info(
'Subscribing to uuid=%s with callback=%s and indication=%s',
uuid, callback, indication)
# Expect notifications on the value handle...
value_handle = self.get_handle(uuid)

# but write to the characteristic config to enable notifications
# TODO with the BGAPI backend we can be smarter and fetch the actual
# characteristic config handle - we can also do that with gattool if we
# use the 'desc' command, so we'll need to change the "get_handle" API
# to be able to get the value or characteristic config handle.
characteristic_config_handle = value_handle + 1

properties = bytearray([
0x2 if indication else 0x1,
0x0
])

try:
self._lock.acquire()

if callback is not None:
self._callbacks[value_handle].add(callback)

if self._subscribed_handlers.get(value_handle, None) != properties:
self.char_write(
characteristic_config_handle,
properties,
wait_for_response=False
)
log.debug("Subscribed to uuid=%s", uuid)
self._subscribed_handlers[value_handle] = properties
else:
log.debug("Already subscribed to uuid=%s", uuid)
finally:
self._lock.release()

def _handle_notification(self, handle, value):
"""
Receive a notification from the connected device and propagate the value
to all registered callbacks.
Returns a list of BLE devices found.
"""

log.info('Received notification on handle=0x%x, value=0x%s',
handle, hexlify(value))
try:
self._lock.acquire()

if handle in self._callbacks:
for callback in self._callbacks[handle]:
callback(handle, value)
finally:
self._lock.release()

def get_handle(self, characteristic_uuid, descriptor_uuid=None):
raise NotImplementedError()

def filtered_scan(self, name_filter="", *args, **kwargs):
Expand All @@ -131,10 +53,30 @@ def filtered_scan(self, name_filter="", *args, **kwargs):
return [device for device in devices
if name_filter in (device['name'] or '')]

def scan(self, *args, **kwargs):
def clear_bond(self, address=None):
raise NotImplementedError()


class Characteristic(object):
"""
A GATT characteristic, including it handle value and associated descriptors.
Only valid for the lifespan of a BLE connection, since the handle values are
dynamic.
"""
def __init__(self, uuid, handle):
"""
Performs a BLE scan.
Sets the characteritic uuid and handle.
Returns a list of BLE devices found.
handle - a bytearray
"""
raise NotImplementedError()
self.uuid = uuid
self.handle = handle
self.descriptors = {
# uuid_string: handle
}

def add_descriptor(self, uuid, handle):
"""
Add a characteristic descriptor to the dictionary of descriptors.
"""
self.descriptors[uuid] = handle
Loading

0 comments on commit 929c868

Please sign in to comment.