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:
parent
c10e63764c
commit
e68746ed57
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue