harden UBX parsing/dispatch and report peer-side disconnects in GUI
A struct size-mismatch or any other parse error in a listener could propagate uncaught out of the socket-reading thread and silently kill it. Catch broadly in the event loop and per-listener in call_listener, and make the NAV-SAT/NAV-SIG/RXM-RAWX group parsers tolerate a payload length that isn't an exact multiple of the group size instead of crashing on a short struct.unpack. Also wire NetworkBackend's socket-loss detection through to ReceiverManager (via a queued signal, since the callback fires from the backend thread) so the GUI reflects a peer-initiated disconnect instead of leaving the row stuck showing "connected". Verified against a real ZED-X20P: it drops the TCP session on its own after ~20-28s regardless of traffic; the GUI now marks the receiver disconnected and frees its ID for reconnecting. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AzmCaNjDb3TAqPpTwunKTY
This commit is contained in:
@@ -10,11 +10,12 @@ from backend.a_backend import ABackend
|
|||||||
|
|
||||||
|
|
||||||
class NetworkBackend(ABackend):
|
class NetworkBackend(ABackend):
|
||||||
def __init__(self, servers: list[dict]):
|
def __init__(self, servers: list[dict], on_disconnect=None):
|
||||||
self.sel = None
|
self.sel = None
|
||||||
self.sock_list = [{'name': s['name'], 'addr': (socket.gethostbyname(s['host']), s['port']), 'xcvr': None, 'queue': Queue()} for s in servers]
|
self.sock_list = [{'name': s['name'], 'addr': (socket.gethostbyname(s['host']), s['port']), 'xcvr': None, 'queue': Queue()} for s in servers]
|
||||||
self.thread = Thread(target=self.event_loop)
|
self.thread = Thread(target=self.event_loop)
|
||||||
self.loop_enable = True
|
self.loop_enable = True
|
||||||
|
self.on_disconnect = on_disconnect
|
||||||
|
|
||||||
def send(self, xcvr: ATransceiver, data: bytes):
|
def send(self, xcvr: ATransceiver, data: bytes):
|
||||||
sock = self.find_by_name(xcvr.name)
|
sock = self.find_by_name(xcvr.name)
|
||||||
@@ -89,6 +90,10 @@ class NetworkBackend(ABackend):
|
|||||||
print(f"Connection {sock['name']} to {sock['addr']} lost: {exc}")
|
print(f"Connection {sock['name']} to {sock['addr']} lost: {exc}")
|
||||||
self.sel.unregister(key.fileobj)
|
self.sel.unregister(key.fileobj)
|
||||||
key.fileobj.close()
|
key.fileobj.close()
|
||||||
|
if self.on_disconnect:
|
||||||
|
self.on_disconnect(sock['name'])
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Connection {sock['name']}: error processing data, ignoring: {exc}")
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("Caught keyboard interrupt, exiting")
|
print("Caught keyboard interrupt, exiting")
|
||||||
|
|||||||
+11
-1
@@ -93,12 +93,14 @@ class ReceiverManager(QObject):
|
|||||||
connection_changed = pyqtSignal(str, bool) # rid, connected
|
connection_changed = pyqtSignal(str, bool) # rid, connected
|
||||||
receiver_added = pyqtSignal(str)
|
receiver_added = pyqtSignal(str)
|
||||||
receiver_removed = pyqtSignal(str)
|
receiver_removed = pyqtSignal(str)
|
||||||
|
_connection_lost = pyqtSignal(str) # internal: backend thread -> GUI thread
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._states: dict = {}
|
self._states: dict = {}
|
||||||
self._configs: dict = {}
|
self._configs: dict = {}
|
||||||
self._lru: list = self._load_lru()
|
self._lru: list = self._load_lru()
|
||||||
|
self._connection_lost.connect(self._handle_connection_lost)
|
||||||
|
|
||||||
# ── LRU ──────────────────────────────────────────────────────────────────
|
# ── LRU ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -164,7 +166,8 @@ class ReceiverManager(QObject):
|
|||||||
|
|
||||||
if config.conn_type == 'tcp':
|
if config.conn_type == 'tcp':
|
||||||
backend = NetworkBackend(
|
backend = NetworkBackend(
|
||||||
[{'name': rid, 'host': config.host, 'port': config.port}]
|
[{'name': rid, 'host': config.host, 'port': config.port}],
|
||||||
|
on_disconnect=lambda name, r=rid: self._connection_lost.emit(r),
|
||||||
)
|
)
|
||||||
elif config.conn_type == 'serial':
|
elif config.conn_type == 'serial':
|
||||||
backend = SerialBackend(config.serial_port, config.baud_rate)
|
backend = SerialBackend(config.serial_port, config.baud_rate)
|
||||||
@@ -211,6 +214,13 @@ class ReceiverManager(QObject):
|
|||||||
self._push_lru(config)
|
self._push_lru(config)
|
||||||
self.connection_changed.emit(rid, True)
|
self.connection_changed.emit(rid, True)
|
||||||
|
|
||||||
|
def _handle_connection_lost(self, rid: str):
|
||||||
|
state = self._states.get(rid)
|
||||||
|
if not state or not state.connected:
|
||||||
|
return
|
||||||
|
print(f"Receiver '{rid}': connection lost")
|
||||||
|
self.remove_receiver(rid)
|
||||||
|
|
||||||
def disconnect_receiver(self, rid: str):
|
def disconnect_receiver(self, rid: str):
|
||||||
state = self._states.get(rid)
|
state = self._states.get(rid)
|
||||||
if not state or not state.connected:
|
if not state or not state.connected:
|
||||||
|
|||||||
@@ -11,5 +11,8 @@ class MsgTalker(ABC):
|
|||||||
|
|
||||||
def call_listener(self, msg: MsgContainer):
|
def call_listener(self, msg: MsgContainer):
|
||||||
for sink in self.sinks:
|
for sink in self.sinks:
|
||||||
|
try:
|
||||||
if sink.is_msg(msg):
|
if sink.is_msg(msg):
|
||||||
sink.on_recv(msg)
|
sink.on_recv(msg)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Listener {sink.__class__.__name__} failed on {msg}: {exc}")
|
||||||
|
|||||||
+6
-2
@@ -34,11 +34,13 @@ class Sig(UbxMsg):
|
|||||||
remain = len(data) - offset
|
remain = len(data) - offset
|
||||||
group_size = 16
|
group_size = 16
|
||||||
n = offset
|
n = offset
|
||||||
while remain:
|
while remain >= group_size:
|
||||||
signal = cls.Group1.from_bytes(data[n:n + group_size])
|
signal = cls.Group1.from_bytes(data[n:n + group_size])
|
||||||
n += group_size
|
n += group_size
|
||||||
remain -= group_size
|
remain -= group_size
|
||||||
obj.signals.append(signal)
|
obj.signals.append(signal)
|
||||||
|
if remain:
|
||||||
|
print(f"Sig.from_bytes: {remain} trailing bytes unparsed (payload size mismatch)")
|
||||||
return obj.__dict__
|
return obj.__dict__
|
||||||
|
|
||||||
class Sat(UbxMsg):
|
class Sat(UbxMsg):
|
||||||
@@ -70,9 +72,11 @@ class Sat(UbxMsg):
|
|||||||
remain = len(data) - offset
|
remain = len(data) - offset
|
||||||
group_size = 12
|
group_size = 12
|
||||||
n = offset
|
n = offset
|
||||||
while remain:
|
while remain >= group_size:
|
||||||
satellite = cls.Group1.from_bytes(data[n:n + group_size])
|
satellite = cls.Group1.from_bytes(data[n:n + group_size])
|
||||||
n += group_size
|
n += group_size
|
||||||
remain -= group_size
|
remain -= group_size
|
||||||
obj.satellites.append(satellite)
|
obj.satellites.append(satellite)
|
||||||
|
if remain:
|
||||||
|
print(f"Sat.from_bytes: {remain} trailing bytes unparsed (payload size mismatch)")
|
||||||
return obj.__dict__
|
return obj.__dict__
|
||||||
|
|||||||
+3
-1
@@ -40,9 +40,11 @@ class RawX(UbxMsg):
|
|||||||
remain = len(data) - offset
|
remain = len(data) - offset
|
||||||
group_size = 32
|
group_size = 32
|
||||||
n = offset
|
n = offset
|
||||||
while remain:
|
while remain >= group_size:
|
||||||
group = cls.Group1.from_bytes(data[n:n + 32])
|
group = cls.Group1.from_bytes(data[n:n + 32])
|
||||||
n += group_size
|
n += group_size
|
||||||
remain -= group_size
|
remain -= group_size
|
||||||
obj.meas.append(group)
|
obj.meas.append(group)
|
||||||
|
if remain:
|
||||||
|
print(f"RawX.from_bytes: {remain} trailing bytes unparsed (payload size mismatch)")
|
||||||
return obj.__dict__
|
return obj.__dict__
|
||||||
|
|||||||
Reference in New Issue
Block a user