/** * proBoardRegistry — runtime registration seam for boards a private overlay * (velxio.com) ships outside the OSS tree. * * The OSS BoardKind union and its compiler-enforced Record maps stay exactly * as they are for the open boards. An overlay calls registerProBoards() at * mount with data-only definitions; registration patches the exported maps in * types/board.ts (labels / FQBN / MicroPython set) so every existing read site * keeps working untouched, and the handful of sites a map can't cover (canvas * render, pin-name mapping, simulator construction, firmware load) consult * getProBoard() as a fallback. * * The OSS build never registers anything here — like the proRoutes / * __velxio_pro_gate__ / registerComponentDoc seams, this module is dead code * until an overlay imports it. Board ad cards (ONLINE_ONLY_BOARD_ADS) hide * automatically for registered kinds: the picker filter checks * `ad.id in BOARD_KIND_LABELS`, and registration inserts the label. */ import type React from 'react'; import type { ESP32BoardOptions } from '../types/boardOptions'; import { BOARD_KIND_LABELS, BOARD_KIND_FQBN, BOARD_KIND_ESPIDF_FQBN, BOARD_SUPPORTS_ESPIDF, BOARD_SUPPORTS_MICROPYTHON, registerPiFamilyKind, type BoardKind, } from '../types/board'; /** Structural contract for an overlay-provided in-browser board simulator. * Mirrors the surface the store already uses on RP2040Simulator — the store * only ever duck-types these members for pro simulators. */ export interface ProBoardSimulator { /** Brand flag so the store can recognize overlay simulators without a class. */ readonly isProBoardSimulator: true; onSerialData: ((ch: string) => void) | null; onPinChangeWithTime: ((pin: number, state: boolean, time: number) => void) | null; stop(): void; detachPioPeripheral?(): void; } export interface ProBoardDef { /** The board id — behaves like a BoardKind everywhere at runtime. */ kind: string; label: string; /** arduino-cli FQBN, or null when the board has no backend compile. */ fqbn: string | null; /** One-line picker description. */ description: string; /** The board's custom-element tag. The overlay's import must have run * customElements.define for it before the board is placed. */ tag: string; /** True pixel size of the element (selection ring + pin overlays). */ size: { w: number; h: number }; supportsMicroPython?: boolean; /** Board can run pure ESP-IDF projects (app_main entry, no Arduino * core). Registers the kind into BOARD_SUPPORTS_ESPIDF, which is what * gates the toolbar's ESP-IDF option AND setBoardLanguageMode — an * overlay board without it silently ignores every attempt to enter * ESP-IDF mode, so an `languageMode: 'espidf'` gallery example opens * in Arduino mode instead. ESP32 family only. */ supportsEspIdf?: boolean; /** FQBN the ESP-IDF lane compiles this board with, when the board has * no Arduino FQBN of its own (`fqbn: null`) or needs a different one. * Only used to derive the backend's IDF target. */ espidfFqbn?: string; /** ESP32 run-path routing: the base chip the board carries. Routes the run * through the ESP32 bridge path and picks the machine/engine type. Omit for * boards that provide createSimulator (RP2350 class) or AVR/RP2040. */ esp32Family?: 'esp32' | 'esp32-s3' | 'esp32-c3' | 'esp32-c6' | 'esp32-p4' | 'esp32-c5'; /** QEMU-Linux run-path routing: route this kind through the Raspberry Pi * bridge (backend qemu WebSocket, VFS panel, boot terminal). The overlay * must also register a matching backend profile for the kind. */ piFamily?: boolean; /** One shell line sent to the guest right after it reaches the boot * prompt (piFamily boards only). Lets a board de-brand the generic * image, e.g. set its own hostname/PS1 and clear the stock motd. */ guestSetup?: string; /** Home directory of the guest user for the VFS panel and uploads * (piFamily boards only; default '/home/pi'). Boards whose guest logs * in as root pass '/root'; these also drop the hello.sh sample. */ guestHome?: string; /** Shell command run automatically after boot: the VFS is uploaded and * this line executed, so a single click on Run boots, uploads and * starts the user's script (piFamily boards only). */ autoRun?: string; /** Suppress the guest's boot chatter in the terminal (piFamily boards * only): the shared rootfs prints another product's banner/motd during * boot, before guestSetup can re-brand it. With quietBoot the terminal * shows a neutral Velxio boot-progress line instead and reveals the * shell right before the auto-run command executes. */ quietBoot?: boolean; /** Canvas renderer. Receives the placed board's props; return a React node. * When omitted, the canvas renders ``. */ render?: (props: { id: string; x: number; y: number; running: boolean }) => React.ReactNode; /** pinInfo name -> GPIO number (power/ground pins -> -1). Falls back to the * generic numeric parse when omitted. Return null for "not mine". */ pinToNumber?: (pinName: string) => number | null; /** * Which pads reach the ADC, and on which channel — the overlay's own version * of the OSS ADC_PIN_MAP. * * That map is keyed by the BoardKind union, which no overlay kind can join, * so the SPICE analog path skipped every pro board outright: a divider or an * LDR wired to one of these boards solved correctly and then had nowhere to * go. Parts that inject directly (a potentiometer through partUtils) were * unaffected, which is why this looked like it worked. * * Names are pad names as they appear in pinInfo. A pad that is analog under a * different silk (the Pimoroni Pico Plus 2 W ties GP26/27/28 to the ADC- * capable GP40/41/42 through 1k) is declared under the name a wire uses. */ adcPins?: Array<{ pinName: string; channel: number }>; /** * Supply pads and logic voltage, for the SPICE netlist — the overlay's * version of BOARD_PIN_GROUPS, which is keyed by the BoardKind union and so * can never name a pro board. * * Without it a board falls back to that table's `default`: 5 V, ground pads * called GND/GND.1/GND.2, supply pads called 5V/VCC. For a 3.3 V board with a * "3V3" pad that is wrong twice over, and silently — the rail is simply * solved at 5 V, so every divider, pull-up and LED current on the board is * off by 1.5x. * * Shape matches BoardPinGroup: { vcc, gnd[], vcc_pins[], aux? }. */ power?: { vcc: number; gnd: string[]; vcc_pins: string[]; aux?: { volts: number; pins: string[] }; }; /** In-browser simulator factory (e.g. the RP2350/Hazard3 emulator). The pm * argument is the store's PinManager instance. */ createSimulator?: (pm: unknown) => ProBoardSimulator; /** Load compiled firmware into a createSimulator() instance at run time — * the overlay owns the whole sequence (PIO attach, binary load, demo I2C * devices, ...). `program` is the compiled artifact exactly as the store * holds it (base64/hex string, same value RP2040Simulator.loadBinary gets). */ loadFirmware?: ( sim: ProBoardSimulator, program: string, ctx: { boardKind: string; boardId: string }, ) => void; /** Built-in bridge sensors registered without wiring (e.g. an on-board I2C * keyboard): pushed into the ESP32 bridge's sensor config on every run. */ builtInSensors?: Array<{ sensor_type: string; pin: number; addr?: number }>; /** Built-in microSD on a shared SPI bus: the CS pin the bridge must gate. * (A standalone SD card component still overrides this to un-gated.) */ builtInSdCsPin?: number; /** Board carries an on-board microphone whose bridge implements * setMicrophoneSource (I2S RX sample injection): shows the canvas-header * Mic toggle that streams the computer's microphone into it. */ builtInMicrophone?: boolean; /** The board carries a camera fed from the host webcam (CameraToggle in the * canvas header; the bridge implements pushCameraFrame). An object form * caps the injected JPEG size for drivers with tight frame buffers. */ builtInCamera?: boolean | { maxFrameBytes?: number }; /** Display-controller identity for panel autodetect probes. Vendor * libraries (M5GFX and friends) identify a board by reading the panel's * RDDID over SPI with the display's own chip-select — and reject the * board, or misdetect it as something else entirely, when the answer is * the bus's idle 0xFF. A bridge that supports this answers `idByte` on * MISO while `csPin` is low. Board DATA, not bridge code: the M5Stack * Core is `{ csPin: 14, idByte: 0xE3 }` (ILI9342C), the Cardputer ADV * `{ csPin: 37, idByte: 0x85 }` (ST7789V2). */ spiPanelId?: { csPin: number; idByte: number }; /** Board carries an IMU whose bridge implements setImuAcceleration / * setImuGyro: shows the canvas-header tilt pad. Without it the emulated * part faithfully reports the board lying flat for the whole run. */ builtInImu?: boolean; /** Board reads a battery through its bridge (setBatteryVoltage): shows the * canvas-header charge slider, so low-battery code paths can be reached. */ builtInBattery?: boolean; /** Power-management IC on the internal I2C bus, when the board carries one * the vendor library probes. Board DATA like spiPanelId: the bridge that * supports the type instantiates the model at the given address, and * setBatteryVoltage drives it. The M5Stack Core is * { type: 'ip5306', addr: 0x75 }. */ i2cPmic?: { type: 'ip5306'; addr?: number }; /** Built-in peripheral attachment for a RUNNING board (LCD decoder onto the * element's own canvas, speaker, on-board button/keyboard event forwarding). * Called shortly after run start with the board's DOM element plus its * simulator shim and ESP32 bridge (either may be null depending on the run * path); must return a cleanup that detaches everything. */ attachBuiltins?: (ctx: { el: HTMLElement; sim: unknown; bridge: unknown; }) => () => void; /** * Seed code a freshly placed board starts with, per language mode. * * Without this the editor falls back to the family default — the Arduino * `LED_BUILTIN` blink, the Pico `Pin(25)` blink, or the Raspberry Pi * `RPi.GPIO` script — and NONE of those run on a board that has no such * LED, no RPi.GPIO and its own vendor library (an M5Stack needs * `M5.begin()`, the UNIHIKER needs pinpong/unihiker). The user's very * first Run then fails on a board they have not touched yet. Keep each * entry as close to the vendor's own first example as the emulator * allows, and self-contained: a freshly placed board has nothing wired * to it, so the code must do something visible on the board itself. * * Keys are the board's language modes plus 'python' for QEMU-Linux * (piFamily) boards, whose seed is the guest script. */ defaultFiles?: Partial< Record<'arduino' | 'micropython' | 'espidf' | 'python', Array<{ name: string; content: string }>> >; /** Library manifest seeded together with `defaultFiles.arduino` — the seed * sketch includes the vendor library, so the board must declare it or the * first compile resolves against nothing. */ defaultLibraries?: string[]; /** * Build options this board is physically born with, merged over the family * defaults whenever the project has not saved its own. * * A module carries the RAM and flash it carries: the ESP32-S3-EYE always has * 8 MB of octal PSRAM, and its camera driver allocates the frame buffer with * MALLOC_CAP_SPIRAM. Compiled with the conservative family default * (CONFIG_SPIRAM=n) the sketch builds fine and then dies at run time with * `cam_dma_config(509): frame buffer malloc failed` — a hardware fact * reported as a mysterious driver error. Boards that ship the RAM say so * here; everything else keeps sending no options at all. */ defaultBoardOptions?: Partial; /** Sidebar / toolbar accents (fall back to a neutral chip icon). */ icon?: string; color?: string; } const registry = new Map(); // Registration happens when the overlay's dynamic import lands - potentially // AFTER a consumer rendered and memoized its board list. Subscribers re-render // on every registration (same contract as proRoutes / registerProExamples). let version = 0; const listeners = new Set<() => void>(); export function subscribeProBoards(cb: () => void): () => void { listeners.add(cb); return () => listeners.delete(cb); } export function getProBoardsVersion(): number { return version; } export function registerProBoards(defs: ProBoardDef[]): void { for (const def of defs) { registry.set(def.kind, def); const kind = def.kind as BoardKind; // Patch the exported maps so every existing read site sees the board — // labels also make the picker's ONLINE ad for this kind disappear. (BOARD_KIND_LABELS as Record)[kind] = def.label; (BOARD_KIND_FQBN as Record)[kind] = def.fqbn; if (def.supportsMicroPython) BOARD_SUPPORTS_MICROPYTHON.add(kind); if (def.supportsEspIdf) BOARD_SUPPORTS_ESPIDF.add(kind); if (def.espidfFqbn) { (BOARD_KIND_ESPIDF_FQBN as Record)[kind] = def.espidfFqbn; } if (def.piFamily) registerPiFamilyKind(def.kind); } version++; for (const l of listeners) l(); } export function getProBoard(kind: string): ProBoardDef | undefined { return registry.get(kind); } // ── Guest setup lines for boards the OSS tree already owns ─────────────── // // A QEMU-Linux board that is NOT overlay-registered (the Raspberry Pi // family) may still need a line run at the boot prompt — an overlay can // mount something on the guest for it. Registering a whole ProBoardDef // just to carry that string is the wrong tool: `getProBoard` is what // BoardOnCanvas checks to decide whether to render the overlay's custom // element instead of the OSS board art, so a stub def silently replaced // the Pi's illustration with a bare schematic box and left the wires // hanging at the corner. This registry carries the line and nothing else. const guestSetups = new Map(); export function registerGuestSetup(kind: string, line: string): void { guestSetups.set(kind, line); } /** The line to run at the guest's boot prompt: an overlay board's own * `guestSetup` first, then anything registered for an OSS board kind. */ export function getGuestSetup(kind: string): string | undefined { return registry.get(kind)?.guestSetup ?? guestSetups.get(kind); } // ── Built-in peripheral attachment for boards the OSS tree already owns ── // // Same reasoning as the guest setups above: an overlay may need to wire // something to a run of an OSS-rendered board — the Pi family has no // built-in screen, but an overlay can route the guest's display frames to // a panel wired on the canvas. Carrying that through a stub ProBoardDef // would take over the board's artwork, so it gets its own registry. type BuiltinsAttach = (ctx: { el: HTMLElement; sim: unknown; bridge: unknown; }) => () => void; const boardBuiltins = new Map(); export function registerBoardBuiltins(kind: string, attach: BuiltinsAttach): void { boardBuiltins.set(kind, attach); } /** How to attach built-in peripherals for a board kind: an overlay board's * own `attachBuiltins` first, then anything registered for an OSS kind. */ export function getBoardBuiltins(kind: string): BuiltinsAttach | undefined { return registry.get(kind)?.attachBuiltins ?? boardBuiltins.get(kind); } /** * Seed files for a board kind in a given language mode, or undefined when the * board carries no seed of its own (the editor's family default applies). * `mode` is 'python' for QEMU-Linux boards — see ProBoardDef.defaultFiles. */ export function getBoardSeedFiles( kind: string, mode: 'arduino' | 'micropython' | 'espidf' | 'python', ): Array<{ name: string; content: string }> | undefined { const files = registry.get(kind)?.defaultFiles?.[mode]; return files && files.length ? files : undefined; } export function listProBoards(): ProBoardDef[] { return Array.from(registry.values()); } export function isProBoardSimulator(sim: unknown): sim is ProBoardSimulator { return !!sim && (sim as { isProBoardSimulator?: boolean }).isProBoardSimulator === true; }