Skip to content

Commit

Permalink
Update docs to reflect new API.
Browse files Browse the repository at this point in the history
  • Loading branch information
peplin committed Oct 8, 2015
1 parent a3a295d commit f787538
Show file tree
Hide file tree
Showing 12 changed files with 127 additions and 161 deletions.
80 changes: 27 additions & 53 deletions README.mkd
Original file line number Diff line number Diff line change
Expand Up @@ -7,69 +7,22 @@ behavior.
pygatt provides a Pythonic API by wrapping two different backends:

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

Requires Python 2.7.
* 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.BLEDevice 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 adapter. BGLib's job is to construct comands for the adapter and parse
the bytes received from the adapter into packets. BGAPIBackend's job is to
manage the timing of commands, handling of the data, and keep track of the state
of the adapter 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
3 changes: 2 additions & 1 deletion pygatt/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .exceptions import BLEError # noqa
from .classes import BLEDevice # noqa
from .device import BLEDevice # noqa
from .backends import BGAPIBackend, GATTToolBackend # noqa
17 changes: 13 additions & 4 deletions pygatt/backends/backend.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import logging

from pygatt.constants import DEFAULT_CONNECT_TIMEOUT_S

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 stop(self):
"""Stop and free any resources required while the backend is running.
"""
raise NotImplementedError()

def supports_unbonded(self):
Expand Down Expand Up @@ -52,7 +59,9 @@ def clear_bond(self, address=None):

class Characteristic(object):
"""
GATT characteristic. For internal use within BGAPIBackend.
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):
"""
Expand Down
24 changes: 10 additions & 14 deletions pygatt/backends/bgapi/bgapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,15 @@ def __init__(self):

class BGAPIBackend(BLEBackend):
"""
Pygatt BLE device backend using a Bluegiga BGAPI compatible adapter.
Only supports 1 device connection at a time.
This object is NOT threadsafe.
A BLE backend for a BGAPI compatible USB adapter.
"""
def __init__(self, serial_port=None):
"""
Initialize the BGAPI device to be ready for use with a BLE device, i.e.,
stop ongoing procedures, disconnect any connections, optionally start
the receiver thread, and optionally delete any stored bonds.
Initialize the backend, but don't start the USB connection yet. Must
call .start().
serial_port -- The name of the serial port for the BGAPI-compatible
USB interface.
USB interface. If not provided, will attempt to auto-detect.
"""
self._lib = bglib.BGLib()
if serial_port is None:
Expand Down Expand Up @@ -115,8 +110,8 @@ def __init__(self, serial_port=None):

def start(self):
"""
Put the interface into a known state to start. And start the receiver
thread.
Connect to the USB adapter, reset it's state and start a backgroud
receiver thread.
"""
if self._running and self._running.is_set():
self.stop()
Expand Down Expand Up @@ -164,8 +159,6 @@ def stop(self):
self._ser = None

