From b6abca50d8e7d900a7ab798d4d51bcf3696eb598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Tue, 11 Aug 2026 14:50:40 -0300 Subject: [PATCH 1/2] core: services: ardupilot_manager: Fix serial autopilot restart reconnect After a Pixhawk reboot the USB device re-enumerates with a new path while the MAVLink router keeps a stale serial handle. Wait for the board to drop and return, then reopen the link on the freshly detected path. On failure, stop the router and clear should_be_running so the stale handle is not left running with its watchdog disarmed. --- .../ardupilot_manager/autopilot_manager.py | 65 +++++++++++++++++-- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/core/services/ardupilot_manager/autopilot_manager.py b/core/services/ardupilot_manager/autopilot_manager.py index 28d980229b..ad8890eaad 100644 --- a/core/services/ardupilot_manager/autopilot_manager.py +++ b/core/services/ardupilot_manager/autopilot_manager.py @@ -44,6 +44,7 @@ def __init__(self) -> None: self.settings.create_app_folders() self._current_board: Optional[FlightController] = None self.should_be_running = False + self._restart_lock = asyncio.Lock() self.mavlink_manager = MavlinkManager() # Kept out of setup() because that runs on every start attempt, which would reset the counter @@ -695,12 +696,66 @@ async def start_ardupilot(self) -> None: finally: self.should_be_running = True + async def _detect_serial_board(self, board: FlightController) -> Optional[FlightController]: + return next( + ( + detected + for detected in await self.available_boards() + if detected.type == PlatformType.Serial and detected.platform == board.platform and detected.path + ), + None, + ) + async def restart_ardupilot(self) -> None: - if self.current_board is None or self.current_board.type in [PlatformType.SITL, PlatformType.Linux]: - await self.kill_ardupilot() - await self.start_ardupilot() - return - await self.vehicle_manager.reboot_vehicle() + # Both the /restart endpoint and the heartbeat watchdog can call this, so serialize them. + async with self._restart_lock: + board = self.current_board + # Serial boards are the only ones rebooted through MAVLink; everything else (SITL, + # Linux, Manual, unknown) is a process/router we can just bounce. + if board is None or board.type != PlatformType.Serial: + await self.kill_ardupilot() + await self.start_ardupilot() + return + + await self.vehicle_manager.reboot_vehicle() + + # The router watchdog would otherwise reopen the stale path the moment the board drops. + # Stand it down until start_serial re-arms it on the freshly detected path below. + self.mavlink_manager.should_be_running = False + + try: + # A serial board re-enumerates on USB after rebooting, so its device path changes and + # the router keeps a stale handle. Wait for the board to drop first: this confirms the + # reboot landed and stops us from reopening the pre-reboot path. Boards reached through a + # separate USB-serial adapter never drop, so this wait is best-effort. + disconnect_deadline = time.monotonic() + 10.0 + while time.monotonic() < disconnect_deadline: + if await self._detect_serial_board(board) is None: + break + await asyncio.sleep(0.5) + else: + logger.warning(f"{board.name} did not disconnect after reboot. Restarting its link anyway.") + + reconnected_board = None + reconnect_deadline = time.monotonic() + 30.0 + while reconnected_board is None and time.monotonic() < reconnect_deadline: + await asyncio.sleep(0.5) + reconnected_board = await self._detect_serial_board(board) + if reconnected_board is None: + raise RuntimeError(f"Timed out waiting for {board.name} to reconnect after reboot.") + + # Release the stale serial handle before reopening on the new path. We avoid the full + # kill/start cycle so runtime-only endpoints and the current router survive the reboot. + await self.mavlink_manager.stop() + await self.start_serial(reconnected_board) + self.should_be_running = True + except Exception: + try: + await self.mavlink_manager.stop() + except Exception as error: + logger.warning(f"Failed to stop Mavlink manager after serial restart failure: {error}") + self.should_be_running = False + raise def _get_configuration_endpoints(self) -> Set[Endpoint]: return {Endpoint(**endpoint) for endpoint in self.configuration.get("endpoints") or []} From c41ee6405728f8ca8ea1675b369d6ee86a54ca0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Tue, 11 Aug 2026 14:50:40 -0300 Subject: [PATCH 2/2] core: frontend: components: autopilot: Raise restart timeout to 60s Serial board restarts wait for USB re-enumeration before reopening the MAVLink link, which can take tens of seconds. --- .../src/components/autopilot/AutopilotManagerUpdater.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/frontend/src/components/autopilot/AutopilotManagerUpdater.ts b/core/frontend/src/components/autopilot/AutopilotManagerUpdater.ts index 9668811517..260a91360f 100644 --- a/core/frontend/src/components/autopilot/AutopilotManagerUpdater.ts +++ b/core/frontend/src/components/autopilot/AutopilotManagerUpdater.ts @@ -177,7 +177,9 @@ export async function restart(): Promise { return back_axios({ method: 'post', url: `${autopilot.API_URL}/restart`, - timeout: 10000, + // Serial boards reboot over MAVLink and re-enumerate on USB; the backend waits for the board to + // drop and come back before reopening the link, which can take tens of seconds. + timeout: 60000, }) .then((response) => response.data) .catch((error) => {