From fdbc37b69b6f912bcd1b2bd559be594c3657529f Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Tue, 17 Mar 2026 02:28:08 -0300 Subject: [PATCH] feat: enhance WebSocket error handling and cleanup logic in simulation and ESP32 libraries --- backend/app/api/routes/simulation.py | 30 ++++++++++++++++------- backend/app/services/esp32_lib_manager.py | 16 +++++++++++- frontend/src/store/useSimulatorStore.ts | 14 +++++++++++ 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/backend/app/api/routes/simulation.py b/backend/app/api/routes/simulation.py index a2f0cab..46f1a97 100644 --- a/backend/app/api/routes/simulation.py +++ b/backend/app/api/routes/simulation.py @@ -40,8 +40,14 @@ async def simulation_websocket(websocket: WebSocket, client_id: str): logger.info('[%s] system event: %s', client_id, data.get('event')) elif event_type == 'error': logger.error('[%s] error: %s', client_id, data.get('message')) + elif event_type == 'serial_output': + text = data.get('data', '') + logger.info('[%s] serial_output uart=%s len=%d: %r', client_id, data.get('uart', 0), len(text), text[:80]) payload = json.dumps({'type': event_type, 'data': data}) - await manager.send(client_id, payload) + try: + await manager.send(client_id, payload) + except Exception as _send_exc: + logger.debug('[%s] qemu_callback send failed (%s): %s', client_id, event_type, _send_exc) def _use_lib() -> bool: return esp_lib_manager.is_available() @@ -177,13 +183,19 @@ async def simulation_websocket(websocket: WebSocket, client_id: str): ) except WebSocketDisconnect: - manager.disconnect(client_id) - qemu_manager.stop_instance(client_id) - await esp_lib_manager.stop_instance(client_id) - esp_qemu_manager.stop_instance(client_id) + # Guard: only clean up if this coroutine still owns the connection for client_id. + # A newer simulation_websocket may have already connected and replaced us. + if manager.active_connections.get(client_id) is websocket: + manager.disconnect(client_id) + qemu_manager.stop_instance(client_id) + await esp_lib_manager.stop_instance(client_id) + esp_qemu_manager.stop_instance(client_id) + else: + logger.info('[%s] old WS session ended; newer session is active — skipping cleanup', client_id) except Exception as exc: logger.error('WebSocket error for %s: %s', client_id, exc) - manager.disconnect(client_id) - qemu_manager.stop_instance(client_id) - await esp_lib_manager.stop_instance(client_id) - esp_qemu_manager.stop_instance(client_id) + if manager.active_connections.get(client_id) is websocket: + manager.disconnect(client_id) + qemu_manager.stop_instance(client_id) + await esp_lib_manager.stop_instance(client_id) + esp_qemu_manager.stop_instance(client_id) diff --git a/backend/app/services/esp32_lib_manager.py b/backend/app/services/esp32_lib_manager.py index 6fbcddd..dd29165 100644 --- a/backend/app/services/esp32_lib_manager.py +++ b/backend/app/services/esp32_lib_manager.py @@ -160,7 +160,10 @@ class EspLibManager: logger.info('Launching esp32_worker for %s (machine=%s, script=%s, python=%s)', client_id, machine, _WORKER_SCRIPT, sys.executable) - await callback('system', {'event': 'booting'}) + try: + await callback('system', {'event': 'booting'}) + except Exception as exc: + logger.warning('start_instance %s: booting event delivery failed: %s', client_id, exc) try: proc = subprocess.Popen( @@ -362,6 +365,17 @@ class EspLibManager: raw = raw.strip() if not raw: continue + # With -nographic, serial0 is connected to the stdio mux so + # qemu_chr_fe_write() writes the raw UART byte to fd 1 just + # before picsimlab_uart_tx_event emits the JSON line. Strip + # any prefix bytes before the JSON object marker. + idx = raw.find(b'{"type":') + if idx > 0: + raw = raw[idx:] + elif idx < 0: + logger.debug('[%s] ignoring non-JSON worker line: %s', + client_id, raw[:200]) + continue try: event = json.loads(raw) except Exception: diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index d780647..c732dda 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -307,6 +307,13 @@ export const useSimulatorStore = create((set, get) => { bridge.onCrash = () => { set({ esp32CrashBoardId: id }); }; + bridge.onDisconnected = () => { + set((s) => { + const boards = s.boards.map((b) => b.id === id ? { ...b, running: false } : b); + const isActive = s.activeBoardId === id; + return { boards, ...(isActive ? { running: false } : {}) }; + }); + }; bridge.onLedcUpdate = (update) => { // Route LEDC duty cycles to PinManager as PWM (0.0–1.0). // If gpio is known (from GPIO out_sel sync), use the actual GPIO pin; @@ -619,6 +626,13 @@ export const useSimulatorStore = create((set, get) => { if (boardPm) boardPm.triggerPinChange(gpioPin, state); }; bridge.onCrash = () => { set({ esp32CrashBoardId: boardId }); }; + bridge.onDisconnected = () => { + set((s) => { + const boards = s.boards.map((b) => b.id === boardId ? { ...b, running: false } : b); + const isActive = s.activeBoardId === boardId; + return { boards, ...(isActive ? { running: false } : {}) }; + }); + }; bridge.onLedcUpdate = (update) => { const boardPm = pinManagerMap.get(boardId); if (boardPm && typeof boardPm.updatePwm === 'function') {