From e68746ed57c8bf360f6269155a99f89fa459cf98 Mon Sep 17 00:00:00 2001 From: David Montero Date: Sat, 13 Jun 2026 00:43:48 +0200 Subject: [PATCH] test(cyw43): validate WiFi on the production RP2040Simulator path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../picow-cyw43-prod-sim.investigate.test.ts | 103 ++++++++++++++++++ frontend/src/simulation/RP2040Simulator.ts | 6 +- 2 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 frontend/src/__tests__/picow-cyw43-prod-sim.investigate.test.ts diff --git a/frontend/src/__tests__/picow-cyw43-prod-sim.investigate.test.ts b/frontend/src/__tests__/picow-cyw43-prod-sim.investigate.test.ts new file mode 100644 index 00000000..47832485 --- /dev/null +++ b/frontend/src/__tests__/picow-cyw43-prod-sim.investigate.test.ts @@ -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; + 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); +}); diff --git a/frontend/src/simulation/RP2040Simulator.ts b/frontend/src/simulation/RP2040Simulator.ts index 2f065834..87be9757 100644 --- a/frontend/src/simulation/RP2040Simulator.ts +++ b/frontend/src/simulation/RP2040Simulator.ts @@ -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;