diff --git a/micropython/bluetooth/aioble/aioble/server.py b/micropython/bluetooth/aioble/aioble/server.py index 0aeb442c..c037cc83 100644 --- a/micropython/bluetooth/aioble/aioble/server.py +++ b/micropython/bluetooth/aioble/aioble/server.py @@ -2,6 +2,7 @@ # MIT license; Copyright (c) 2021 Jim Mussared from micropython import const +from collections import deque import bluetooth import uasyncio as asyncio @@ -34,10 +35,15 @@ _FLAG_WRITE_ENCRYPTED = const(0x1000) _FLAG_WRITE_AUTHENTICATED = const(0x2000) _FLAG_WRITE_AUTHORIZED = const(0x4000) +_FLAG_WRITE_CAPTURE = const(0x10000) + _FLAG_DESC_READ = const(1) _FLAG_DESC_WRITE = const(2) +_WRITE_CAPTURE_QUEUE_LIMIT = const(10) + + def _server_irq(event, data): if event == _IRQ_GATTS_WRITE: conn_handle, attr_handle = data @@ -89,26 +95,54 @@ class BaseCharacteristic: else: ble.gatts_write(self._value_handle, data) - # Wait for a write on this characteristic. - # Returns the device that did the write. + # Wait for a write on this characteristic. Returns the connection that did + # the write, or a tuple of (connection, value) if capture is enabled for + # this characteristics. async def written(self, timeout_ms=None): if not self._write_event: raise ValueError() - data = self._write_connection - if data is None: + + # If the queue is empty, then we need to wait. However, if the queue + # has a single item, we also need to do a no-op wait in order to + # clear the event flag (because the queue will become empty and + # therefore the event should be cleared). + if len(self._write_queue) <= 1: with DeviceTimeout(None, timeout_ms): await self._write_event.wait() - data = self._write_connection - self._write_connection = None - return data + + # Either we started > 1 item, or the wait completed successfully, return + # the front of the queue. + return self._write_queue.popleft() def on_read(self, connection): return 0 def _remote_write(conn_handle, value_handle): if characteristic := _registered_characteristics.get(value_handle, None): - characteristic._write_connection = DeviceConnection._connected.get(conn_handle, None) - characteristic._write_event.set() + # If we've gone from empty to one item, then wake something + # blocking on `await char.written()`. + wake = len(characteristic._write_queue) == 0 + + conn = DeviceConnection._connected.get(conn_handle, None) + q = characteristic._write_queue + + if characteristic.flags & _FLAG_WRITE_CAPTURE: + # For capture, we append both the connection and the written + # value to the queue. The deque will enforce the max queue len. + data = characteristic.read() + q.append((conn, data)) + else: + # Use the queue as a single slot -- it has max length of 1, + # so if there's an existing item it will be replaced. + q.append(conn) + + if wake: + # Queue is now non-empty. If something is waiting, it will be + # worken. If something isn't waiting right now, then a future + # caller to `await char.written()` will see the queue is + # non-empty, and wait on the event if it's going to empty the + # queue. + characteristic._write_event.set() def _remote_read(conn_handle, value_handle): if characteristic := _registered_characteristics.get(value_handle, None): @@ -126,6 +160,7 @@ class Characteristic(BaseCharacteristic): notify=False, indicate=False, initial=None, + capture=False, ): service.characteristics.append(self) self.descriptors = [] @@ -137,8 +172,13 @@ class Characteristic(BaseCharacteristic): flags |= (_FLAG_WRITE if write else 0) | ( _FLAG_WRITE_NO_RESPONSE if write_no_response else 0 ) - self._write_connection = None + if capture: + # Capture means that we keep track of all writes, and capture + # their values (and connection) in a queue. Otherwise we just + # track the most recent connection. + flags |= _FLAG_WRITE_CAPTURE self._write_event = asyncio.ThreadSafeFlag() + self._write_queue = deque((), _WRITE_CAPTURE_QUEUE_LIMIT if capture else 1) if notify: flags |= _FLAG_NOTIFY if indicate: