22# MIT license; Copyright (c) 2021 Jim Mussared
33
44from micropython import const
5+ from collections import deque
56import uasyncio as asyncio
67import struct
78
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