Skip to content

Commit dc03b4a

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 dc03b4a

File tree

1 file changed

+91
-47
lines changed

1 file changed

+91
-47
lines changed

micropython/bluetooth/aioble/aioble/client.py

Lines changed: 91 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,15 @@ 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 (
254+
response is None
255+
and (self.properties & _FLAGS_WRITE)
256+
and not (self.properties & _FLAG_WRITE_NO_RESPONSE)
257+
):
258+
response = True
242259

243260
if response:
244261
# Same as read.
@@ -281,28 +298,32 @@ def __init__(self, service, def_handle, value_handle, properties, uuid):
281298
# Allows comparison to a known uuid.
282299
self.uuid = uuid
283300

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
301+
if properties & _FLAG_READ:
302+
# Fired for each read result and read done IRQ.
303+
self._read_event = None
304+
self._read_data = None
305+
# Used to indicate that the read is complete.
306+
self._read_status = None
307+
308+
if (properties & _FLAG_WRITE) or (properties & _FLAG_WRITE_NO_RESPONSE):
309+
# Fired for the write done IRQ.
310+
self._write_event = None
311+
# Used to indicate that the write is complete.
312+
self._write_status = None
294313

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
314+
if properties & _FLAG_NOTIFY:
315+
# Fired when a notification arrives.
316+
self._notify_event = asyncio.ThreadSafeFlag()
317+
# Data for the most recent notification.
318+
self._notify_queue = deque((), 1)
319+
if properties & _FLAG_INDICATE:
320+
# Same for indications.
321+
self._indicate_event = asyncio.ThreadSafeFlag()
322+
self._indicate_queue = deque((), 1)
302323

303324
def __str__(self):
304325
return "Characteristic: {} {} {} {}".format(
305-
self._def_handle, self._value_handle, self._properties, self.uuid
326+
self._def_handle, self._value_handle, self.properties, self.uuid
306327
)
307328

308329
def _connection(self):
@@ -334,45 +355,65 @@ def _start_discovery(service, uuid=None):
334355
uuid,
335356
)
336357

358+
# Helper for notified() and indicated().
359+
async def _notified_indicated(self, queue, event, timeout_ms):
360+
# Ensure that events for this connection can route to this characteristic.
361+
self._register_with_connection()
362+
363+
# If the queue is empty, then we need to wait. However, if the queue
364+
# has a single item, we also need to do a no-op wait in order to
365+
# clear the event flag (because the queue will become empty and
366+
# therefore the event should be cleared).
367+
if len(queue) <= 1:
368+
with self._connection().timeout(timeout_ms):
369+
await event.wait()
370+
371+
# Either we started > 1 item, or the wait completed successfully, return
372+
# the front of the queue.
373+
return queue.popleft()
374+
337375
# Wait for the next notification.
338376
# Will return immediately if a notification has already been received.
339377
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
378+
self._check(_FLAG_NOTIFY)
379+
return await self._notified_indicated(self._notify_queue, self._notify_event, timeout_ms)
380+
381+
def _on_notify_indicate(self, queue, event, data):
382+
# If we've gone from empty to one item, then wake something
383+
# blocking on `await char.notified()` (or `await char.indicated()`).
384+
wake = len(queue) == 0
385+
# Append the data. By default this is a deque with max-length==1, so it
386+
# replaces. But if capture is enabled then it will append.
387+
queue.append(data)
388+
if wake:
389+
# Queue is now non-empty. If something is waiting, it will be
390+
# worken. If something isn't waiting right now, then a future
391+
# caller to `await char.written()` will see the queue is
392+
# non-empty, and wait on the event if it's going to empty the
393+
# queue.
394+
event.set()
349395

350396
# Map an incoming notify IRQ to a registered characteristic.
351397
def _on_notify(conn_handle, value_handle, notify_data):
352398
if characteristic := ClientCharacteristic._find(conn_handle, value_handle):
353-
characteristic._notify_data = notify_data
354-
if characteristic._notify_event:
355-
characteristic._notify_event.set()
399+
characteristic._on_notify_indicate(
400+
characteristic._notify_queue, characteristic._notify_event, notify_data
401+
)
356402

357403
# Wait for the next indication.
358404
# Will return immediately if an indication has already been received.
359405
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
406+
self._check(_FLAG_INDICATE)
407+
return await self._notified_indicated(
408+
self._indicate_queue, self._indicate_event, timeout_ms
409+
)
369410

370411
# Map an incoming indicate IRQ to a registered characteristic.
371412
def _on_indicate(conn_handle, value_handle, indicate_data):
372413
if characteristic := ClientCharacteristic._find(conn_handle, value_handle):
373-
characteristic._indicate_data = indicate_data
374-
if characteristic._indicate_event:
375-
characteristic._indicate_event.set()
414+
characteristic._on_notify_indicate(
415+
characteristic._indicate_queue, characteristic._indicate_event, indicate_data
416+
)
376417

377418
# Write to the Client Characteristic Configuration to subscribe to
378419
# notify/indications for this characteristic.
@@ -399,9 +440,12 @@ def __init__(self, characteristic, dsc_handle, uuid):
399440
# Used for read/write.
400441
self._value_handle = dsc_handle
401442

443+
# Default flags
444+
self.properties = _FLAG_READ | _FLAG_WRITE_NO_RESPONSE
445+
402446
def __str__(self):
403447
return "Descriptor: {} {} {} {}".format(
404-
self._def_handle, self._value_handle, self._properties, self.uuid
448+
self._def_handle, self._value_handle, self.properties, self.uuid
405449
)
406450

407451
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