Skip to content

aioble/server: Support singleton re-registration across restart cycles#1082

Open
andrewleech wants to merge 1 commit intomicropython:masterfrom
andrewleech:aioble-fix-init-capture-guard-mismatch
Open

aioble/server: Support singleton re-registration across restart cycles#1082
andrewleech wants to merge 1 commit intomicropython:masterfrom
andrewleech:aioble-fix-init-capture-guard-mismatch

Conversation

@andrewleech
Copy link
Contributor

@andrewleech andrewleech commented Feb 18, 2026

Summary

The current aioble pattern requires creating new Service and Characteristic objects for each BLE restart cycle. On memory-constrained targets (e.g. STM32WB55 with ~100KB heap), this repeated allocation and deallocation fragments the heap over time, eventually leading to MemoryError during long-running operation.

This PR enables a singleton pattern where service and characteristic objects are created once at boot and re-registered via register_services() after each radio restart. This eliminates per-cycle GATT allocations and keeps the heap stable across BLE lifecycle events.

Three issues in server.py currently prevent this pattern from working:

sequenceDiagram
    participant App
    participant aioble
    participant BLE Radio

    Note over App: Create services + characteristics once

    loop Each BLE restart cycle
        App->>aioble: register_services(*services)
        Note over aioble: _register() writes initial= values<br/>to GATT database
        aioble->>BLE Radio: gatts_register_services()
        App->>aioble: advertise()
        Note over App,BLE Radio: Connection / data exchange

        App->>aioble: aioble.stop()
        Note over aioble: _server_shutdown()

        rect rgb(255, 230, 230)
            Note over aioble: Before this PR:<br/>• initial= values consumed on first cycle<br/>• written() callers blocked forever<br/>• capture task destroyed, never rebuilt
        end

        rect rgb(230, 255, 230)
            Note over aioble: After this PR:<br/>• initial= values preserved for next cycle<br/>• written() callers get DeviceDisconnectedError<br/>• capture infrastructure auto-rebuilt
        end
    end
Loading

Issues fixed

1. initial= values consumed after first registration

_register() writes the initial value to the GATT database then sets self._initial = None. When the same object is re-registered on the next cycle, the value is gone — reads return empty bytes.

Fix: don't clear _initial after writing. The value is idempotently re-written on each registration.

2. written() callers block forever after shutdown

_server_shutdown() clears the characteristic registry but never wakes tasks blocked on await characteristic.written(). With singleton objects these tasks persist across cycles, holding memory and never completing.

Fix: before clearing the registry, iterate all registered characteristics, set their write events (to wake blocked callers), and invalidate their value handles. After waking, written() detects the invalidated handle and raises DeviceDisconnectedError, giving callers a clean signal to exit or loop back.

3. Capture infrastructure destroyed without rebuild

_server_shutdown() deletes the capture task, queue, and events. On the next cycle, _init_capture() is never called because Characteristic.__init__() only runs at object creation time — which already happened on cycle 1.

Fix: register_services() scans for capture-enabled characteristics after registration and calls _init_capture() if needed.

Testing

  • Added ble_reregister.py multitest exercising 3 stop/start cycles with singleton services, verifying initial= values persist and writable characteristics accept new values after each re-registration.
  • Tested on STM32WB55 hardware with repeated BLE restart cycles under the singleton pattern.
  • Tested on unix port with direct state manipulation of shutdown/re-registration sequences.

@andrewleech andrewleech force-pushed the aioble-fix-init-capture-guard-mismatch branch from 6b70880 to 6cafc0e Compare February 18, 2026 02:24
@andrewleech andrewleech changed the title aioble/server: Fix _init_capture() guard to match _server_shutdown() aioble/server: Fix _init_capture() guard to match _server_shutdown(). Feb 18, 2026
@andrewleech andrewleech force-pushed the aioble-fix-init-capture-guard-mismatch branch from 6cafc0e to 87bc782 Compare February 26, 2026 03:55
@andrewleech andrewleech changed the title aioble/server: Fix _init_capture() guard to match _server_shutdown(). aioble/server: Support singleton service re-registration across BLE restart cycles Feb 26, 2026
@andrewleech andrewleech force-pushed the aioble-fix-init-capture-guard-mismatch branch 4 times, most recently from 40e70da to 99cd4b6 Compare February 26, 2026 04:22
@andrewleech andrewleech changed the title aioble/server: Support singleton service re-registration across BLE restart cycles aioble/server: Support singleton re-registration across restart cycles Feb 26, 2026
Enable a pattern where Service and Characteristic objects are created
once and re-registered via register_services() after each BLE radio
restart, avoiding per-cycle heap allocations on constrained targets.

Fix _register() consuming initial= values after first registration.
Fix _server_shutdown() not waking tasks blocked in written().
Fix _init_capture() guard mismatch with _server_shutdown() preventing
capture infrastructure rebuild after shutdown.
Add register_services() auto-rebuild of capture infrastructure.
Add ble_reregister multitest covering 3 stop/start cycles.

Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
@andrewleech andrewleech force-pushed the aioble-fix-init-capture-guard-mismatch branch from 99cd4b6 to 7c22e42 Compare February 26, 2026 05:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants