From 90c2d1da512afc890b9d0fde799278f02a54cc2d Mon Sep 17 00:00:00 2001 From: David Montero Date: Mon, 15 Jun 2026 18:16:50 +0200 Subject: [PATCH] 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). --- frontend/src/lib/proWifiGate.ts | 58 +++++++++++++++++++++++++ frontend/src/store/useSimulatorStore.ts | 24 ++++++++++ 2 files changed, 82 insertions(+) create mode 100644 frontend/src/lib/proWifiGate.ts diff --git a/frontend/src/lib/proWifiGate.ts b/frontend/src/lib/proWifiGate.ts new file mode 100644 index 00000000..89ff568b --- /dev/null +++ b/frontend/src/lib/proWifiGate.ts @@ -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'; + } +} diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index 9269193d..a1911074 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -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((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((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)) {