Skip to content

Commit

Permalink
Merge pull request #38 from ThomasGerstenberg/gattc-write-without-res…
Browse files Browse the repository at this point in the history
…ponse

adds support for performing write without responses as a GATT Client
  • Loading branch information
ThomasGerstenberg authored May 29, 2020
2 parents 7ed95fa + bde59a2 commit 3c88bd0
Show file tree
Hide file tree
Showing 13 changed files with 354 additions and 72 deletions.
4 changes: 2 additions & 2 deletions blatann/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def suppress(self, *nrf_event_types):
def on_driver_event(self, nrf_driver, event):
with self._lock:
if type(event) not in self._suppressed_events:
logger.debug("Got NRF Driver event: {}".format(event))
logger.debug("Got NRF Driver event: %s", event)


class _UuidManager(object):
Expand Down Expand Up @@ -92,7 +92,7 @@ def __init__(self, comport="COM1", baud=1000000, log_driver_comms=False):
self.ble_driver.observer_register(self)
self.ble_driver.event_subscribe(self._on_user_mem_request, nrf_events.EvtUserMemoryRequest)
self._ble_configuration = self.ble_driver.ble_enable_params_setup()
self._default_conn_config = nrf_types.BleConnConfig()
self._default_conn_config = nrf_types.BleConnConfig(event_length=6) # The minimum event length which supports max DLE

self.bond_db_loader = default_bond_db.DefaultBondDatabaseLoader()
self.bond_db = default_bond_db.DefaultBondDatabase()
Expand Down
29 changes: 20 additions & 9 deletions blatann/event_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,26 @@ def __init__(self, reason):
self.reason = reason


class MtuSizeUpdatedEventArgs(EventArgs):
"""
Event arguments for when the effective MTU size on a connection is updated
"""
def __init__(self, previous_mtu_size: int, current_mtu_size: int):
self.previous_mtu_size = previous_mtu_size
self.current_mtu_size = current_mtu_size


class DataLengthUpdatedEventArgs(EventArgs):
"""
Event arguments for when the Data Length of the link layer has been changed
"""
def __init__(self, tx_bytes: int, rx_bytes: int, tx_time_us: int, rx_time_us: int):
self.tx_bytes = tx_bytes
self.rx_bytes = rx_bytes
self.tx_time_us = tx_time_us
self.rx_time_us = rx_time_us


# SMP Event Args

class PairingCompleteEventArgs(EventArgs):
Expand Down Expand Up @@ -285,12 +305,3 @@ def from_read_complete_event_args(read_complete_event_args, decoded_stream=None)
return DecodedReadCompleteEventArgs(read_complete_event_args.id, read_complete_event_args.value,
read_complete_event_args.status, read_complete_event_args.reason,
decoded_stream)


class MtuSizeUpdatedEventArgs(EventArgs):
"""
Event arguments for when the effective MTU size on a connection is updated
"""
def __init__(self, previous_mtu_size: int, current_mtu_size: int):
self.previous_mtu_size = previous_mtu_size
self.current_mtu_size = current_mtu_size
62 changes: 42 additions & 20 deletions blatann/gatt/gattc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Callable, List, Optional, Iterable

from blatann import gatt
from blatann.utils import SynchronousMonotonicCounter
from blatann.uuid import Uuid
from blatann.event_type import EventSource, Event
from blatann.gatt.reader import GattcReader
Expand Down Expand Up @@ -73,6 +74,13 @@ def writable(self) -> bool:
"""
return self._properties.write

@property
def writable_without_response(self) -> bool:
"""
Gets if the characteristic accepts write commands that don't require a confirmation response
"""
return self._properties.write_no_response

@property
def subscribable(self) -> bool:
"""
Expand Down Expand Up @@ -166,7 +174,7 @@ def read(self) -> EventWaitable[GattcCharacteristic, ReadCompleteEventArgs]:
def write(self, data) -> EventWaitable[GattcCharacteristic, WriteCompleteEventArgs]:
"""
Initiates a write of the data provided to the characteristic and returns a Waitable that executes
when the write completes.
when the write completes and the confirmation response is received from the other device.
The Waitable returns two parameters: (GattcCharacteristic this, WriteCompleteEventArgs event args)
Expand All @@ -175,12 +183,31 @@ def write(self, data) -> EventWaitable[GattcCharacteristic, WriteCompleteEventAr
:return: A waitable that returns when the write finishes
:raises: InvalidOperationException if characteristic is not writable
"""
if isinstance(data, str):
data = data.encode(self.string_encoding)

if not self.writable:
raise InvalidOperationException("Characteristic {} is not writable".format(self.uuid))
write_id = self._manager.write(self.value_handle, bytes(data))
return self._write(data, with_response=True)

