velxio/frontend/src/__tests__/pi3-pico-uart.test.ts

191 lines
6.1 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
/**
* Raspberry Pi 3B Raspberry Pi Pico UART across QEMU+browser
* ==============================================================
*
* Pi3B's UART0 default pins on the BCM map: BCM14 (TX, physical pin 8),
* BCM15 (RX, physical pin 10). Pico's UART0: GP0 (TX), GP1 (RX).
*
* Wire: Pi3B.physical8 Pico.GP1, Pico.GP0 Pi3B.physical10.
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
vi.mock('../simulation/AVRSimulator', () => ({
AVRSimulator: vi.fn(function (this: any) {
this.onSerialData = null;
this.onBaudRateChange = null;
this.onPinChangeWithTime = null;
this.start = vi.fn();
this.stop = vi.fn();
this.reset = vi.fn();
this.loadHex = vi.fn();
this.serialWrite = vi.fn();
this.feedUart = vi.fn();
this.addI2CDevice = vi.fn();
this.setPinState = vi.fn();
}),
}));
vi.mock('../simulation/RP2040Simulator', () => ({
RP2040Simulator: vi.fn(function (this: any) {
this.onSerialData = null;
this.onUartByte = null;
this.onPinChangeWithTime = null;
this.start = vi.fn();
this.stop = vi.fn();
this.reset = vi.fn();
this.loadBinary = vi.fn();
this.serialWrite = vi.fn();
this.feedUart = vi.fn();
this.addI2CDevice = vi.fn();
this.setPinState = vi.fn();
feat(opencore): extract Pico W WiFi to a pluggable PIO peripheral seam Move the CYW43439 (Pico W) WiFi emulation out of the open-source tree so it can ship as a paid feature in a private overlay. OSS keeps a plain Pico W (no WiFi); the overlay registers the cyw43 protocol + backend network stack at runtime via generic seams. Frontend: - Add simulation/PioPeripheral.ts: a generic "PIO bus peripheral" seam (feedWord / inDiscardableWriteData / resetFraming / hostWakeLevel / onHostWake / onSimulationStart). No factory is installed in OSS, so createPioPeripheral() returns null and a Pico W simulates as a plain Pico. - RP2040Simulator: keep the fragile PIO-FIFO plumbing (it must re-run after loadMicroPython swaps the chip) but drive it through PioPeripheral instead of an inlined cyw43 import (attachCyw43 -> attachPioPeripheral, etc.). - useSimulatorStore: generic attach/detach + setBoardWifiStatus; drop the cyw43 bridge map. - MicroPythonLoader: add registerFirmwareVariant() so an overlay can add the RPI_PICO_W build; remove the OSS pico-w config + bundled .uf2. - Delete simulation/cyw43/ (moved to the overlay). Backend: - core/hooks.py: add generic register_ws_sim_handler / dispatch_ws_sim_message and register_gateway_proxy / dispatch_gateway_proxy seams. - simulation.py: route start_picow / stop_picow / picow_packet_out through the ws_sim_handler hook (the overlay handles + gates them). - iot_gateway.py: resolve the Pico W gateway through the gateway_proxy hook. - Delete services/picow_net/ + picow_net_bridge.py (moved to the overlay). Tests: move the cyw43/picow suites to the overlay; update RP2040Simulator mock stubs to attachPioPeripheral.
2026-06-15 13:33:28 +07:00
this.attachPioPeripheral = vi.fn();
fix(tests): restore Frontend Tests CI — patch stale RP2040 mocks + install-libraries CI's Frontend Tests workflow had been failing on master for ~15 runs. Two pre-existing issues, neither related to the SPI refactor in 8b1433d or the ESP32-CAM work: 1. RP2040Simulator mock missing attachCyw43 method (23 test files) PR #126 (8e769f8 "feat(multi-board): add wire-aware cross-board interconnect router", merged 2026-04-25) added a Pico-W-specific `sim.attachCyw43(bridge)` call inside addBoard(). The 23 test files that mock RP2040Simulator with vi.fn weren't updated; whenever a test path created a Pico W board the mock threw "TypeError: sim.attachCyw43 is not a function" and aborted addBoard. Fix: add `this.attachCyw43 = vi.fn()` to every affected mock. Also pre-populate `this.spi = { onByte: null, completeTransfer: vi.fn() }` so any future SPI-part tests don't trip on the new generic .spi adapter from 8b1433d. 2. install-libraries.test.ts payload mismatch PR #135 (b1026ec7 "library-version-uninstall", merged 2026-04-29) extended `installLibrary(name)` to `installLibrary(name, version?)` and now sends `{name, version: version ?? null}` over the wire. The test still asserted `{name}` only and failed. Fix: assert `{name, version: null}` for the no-version call. Verified locally: 1161 passed | 1 skipped (was 1117 passed | 44 failed). Backend E2E "Run HC-SR04 e2e test" is a separate failure that needs its own investigation — it downloads QEMU binaries from a release and runs real firmware compilation, which I can't reproduce on Windows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 08:46:32 +07:00
this.spi = { onByte: null, completeTransfer: vi.fn() };
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
}),
}));
vi.mock('../simulation/RiscVSimulator', () => ({
RiscVSimulator: vi.fn(function (this: any) {
this.onSerialData = null;
this.serialWrite = vi.fn();
this.feedUart = vi.fn();
this.start = vi.fn();
this.stop = vi.fn();
this.reset = vi.fn();
this.setPinState = vi.fn();
}),
}));
vi.mock('../simulation/Esp32C3Simulator', () => ({
Esp32C3Simulator: vi.fn(function (this: any) {
this.onSerialData = null;
this.serialWrite = vi.fn();
this.feedUart = vi.fn();
this.start = vi.fn();
this.stop = vi.fn();
this.reset = vi.fn();
this.setPinState = vi.fn();
}),
}));
vi.mock('../simulation/Esp32Bridge', () => ({
Esp32Bridge: vi.fn(function (this: any, _id: string, _kind: string) {
this.onSerialData = null;
this.onPinChange = null;
this.onPinDir = null;
this.onCrash = null;
this.onDisconnected = null;
this.onWs2812Update = null;
this.onWifiStatus = null;
this.onBleStatus = null;
this.onI2cEvent = null;
this.onI2cTransaction = null;
this.onSpiEvent = null;
this.connect = vi.fn();
this.disconnect = vi.fn();
this.connected = true;
this.sendSerialByte = vi.fn();
this.sendSerialBytes = vi.fn();
this.sendPinEvent = vi.fn();
this.setAdc = vi.fn();
this.setAdcWaveform = vi.fn();
this.setI2cResponse = vi.fn();
this.setSpiResponse = vi.fn();
this.sendSensorAttach = vi.fn();
this.sendSensorUpdate = vi.fn();
this.sendSensorDetach = vi.fn();
}),
Esp32BridgeShim: vi.fn(function (this: any) {
this.onSerialData = null;
this.serialWrite = vi.fn();
this.feedUart = vi.fn();
this.setPinState = vi.fn();
this.start = vi.fn();
this.stop = vi.fn();
}),
}));
vi.mock('../simulation/RaspberryPi3Bridge', () => ({
RaspberryPi3Bridge: vi.fn(function (this: any, _id: string) {
this.onSerialData = null;
this.onPinChange = null;
this.onSystemEvent = null;
this.onError = null;
this.connect = vi.fn();
this.disconnect = vi.fn();
this.connected = true;
this.sendSerialByte = vi.fn();
this.sendSerialBytes = vi.fn();
this.sendPinEvent = vi.fn();
}),
}));
vi.mock('../simulation/I2CBusManager', async () => {
const actual = await vi.importActual<typeof import('../simulation/I2CBusManager')>(
'../simulation/I2CBusManager',
);
return actual;
});
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
vi.mock('../store/useOscilloscopeStore', () => ({
useOscilloscopeStore: {
getState: vi.fn().mockReturnValue({ channels: [], pushSample: vi.fn() }),
},
}));
vi.stubGlobal('requestAnimationFrame', (_cb: FrameRequestCallback) => 1);
vi.stubGlobal('cancelAnimationFrame', vi.fn());
import { setWires, resetStore, clearAllPinManagerState } from './helpers/multiBoardSetup';
import { resetInterconnect } from '../simulation/Interconnect';
import {
useSimulatorStore,
getBoardSimulator,
getBoardBridge,
getBoardPinManager,
} from '../store/useSimulatorStore';
function fullReset() {
clearAllPinManagerState(useSimulatorStore, getBoardPinManager);
resetInterconnect();
resetStore(useSimulatorStore);
}
describe('Raspberry Pi 3B ↔ Pico W — UART', () => {
beforeEach(() => {
fullReset();
});
function setupPi3Pico() {
const store = useSimulatorStore.getState();
const piId = store.addBoard('raspberry-pi-3', 100, 100);
const picoId = store.addBoard('pi-pico-w', 400, 100);
setWires(useSimulatorStore, [
// Pi3B physical 8 (BCM14, UART0 TX) → Pico GP1 (UART0 RX)
{ fromBoard: piId, fromPin: '8', toBoard: picoId, toPin: 'GP1' },
// Pico GP0 (UART0 TX) → Pi3B physical 10 (BCM15, UART0 RX)
{ fromBoard: picoId, fromPin: 'GP0', toBoard: piId, toPin: '10' },
// GND
{ fromBoard: piId, fromPin: '6', toBoard: picoId, toPin: 'GND' },
]);
return { piId, picoId };
}
it('Pi3B UART0 TX → Pico.UART0 RX (feedUart/serialWrite)', () => {
const { piId, picoId } = setupPi3Pico();
const piBridge = getBoardBridge(piId) as any;
const simPico = getBoardSimulator(picoId) as any;
expect(typeof piBridge.onSerialData).toBe('function');
piBridge.onSerialData('Q');
const fed =
(simPico.feedUart as any).mock.calls.some((c: any[]) => c[0] === 0 && c[1] === 'Q') ||
(simPico.serialWrite as any).mock.calls.some((c: any[]) => c[0] === 'Q');
expect(fed).toBe(true);
});
it('Pico.UART0 TX → Pi3B bridge.sendSerialBytes', () => {
const { piId, picoId } = setupPi3Pico();
const piBridge = getBoardBridge(piId) as any;
const simPico = getBoardSimulator(picoId) as any;
expect(typeof simPico.onSerialData).toBe('function');
simPico.onSerialData('R');
const matched = (piBridge.sendSerialBytes as any).mock.calls.some(
(c: any[]) => Array.isArray(c[0]) && c[0][0] === 'R'.charCodeAt(0),
);
expect(matched).toBe(true);
});
});