fix(multi-board): initSimulator wiped Interconnect's UART wrapper

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).
This commit is contained in:
David Montero 2026-05-26 17:08:04 +02:00
parent 1663236184
commit 5480052379
3 changed files with 15 additions and 15 deletions

View File

@ -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;
/**

View File

@ -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));

View File

@ -1787,8 +1787,18 @@ export const useSimulatorStore = create<SimulatorState>((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);