Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions micropython/bluetooth/aioble/aioble/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
register_irq_handler,
GattError,
)
from .device import DeviceConnection, DeviceTimeout
from .device import DeviceConnection, DeviceDisconnectedError, DeviceTimeout

_registered_characteristics = {}

Expand Down Expand Up @@ -56,6 +56,10 @@ def _server_irq(event, data):

def _server_shutdown():
global _registered_characteristics
for characteristic in _registered_characteristics.values():
if hasattr(characteristic, "_write_event"):
characteristic._write_event.set()
characteristic._value_handle = None
_registered_characteristics = {}
if hasattr(BaseCharacteristic, "_capture_task"):
BaseCharacteristic._capture_task.cancel()
Expand Down Expand Up @@ -84,7 +88,6 @@ def _register(self, value_handle):
_registered_characteristics[value_handle] = self
if self._initial is not None:
self.write(self._initial)
self._initial = None

# Read value from local db.
def read(self):
Expand All @@ -100,13 +103,21 @@ def write(self, data, send_update=False):
else:
ble.gatts_write(self._value_handle, data, send_update)

# When the a capture-enabled characteristic is created, create the
# When a capture-enabled characteristic is created, create the
# necessary events (if not already created).
# Guard on _capture_task (not _capture_queue) to match _server_shutdown()
# which guards on _capture_task. This ensures partial teardown (task gone
# but queue remains) self-heals instead of silently no-oping.
@staticmethod
def _init_capture():
if hasattr(BaseCharacteristic, "_capture_queue"):
if hasattr(BaseCharacteristic, "_capture_task"):
return

# Clean up any partial state from incomplete shutdown
for attr in ("_capture_queue", "_capture_write_event", "_capture_consumed_event"):
try:
delattr(BaseCharacteristic, attr)
except AttributeError:
pass
BaseCharacteristic._capture_queue = deque((), _WRITE_CAPTURE_QUEUE_LIMIT)
BaseCharacteristic._capture_write_event = asyncio.ThreadSafeFlag()
BaseCharacteristic._capture_consumed_event = asyncio.ThreadSafeFlag()
Expand Down Expand Up @@ -152,6 +163,9 @@ async def written(self, timeout_ms=None):
with DeviceTimeout(None, timeout_ms):
await self._write_event.wait()

if self._value_handle is None:
raise DeviceDisconnectedError

# Return the write data and clear the stored copy.
# In default usage this will be just the connection handle.
# In capture mode this will be a tuple of (connection_handle, received_data)
Expand Down Expand Up @@ -338,3 +352,8 @@ def register_services(*services):
for descriptor in characteristic.descriptors:
descriptor._register(service_handles[n])
n += 1

for characteristic in _registered_characteristics.values():
if characteristic.flags & _FLAG_WRITE_CAPTURE:
BaseCharacteristic._init_capture()
break
114 changes: 114 additions & 0 deletions micropython/bluetooth/aioble/multitests/ble_reregister.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Test that singleton service/characteristic instances can be re-registered
# across multiple stop/start cycles without data loss.

import sys

# ruff: noqa: E402
sys.path.append("")

from micropython import const
import machine
import time

import asyncio
import aioble
import bluetooth

TIMEOUT_MS = 5000

SERVICE_UUID = bluetooth.UUID("A5A5A5A5-FFFF-9999-1111-5A5A5A5A5A5A")
CHAR_INITIAL_UUID = bluetooth.UUID("00000000-1111-2222-3333-444444444444")
CHAR_WRITE_UUID = bluetooth.UUID("00000000-1111-2222-3333-555555555555")


# Acting in peripheral role.
async def instance0_task():
# Create service and characteristics ONCE (singleton pattern).
service = aioble.Service(SERVICE_UUID)
aioble.Characteristic(service, CHAR_INITIAL_UUID, read=True, initial=b"hello")
char_write = aioble.Characteristic(service, CHAR_WRITE_UUID, read=True, write=True)

multitest.globals(BDADDR=aioble.config("mac"))
multitest.next()

for i in range(3):
# Re-register the same service instances.
aioble.register_services(service)

# Write a cycle-specific value to the writable characteristic.
char_write.write("periph{}".format(i))

multitest.broadcast("connect-{}".format(i))

# Wait for central to connect.
print("advertise", i)
connection = await aioble.advertise(
20_000, adv_data=b"\x02\x01\x06\x04\xffMPY", timeout_ms=TIMEOUT_MS
)
print("connected", i)

# Wait for the central to write.
await char_write.written(timeout_ms=TIMEOUT_MS)
print("written", i)

# Wait for the central to disconnect.
await connection.disconnected(timeout_ms=TIMEOUT_MS)
print("disconnected", i)

# Shutdown aioble.
print("shutdown", i)
aioble.stop()

await asyncio.sleep_ms(100)


def instance0():
try:
asyncio.run(instance0_task())
finally:
aioble.stop()


# Acting in central role.
async def instance1_task():
multitest.next()

for i in range(3):
multitest.wait("connect-{}".format(i))

# Connect to peripheral.
print("connect", i)
device = aioble.Device(*BDADDR)
connection = await device.connect(timeout_ms=TIMEOUT_MS)

# Discover characteristics.
service = await connection.service(SERVICE_UUID)
char_initial = await service.characteristic(CHAR_INITIAL_UUID)
char_write = await service.characteristic(CHAR_WRITE_UUID)

# Read the initial= characteristic — must be the same every cycle.
print("read initial", await char_initial.read(timeout_ms=TIMEOUT_MS))

# Read the writable characteristic — should have cycle-specific value.
print("read written", await char_write.read(timeout_ms=TIMEOUT_MS))

# Write to the writable characteristic.
print("write", i)
await char_write.write("central{}".format(i), response=True, timeout_ms=TIMEOUT_MS)

# Disconnect from peripheral.
print("disconnect", i)
await connection.disconnect(timeout_ms=TIMEOUT_MS)
print("disconnected", i)

# Shutdown aioble.
aioble.stop()

await asyncio.sleep_ms(100)


def instance1():
try:
asyncio.run(instance1_task())
finally:
aioble.stop()
35 changes: 35 additions & 0 deletions micropython/bluetooth/aioble/multitests/ble_reregister.py.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
--- instance0 ---
advertise 0
connected 0
written 0
disconnected 0
shutdown 0
advertise 1
connected 1
written 1
disconnected 1
shutdown 1
advertise 2
connected 2
written 2
disconnected 2
shutdown 2
--- instance1 ---
connect 0
read initial b'hello'
read written b'periph0'
write 0
disconnect 0
disconnected 0
connect 1
read initial b'hello'
read written b'periph1'
write 1
disconnect 1
disconnected 1
connect 2
read initial b'hello'
read written b'periph2'
write 2
disconnect 2
disconnected 2
Loading