Skip to content

Commit b283332

Browse files
authored
Merge branch 'micropython:master' into master
2 parents 25ebe4a + cdd260f commit b283332

19 files changed

+762
-86
lines changed

micropython/bluetooth/aioble/README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ aioble
33

44
This library provides an object-oriented, asyncio-based wrapper for MicroPython's [ubluetooth](https://docs.micropython.org/en/latest/library/ubluetooth.html) API.
55

6-
**Note**: aioble requires MicroPython v1.15 or higher.
6+
**Note**: aioble requires MicroPython v1.17 or higher.
77

88
Features
99
--------
@@ -118,11 +118,30 @@ temp_char = await temp_service.characteristic(_ENV_SENSE_TEMP_UUID)
118118

119119
data = await temp_char.read(timeout_ms=1000)
120120

121-
temp_char.subscribe(notify=True)
121+
await temp_char.subscribe(notify=True)
122122
while True:
123123
data = await temp_char.notified()
124124
```
125125

126+
Open L2CAP channels: (Listener)
127+
128+
```py
129+
channel = await connection.l2cap_accept(_L2CAP_PSN, _L2CAP_MTU)
130+
buf = bytearray(64)
131+
n = channel.recvinto(buf)
132+
channel.send(b'response')
133+
```
134+
135+
Open L2CAP channels: (Initiator)
136+
137+
```py
138+
channel = await connection.l2cap_connect(_L2CAP_PSN, _L2CAP_MTU)
139+
channel.send(b'request')
140+
buf = bytearray(64)
141+
n = channel.recvinto(buf)
142+
```
143+
144+
126145
Examples
127146
--------
128147

micropython/bluetooth/aioble/aioble/central.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,13 @@ def _central_irq(event, data):
8686
connection._event.set()
8787

8888

89-
register_irq_handler(_central_irq)
89+
def _central_shutdown():
90+
global _active_scanner, _connecting
91+
_active_scanner = None
92+
_connecting = set()
93+
94+
95+
register_irq_handler(_central_irq, _central_shutdown)
9096

9197

9298
# Cancel an in-progress scan.

micropython/bluetooth/aioble/aioble/client.py

Lines changed: 95 additions & 48 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
@@ -71,7 +78,7 @@ def _client_irq(event, data):
7178
ClientCharacteristic._on_indicate(conn_handle, value_handle, bytes(indicate_data))
7279

7380

74-
register_irq_handler(_client_irq)
81+
register_irq_handler(_client_irq, None)
7582

7683

7784
# Async generator for discovering services, characteristics, descriptors.
@@ -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,49 +355,72 @@ 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.
379420
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()
380424
if cccd := await self.descriptor(bluetooth.UUID(_CCCD_UUID)):
381425
await cccd.write(struct.pack("<H", _CCCD_NOTIFY * notify + _CCCD_INDICATE * indicate))
382426
else:
@@ -396,9 +440,12 @@ def __init__(self, characteristic, dsc_handle, uuid):
396440
# Used for read/write.
397441
self._value_handle = dsc_handle
398442

443+
# Default flags
444+
self.properties = _FLAG_READ | _FLAG_WRITE_NO_RESPONSE
445+
399446
def __str__(self):
400447
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
402449
)
403450

404451
def _connection(self):

micropython/bluetooth/aioble/aioble/core.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,24 @@ def config(*args, **kwargs):
4343
return ble.config(*args, **kwargs)
4444

4545

46-
def stop():
47-
ble.active(False)
46+
# Because different functionality is enabled by which files are available the
47+
# different modules can register their IRQ handlers and shutdown handlers
48+
# dynamically.
49+
_irq_handlers = []
50+
_shutdown_handlers = []
4851

4952

50-
# Because different functionality is enabled by which files are available
51-
# the different modules can register their IRQ handlers dynamically.
52-
_irq_handlers = []
53+
def register_irq_handler(irq, shutdown):
54+
if irq:
55+
_irq_handlers.append(irq)
56+
if shutdown:
57+
_shutdown_handlers.append(shutdown)
5358

5459

55-
def register_irq_handler(handler):
56-
_irq_handlers.append(handler)
60+
def stop():
61+
ble.active(False)
62+
for handler in _shutdown_handlers:
63+
handler()
5764

5865

5966
# Dispatch IRQs to the registered sub-modules.

micropython/bluetooth/aioble/aioble/device.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def _device_irq(event, data):
2626
device._mtu_event.set()
2727

2828

29-
register_irq_handler(_device_irq)
29+
register_irq_handler(_device_irq, None)
3030

3131

3232
# Context manager to allow an operation to be cancelled by timeout or device

micropython/bluetooth/aioble/aioble/l2cap.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def _l2cap_irq(event, data):
4444
_, _, psm, status = data
4545
channel._status = status
4646
channel._cid = None
47+
connection._l2cap_channel = None
4748
elif event == _IRQ_L2CAP_RECV:
4849
channel._data_ready = True
4950
elif event == _IRQ_L2CAP_SEND_READY:
@@ -53,7 +54,12 @@ def _l2cap_irq(event, data):
5354
channel._event.set()
5455

5556

56-
register_irq_handler(_l2cap_irq)
57+
def _l2cap_shutdown():
58+
global _listening
59+
_listening = False
60+
61+
62+
register_irq_handler(_l2cap_irq, _l2cap_shutdown)
5763

5864

5965
# The channel was disconnected during a send/recvinto/flush.
@@ -155,8 +161,11 @@ async def disconnect(self, timeout_ms=1000):
155161
return
156162

157163
# Wait for the cid to be cleared by the disconnect IRQ.
164+
ble.l2cap_disconnect(self._connection._conn_handle, self._cid)
165+
await self.disconnected(timeout_ms)
166+
167+
async def disconnected(self, timeout_ms=1000):
158168
with self._connection.timeout(timeout_ms):
159-
ble.l2cap_disconnect(self._connection._conn_handle, self._cid)
160169
while self._cid is not None:
161170
await self._event.wait()
162171

micropython/bluetooth/aioble/aioble/peripheral.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,13 @@ def _peripheral_irq(event, data):
6363
connection._event.set()
6464

6565

66-
register_irq_handler(_peripheral_irq)
66+
def _peripheral_shutdown():
67+
global _incoming_connection, _connect_event
68+
_incoming_connection = None
69+
_connect_event = None
70+
71+
72+
register_irq_handler(_peripheral_irq, _peripheral_shutdown)
6773

6874

6975
# Advertising payloads are repeated packets of the following form:

micropython/bluetooth/aioble/aioble/security.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,14 @@ def _security_irq(event, data):
149149
# log_warn("unknown passkey action")
150150

151151

152-
register_irq_handler(_security_irq)
152+
def _security_shutdown():
153+
global _secrets, _modified, _path
154+
_secrets = {}
155+
_modified = False
156+
_path = None
157+
158+
159+
register_irq_handler(_security_irq, _security_shutdown)
153160

154161

155162
# Use device.pair() rather than calling this directly.

0 commit comments

Comments
 (0)