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:
parent
1663236184
commit
5480052379
|
|
@ -301,13 +301,7 @@ export class AVRSimulator {
|
||||||
private scheduledPinChanges: Array<{ cycle: number; pin: number; state: boolean }> = [];
|
private scheduledPinChanges: Array<{ cycle: number; pin: number; state: boolean }> = [];
|
||||||
|
|
||||||
/** Serial output buffer — subscribers receive each byte or line */
|
/** Serial output buffer — subscribers receive each byte or line */
|
||||||
private _onSerialData: ((char: string) => void) | null = null;
|
public 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;
|
|
||||||
}
|
|
||||||
/** Fires whenever the sketch changes Serial baud rate (Serial.begin) */
|
/** Fires whenever the sketch changes Serial baud rate (Serial.begin) */
|
||||||
public onBaudRateChange: ((baudRate: number) => void) | null = null;
|
public onBaudRateChange: ((baudRate: number) => void) | null = null;
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -174,13 +174,12 @@ function pushPinState(boardId: string, pin: number, state: boolean): void {
|
||||||
|
|
||||||
/** Push a UART byte into the receiving board's UART RX. */
|
/** Push a UART byte into the receiving board's UART RX. */
|
||||||
function pushSerialByte(boardId: string, ch: string, uart: number): void {
|
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);
|
const entry = boards.get(boardId);
|
||||||
if (!entry) { console.log('[IC-DBG] pushSerialByte: no entry for', boardId); return; }
|
if (!entry) return;
|
||||||
|
|
||||||
if (isBrowserSim(entry.kind)) {
|
if (isBrowserSim(entry.kind)) {
|
||||||
const sim = runtime.getBoardSimulator(boardId);
|
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
|
// RP2040Simulator doesn't yet expose feedUart per-UART — fall back
|
||||||
// to serialWrite (which feeds UART0) for uart === 0.
|
// to serialWrite (which feeds UART0) for uart === 0.
|
||||||
if (sim?.feedUart) {
|
if (sim?.feedUart) {
|
||||||
|
|
@ -287,7 +286,6 @@ function ensureSerialHook(entry: BoardEntry): void {
|
||||||
if ((sim as any).__icSerialHookInstalled) return;
|
if ((sim as any).__icSerialHookInstalled) return;
|
||||||
(sim as any).__icSerialHookInstalled = true;
|
(sim as any).__icSerialHookInstalled = true;
|
||||||
entry.origSerialCallback = sim.onSerialData ?? null;
|
entry.origSerialCallback = sim.onSerialData ?? null;
|
||||||
console.log('[IC-DBG] ensureSerialHook installed on', boardId, 'origCb=', !!entry.origSerialCallback);
|
|
||||||
sim.onSerialData = (ch: string, uart?: number) => {
|
sim.onSerialData = (ch: string, uart?: number) => {
|
||||||
const liveEntry = boards.get(boardId);
|
const liveEntry = boards.get(boardId);
|
||||||
liveEntry?.origSerialCallback?.(ch, uart);
|
liveEntry?.origSerialCallback?.(ch, uart);
|
||||||
|
|
@ -295,7 +293,6 @@ function ensureSerialHook(entry: BoardEntry): void {
|
||||||
// same callback. Default to UART0 for routing.
|
// same callback. Default to UART0 for routing.
|
||||||
const u = uart ?? 0;
|
const u = uart ?? 0;
|
||||||
const subs = liveEntry?.serialFanout.get(u);
|
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);
|
if (subs) for (const cb of subs) cb(ch);
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
|
|
@ -388,7 +385,6 @@ function buildRouteForWire(wire: Wire): RouteHandle | null {
|
||||||
const aRoleIsTx = classifyPin(aEntry.kind, wire.start.pinName).kind === 'uart-tx';
|
const aRoleIsTx = classifyPin(aEntry.kind, wire.start.pinName).kind === 'uart-tx';
|
||||||
const aUart = uartInfo.uartA;
|
const aUart = uartInfo.uartA;
|
||||||
const bUart = uartInfo.uartB;
|
const bUart = uartInfo.uartB;
|
||||||
console.log('[IC-DBG] UART route built:', aEntry.id, '(tx?', aRoleIsTx, ')', '<->', bEntry.id, 'uartA=', aUart, 'uartB=', bUart);
|
|
||||||
if (aRoleIsTx) {
|
if (aRoleIsTx) {
|
||||||
// A.TX → B.RX
|
// A.TX → B.RX
|
||||||
teardowns.push(installSerialFanout(aEntry.id, aUart, bEntry.id, bUart));
|
teardowns.push(installSerialFanout(aEntry.id, aUart, bEntry.id, bUart));
|
||||||
|
|
|
||||||
|
|
@ -1787,8 +1787,18 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
||||||
const boardId = activeBoardId ?? INITIAL_BOARD_ID;
|
const boardId = activeBoardId ?? INITIAL_BOARD_ID;
|
||||||
const pm = getBoardPinManager(boardId) ?? legacyPinManager;
|
const pm = getBoardPinManager(boardId) ?? legacyPinManager;
|
||||||
|
|
||||||
getBoardSimulator(boardId)?.stop();
|
// Multi-board flows (addBoard, loadProjectState) already create
|
||||||
simulatorMap.delete(boardId);
|
// 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();
|
getEsp32Bridge(boardId)?.disconnect();
|
||||||
esp32BridgeMap.delete(boardId);
|
esp32BridgeMap.delete(boardId);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue