Skip to content

Commit 7f33841

Browse files
committed
aioble: Fix notified/indicated event waiting.
After a client does a successful `await char.notified()`, then before the next call to `notified()` a notification arrives, then they call `notified()` twice before the _next_ notification, the second call will return None rather than waiting. This applies the same fix as in 5a86aa5 which solved a similar problem for server-side `char.written()`. Using a deque is slightly overkill here, but it's consistent with the server side, and also makes it very easy to support having a notification queue in the future. Also makes the client characteristic properly flags/properties-aware (i.e. explicitly fail operations that aren't supported). Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
1 parent 43cad17 commit 7f33841

File tree

1 file changed

+81
-47
lines changed

1 file changed

+81
-47
lines changed

micropython/bluetooth/aioble/aioble/client.py

Lines changed: 81 additions & 47 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 uasyncio as asyncio
67
import struct
78

@@ -27,6 +28,12 @@
2728
_CCCD_NOTIFY = const(1)
2829
_CCCD_INDICATE = const(2)
2930

31+
_FLAG_READ = const(0x0002)
32+
_FLAG_WRITE_NO_RESPONSE = const(0x0004)
33+
_FLAG_WRITE = const(0x0008)
34+
_FLAG_NOTIFY = const(0x0010)
35+
_FLAG_INDICATE = const(0x0020)
36+
3037
# Forward IRQs directly to static methods on the type that handles them and
3138
# knows how to map handles to instances. Note: We copy all uuid and data
3239
# params here for safety, but a future optimisation might be able to avoid
@@ -202,8 +209,13 @@ def _find(conn_handle, value_handle):
202209
# value handle for the done event.
203210
return None
204211

212+
def _check(self, flag):
213+
if not (self.properties & flag):
214+
raise ValueError("Unsupported")
215+
205216
# Issue a read to the characteristic.
206217
async def read(self, timeout_ms=1000):
218+
self._check(_FLAG_READ)
207219
# Make sure this conn_handle/value_handle is known.
208220
self._register_with_connection()
209221
# This will be set by the done IRQ.
@@ -235,10 +247,11 @@ def _read_done(conn_handle, value_handle, status):
235247
characteristic._read_event.set()
236248

237249
async def write(self, data, response=False, timeout_ms=1000):
238-
# TODO: default response to True if properties includes WRITE and is char.
239-
# Something like:
240-
# if response is None and self.properties & _FLAGS_WRITE:
241-
# response = True
250+
self._check(_FLAG_WRITE | _FLAG_WRITE_NO_RESPONSE)
251+
252+
# If we only support write-with-response, then force sensible default.
253+
if response is None and (self.properties & _FLAGS_WRITE) and not (self.properties & _FLAG_WRITE_NO_RESPONSE):
254+
response = True
242255

243256
if response:
244257
# Same as read.
@@ -281,28 +294,32 @@ def __init__(self, service, def_handle, value_handle, properties, uuid):
281294
# Allows comparison to a known uuid.
282295
self.uuid = uuid
283296

284-
# Fired for each read result and read done IRQ.
285-
self._read_event = None
286-
self._read_data = None
287-
# Used to indicate that the read is complete.
288-
self._read_status = None
289-
290-
# Fired for the write done IRQ.
291-
self._write_event = None
292-
# Used to indicate that the write is complete.
293-
self._write_status = None
297+
if properties & _FLAG_READ:
298+
# Fired for each read result and read done IRQ.
299+
self._read_event = None
300+
self._read_data = None
301+
# Used to indicate that the read is complete.
302+
self._read_status = None
303+
304+
if (properties & _FLAG_WRITE) or (properties & _FLAG_WRITE_NO_RESPONSE):
305+
# Fired for the write done IRQ.
306+
self._write_event = None
307+
# Used to indicate that the write is complete.
308+
self._write_status = None
294309

295-
# Fired when a notification arrives.
296-
self._notify_event = None
297-
# Data for the most recent notification.
298-
self._notify_data = None
299-
# Same for indications.
300-
self._indicate_event = None
301-
self._indicate_data = None
310+
if properties & _FLAG_NOTIFY:
311+
# Fired when a notification arrives.
312+
self._notify_event = asyncio.ThreadSafeFlag()
313+
# Data for the most recent notification.
314+
self._notify_queue = deque((), 1)
315+
if properties & _FLAG_INDICATE:
316+
# Same for indications.
317+
self._indicate_event = asyncio.ThreadSafeFlag()
318+
self._indicate_queue = deque((), 1)
302319