def write_without_response(self, data) -> EventWaitable[GattcCharacteristic, WriteCompleteEventArgs]:
"""
Peforms a Write command, which does not require the peripheral to send a confirmation response packet.
This is a faster but lossy operation, in the case that the packet is dropped/never received by the other device.
This returns a waitable that executes when the write is transmitted to the peripheral device.
.. note:: Data sent without responses must fit within a single MTU minus 3 bytes for the operation overhead.
:param data: The data to write. Can be a string, bytes, or anything that can be converted to bytes
:type data: str or bytes or bytearray
:return: A waitable that returns when the write finishes
:raises: InvalidOperationException if characteristic is not writable without responses
"""
if not self.writable_without_response:
raise InvalidOperationException("Characteristic {} does not accept writes without responses".format(self.uuid))
return self._write(data, with_response=False)

def _write(self, data, with_response=True) -> EventWaitable[GattcCharacteristic, WriteCompleteEventArgs]:
if isinstance(data, str):
data = data.encode(self.string_encoding)
write_id = self._manager.write(self.value_handle, bytes(data), with_response)
return IdBasedEventWaitable(self._on_write_complete_event, write_id)

"""
Expand Down Expand Up @@ -290,7 +317,7 @@ def from_discovered_service(cls, ble_device, peer, read_write_manager, nrf_servi
Also takes care of creating and adding all characteristics within the service
:type ble_device: blatann.device.BleDevice
:type peer: blatann.peer.Peripheral
:type peer: blatann.peer.Peer
:type read_write_manager: _ReadWriteManager
:type nrf_service: nrf_types.BLEGattService
"""
Expand Down Expand Up @@ -372,29 +399,24 @@ def add_discovered_services(self, nrf_services):


class _ReadTask(object):
_id_counter = 1
_lock = threading.Lock()
_id_generator = SynchronousMonotonicCounter(1)

def __init__(self, handle):
with _ReadTask._lock:
self.id = _ReadTask._id_counter
_ReadTask._id_counter += 1
self.id = _ReadTask._id_generator.next()
self.handle = handle
self.data = ""
self.status = gatt.GattStatusCode.unknown
self.reason = GattOperationCompleteReason.FAILED


class _WriteTask(object):
_id_counter = 1
_lock = threading.Lock()
_id_generator = SynchronousMonotonicCounter(1)

def __init__(self, handle, data):
with _WriteTask._lock:
self.id = _WriteTask._id_counter
_WriteTask._id_counter += 1
def __init__(self, handle, data, with_response=True):
self.id = _WriteTask._id_generator.next()
self.handle = handle
self.data = data
self.with_response = with_response
self.status = gatt.GattStatusCode.unknown
self.reason = GattOperationCompleteReason.FAILED

Expand Down Expand Up @@ -422,8 +444,8 @@ def read(self, handle):
self._add_task(read_task)
return read_task.id

def write(self, handle, value):
write_task = _WriteTask(handle, value)
def write(self, handle, value, with_response=True):
write_task = _WriteTask(handle, value, with_response)
self._add_task(write_task)
return write_task.id

Expand All @@ -435,7 +457,7 @@ def _handle_task(self, task):
self._reader.read(task.handle)
self._cur_read_task = task
elif isinstance(task, _WriteTask):
self._writer.write(task.handle, task.data)
self._writer.write(task.handle, task.data, task.with_response)
self._cur_write_task = task
else:
return True
Expand Down
8 changes: 3 additions & 5 deletions blatann/gatt/gatts.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import binascii
from blatann.nrf import nrf_types, nrf_events
from blatann import gatt
from blatann.utils import SynchronousMonotonicCounter
from blatann.uuid import Uuid
from blatann.waitables.event_waitable import IdBasedEventWaitable, EventWaitable
from blatann.exceptions import InvalidOperationException, InvalidStateException
Expand Down Expand Up @@ -488,13 +489,10 @@ def _on_rw_auth_request(self, driver, event):


class _Notification(object):
_id_counter = 0
_lock = threading.Lock()
_id_generator = SynchronousMonotonicCounter(1)

def __init__(self, characteristic, handle, on_complete, data):
with _Notification._lock:
self.id = _Notification._id_counter
_Notification._id_counter += 1
self.id = _Notification._id_generator.next()
self.char = characteristic
self.handle = handle
self.on_complete = on_complete
Expand Down
39 changes: 30 additions & 9 deletions blatann/gatt/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from blatann.event_type import EventSource, Event
from blatann.nrf import nrf_types, nrf_events
from blatann.waitables.event_waitable import EventWaitable
from blatann.exceptions import InvalidStateException
from blatann.exceptions import InvalidStateException, InvalidOperationException
from blatann.event_args import EventArgs

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -35,6 +35,7 @@ def __init__(self, ble_device, peer):
self._handle = 0x0000
self._offset = 0
self.peer.driver_event_subscribe(self._on_write_response, nrf_events.GattcEvtWriteResponse)
self.peer.driver_event_subscribe(self._on_write_tx_complete, nrf_events.GattcEvtWriteCmdTxComplete)
self._len_bytes_written = 0

@property
Expand All @@ -49,28 +50,50 @@ def on_write_complete(self):
"""
return self._on_write_complete

def write(self, handle, data):
def write(self, handle, data, with_response=True):
"""
Writes data to the attribute at the handle provided. Can only write to a single attribute at a time.
If a write is in progress, raises an InvalidStateException
:param handle: The attribute handle to write
:param data: The data to write
:param with_response: True to do a BLE Request operation write which requires a confirmation response from the peripheral.
False to do a BLE Command operation which does not have a response from the peripheral
:return: A Waitable that will fire when the write finishes. see on_write_complete for the values returned from the waitable
:rtype: EventWaitable
"""
if self._busy:
raise InvalidStateException("Gattc Writer is busy")
if len(data) == 0:
raise ValueError("Data must be at least one byte")

self._offset = 0
self._handle = handle
self._data = data
logger.debug("Starting write to handle {}, len: {}".format(self._handle, len(self._data)))
self._write_next_chunk()
self._busy = True
try:
self._busy = True
if with_response:
self._write_next_chunk()
else:
self._write_no_response()
except Exception:
self._busy = False
raise
return EventWaitable(self.on_write_complete)

def _write_no_response(self):
# Verify that the write can fit into a single MTU
data_len = len(self._data)
if data_len > self.peer.mtu_size - self._WRITE_OVERHEAD:
raise InvalidOperationException(f"Writing data without response must fit within a "
f"single MTU minus the write overhead ({self._WRITE_OVERHEAD} bytes). "
f"MTU: {self.peer.mtu_size}bytes, data: {data_len}bytes")
write_operation = nrf_types.BLEGattWriteOperation.write_cmd
flags = nrf_types.BLEGattExecWriteFlag.unused
write_params = nrf_types.BLEGattcWriteParams(write_operation, flags, self._handle, self._data, self._offset)
self.ble_device.ble_driver.ble_gattc_write(self.peer.conn_handle, write_params)

def _write_next_chunk(self):
flags = nrf_types.BLEGattExecWriteFlag.unused
if self._offset != 0 or len(self._data) > (self.peer.mtu_size - self._WRITE_OVERHEAD):
Expand All @@ -92,12 +115,10 @@ def _write_next_chunk(self):
len(data_to_write), write_operation))
self.ble_device.ble_driver.ble_gattc_write(self.peer.conn_handle, write_params)

def _on_write_response(self, driver, event):
"""
Handler for GattcEvtWriteResponse
def _on_write_tx_complete(self, driver, event: nrf_events.GattcEvtWriteCmdTxComplete):
self._complete()

:type event: nrf_events.GattcEvtWriteResponse
"""
def _on_write_response(self, driver, event: nrf_events.GattcEvtWriteResponse):
if event.conn_handle != self.peer.conn_handle:
return
if event.attr_handle != self._handle and event.write_op != nrf_types.BLEGattWriteOperation.execute_write_req:
Expand Down
2 changes: 1 addition & 1 deletion blatann/nrf/nrf_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def NordicSemiErrorCheck(wrapped=None, expected=driver.NRF_SUCCESS):

@wrapt.decorator
def wrapper(wrapped, instance, args, kwargs):
logger.debug("[{}] {}{}".format(instance.serial_port, wrapped.__name__, args))
logger.debug("[%s] %s%s", instance.serial_port, wrapped.__name__, args)
result = wrapped(*args, **kwargs)
if isinstance(result, (list, tuple)):
err_code = result[0]
Expand Down
2 changes: 1 addition & 1 deletion blatann/nrf/nrf_events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@
GattcEvtAttrInfoDiscoveryResponse,
GattcEvtMtuExchangeResponse,
GattcEvtTimeout,
GattcEvtWriteCmdTxComplete,
# TODO:
# driver.BLE_GATTC_EVT_REL_DISC_RSP
# driver.BLE_GATTC_EVT_CHAR_VAL_BY_UUID_READ_RSP
# driver.BLE_GATTC_EVT_CHAR_VALS_READ_RSP
# driver.BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE

# Gatts
GattsEvtWrite,
Expand Down
Loading

0 comments on commit 3c88bd0

Please sign in to comment.