test(cyw43): validate WiFi on the production RP2040Simulator path

Headless test that drives the REAL RP2040Simulator (attachCyw43 +
installCyw43PioHooks + lockstep PIO stepping in runFrameForTime), boots
the Pico W firmware, injects a WiFi-connect snippet over the raw REPL, and
asserts isconnected(). Result:

  PYBOOT
  ACTIVE False            (this fw's active() getter reports link status)
  CONN_OK 192.168.4.2     (DHCP-leased IP, isconnected() == True)
  MAINPY_DONE

Reaches link-up in ~31s wall — the production lockstep PIO stepping is
faster than the harness's setTimeout-cranked PIO.

Also fixes a real production bug: the RP2040 logger was
ConsoleLogger(LogLevel.Error) which THROWS on rp2040js unaligned-read
warnings — lwIP reads the IPv4 header at ethernet offset 14 on every
received packet, so WiFi would have crashed on the first DHCP reply.
Now constructed with throwOnError=false.

Gated behind CYW43_PROD_HARNESS=1 (boots real firmware, ~30s).
This commit is contained in:
David Montero 2026-06-13 00:43:48 +02:00
parent c10e63764c
commit e68746ed57
2 changed files with 106 additions and 3 deletions

View File

@ -0,0 +1,103 @@
/**
* picow-cyw43-prod-sim.investigate.test.ts
*
* PRODUCTION-PATH validation (not CI run explicitly).
*
* Unlike picow-cyw43-boot-harness.investigate.test.ts (which wires the cyw43
* emulator to a raw rp2040js Simulator with its own FIFO/PIO plumbing), this
* drives the REAL `RP2040Simulator` the exact code that runs in the browser:
* attachCyw43() + installCyw43PioHooks() (non-dropping FIFO, firmware fast-path,
* host-wake GPIO24) + the lockstep PIO stepping in runFrameForTime(). It boots
* the real Pico W firmware, injects a WiFi-connect snippet via the raw REPL, and
* asserts the link reaches isconnected()==True.
*
* Run:
* CYW43_PROD_HARNESS=1 npx vitest run \
* src/__tests__/picow-cyw43-prod-sim.investigate.test.ts
*/
import { describe, it, expect, vi } from 'vitest';
import { readFileSync } from 'node:fs';
const FW_PATH =
'/home/dave/velxio-prod/velxio/frontend/public/firmware/micropython-rp2040w.uf2';
// getFirmware = IndexedDB/fetch (browser only) -> serve from disk.
// loadUserFiles = LittleFS WASM fetch (browser only) -> no-op; we inject the
// WiFi snippet over the raw REPL instead of writing main.py to the filesystem.
vi.mock('../simulation/MicroPythonLoader', async (orig) => {
const actual = (await orig()) as Record<string, unknown>;
return {
...actual,
getFirmware: async () => new Uint8Array(readFileSync(FW_PATH)),
loadUserFiles: async () => { /* skip LittleFS */ },
};
});
const INJECT_CODE = [
'import network, time',
'print("PYBOOT")',
'w = network.WLAN(network.STA_IF)',
'w.active(True)',
'print("ACTIVE", w.active())',
'w.connect("Velxio-GUEST", "")',
'ok = False',
'for i in range(100):',
' if w.isconnected():',
' ok = True',
' print("CONN_OK", w.ifconfig()[0])',
' break',
' time.sleep_ms(150)',
'if not ok:',
' print("CONN_TIMEOUT status", w.status())',
'print("MAINPY_DONE")',
].join('\n');
describe.skipIf(!process.env.CYW43_PROD_HARNESS)('Pico W cyw43 — production RP2040Simulator path', () => {
it('boots, connects WiFi, and reaches isconnected()', async () => {
const { RP2040Simulator } = await import('../simulation/RP2040Simulator');
const { PinManager } = await import('../simulation/PinManager');
const sim = new RP2040Simulator(new PinManager());
let serial = '';
let buf = '';
sim.attachCyw43(); // selects Pico W firmware + registers host-wake listener
await sim.loadMicroPython([]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cdc = (sim as any).usbCDC as { sendSerialByte: (b: number) => void } | null;
expect(cdc).toBeTruthy();
const send = (s: string) => { for (const c of s) cdc!.sendSerialByte(c.charCodeAt(0)); };
// Raw-REPL injection state machine, fed by the serial stream.
let state: 'idle' | 'prompt' | 'raw' | 'sent' = 'idle';
sim.onSerialData = (ch: string) => {
serial += ch; buf += ch;
if (serial.length > 60000) serial = serial.slice(-20000);
if (state === 'idle' && buf.includes('>>>')) {
state = 'prompt'; buf = '';
cdc!.sendSerialByte(0x01); // Ctrl-A -> raw REPL
} else if (state === 'prompt' && buf.includes('raw REPL')) {
state = 'raw'; buf = '';
send(INJECT_CODE);
cdc!.sendSerialByte(0x04); // Ctrl-D -> execute
state = 'sent';
}
};
const deadline = Date.now() + 175_000;
let frames = 0;
while (Date.now() < deadline) {
for (let i = 0; i < 16; i++) { sim.runFrameForTime(50); frames++; }
if (serial.includes('MAINPY_DONE') || serial.includes('CONN_OK')) break;
await new Promise((r) => setTimeout(r, 0));
}
try { sim.stop(); } catch { /* noop */ }
// eslint-disable-next-line no-console
console.log(`\n===== PROD-SIM SERIAL (frames=${frames}, state=${state}) =====\n` + serial.slice(-1500));
expect(serial).toContain('PYBOOT');
expect(serial).toContain('CONN_OK');
}, 200_000);
});

View File

@ -291,7 +291,7 @@ export class RP2040Simulator {
// 2. Create fresh RP2040 instance // 2. Create fresh RP2040 instance
this.rp2040 = new RP2040(); this.rp2040 = new RP2040();
this.rp2040.logger = new ConsoleLogger(LogLevel.Error); this.rp2040.logger = new ConsoleLogger(LogLevel.Error, false);
this.rp2040.loadBootrom(bootromB1); this.rp2040.loadBootrom(bootromB1);
// 3. Load UF2 firmware into flash // 3. Load UF2 firmware into flash
@ -598,7 +598,7 @@ export class RP2040Simulator {
this.rp2040 = new RP2040(); this.rp2040 = new RP2040();
// Suppress noisy internal logs (only show errors) // Suppress noisy internal logs (only show errors)
this.rp2040.logger = new ConsoleLogger(LogLevel.Error); this.rp2040.logger = new ConsoleLogger(LogLevel.Error, false);
// Load RP2040 B1 bootrom — needed for proper boot sequence // Load RP2040 B1 bootrom — needed for proper boot sequence
this.rp2040.loadBootrom(bootromB1); this.rp2040.loadBootrom(bootromB1);
@ -979,7 +979,7 @@ export class RP2040Simulator {
if (this.micropythonMode) { if (this.micropythonMode) {
// In MicroPython mode, restore the full flash snapshot (UF2 + LittleFS) // In MicroPython mode, restore the full flash snapshot (UF2 + LittleFS)
this.rp2040 = new RP2040(); this.rp2040 = new RP2040();
this.rp2040.logger = new ConsoleLogger(LogLevel.Error); this.rp2040.logger = new ConsoleLogger(LogLevel.Error, false);
this.rp2040.loadBootrom(bootromB1); this.rp2040.loadBootrom(bootromB1);
this.rp2040.flash.set(this.flashCopy); this.rp2040.flash.set(this.flashCopy);
this.rp2040.core.PC = 0x10000000; this.rp2040.core.PC = 0x10000000;