303320
def __str__(self):
304321
return "Characteristic: {} {} {} {}".format(
305-
self._def_handle, self._value_handle, self._properties, self.uuid
322+
self._def_handle, self._value_handle, self.properties, self.uuid
306323
)
307324

308325
def _connection(self):
@@ -334,45 +351,59 @@ def _start_discovery(service, uuid=None):
334351
uuid,
335352
)
336353

354+
# Helper for notified() and indicated().
355+
async def _notified_indicated(self, queue, event, timeout_ms):
356+
# Ensure that events for this connection can route to this characteristic.
357+
self._register_with_connection()
358+
359+
# If the queue is empty, then we need to wait. However, if the queue
360+
# has a single item, we also need to do a no-op wait in order to
361+
# clear the event flag (because the queue will become empty and
362+
# therefore the event should be cleared).
363+
if len(queue) <= 1:
364+
with self._connection().timeout(timeout_ms):
365+
await event.wait()
366+
367+
# Either we started > 1 item, or the wait completed successfully, return
368+
# the front of the queue.
369+
return queue.popleft()
370+
337371
# Wait for the next notification.
338372
# Will return immediately if a notification has already been received.
339373
async def notified(self, timeout_ms=None):
340-
self._register_with_connection()
341-
data = self._notify_data
342-
if data is None:
343-
self._notify_event = self._notify_event or asyncio.ThreadSafeFlag()
344-
with self._connection().timeout(timeout_ms):
345-
await self._notify_event.wait()
346-
data = self._notify_data
347-
self._notify_data = None
348-
return data
374+
self._check(_FLAG_NOTIFY)
375+
return await self._notified_indicated(self._notify_queue, self._notify_event, timeout_ms)
376+
377+
def _on_notify_indicate(self, queue, event, data):
378+
# If we've gone from empty to one item, then wake something
379+
# blocking on `await char.notified()` (or `await char.indicated()`).
380+
wake = len(queue) == 0
381+
# Append the data. By default this is a deque with max-length==1, so it
382+
# replaces. But if capture is enabled then it will append.
383+
queue.append(data)
384+
if wake:
385+
# Queue is now non-empty. If something is waiting, it will be
386+
# worken. If something isn't waiting right now, then a future
387+
# caller to `await char.written()` will see the queue is
388+
# non-empty, and wait on the event if it's going to empty the
389+
# queue.
390+
event.set()
349391

350392
# Map an incoming notify IRQ to a registered characteristic.
351393
def _on_notify(conn_handle, value_handle, notify_data):
352394
if characteristic := ClientCharacteristic._find(conn_handle, value_handle):
353-
characteristic._notify_data = notify_data
354-
if characteristic._notify_event:
355-
characteristic._notify_event.set()
395+
characteristic._on_notify_indicate(characteristic._notify_queue, characteristic._notify_event, notify_data)
356396

357397
# Wait for the next indication.
358398
# Will return immediately if an indication has already been received.
359399
async def indicated(self, timeout_ms=None):
360-
self._register_with_connection()
361-
data = self._indicate_data
362-
if data is None:
363-
self._indicate_event = self._indicate_event or asyncio.ThreadSafeFlag()
364-
with self._connection().timeout(timeout_ms):
365-
await self._indicate_event.wait()
366-
data = self._indicate_data
367-
self._indicate_data = None
368-
return data
400+
self._check(_FLAG_INDICATE)
401+
return await self._notified_indicated(self._indicate_queue, self._indicate_event, timeout_ms)
369402

370403
# Map an incoming indicate IRQ to a registered characteristic.
371404
def _on_indicate(conn_handle, value_handle, indicate_data):
372405
if characteristic := ClientCharacteristic._find(conn_handle, value_handle):
373-
characteristic._indicate_data = indicate_data
374-
if characteristic._indicate_event:
375-
characteristic._indicate_event.set()
406+
characteristic._on_notify_indicate(characteristic._indicate_queue, characteristic._indicate_event, indicate_data)
376407

377408
# Write to the Client Characteristic Configuration to subscribe to
378409
# notify/indications for this characteristic.
@@ -399,9 +430,12 @@ def __init__(self, characteristic, dsc_handle, uuid):
399430
# Used for read/write.
400431
self._value_handle = dsc_handle
401432

433+
# Default flags
434+
self.properties = _FLAG_READ | _FLAG_WRITE_NO_RESPONSE
435+
402436
def __str__(self):
403437
return "Descriptor: {} {} {} {}".format(
404-
self._def_handle, self._value_handle, self._properties, self.uuid
438+
self._def_handle, self._value_handle, self.properties, self.uuid
405439
)
406440

407441
def _connection(self):

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy