feat(sim): WiFi run-gate seam so free users get an upgrade prompt, not a crash

Pico W WiFi is a paid overlay feature. A free/web user running a Pico W sketch
that uses WiFi had no peripheral attached -> the simulator picked the plain Pico
firmware (no `network` module) -> the run crashed on `import network` with a
raw Python traceback (on a public example page, no less).

Add lib/proWifiGate.ts (mirrors proBoardGate): a stable doorbell the overlay
fills in. useSimulatorStore gates both loadMicroPythonProgram (before loading
firmware) and startBoard (run backstop for example/loaded boards): if the gate
blocks, fire the upgrade prompt and skip the run. Non-WiFi Pico W sketches still
run for free. No-op in OSS (default 'allow' -> a Pico W runs as a plain Pico).
This commit is contained in:
David Montero 2026-06-15 18:16:50 +02:00
parent cd3fdded3d
commit 90c2d1da51
2 changed files with 82 additions and 0 deletions

View File

@ -0,0 +1,58 @@
/**
* Pro WiFi-gate registry.
*
* Pico W (CYW43439) WiFi emulation is a paid overlay feature. Unlike
* `proBoardGate` (which gates whole Pro boards: STM32 / QEMU Raspberry Pi),
* the Pico W BOARD itself is free it runs as a plain Pico in the browser.
* ONLY WiFi is gated, so this doorbell fires only when a Pico W sketch
* actually USES WiFi (so a free user can still blink an LED on a Pico W).
*
* Mirrors the other OSS->Pro seams (`proBoardGate.ts`, `proSaveAction.ts`,
* `proSession.ts`): the OSS app defines a stable doorbell and hands over the
* sketch files; the overlay plugs in the real decision.
*
* - OSS without an overlay -> default 'allow'. Self-hosted builds have no
* WiFi engine at all; a Pico W simulates as a plain Pico.
* - With the pro overlay -> 'block' for a WiFi-using sketch run by a
* non-paid web user; the caller fires the upgrade prompt and skips the run
* (so it never boots the plain-Pico firmware and crashes on
* `import network`).
*/
import type { BoardKind } from '../types/board';
export type WifiGateDecision = 'allow' | 'block';
type WifiGateFile = { name: string; content: string };
type WifiGateImpl = (kind: BoardKind | string, files: WifiGateFile[]) => WifiGateDecision;
let _impl: WifiGateImpl | null = null;
/** Installed by the pro overlay (mountPro). Pass null to clear (hot reload). */
export function installWifiGateImpl(impl: WifiGateImpl | null): void {
_impl = impl;
}
/** Whether the pro overlay has installed a WiFi gate. */
export function hasWifiGateImpl(): boolean {
return _impl !== null;
}
/**
* Decide whether running `kind` with these source files is allowed. With no
* overlay the OSS default is 'allow'. The overlay's impl sniffs the files for
* WiFi usage and checks the signed-in user's entitlement.
*/
export function wifiGateDecision(
kind: BoardKind | string,
files: WifiGateFile[],
): WifiGateDecision {
if (!_impl) return 'allow';
try {
return _impl(kind, files);
} catch (err) {
// eslint-disable-next-line no-console
console.warn('[oss] wifi-gate impl threw:', err);
return 'allow';
}
}

View File

@ -19,6 +19,7 @@ import type { Wire, WireInProgress, WireEndpoint } from '../types/wire';
import type { BoardKind, BoardInstance, LanguageMode, WifiStatus } from '../types/board';
import { BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind, isStm32BoardKind } from '../types/board';
import { boardGateDecision, proBoardFeatureName, triggerProUpgradePrompt } from '../lib/proBoardGate';
import { wifiGateDecision } from '../lib/proWifiGate';
import { calculatePinPosition } from '../utils/pinPositionCalculator';
import { useOscilloscopeStore } from './useOscilloscopeStore';
import { RaspberryPi3Bridge } from '../simulation/RaspberryPi3Bridge';
@ -1413,6 +1414,15 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
if (!board) return;
if (!BOARD_SUPPORTS_MICROPYTHON.has(board.boardKind)) return;
// Pro WiFi gate: a non-paid web user running a Pico W sketch that uses
// WiFi gets the upgrade prompt instead of loading the plain-Pico firmware
// (which lacks `network` -> ImportError). Non-WiFi Pico W sketches run
// fine. No-op in OSS. See lib/proWifiGate.ts + the pro installCyw43.
if (wifiGateDecision(board.boardKind, files) === 'block') {
triggerProUpgradePrompt('Pico W WiFi emulation');
return;
}
if (isEsp32Kind(board.boardKind)) {
// ESP32 path: load MicroPython firmware via QEMU bridge, inject code via raw-paste REPL
const { getEsp32Firmware, padToFlashSize, uint8ArrayToBase64 } =
@ -1598,6 +1608,20 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
return;
}
// Pro WiFi gate (run backstop): a Pico W can enter the canvas via an
// example or a loaded project, bypassing loadMicroPythonProgram's gate.
// A non-paid web user whose Pico W sketch uses WiFi gets the upgrade
// prompt instead of a broken plain-Pico boot. No-op in OSS.
{
const ed = useEditorStore.getState();
const grp = ed.fileGroups[board.activeFileGroupId];
const boardFilesForGate = grp && grp.length > 0 ? grp : ed.files;
if (wifiGateDecision(board.boardKind, boardFilesForGate) === 'block') {
triggerProUpgradePrompt('Pico W WiFi emulation');
return;
}
}
if (isPiBoardKind(board.boardKind)) {
getBoardBridge(boardId)?.connect();
} else if (isEsp32Kind(board.boardKind)) {