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
@@ -71,7 +78,7 @@ def _client_irq(event, data):
71
78
ClientCharacteristic ._on_indicate (conn_handle , value_handle , bytes (indicate_data ))
72
79
73
80
74
- register_irq_handler (_client_irq )
81
+ register_irq_handler (_client_irq , None )
75
82
76
83
77
84
# Async generator for discovering services, characteristics, descriptors.
@@ -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,15 @@ 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 (
254
+ response is None
255
+ and (self .properties & _FLAGS_WRITE )
256
+ and not (self .properties & _FLAG_WRITE_NO_RESPONSE )
257
+ ):
258
+ response = True
242
259
243
260
if response :
244
261
# Same as read.
@@ -281,28 +298,32 @@ def __init__(self, service, def_handle, value_handle, properties, uuid):
281
298
# Allows comparison to a known uuid.
282
299
self .uuid = uuid
283
300
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
294
313
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 )
302
323
303
324
def __str__ (self ):
304
325
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
306
327
)
307
328
308
329
def _connection (self ):
@@ -334,49 +355,72 @@ def _start_discovery(service, uuid=None):
334
355
uuid ,
335
356
)
336
357
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
+
337
375
# Wait for the next notification.
338
376
# Will return immediately if a notification has already been received.
339
377
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 ()
349
395
350
396
# Map an incoming notify IRQ to a registered characteristic.
351
397
def _on_notify (conn_handle , value_handle , notify_data ):
352
398
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
+ )
356
402
357
403
# Wait for the next indication.
358
404
# Will return immediately if an indication has already been received.
359
405
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
+ )
369
410
370
411
# Map an incoming indicate IRQ to a registered characteristic.
371
412
def _on_indicate (conn_handle , value_handle , indicate_data ):
372
413
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
+ )
376
417
377
418
# Write to the Client Characteristic Configuration to subscribe to
378
419
# notify/indications for this characteristic.
379
420
async def subscribe (self , notify = True , indicate = False ):
421
+ # Ensure that the generated notifications are dispatched in case the app
422
+ # hasn't awaited on notified/indicated yet.
423
+ self ._register_with_connection ()
380
424
if cccd := await self .descriptor (bluetooth .UUID (_CCCD_UUID )):
381
425
await cccd .write (struct .pack ("<H" , _CCCD_NOTIFY * notify + _CCCD_INDICATE * indicate ))
382
426
else :
@@ -396,9 +440,12 @@ def __init__(self, characteristic, dsc_handle, uuid):
396
440
# Used for read/write.
397
441
self ._value_handle = dsc_handle
398
442
443
+ # Default flags
444
+ self .properties = _FLAG_READ | _FLAG_WRITE_NO_RESPONSE
445
+
399
446
def __str__ (self ):
400
447
return "Descriptor: {} {} {} {}" .format (
401
- self ._def_handle , self ._value_handle , self ._properties , self .uuid
448
+ self ._def_handle , self ._value_handle , self .properties , self .uuid
402
449
)
403
450
404
451
def _connection (self ):
0 commit comments