G4: F04 + NEW-A — symmetric send() failure signaling across all transports

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
This commit is contained in:
Matthew Wolter 2026-04-11 20:23:27 -07:00
parent fbf84cbdac
commit e475a567f0
3 changed files with 23 additions and 3 deletions

View file

@ -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."""

View file

@ -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."""

View file

@ -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."""