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
|
||||
this.rp2040 = new RP2040();
|
||||
this.rp2040.logger = new ConsoleLogger(LogLevel.Error);
|
||||
this.rp2040.logger = new ConsoleLogger(LogLevel.Error, false);
|
||||
this.rp2040.loadBootrom(bootromB1);
|
||||
|
||||
// 3. Load UF2 firmware into flash
|
||||
|
|
@ -598,7 +598,7 @@ export class RP2040Simulator {
|
|||
this.rp2040 = new RP2040();
|
||||
|
||||
// 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
|
||||
this.rp2040.loadBootrom(bootromB1);
|
||||
|
|
@ -979,7 +979,7 @@ export class RP2040Simulator {
|
|||
if (this.micropythonMode) {
|
||||
// In MicroPython mode, restore the full flash snapshot (UF2 + LittleFS)
|
||||
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.flash.set(this.flashCopy);
|
||||
this.rp2040.core.PC = 0x10000000;
|
||||
|
|
|
|||
Loading…
Reference in New Issue