From a51c7371fb1b3530c1481403012d25087af941cc Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Tue, 7 Dec 2021 10:58:20 +1100 Subject: [PATCH 1/2] aioble: Use DeviceDisconnectedError for disconnected guards. 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 --- micropython/bluetooth/aioble/aioble/device.py | 2 +- micropython/bluetooth/aioble/aioble/l2cap.py | 6 +++--- micropython/bluetooth/aioble/aioble/server.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/micropython/bluetooth/aioble/aioble/device.py b/micropython/bluetooth/aioble/aioble/device.py index 93819bc1e..feece62ea 100644 --- a/micropython/bluetooth/aioble/aioble/device.py +++ b/micropython/bluetooth/aioble/aioble/device.py @@ -273,7 +273,7 @@ def timeout(self, timeout_ms): async def exchange_mtu(self, mtu=None, timeout_ms=1000): if not self.is_connected(): - raise ValueError("Not connected") + raise DeviceDisconnectedError if mtu: ble.config(mtu=mtu) diff --git a/micropython/bluetooth/aioble/aioble/l2cap.py b/micropython/bluetooth/aioble/aioble/l2cap.py index 397b7ceb6..8330f6535 100644 --- a/micropython/bluetooth/aioble/aioble/l2cap.py +++ b/micropython/bluetooth/aioble/aioble/l2cap.py @@ -6,7 +6,7 @@ import asyncio from .core import ble, log_error, register_irq_handler -from .device import DeviceConnection +from .device import DeviceConnection, DeviceDisconnectedError _IRQ_L2CAP_ACCEPT = const(22) @@ -63,7 +63,7 @@ def _l2cap_shutdown(): # The channel was disconnected during a send/recvinto/flush. -class L2CAPDisconnectedError(Exception): +class L2CAPDisconnectedError(DeviceDisconnectedError): pass @@ -75,7 +75,7 @@ class L2CAPConnectionError(Exception): class L2CAPChannel: def __init__(self, connection): if not connection.is_connected(): - raise ValueError("Not connected") + raise L2CAPDisconnectedError if connection._l2cap_channel: raise ValueError("Already has channel") diff --git a/micropython/bluetooth/aioble/aioble/server.py b/micropython/bluetooth/aioble/aioble/server.py index 5d5d7399b..392381923 100644 --- a/micropython/bluetooth/aioble/aioble/server.py +++ b/micropython/bluetooth/aioble/aioble/server.py @@ -15,7 +15,7 @@ register_irq_handler, GattError, ) -from .device import DeviceConnection, DeviceTimeout +from .device import DeviceConnection, DeviceDisconnectedError, DeviceTimeout _registered_characteristics = {} @@ -263,7 +263,7 @@ async def indicate(self, connection, data=None, timeout_ms=1000): if self._indicate_connection is not None: raise ValueError("In progress") if not connection.is_connected(): - raise ValueError("Not connected") + raise DeviceDisconnectedError self._indicate_connection = connection self._indicate_status = None From 14d34e8a70cc70a44e70df59645f3f94454c912f Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Tue, 24 Mar 2026 14:33:12 +1100 Subject: [PATCH 2/2] aioble/l2cap: Convert OSError EINVAL to L2CAPDisconnectedError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- micropython/bluetooth/aioble/aioble/l2cap.py | 38 +++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/micropython/bluetooth/aioble/aioble/l2cap.py b/micropython/bluetooth/aioble/aioble/l2cap.py index 8330f6535..02cb74c35 100644 --- a/micropython/bluetooth/aioble/aioble/l2cap.py +++ b/micropython/bluetooth/aioble/aioble/l2cap.py @@ -4,6 +4,7 @@ from micropython import const import asyncio +import errno from .core import ble, log_error, register_irq_handler from .device import DeviceConnection, DeviceDisconnectedError @@ -118,10 +119,17 @@ async def recvinto(self, buf, timeout_ms=None): self._assert_connected() # Extract up to len(buf) bytes from the channel buffer. - n = ble.l2cap_recvinto(self._connection._conn_handle, self._cid, buf) + try: + n = ble.l2cap_recvinto(self._connection._conn_handle, self._cid, buf) - # Check if there's still remaining data in the channel buffers. - self._data_ready = ble.l2cap_recvinto(self._connection._conn_handle, self._cid, None) > 0 + # Check if there's still remaining data in the channel buffers. + self._data_ready = ( + ble.l2cap_recvinto(self._connection._conn_handle, self._cid, None) > 0 + ) + except OSError as e: + if e.errno == errno.EINVAL: + raise L2CAPDisconnectedError() + raise return n @@ -141,11 +149,16 @@ async def send(self, buf, timeout_ms=None, chunk_size=None): await self.flush(timeout_ms) # l2cap_send returns True if you can send immediately. self._assert_connected() - self._stalled = not ble.l2cap_send( - self._connection._conn_handle, - self._cid, - mv[offset : offset + chunk_size], - ) + try: + self._stalled = not ble.l2cap_send( + self._connection._conn_handle, + self._cid, + mv[offset : offset + chunk_size], + ) + except OSError as e: + if e.errno == errno.EINVAL: + raise L2CAPDisconnectedError() + raise offset += chunk_size async def flush(self, timeout_ms=None): @@ -161,7 +174,14 @@ async def disconnect(self, timeout_ms=1000): return # Wait for the cid to be cleared by the disconnect IRQ. - ble.l2cap_disconnect(self._connection._conn_handle, self._cid) + try: + ble.l2cap_disconnect(self._connection._conn_handle, self._cid) + except OSError as e: + if e.errno == errno.EINVAL: + # Channel already closed by peer — treat as disconnected. + self._cid = None + return + raise await self.disconnected(timeout_ms) async def disconnected(self, timeout_ms=1000):