From 548005237942921ca0fe363788989695acfe2668 Mon Sep 17 00:00:00 2001 From: David Montero Date: Tue, 26 May 2026 17:08:04 +0200 Subject: [PATCH] fix(multi-board): initSimulator wiped Interconnect's UART wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-board UART forwarding silently broke for any project loaded with > 1 board. User report: Arduino Uno → Arduino Nano serial echo test where the Uno transmits fine but the Nano's Serial.available() is never true. Root cause traced live with chrome-devtools-mcp + temporary debug logs in AVRSimulator.onSerialData setter and Interconnect: 1. loadProjectState → addBoard(uno) → createSimulator → sim.onSerialData = appendSerial 2. addBoard(nano) → same 3. setWires → Interconnect.updateWires → ensureSerialHook(uno) wraps sim.onSerialData with a fan-out callback that ALSO pushes to the Nano's RX queue. __icSerialHookInstalled flag set. 4. SimulatorCanvas mounts → useEffect calls store.initSimulator() 5. initSimulator unconditionally did: simulatorMap.delete(boardId); const sim = createSimulator(...); // ← brand-new sim simulatorMap.set(boardId, sim); // ← Interconnect's wrapper is gone The new sim's onSerialData is just appendSerial. The old sim (where the wrapper lived) has been orphaned; Interconnect never re-installs because its flag was on the discarded sim. 6. Run all boards → Uno.usart.onByteTransmit → this.onSerialData → appendSerial (Uno's monitor shows TX) but no fan-out call → Nano never receives anything. initSimulator is a legacy single-board helper from the days when the store only knew about one MCU. Multi-board flows already create their sims in addBoard. Bail out early if a sim for the active boardId already exists, so the legacy helper becomes a no-op when the multi-board path has already done the work. Verified the 3 related test suites still pass (AVRSimulator, dual-arduino-software-serial, interconnect-routing). --- frontend/src/simulation/AVRSimulator.ts | 8 +------- frontend/src/simulation/Interconnect.ts | 8 ++------ frontend/src/store/useSimulatorStore.ts | 14 ++++++++++++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/frontend/src/simulation/AVRSimulator.ts b/frontend/src/simulation/AVRSimulator.ts index 5b261a6e..5a366c50 100644 --- a/frontend/src/simulation/AVRSimulator.ts +++ b/frontend/src/simulation/AVRSimulator.ts @@ -301,13 +301,7 @@ export class AVRSimulator { private scheduledPinChanges: Array<{ cycle: number; pin: number; state: boolean }> = []; /** Serial output buffer — subscribers receive each byte or line */ - private _onSerialData: ((char: string) => void) | null = null; - public get onSerialData(): ((char: string) => void) | null { return this._onSerialData; } - public set onSerialData(v: ((char: string) => void) | null) { - const who = new Error().stack?.split('\n').slice(1, 6).join(' | '); - console.log('[AVR-DBG] onSerialData SET (fn?', typeof v === 'function', ') from', who); - this._onSerialData = v; - } + public onSerialData: ((char: string) => void) | null = null; /** Fires whenever the sketch changes Serial baud rate (Serial.begin) */ public onBaudRateChange: ((baudRate: number) => void) | null = null; /** diff --git a/frontend/src/simulation/Interconnect.ts b/frontend/src/simulation/Interconnect.ts index f8011c29..667aa765 100644 --- a/frontend/src/simulation/Interconnect.ts +++ b/frontend/src/simulation/Interconnect.ts @@ -174,13 +174,12 @@ function pushPinState(boardId: string, pin: number, state: boolean): void { /** Push a UART byte into the receiving board's UART RX. */ function pushSerialByte(boardId: string, ch: string, uart: number): void { - if (!runtime) { console.log('[IC-DBG] pushSerialByte: no runtime'); return; } + if (!runtime) return; const entry = boards.get(boardId); - if (!entry) { console.log('[IC-DBG] pushSerialByte: no entry for', boardId); return; } + if (!entry) return; if (isBrowserSim(entry.kind)) { const sim = runtime.getBoardSimulator(boardId); - console.log('[IC-DBG] pushSerialByte to', boardId, 'ch=', JSON.stringify(ch), 'sim=', !!sim, 'feedUart=', !!sim?.feedUart, 'serialWrite=', !!sim?.serialWrite); // RP2040Simulator doesn't yet expose feedUart per-UART — fall back // to serialWrite (which feeds UART0) for uart === 0. if (sim?.feedUart) { @@ -287,7 +286,6 @@ function ensureSerialHook(entry: BoardEntry): void { if ((sim as any).__icSerialHookInstalled) return; (sim as any).__icSerialHookInstalled = true; entry.origSerialCallback = sim.onSerialData ?? null; - console.log('[IC-DBG] ensureSerialHook installed on', boardId, 'origCb=', !!entry.origSerialCallback); sim.onSerialData = (ch: string, uart?: number) => { const liveEntry = boards.get(boardId); liveEntry?.origSerialCallback?.(ch, uart); @@ -295,7 +293,6 @@ function ensureSerialHook(entry: BoardEntry): void { // same callback. Default to UART0 for routing. const u = uart ?? 0; const subs = liveEntry?.serialFanout.get(u); - console.log('[IC-DBG] wrapper invoked for', boardId, 'ch=', JSON.stringify(ch), 'subs=', subs ? subs.size : 'none'); if (subs) for (const cb of subs) cb(ch); }; return; @@ -388,7 +385,6 @@ function buildRouteForWire(wire: Wire): RouteHandle | null { const aRoleIsTx = classifyPin(aEntry.kind, wire.start.pinName).kind === 'uart-tx'; const aUart = uartInfo.uartA; const bUart = uartInfo.uartB; - console.log('[IC-DBG] UART route built:', aEntry.id, '(tx?', aRoleIsTx, ')', '<->', bEntry.id, 'uartA=', aUart, 'uartB=', bUart); if (aRoleIsTx) { // A.TX → B.RX teardowns.push(installSerialFanout(aEntry.id, aUart, bEntry.id, bUart)); diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index 9e0ec36c..a435816d 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -1787,8 +1787,18 @@ export const useSimulatorStore = create((set, get) => { const boardId = activeBoardId ?? INITIAL_BOARD_ID; const pm = getBoardPinManager(boardId) ?? legacyPinManager; - getBoardSimulator(boardId)?.stop(); - simulatorMap.delete(boardId); + // Multi-board flows (addBoard, loadProjectState) already create + // sims + register them in simulatorMap, AND Interconnect wraps + // sim.onSerialData for cross-board UART forwarding. SimulatorCanvas + // runs initSimulator() once on mount as a legacy single-board + // "make sure a sim exists for the active board" helper. If we let + // it through here when a sim ALREADY exists we wipe simulatorMap, + // recreate the sim, and silently drop the Interconnect wrapper — + // every cross-board wire stops forwarding bytes (Nano never sees + // anything the Uno sends). Skip out early in that case. + const existingSim = getBoardSimulator(boardId); + if (existingSim) return; + getEsp32Bridge(boardId)?.disconnect(); esp32BridgeMap.delete(boardId);