From 24ea1a73f8864f16c33ac2c49e6f7b8c48823e86 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Fri, 21 Aug 2026 21:03:47 +0200 Subject: [PATCH] fix(cyw43): WiFi only worked on the first Run after a page load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CYW43439 emulator is stateful — bus handshake, backplane window, SDPCM sequence and credit, event mask, link state — and attachPioPeripheral is idempotent, called once at board-add. So the chip's state SURVIVED every later Run while the guest driver started over from scratch. The second Run had a fresh driver talking to a chip mid-conversation: the handshake "passed" against stale registers, IOCTL replies came back zeroed (WiFi.macAddress() read all 00s), and the first call that actually waited on the chip — WiFi.begin(), or the second IOCTL of any kind — blocked forever. A rebuilt MCU means a rebooted board, so the chip is power-cycled with it: the peripheral is replaced wholesale (it owns its network bridge and its whole register model, so there is no partial-reset path to get wrong) right before the FIFO hooks are re-installed on the new PIO. Also: overlay board simulators now get onSimulationStart. The Pimoroni Pico Plus 2 W and Badger 2350 carry the same CYW43439 die on an RP2350 simulator, which is not an RP2040Simulator, so they never had their network bridge opened — WiFi that reached an IP and could then reach nothing. Verified on the RP2350 side with a harness that boots a real arduino-pico binary twice in one simulator (pro overlay); before the fix run 2 hung with a zeroed MAC, after it both runs associate. --- frontend/src/simulation/RP2040Simulator.ts | 47 ++++++++++++++++++++++ frontend/src/store/useSimulatorStore.ts | 14 ++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/frontend/src/simulation/RP2040Simulator.ts b/frontend/src/simulation/RP2040Simulator.ts index ca6d9609..56c659e6 100644 --- a/frontend/src/simulation/RP2040Simulator.ts +++ b/frontend/src/simulation/RP2040Simulator.ts @@ -183,6 +183,9 @@ export class RP2040Simulator { // ── Generic PIO/gSPI bus peripheral (e.g. the pro WiFi co-processor). Null // in OSS (no factory installed); attached for boards a factory supports. private pioPeripheral: PioPeripheral | null = null; + /** Board id the PIO peripheral was created for, so a rebooting guest can be + * handed a freshly powered chip (see powerCyclePioPeripheral). */ + private pioBoardId: string | null = null; private pioHookedFifos: Array<{ restore: () => void }> = []; // The board kind this simulator runs (set by attachPioPeripheral). A // 'pi-pico-w' boots the RPI_PICO_W firmware (with the `network` module) @@ -386,6 +389,7 @@ export class RP2040Simulator { // RP2040, so those hooks now point at the discarded instance. Re-install // them on the new PIO FIFOs or the peripheral never sees the bus traffic. if (this.pioPeripheral) { + this.powerCyclePioPeripheral(); this.pioHookedFifos = []; this.installPioPeripheralHooks(); } @@ -418,6 +422,7 @@ export class RP2040Simulator { // factory-not-installed-yet) so loadMicroPython still picks the W firmware // for a pi-pico-w board. this.boardKind = boardKind; + this.pioBoardId = boardId; if (this.pioPeripheral) return this.pioPeripheral; const peripheral = createPioPeripheral(boardKind, boardId); if (!peripheral) return null; @@ -435,6 +440,48 @@ export class RP2040Simulator { return peripheral; } + /** + * Give a rebooting guest a chip that has also rebooted. + * + * The CYW43439 emulator is stateful: the bus handshake result, the backplane + * window, the SDPCM sequence counters and credit, the event mask, the link + * state. attachPioPeripheral is idempotent and the store only calls it at + * board-add, so that state SURVIVED every later Run while the guest driver + * started over from scratch. The second Run then had a fresh driver talking + * to a chip mid-conversation: the handshake "passed" against stale + * registers, IOCTL replies came back zeroed, and the first call that waited + * on the chip blocked forever. Only the first Run after a page load worked. + * + * Called from the paths that rebuild the MCU, before the FIFO hooks are + * re-installed, so the new chip is what gets wired to the new PIO. + */ + private powerCyclePioPeripheral(): void { + if (!this.pioPeripheral || !this.pioBoardId) return; + const kind = this.boardKind; + const boardId = this.pioBoardId; + try { + this.pioPeripheral.detach?.(); + } catch { + /* the old chip is being thrown away either way */ + } + for (const h of this.pioHookedFifos) { + try { + h.restore(); + } catch { + /* noop */ + } + } + this.pioHookedFifos = []; + this.pioPeripheral = null; + const fresh = createPioPeripheral(kind, boardId); + if (!fresh) return; + this.pioPeripheral = fresh; + fresh.onHostWake((active: boolean) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + try { (this.rp2040 as any)?.gpio?.[24]?.setInputValue(active); } catch { /* noop */ } + }); + } + /** Detach the PIO peripheral (called from teardown). */ detachPioPeripheral(): void { for (const h of this.pioHookedFifos) h.restore(); diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index c9715558..0c1d5b9a 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -2571,12 +2571,22 @@ export const useSimulatorStore = create((set, get) => { // Notify an attached PIO peripheral (the pro CYW43 WiFi co-processor) // that the simulation started, with the board's source files so it can // detect WiFi usage and open its network bridge. No-op in OSS. - if (rpSim instanceof RP2040Simulator) { + // + // Overlay boards carry the same peripheral: the Pimoroni Pico Plus 2 W + // and Badger 2350 have the RM2 (the same CYW43439 die) on an RP2350 + // simulator, which is NOT an RP2040Simulator. Without this branch they + // associated to the local virtual AP but never got the network bridge + // opened, so real internet and the Pro custom-AP config never applied + // — WiFi that reached an IP and could reach nothing. + const pioSim = rpSim as + | (typeof rpSim & { getPioPeripheral?: () => { onSimulationStart?: (f: unknown[]) => void } | null }) + | null; + if (typeof pioSim?.getPioPeripheral === 'function') { const editorState = useEditorStore.getState(); const rawFiles = editorState.fileGroups[board.activeFileGroupId]; const boardFiles = rawFiles && rawFiles.length > 0 ? rawFiles : editorState.files; - rpSim.getPioPeripheral()?.onSimulationStart?.(boardFiles); + pioSim.getPioPeripheral()?.onSimulationStart?.(boardFiles); } }