def set_bondable(self, bondable):
# If we are in bondable mode, every time we connect it upgrades to
# bonded. Is there a way to stop this?
self.send_command(
CommandBuilder.sm_set_bondable_mode(
constants.bondable['yes' if bondable else 'no']))
Expand All @@ -190,6 +183,9 @@ def clear_bond(self, address=None):
"""
Delete the bonds stored on the adapter.
address - the address of the device to unbond. If not provided, will
erase all bonds.
Note: this does not delete the corresponding bond stored on the remote
device.
"""
Expand Down Expand Up @@ -220,11 +216,11 @@ def scan(self, timeout=10, scan_interval=75, scan_window=50, active=True,
"""
Perform a scan to discover BLE devices.
timeout -- the number of seconds this scan should last.
scan_interval -- the number of miliseconds until scanning is restarted.
scan_window -- the number of miliseconds the scanner will listen on one
frequency for advertisement packets.
active -- True --> ask sender for scan response data. False --> don't.
timeout -- the number of seconds this scan should last.
discover_mode -- one of the gap_discover_mode constants.
"""
parameters = 1 if active else 0
Expand Down
33 changes: 4 additions & 29 deletions pygatt/backends/bgapi/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@


def connection_required(func):
"""Raise an exception if the device is not connected before calling the
actual function.
"""
def wrapper(self, *args, **kwargs):
if self._handle is None:
raise exceptions.NotConnectedError()
Expand All @@ -22,7 +25,6 @@ def wrapper(self, *args, **kwargs):
class BGAPIBLEDevice(BLEDevice):
def __init__(self, address, handle, backend):
super(BGAPIBLEDevice, self).__init__(address)
self._address = address
self._handle = handle
self._backend = backend
self._characteristics = {}
Expand All @@ -31,8 +33,6 @@ def __init__(self, address, handle, backend):
def bond(self, permanent=False):
"""
Create a bond and encrypted connection with the device.
This requires that a connection is already extablished with the device.
"""

# Set to bondable mode so bonds are store permanently
Expand All @@ -55,8 +55,6 @@ def get_rssi(self):
"""
Get the receiver signal strength indicator (RSSI) value from the device.
This requires that a connection is already established with the device.
Returns the RSSI as in integer in dBm.
"""
# The BGAPI has some strange behavior where it will return 25 for
Expand All @@ -74,16 +72,6 @@ def get_rssi(self):

@connection_required
def char_read(self, uuid):
"""
Read a value from a characteristic on the device.
This requires that a connection is already established with the device.
handle -- the characteristic handle (integer) to read from.
Returns a bytearray containing the value read, on success.
Raised BGAPIError on failure.
"""
handle = self.get_handle(uuid)
log.info("Reading characteristic at handle %d", handle)
self._backend.send_command(
Expand All @@ -103,18 +91,8 @@ def char_read(self, uuid):

@connection_required
def char_write_handle(self, char_handle, value, wait_for_response=False):
"""
Write a value to a characteristic on the device.
This requires that a connection is already extablished with the device.
handle -- the characteristic/descriptor handle (integer) to write to.
value -- a bytearray holding the value to write.
Raises BGAPIError on failure.
"""
if wait_for_response:
raise NotImplementedError("bgapi subscribe wait for response")
raise NotImplementedError()

while True:
value_list = [b for b in value]
Expand All @@ -133,9 +111,6 @@ def char_write_handle(self, char_handle, value, wait_for_response=False):

@connection_required
def disconnect(self):
"""
Disconnect from the device if connected.
"""
log.debug("Disconnecting from %s", self._address)
self._backend.send_command(
CommandBuilder.connection_disconnect(self._handle))
Expand Down
13 changes: 0 additions & 13 deletions pygatt/backends/bgapi/util.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import re
import logging
import serial.tools.list_ports
from binascii import unhexlify

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -90,15 +89,3 @@ def find_usb_serial_devices(vendor_id=None, product_id=None):
devices.append(dev)
log.info("USB device: %s", dev)
return devices


def uuid_to_bytearray(uuid_str):
"""
Turns a UUID string in the format "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
to a bytearray.
uuid -- the UUID to convert.
Returns a bytearray containing the UUID.
"""
return unhexlify(uuid_str.replace('-', ''))
10 changes: 10 additions & 0 deletions pygatt/backends/gatttool/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@


def connection_required(func):
"""Raise an exception before calling the actual function if the device is
not connection.
"""
def wrapper(self, *args, **kwargs):
if not self._connected:
raise exceptions.NotConnectedError()
Expand All @@ -14,6 +17,13 @@ def wrapper(self, *args, **kwargs):


class GATTToolBLEDevice(BLEDevice):
"""A BLE device connection initiated by the GATTToolBackend.
Since the GATTToolBackend can only support 1 device connection at at time,
the device implementation defers to the backend for all functionality -
every command has to synchronize around a the same interactive gatttool
session, using the same connection.
"""
def __init__(self, address, backend):
super(GATTToolBLEDevice, self).__init__(address)
self._backend = backend
Expand Down
Loading

0 comments on commit f787538

Please sign in to comment.