velxio/frontend/src/__tests__/helpers/multiBoardSetup.ts

81 lines
2.6 KiB
TypeScript
Raw Normal View History

feat(multi-board): add wire-aware cross-board interconnect router Fixes the user-reported bug where two RPi Pico W boards wired GP0↔GP1 running SerialPassthrough don't communicate. Replaces the broken broadcast-style cross-board logic in addBoard (only routed AVR↔Pi3B, ignored wires entirely, no RP2040↔anything path) with a wire-aware Interconnect singleton. Architecture: digital pin transitions are the lowest-common-denominator abstraction. Each simulator's hardware peripherals (UART/I2C/SPI) and bit-banging libraries (SoftwareSerial, software I2C) decode the transitions naturally — propagate the pin and the protocols come for free. For cross-process boards (ESP32 backend QEMU, Pi3B QEMU) a byte-level shortcut is additionally enabled on hardware-UART pin pairs to handle high-baud links over WebSocket latency. Implementation: - New simulation/Interconnect.ts singleton subscribes to wire/board changes via the Zustand store. Handlers per tier: browser-sim → pinManager.onPinChange, ESP32 → Esp32Bridge.sendPinEvent, Pi3B → bridge.sendPinEvent. Re-entrancy guard via per-(board,pin) Set. - New utils/boardProtocols.ts classifies pins (uart-tx, i2c-sda, etc.) per board kind, used as optimization hint for the byte shortcut. - types/wire.ts: added signalType field, exports WireSignalType / WireColorMap (fixes a pre-existing TS import error in wireColors). - Deleted the bridgeMap/simulatorMap broadcast forEach blocks in addBoard. Initial board + future boards register with Interconnect via setInterconnectRuntime + store subscription. - PinManager.resetPinStates() helper for test isolation. Tests (16 new files, 96 tests, all passing): - Per-pair × per-protocol matrix: dual-arduino-digital, dual-pico-digital, arduino-pico-digital, triple-pico-digital-chain, dual-arduino-hw-uart, dual-arduino-software-serial, arduino-pico-mixed-uart, arduino-esp32-uart, dual-esp32-uart, pi3-pico-uart, arduino-pico-i2c, arduino-arduino-spi, interconnect-routing, dual-arduino-multi-protocol (UART+I2C+SPI+ digital + concurrent), dual-pico-multi-protocol (UART0+UART1 alt+ I2C0+I2C1+SPI0+digital + 3-Pico star topology) - Updated dual-pico-serial-passthrough to assert correct behaviour - Backend test/multi_board_esp32/test_dual_esp32_serial.py for two real QEMU instances (skip-graceful when lcgamboa lib absent) Verified: 1107/1107 tests pass, zero regressions, vite build OK. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 05:47:28 +07:00
/**
* Shared helpers for cross-board interconnect tests.
*
* NOTE: vi.mock factories must be hoist-safe so the simulator class
* mocks live INLINE in each test file. This module hosts only the
* non-hoisted helpers (wire builders, store reset).
*/
import type { Wire } from '../../types/wire';
// ── Wire builder ─────────────────────────────────────────────────────────────
export interface WireSpec {
id?: string;
fromBoard: string;
fromPin: string;
toBoard: string;
toPin: string;
color?: string;
}
export function buildWire(spec: WireSpec, idx = 0): Wire {
return {
id: spec.id ?? `w-${idx}`,
start: { componentId: spec.fromBoard, pinName: spec.fromPin, x: 0, y: 0 },
end: { componentId: spec.toBoard, pinName: spec.toPin, x: 0, y: 0 },
waypoints: [],
color: spec.color ?? '#00ff00',
};
}
export function setWires(useSimulatorStore: any, specs: WireSpec[]): void {
const wires = specs.map((s, i) => buildWire(s, i));
useSimulatorStore.setState({ wires });
}
export function resetStore(useSimulatorStore: any): void {
useSimulatorStore.setState(useSimulatorStore.getInitialState?.() ?? {});
}
/**
* Clear cached pin states on every currently-bound PinManager. Pass
* the `getBoardPinManager` accessor exported from the store and the
* current board list. Required between tests so that the same-state
* short-circuit in PinManager.triggerPinChange doesn't suppress fresh
* events.
*/
export function clearAllPinManagerState(
useSimulatorStore: any,
getBoardPinManager: (id: string) => any,
): void {
const state = useSimulatorStore.getState();
for (const b of state.boards ?? []) {
const pm = getBoardPinManager(b.id);
pm?.resetPinStates?.();
}
}
/**
* Force a full re-bind of every current board in the store with the
* Interconnect singleton — call this after `resetStore` so that the
* router's `boards` map matches the freshly-reset store state.
*
* Without this the Interconnect can carry stale (boardId → kind)
* bindings across tests and quietly skip routing because `bindBoard`
* is idempotent.
*/
export function rebindInterconnect(
useSimulatorStore: any,
ic: {
resetInterconnect: () => void;
bindBoard: (id: string, kind: string) => void;
updateWires: (wires: readonly any[]) => void;
},
): void {
ic.resetInterconnect();
const { boards, wires } = useSimulatorStore.getState();
for (const b of boards) ic.bindBoard(b.id, b.boardKind);
ic.updateWires(wires);
}