From ae0aa33dc8dfc8b68f0ff9fbf88383d1e0a7f509 Mon Sep 17 00:00:00 2001 From: Matthew Wolter Date: Sat, 11 Apr 2026 19:47:49 -0700 Subject: [PATCH] =?UTF-8?q?G3:=20F02=20=E2=80=94=20inject=20reconnect=20ca?= =?UTF-8?q?llback=20for=20send=5Fappstart=20after=20reconnect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ConnectionManager._attempt_reconnect called self.connection.connect() directly, bypassing MeshCore.connect() which runs send_appstart(). Firmware requires CMD_APP_START after every transport-level connection to initialize the session. Without it, the reconnected transport has no active session — sends go unanswered, tcp_no_response fires after 5 attempts, handle_disconnect re-enters _attempt_reconnect, and the reconnect storm begins. Fix: add an optional reconnect_callback parameter to ConnectionManager.__init__. MeshCore passes self._on_reconnect which calls send_appstart() after the transport reconnects. The callback is invoked inside _attempt_reconnect immediately after a successful connect(), before the CONNECTED event is emitted. Callback failures are logged as warnings but do not break the reconnect — the transport is up regardless. Default None keeps the API backwards-compatible for direct ConnectionManager users. Refs: Forensics report finding F02 --- src/meshcore/connection_manager.py | 12 ++++++++++++ src/meshcore/meshcore.py | 20 ++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/meshcore/connection_manager.py b/src/meshcore/connection_manager.py index ec02e72..bcd09ff 100644 --- a/src/meshcore/connection_manager.py +++ b/src/meshcore/connection_manager.py @@ -48,11 +48,13 @@ class ConnectionManager: event_dispatcher=None, auto_reconnect: bool = False, max_reconnect_attempts: int = 3, + reconnect_callback: Optional[Callable[[], Awaitable[None]]] = None, ): self.connection = connection self.event_dispatcher = event_dispatcher self.auto_reconnect = auto_reconnect self.max_reconnect_attempts = max_reconnect_attempts + self._reconnect_callback = reconnect_callback self._reconnect_attempts = 0 self._is_connected = False @@ -139,6 +141,16 @@ class ConnectionManager: if result is not None: self._is_connected = True self._reconnect_attempts = 0 + + # Invoke reconnect callback (e.g. send_appstart) if provided + if self._reconnect_callback is not None: + try: + await self._reconnect_callback() + except Exception as cb_err: + logger.warning( + f"Reconnect callback failed: {cb_err}" + ) + await self._emit_event( EventType.CONNECTED, {"connection_info": result, "reconnected": True}, diff --git a/src/meshcore/meshcore.py b/src/meshcore/meshcore.py index bdd0db3..af1cfa4 100644 --- a/src/meshcore/meshcore.py +++ b/src/meshcore/meshcore.py @@ -28,10 +28,17 @@ class MeshCore: auto_reconnect: bool = False, max_reconnect_attempts: int = 3, ): - # Wrap connection with ConnectionManager + # Wrap connection with ConnectionManager. + # The reconnect callback ensures send_appstart() runs after every + # transport-level reconnect, which is required by firmware to + # initialize the session (F02). self.dispatcher = EventDispatcher() self.connection_manager = ConnectionManager( - cx, self.dispatcher, auto_reconnect, max_reconnect_attempts + cx, + self.dispatcher, + auto_reconnect, + max_reconnect_attempts, + reconnect_callback=self._on_reconnect, ) self.cx = self.connection_manager # For backward compatibility @@ -174,6 +181,15 @@ class MeshCore: return None return mc + async def _on_reconnect(self): + """Callback invoked by ConnectionManager after a successful reconnect. + + Firmware requires CMD_APP_START after every transport-level connection + to initialize the session. MeshCore.connect() does this on the initial + connection; this callback ensures it also happens on reconnects (F02). + """ + await self.commands.send_appstart() + async def connect(self): await self.dispatcher.start() result = await self.connection_manager.connect()