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:
parent
cd3fdded3d
commit
90c2d1da51
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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)) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue