Skip to content

Commit 5a86aa5

Browse files
jimmodpgeorge
authored andcommitted
aioble: Add a write queue for gatt server.
This fixes a bug where an incoming write before `written` is awaited causes `written` to return None. It also introduces a mechanism for a server to "capture" all incoming written values (instead of only having access to the most recent value). Signed-off-by: Jim Mussared <[email protected]>
1 parent 3c383f6 commit 5a86aa5

File tree

1 file changed

+50
-10
lines changed

1 file changed

+50
-10
lines changed

micropython/bluetooth/aioble/aioble/server.py

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# MIT license; Copyright (c) 2021 Jim Mussared
33

44
from micropython import const
5+
from collections import deque
56
import bluetooth
67
import uasyncio as asyncio
78

@@ -34,10 +35,15 @@
3435
_FLAG_WRITE_AUTHENTICATED = const(0x2000)
3536
_FLAG_WRITE_AUTHORIZED = const(0x4000)
3637

38+
_FLAG_WRITE_CAPTURE = const(0x10000)
39+
3740
_FLAG_DESC_READ = const(1)
3841
_FLAG_DESC_WRITE = const(2)
3942

4043

44+
_WRITE_CAPTURE_QUEUE_LIMIT = const(10)
45+
46+
4147
def _server_irq(event, data):
4248
if event == _IRQ_GATTS_WRITE:
4349
conn_handle, attr_handle = data
@@ -89,26 +95,54 @@ def write(self, data):
8995
else:
9096
ble.gatts_write(self._value_handle, data)
9197

92-
# Wait for a write on this characteristic.
93-
# Returns the device that did the write.
98+
# Wait for a write on this characteristic. Returns the connection that did
99+
# the write, or a tuple of (connection, value) if capture is enabled for
100+
# this characteristics.
94101
async def written(self, timeout_ms=None):
95102
if not self._write_event:
96103
raise ValueError()
97-
data = self._write_connection
98-
if data is None:
104+
105+
# If the queue is empty, then we need to wait. However, if the queue
106+
# has a single item, we also need to do a no-op wait in order to
107+
# clear the event flag (because the queue will become empty and
108+
# therefore the event should be cleared).
109+
if len(self._write_queue) <= 1:
99110
with DeviceTimeout(None, timeout_ms):
100111
await self._write_event.wait()
101-
data = self._write_connection
102-
self._write_connection = None
103-
return data
112+
113+
# Either we started > 1 item, or the wait completed successfully, return
114+
# the front of the queue.
115+
return self._write_queue.popleft()
104116

105117
def on_read(self, connection):
106118
return 0
107119

108120
def _remote_write(conn_handle, value_handle):
109121
if characteristic := _registered_characteristics.get(value_handle, None):
110-
characteristic._write_connection = DeviceConnection._connected.get(conn_handle, None)
111-
characteristic._write_event.set()
122+
# If we've gone from empty to one item, then wake something
123+
# blocking on `await char.written()`.
124+
wake = len(characteristic._write_queue) == 0
125+
126+
conn = DeviceConnection._connected.get(conn_handle, None)
127+
q = characteristic._write_queue
128+
129+
if characteristic.flags & _FLAG_WRITE_CAPTURE:
130+
# For capture, we append both the connection and the written
131+
# value to the queue. The deque will enforce the max queue len.
132+
data = characteristic.read()
133+
q.append((conn, data))
134+
else:
135+
# Use the queue as a single slot -- it has max length of 1,
136+
# so if there's an existing item it will be replaced.
137+
q.append(conn)
138+
139+
if wake:
140+
# Queue is now non-empty. If something is waiting, it will be
141+
# worken. If something isn't waiting right now, then a future
142+
# caller to `await char.written()` will see the queue is
143+
# non-empty, and wait on the event if it's going to empty the
144+
# queue.
145+
characteristic._write_event.set()
112146

113147
def _remote_read(conn_handle, value_handle):
114148
if characteristic := _registered_characteristics.get(value_handle, None):
@@ -126,6 +160,7 @@ def __init__(
126160
notify=False,
127161
indicate=False,
128162
initial=None,
163+
capture=False,
129164
):
130165
service.characteristics.append(self)
131166
self.descriptors = []
@@ -137,8 +172,13 @@ def __init__(
137172
flags |= (_FLAG_WRITE if write else 0) | (
138173
_FLAG_WRITE_NO_RESPONSE if write_no_response else 0
139174
)
140-
self._write_connection = None
175+
if capture:
176+
# Capture means that we keep track of all writes, and capture
177+
# their values (and connection) in a queue. Otherwise we just
178+
# track the most recent connection.
179+
flags |= _FLAG_WRITE_CAPTURE
141180
self._write_event = asyncio.ThreadSafeFlag()
181+
self._write_queue = deque((), _WRITE_CAPTURE_QUEUE_LIMIT if capture else 1)
142182
if notify:
143183
flags |= _FLAG_NOTIFY
144184
if indicate:

0 commit comments

Comments
 (0)