2
2
# MIT license; Copyright (c) 2021 Jim Mussared
3
3
4
4
from micropython import const
5
+ from collections import deque
5
6
import uasyncio as asyncio
6
7
import struct
7
8
27
28
_CCCD_NOTIFY = const (1 )
28
29
_CCCD_INDICATE = const (2 )
29
30
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
+
30
37
# Forward IRQs directly to static methods on the type that handles them and
31
38
# knows how to map handles to instances. Note: We copy all uuid and data
32
39
# params here for safety, but a future optimisation might be able to avoid
@@ -202,8 +209,13 @@ def _find(conn_handle, value_handle):
202
209
# value handle for the done event.
203
210
return None
204
211
212
+ def _check (self , flag ):
213
+ if not (self .properties & flag ):
214
+ raise ValueError ("Unsupported" )
215
+
205
216
# Issue a read to the characteristic.
206
217
async def read (self , timeout_ms = 1000 ):
218
+ self ._check (_FLAG_READ )
207
219
# Make sure this conn_handle/value_handle is known.
208
220
self ._register_with_connection ()
209
221
# This will be set by the done IRQ.
@@ -235,10 +247,11 @@ def _read_done(conn_handle, value_handle, status):
235
247
characteristic ._read_event .set ()
236
248
237
249
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
242
255
243
256
if response :
244
257
# Same as read.
@@ -281,28 +294,32 @@ def __init__(self, service, def_handle, value_handle, properties, uuid):
281
294
# Allows comparison to a known uuid.
282
295
self .uuid = uuid
283
296
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
294
309
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 )
302
319
303
320
def __str__ (self ):
304
321
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
306
323
)
307
324
308
325
def _connection (self ):
@@ -334,45 +351,59 @@ def _start_discovery(service, uuid=None):
334
351
uuid ,
335
352
)
336
353
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
+
337
371
# Wait for the next notification.
338
372
# Will return immediately if a notification has already been received.
339
373
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 ()
349
391
350
392
# Map an incoming notify IRQ to a registered characteristic.
351
393
def _on_notify (conn_handle , value_handle , notify_data ):
352
394
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 )
356
396
357
397
# Wait for the next indication.
358
398
# Will return immediately if an indication has already been received.
359
399
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 )
369
402
370
403
# Map an incoming indicate IRQ to a registered characteristic.
371
404
def _on_indicate (conn_handle , value_handle , indicate_data ):
372
405
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 )
376
407
377
408
# Write to the Client Characteristic Configuration to subscribe to
378
409
# notify/indications for this characteristic.
@@ -399,9 +430,12 @@ def __init__(self, characteristic, dsc_handle, uuid):
399
430
# Used for read/write.
400
431
self ._value_handle = dsc_handle
401
432
433
+ # Default flags
434
+ self .properties = _FLAG_READ | _FLAG_WRITE_NO_RESPONSE
435
+
402
436
def __str__ (self ):
403
437
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
405
439
)
406
440
407
441
def _connection (self ):
0 commit comments