Skip to content

aioble: Use DeviceDisconnectedError for disconnected guards.#472

Open
andrewleech wants to merge 2 commits intomicropython:masterfrom
andrewleech:l2cap_disconnect
Open

aioble: Use DeviceDisconnectedError for disconnected guards.#472
andrewleech wants to merge 2 commits intomicropython:masterfrom
andrewleech:l2cap_disconnect

Conversation

@andrewleech
Copy link
Copy Markdown
Contributor

@andrewleech andrewleech commented Dec 7, 2021

Summary

When a BLE peer disconnects during L2CAP operations, callers get a mix of ValueError("Not connected"), bare L2CAPDisconnectedError(Exception), and OSError: [Errno 22] EINVAL depending on exactly which code path detects the disconnection first. Application code that catches DeviceDisconnectedError for clean disconnect handling (as the l2cap_file_server example does) misses all three.

This PR unifies the disconnect signalling:

  • L2CAPDisconnectedError becomes a subclass of DeviceDisconnectedError, so except DeviceDisconnectedError catches mid-operation L2CAP disconnections.
  • ValueError("Not connected") guards in L2CAPChannel.__init__, DeviceConnection.exchange_mtu, and Characteristic.indicate are replaced with the appropriate DeviceDisconnectedError (or subclass).
  • OSError(EINVAL) from the low-level ble.l2cap_send(), ble.l2cap_recvinto(), and ble.l2cap_disconnect() calls is caught and converted to L2CAPDisconnectedError. The BLE stack returns EINVAL when the connection handle or CID is no longer valid — semantically identical to a disconnection but previously not converted. disconnect() additionally sets _cid = None and returns cleanly since the channel is already gone.

Testing

Tested on STM32WB55 with an OTS (Object Transfer Service) file transfer interrupted by client disconnect. Before: three OSError: [Errno 22] EINVAL ERROR tracebacks per disconnect cycle. After: single INFO log "Transfer incomplete due to client disconnect".

Trade-offs and Alternatives

Could catch all OSError rather than checking errno == EINVAL specifically, but EINVAL is the only errno observed for this failure mode and catching broadly could mask real errors (e.g. ENOMEM).

@andrewleech andrewleech force-pushed the l2cap_disconnect branch 2 times, most recently from 7513416 to 021db78 Compare March 3, 2026 04:44
Replace ValueError("Not connected") with DeviceDisconnectedError in
the is_connected() guards in L2CAPChannel.__init__,
DeviceConnection.exchange_mtu, and Characteristic.indicate.  Callers
already catch DeviceDisconnectedError for disconnect handling; the
ValueError was not semantically correct and required string matching
to distinguish from other ValueErrors.

Also make L2CAPDisconnectedError a subclass of DeviceDisconnectedError
so that mid-operation L2CAP disconnections are caught by the same
handler (e.g. in the l2cap_file_server example).

Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
@andrewleech andrewleech changed the title aioble/l2cap: Catch disconnection and raise as DeviceDisconnectedError. aioble: Use DeviceDisconnectedError for disconnected guards. Mar 3, 2026
When the BLE peer disconnects during an L2CAP transfer, ble.l2cap_send(),
ble.l2cap_recvinto(), and ble.l2cap_disconnect() raise OSError with errno
EINVAL because the connection handle or CID is no longer valid.

Callers catch L2CAPDisconnectedError (now a DeviceDisconnectedError
subclass) for clean disconnect handling, but the raw OSError was not
being converted — it fell through to generic exception handlers.

Wrap all three call sites:
- send(): catch OSError EINVAL, raise L2CAPDisconnectedError
- recvinto(): catch OSError EINVAL, raise L2CAPDisconnectedError
- disconnect(): catch OSError EINVAL, set _cid = None, return cleanly
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