velxio/frontend/src/__tests__/picow-cyw43-integration.tes...

204 lines
6.7 KiB
TypeScript

/**
* picow-cyw43-integration.test.ts
*
* Verifies the Pico W (CYW43439) frontend integration:
* 1. The cyw43/ barrel exports a stable surface.
* 2. Cyw43Bridge speaks the documented WS protocol shape.
* 3. The Cyw43Emulator accepts the bus init handshake exactly the
* way the prototype tests in
* test/test_Raspberry_Pi_Pico_W/test_code/
* verify it. We re-run a tight subset here so any regression
* caught in the frontend tree fails CI close to the offending
* change rather than waiting for the prototype suite.
*
* Reference upstream IoT projects:
* github.com/KritishMohapatra/100_Days_100_IoT_Projects
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
Cyw43Emulator,
Cyw43Bridge,
DEFAULT_AP,
DEFAULT_STA_MAC,
PioBusSniffer,
TEST_PATTERN,
WLC,
WLC_E,
ClockCsr,
F0,
F1,
SdpcmChannel,
encodeIoctlRequest,
decodeSdpcm,
decodeCdc,
decodeEventBody,
} from '../simulation/cyw43';
import { decodeHeader } from '../simulation/cyw43/PioBusSniffer';
function makeHdr(opts: {
write: boolean;
func: 0 | 1 | 2 | 3;
addr: number;
length: number;
inc?: boolean;
}) {
return decodeHeader(
(((opts.write ? 1 : 0) << 31) |
((opts.inc ? 1 : 0) << 30) |
(opts.func << 28) |
((opts.addr & 0x1ffff) << 11) |
(opts.length & 0x7ff)) >>>
0,
);
}
function readU32(b: Uint8Array): number {
return ((b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24)) >>> 0);
}
describe('cyw43 barrel exports', () => {
it('exposes the public surface used by RP2040Simulator', () => {
expect(Cyw43Emulator).toBeDefined();
expect(Cyw43Bridge).toBeDefined();
expect(PioBusSniffer).toBeDefined();
expect(typeof TEST_PATTERN).toBe('number');
expect(WLC.GET_MAGIC).toBe(0);
expect(WLC_E.LINK).toBe(16);
expect(F0.READ_TEST).toBe(0x14);
expect(F1.SDIO_CHIP_CLOCK_CSR).toBe(0x1000e);
expect(SdpcmChannel.CONTROL).toBe(0);
expect(DEFAULT_AP.ssid).toBe('Velxio-GUEST');
expect(DEFAULT_STA_MAC.length).toBe(6);
});
});
describe('Cyw43Emulator bus handshake', () => {
it('returns 0 then 0xFEEDBEAD on F0:0x14 reads', () => {
const chip = new Cyw43Emulator();
const r1 = chip.onCommand(makeHdr({ write: false, func: 0, addr: F0.READ_TEST, length: 4 }), new Uint8Array(0))!;
const r2 = chip.onCommand(makeHdr({ write: false, func: 0, addr: F0.READ_TEST, length: 4 }), new Uint8Array(0))!;
expect(readU32(r1)).toBe(0);
expect(readU32(r2)).toBe(TEST_PATTERN);
});
it('flips HT_AVAIL after a HT_AVAIL_REQ write', () => {
const chip = new Cyw43Emulator();
chip.onCommand(
makeHdr({ write: true, func: 1, addr: F1.SDIO_CHIP_CLOCK_CSR, length: 1 }),
new Uint8Array([ClockCsr.HT_AVAIL_REQ]),
);
const r = chip.onCommand(
makeHdr({ write: false, func: 1, addr: F1.SDIO_CHIP_CLOCK_CSR, length: 1 }),
new Uint8Array(0),
)!;
expect(r[0] & ClockCsr.HT_AVAIL).toBeGreaterThan(0);
});
});
describe('Cyw43Emulator IOCTL — SET_SSID Velxio-GUEST', () => {
it('drives the chip to link-up and emits WLC_E_LINK', () => {
const chip = new Cyw43Emulator();
// WLC_UP
pushIoctl(chip, WLC.UP, new Uint8Array(0), 1);
expect(chip.isUp()).toBe(true);
// SET_SSID
const ssid = new TextEncoder().encode('Velxio-GUEST');
const payload = new Uint8Array(36);
new DataView(payload.buffer).setUint32(0, ssid.length, true);
payload.set(ssid, 4);
const events = pushIoctl(chip, WLC.SET_SSID, payload, 1);
expect(chip.getLinkState()).toBe('up');
expect(events.some((e) => e.type === WLC_E.LINK && e.reason === 1)).toBe(true);
expect(events.some((e) => e.type === WLC_E.SET_SSID && e.status === 0)).toBe(true);
});
});
describe('Cyw43Bridge WS protocol shape', () => {
let createdSockets: Array<{
send: ReturnType<typeof vi.fn>;
close: ReturnType<typeof vi.fn>;
readyState: number;
onopen?: () => void;
onmessage?: (e: MessageEvent) => void;
onclose?: () => void;
onerror?: () => void;
}>;
beforeEach(() => {
createdSockets = [];
class FakeSocket {
static readonly CONNECTING = 0;
static readonly OPEN = 1;
static readonly CLOSING = 2;
static readonly CLOSED = 3;
send = vi.fn();
close = vi.fn();
readyState = 1;
onopen?: () => void;
onmessage?: (e: MessageEvent) => void;
onclose?: () => void;
onerror?: () => void;
constructor(_url: string) {
createdSockets.push(this);
}
}
// @ts-expect-error — we substitute a stub that satisfies the shape we use
globalThis.WebSocket = FakeSocket;
});
it('skips connect() when wifi is disabled', () => {
const b = new Cyw43Bridge('test-board');
b.wifiEnabled = false;
b.connect();
expect(createdSockets).toHaveLength(0);
});
it('opens a WS, sends start_picow, and forwards outbound packets', () => {
const b = new Cyw43Bridge('test-board');
b.wifiEnabled = true;
b.connect();
expect(createdSockets).toHaveLength(1);
const sock = createdSockets[0];
sock.onopen?.();
expect(sock.send).toHaveBeenCalledWith(
expect.stringContaining('"type":"start_picow"'),
);
expect(sock.send).toHaveBeenCalledWith(
expect.stringContaining('"wifi_enabled":true'),
);
b.sendPacket(new Uint8Array([1, 2, 3, 4, 5]));
const lastCall = sock.send.mock.calls.at(-1)?.[0] as string | undefined;
expect(lastCall).toContain('"type":"picow_packet_out"');
expect(lastCall).toContain('"ether_b64"');
});
});
// ── helper ────────────────────────────────────────────────────────
function pushIoctl(
chip: Cyw43Emulator,
cmd: number,
payload: Uint8Array,
isSet: number,
): Array<{ type: number; status: number; reason: number; data: Uint8Array }> {
const sdpcm = encodeIoctlRequest(0, cmd, isSet, 0, payload);
chip.onCommand(makeHdr({ write: true, func: 2, addr: 0, length: sdpcm.length, inc: true }), sdpcm);
const events: Array<{ type: number; status: number; reason: number; data: Uint8Array }> = [];
for (let i = 0; i < 32; i++) {
const out = chip.onCommand(makeHdr({ write: false, func: 2, addr: 0, length: 1600, inc: true }), new Uint8Array(0))!;
if (!out || out.every((b) => b === 0)) break;
const f = decodeSdpcm(out);
if (!f) break;
if (f.channel === SdpcmChannel.EVENT) {
const ev = decodeEventBody(f.payload);
if (ev) events.push({ type: ev.eventType, status: ev.status, reason: ev.reason, data: ev.data });
} else if (f.channel === SdpcmChannel.CONTROL) {
decodeCdc(f.payload); // ignore — we just care about events
}
}
return events;
}