From e475a567f08460a7936de9cdc796bf9d40082e46 Mon Sep 17 00:00:00 2001 From: Matthew Wolter Date: Sat, 11 Apr 2026 20:23:27 -0700 Subject: [PATCH] =?UTF-8?q?G4:=20F04=20+=20NEW-A=20=E2=80=94=20symmetric?= =?UTF-8?q?=20send()=20failure=20signaling=20across=20all=20transports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: TCP was the only transport that fired _disconnect_callback on a dead transport or send failure. Serial and BLE returned silently, leaving ConnectionManager unaware the link was down. Additionally, transport.write() (TCP/serial) and write_gatt_char (BLE) had no try/except — any write-time exception escaped the send() coroutine unhandled. This commit makes all three transports symmetric: (a) fire _disconnect_callback when transport is None/dead, (b) wrap the write call in try/except and fire _disconnect_callback on failure. Refs: Forensics report findings F04, NEW-A --- src/meshcore/ble_cx.py | 10 +++++++++- src/meshcore/serial_cx.py | 9 ++++++++- src/meshcore/tcp_cx.py | 7 ++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/meshcore/ble_cx.py b/src/meshcore/ble_cx.py index 0ce06d9..f1213c2 100644 --- a/src/meshcore/ble_cx.py +++ b/src/meshcore/ble_cx.py @@ -171,11 +171,19 @@ class BLEConnection: async def send(self, data): if not self.client: logger.error("Client is not connected") + if self._disconnect_callback: + await self._disconnect_callback("ble_transport_lost") return False if not self.rx_char: logger.error("RX characteristic not found") return False - await self.client.write_gatt_char(self.rx_char, bytes(data), response=True) + try: + await self.client.write_gatt_char(self.rx_char, bytes(data), response=True) + except Exception as exc: + logger.warning(f"BLE write failed: {exc}") + if self._disconnect_callback: + await self._disconnect_callback(f"ble_write_failed: {exc}") + return False async def disconnect(self): """Disconnect from the BLE device.""" diff --git a/src/meshcore/serial_cx.py b/src/meshcore/serial_cx.py index 61163bd..24aaef7 100644 --- a/src/meshcore/serial_cx.py +++ b/src/meshcore/serial_cx.py @@ -125,11 +125,18 @@ class SerialConnection: async def send(self, data): if not self.transport: logger.error("Transport not connected, cannot send data") + if self._disconnect_callback: + await self._disconnect_callback("serial_transport_lost") return size = len(data) pkt = b"\x3c" + size.to_bytes(2, byteorder="little") + data logger.debug(f"sending pkt : {pkt}") - self.transport.write(pkt) + try: + self.transport.write(pkt) + except OSError as exc: + logger.warning(f"Serial write failed: {exc}") + if self._disconnect_callback: + await self._disconnect_callback(f"serial_write_failed: {exc}") async def disconnect(self): """Close the serial connection.""" diff --git a/src/meshcore/tcp_cx.py b/src/meshcore/tcp_cx.py index 497c3b2..2a66b7f 100644 --- a/src/meshcore/tcp_cx.py +++ b/src/meshcore/tcp_cx.py @@ -137,7 +137,12 @@ class TCPConnection: size = len(data) pkt = b"\x3c" + size.to_bytes(2, byteorder="little") + data logger.debug(f"sending pkt : {pkt}") - self.transport.write(pkt) + try: + self.transport.write(pkt) + except (OSError, ConnectionResetError) as exc: + logger.warning(f"TCP write failed: {exc}") + if self._disconnect_callback: + await self._disconnect_callback(f"tcp_write_failed: {exc}") async def disconnect(self): """Close the TCP connection."""