From 32958640a3bf9ffd15fc994e8f91ed2bb0caa7a5 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Wed, 22 Jul 2026 21:21:21 +0200 Subject: [PATCH] feat(boards): runtime board-registration seam for private overlays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit registerProBoards() lets a hosted overlay ship boards outside the OSS tree as data-only definitions: registration patches the exported BoardKind maps (labels / FQBN / MicroPython) so every existing read site keeps working, and the sites a map can't cover consult the registry — canvas render (custom element or overlay render fn), BOARD_SIZE, pin-name mapping, picker list + descriptions + tag, ESP32 family routing, in-browser simulator construction and firmware load (structural ProBoardSimulator contract, duck-typed PIO attach/detach), built-in bridge sensors, and a CS-gated built-in microSD (sdCsPin -> sd_card.cs_pin worker config). Esp32Bridge additionally gains the esp32-c6 machine type + TX pin (public chip knowledge — the C6 compile path already ships) and a generic sendKey() for built-in matrix keyboards. registerProExamples() appends gallery examples at runtime; the board ONLINE ads recompute at render so registration hides them. OSS behavior without an overlay is unchanged — the registry is dead code, same as the other seams. --- .../src/components/ComponentPickerModal.tsx | 24 ++-- .../src/components/editor/FileExplorer.tsx | 13 ++- .../components/simulator/BoardOnCanvas.tsx | 15 ++- .../components/simulator/BoardPickerModal.tsx | 11 +- frontend/src/data/examples.ts | 15 ++- frontend/src/lib/proBoardRegistry.ts | 109 ++++++++++++++++++ frontend/src/simulation/Esp32Bridge.ts | 32 ++++- frontend/src/store/useSimulatorStore.ts | 48 ++++++-- frontend/src/utils/boardPinMapping.ts | 7 ++ 9 files changed, 252 insertions(+), 22 deletions(-) create mode 100644 frontend/src/lib/proBoardRegistry.ts diff --git a/frontend/src/components/ComponentPickerModal.tsx b/frontend/src/components/ComponentPickerModal.tsx index 73070515..11319574 100644 --- a/frontend/src/components/ComponentPickerModal.tsx +++ b/frontend/src/components/ComponentPickerModal.tsx @@ -33,6 +33,7 @@ interface CardHoverApi { import type { BoardKind } from '../types/board'; import { BOARD_KIND_LABELS } from '../types/board'; import { isProBoardKind } from '../lib/proBoardGate'; +import { getProBoard, listProBoards } from '../lib/proBoardRegistry'; import { ONLINE_ONLY_BOARD_ADS, ONLINE_ONLY_COMPONENT_ADS, @@ -205,6 +206,11 @@ export const ComponentPickerModal: React.FC = ({ return components; }, [searchQuery, selectedCategory, registry, isLoading]); + // Boards list: static OSS kinds + overlay-registered boards (proBoardRegistry). + const allBoards = useMemo(() => { + return [...ALL_BOARDS, ...(listProBoards().map((d) => d.kind) as BoardKind[])]; + }, []); + // Online-only component ads: shown where the real component would sit, and // hidden automatically in any build whose registry has the real component // (the hosted overlay merges it in) — same contract as VISIBLE_BOARD_ADS. @@ -317,7 +323,7 @@ export const ComponentPickerModal: React.FC = ({ {/* Boards Panel */} {selectedCategory === 'boards' ? (
- {ALL_BOARDS.map((kind) => ( + {allBoards.map((kind) => ( = ({ hoverApi={hoverApi} /> ))} - {VISIBLE_BOARD_ADS.map((ad) => ( + {visibleBoardAds().map((ad) => ( ))}
@@ -343,7 +349,7 @@ export const ComponentPickerModal: React.FC = ({ className="components-grid components-grid--inline" style={{ borderBottom: '1px solid #333', paddingBottom: 8, marginBottom: 4 }} > - {ALL_BOARDS.filter( + {allBoards.filter( (k) => !searchQuery || BOARD_KIND_LABELS[k].toLowerCase().includes(searchQuery.toLowerCase()), @@ -358,7 +364,7 @@ export const ComponentPickerModal: React.FC = ({ hoverApi={hoverApi} /> ))} - {VISIBLE_BOARD_ADS.filter( + {visibleBoardAds().filter( (ad) => !searchQuery || ad.label.toLowerCase().includes(searchQuery.toLowerCase()), ).map((ad) => ( @@ -672,7 +678,7 @@ const BoardCard: React.FC = ({ kind, onSelect, hoverApi }) => { id: kind, name: BOARD_KIND_LABELS[kind], category: 'Boards', - description: BOARD_DESCRIPTIONS[kind], + description: BOARD_DESCRIPTIONS[kind] ?? getProBoard(kind)?.description ?? '', pinCount: 0, properties: [], tags: [], @@ -695,7 +701,7 @@ const BoardCard: React.FC = ({ kind, onSelect, hoverApi }) => { ) return; - const tag = BOARD_TAG[kind]; + const tag = BOARD_TAG[kind] ?? getProBoard(kind)?.tag; if (!tag) return; const el = document.createElement(tag) as HTMLElement; @@ -750,7 +756,7 @@ const BoardCard: React.FC = ({ kind, onSelect, hoverApi }) => {
{BOARD_KIND_LABELS[kind]}
-
{BOARD_DESCRIPTIONS[kind]}
+
{BOARD_DESCRIPTIONS[kind] ?? getProBoard(kind)?.description}
); @@ -759,7 +765,9 @@ const BoardCard: React.FC = ({ kind, onSelect, hoverApi }) => { // ── Online-only board ads ─────────────────────────────────────────────────── // Boards implemented by the hosted editor (velxio.com), free to use there. // Hidden automatically in any build that registers the real BoardKind. -const VISIBLE_BOARD_ADS = ONLINE_ONLY_BOARD_ADS.filter((ad) => !(ad.id in BOARD_KIND_LABELS)); +/** Recomputed on access (not module load): overlay board registration patches + * BOARD_KIND_LABELS at mount, which must hide the corresponding ad. */ +const visibleBoardAds = () => ONLINE_ONLY_BOARD_ADS.filter((ad) => !(ad.id in BOARD_KIND_LABELS)); /** Teal "ONLINE" pill: the board runs (free) in the hosted editor. */ const OnlineBadge: React.FC = () => ( diff --git a/frontend/src/components/editor/FileExplorer.tsx b/frontend/src/components/editor/FileExplorer.tsx index 834782a4..1c0d28d2 100644 --- a/frontend/src/components/editor/FileExplorer.tsx +++ b/frontend/src/components/editor/FileExplorer.tsx @@ -14,6 +14,15 @@ import { importProjectFile, PROJECT_FILE_ACCEPT } from '../../utils/importProjec import { showMessageDialog, showConfirmDialog } from '../../store/useMessageDialogStore'; import './FileExplorer.css'; +/** Neutral chip glyph for overlay-registered boards without a bespoke icon. */ +const PRO_FALLBACK_ICON = ( + + + + +); + + // SVG icons — same style as EditorToolbar (stroke-based, 16x16) const IcoFile = () => ( = ({ onSaveClick, onNewCl const groupFiles = fileGroups[groupId] ?? []; const isActiveBoard = board.id === activeBoardId; const isOpen = !collapsed[board.id]; - const color = BOARD_COLOR[board.boardKind]; + const color = BOARD_COLOR[board.boardKind] ?? '#8b5cf6'; // Status dot color const statusColor = board.running @@ -614,7 +623,7 @@ export const FileExplorer: React.FC = ({ onSaveClick, onNewCl - {BOARD_ICON[board.boardKind]} + {BOARD_ICON[board.boardKind] ?? PRO_FALLBACK_ICON} {renamingSection?.id === board.id && renamingSection.kind === 'board' ? ( diff --git a/frontend/src/components/simulator/BoardOnCanvas.tsx b/frontend/src/components/simulator/BoardOnCanvas.tsx index 25d8fd7d..3181369d 100644 --- a/frontend/src/components/simulator/BoardOnCanvas.tsx +++ b/frontend/src/components/simulator/BoardOnCanvas.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { getProBoard } from '../../lib/proBoardRegistry'; import type { BoardInstance } from '../../types/board'; import { ArduinoUno } from '../velxio-components/ArduinoUno'; import { ArduinoNano } from '../velxio-components/ArduinoNano'; @@ -101,12 +102,24 @@ export const BoardOnCanvas = ({ zoom = 1, }: BoardOnCanvasProps) => { const { id, boardKind, x, y } = board; - const size = BOARD_SIZE[boardKind] ?? { w: 300, h: 200 }; + const size = BOARD_SIZE[boardKind] ?? getProBoard(boardKind)?.size ?? { w: 300, h: 200 }; // Status dot color: green=running, amber=compiled, gray=idle const statusColor = board.running ? '#22c55e' : board.compiledProgram ? '#f59e0b' : '#6b7280'; const boardEl = (() => { + // Overlay-registered board (proBoardRegistry): the overlay either provides + // a render function or we mount its custom element directly — the element + // was defined by the overlay's import, and pinInfo lives on the DOM node + // like any other board Web Component. + const proDef = getProBoard(boardKind); + if (proDef) { + if (proDef.render) return proDef.render({ id, x, y, running: !!board.running }); + return React.createElement(proDef.tag, { + id, + style: { position: 'absolute', left: x, top: y }, + }); + } switch (boardKind) { case 'arduino-uno': return ; diff --git a/frontend/src/components/simulator/BoardPickerModal.tsx b/frontend/src/components/simulator/BoardPickerModal.tsx index 26b21f6a..f3e18d4c 100644 --- a/frontend/src/components/simulator/BoardPickerModal.tsx +++ b/frontend/src/components/simulator/BoardPickerModal.tsx @@ -4,6 +4,15 @@ import { useTranslation } from 'react-i18next'; import type { BoardKind } from '../../types/board'; import { BOARD_KIND_LABELS } from '../../types/board'; +/** Neutral chip glyph for overlay-registered boards without a bespoke icon. */ +const PRO_FALLBACK_ICON = ( + + + + +); + + const BOARD_DESCRIPTIONS: Record = { 'arduino-uno': '8-bit AVR, 32KB flash, 14 digital I/O', 'arduino-nano': 'Compact 8-bit AVR, same as Uno', @@ -123,7 +132,7 @@ export const BoardPickerModal = ({ isOpen, onClose, onSelectBoard }: BoardPicker : '#4af', }} > - {BOARD_ICON[kind]} + {BOARD_ICON[kind] ?? PRO_FALLBACK_ICON}
{BOARD_KIND_LABELS[kind]}
diff --git a/frontend/src/data/examples.ts b/frontend/src/data/examples.ts index 4eb8299b..3e1674b2 100644 --- a/frontend/src/data/examples.ts +++ b/frontend/src/data/examples.ts @@ -50,7 +50,9 @@ export interface ExampleProject { | 'esp32' | 'esp32-s3' | 'esp32-c3' - | 'esp32-cam'; + | 'esp32-cam' + // Overlay-registered boards (proBoardRegistry) use their kind string. + | (string & {}); /** Board filter key used in the gallery board selector. Derived from boardType if omitted. */ boardFilter?: string; /** @@ -10016,6 +10018,17 @@ export function getExampleById(id: string): ExampleProject | undefined { return exampleProjects.find((example) => example.id === id); } +/** + * Overlay seam: a private build can append gallery examples for the boards it + * registers at runtime. Push-based (the exported array is the single source + * the gallery and /example/:slug both read), idempotent per example id. + */ +export function registerProExamples(examples: ExampleProject[]): void { + for (const ex of examples) { + if (!exampleProjects.some((e) => e.id === ex.id)) exampleProjects.push(ex); + } +} + // Get all categories export function getCategories(): ExampleProject['category'][] { return ['basics', 'sensors', 'displays', 'communication', 'games', 'robotics']; diff --git a/frontend/src/lib/proBoardRegistry.ts b/frontend/src/lib/proBoardRegistry.ts new file mode 100644 index 00000000..607474b3 --- /dev/null +++ b/frontend/src/lib/proBoardRegistry.ts @@ -0,0 +1,109 @@ +/** + * 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 { + BOARD_KIND_LABELS, + BOARD_KIND_FQBN, + BOARD_SUPPORTS_MICROPYTHON, + 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; + /** 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'; + /** 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; + /** 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 binary the backend returned. */ + loadFirmware?: ( + sim: ProBoardSimulator, + program: Uint8Array, + 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; + /** Sidebar / toolbar accents (fall back to a neutral chip icon). */ + icon?: string; + color?: string; +} + +const registry = new Map(); + +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); + } +} + +export function getProBoard(kind: string): ProBoardDef | undefined { + return registry.get(kind); +} + +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; +} diff --git a/frontend/src/simulation/Esp32Bridge.ts b/frontend/src/simulation/Esp32Bridge.ts index d9dcea87..75920542 100644 --- a/frontend/src/simulation/Esp32Bridge.ts +++ b/frontend/src/simulation/Esp32Bridge.ts @@ -35,13 +35,17 @@ */ import type { BoardKind } from '../types/board'; +import { getProBoard } from '../lib/proBoardRegistry'; import { generateUUID } from '../utils/uuid'; /** * Map any ESP32-family board kind to the 3 base QEMU machine types understood * by the backend esp_qemu_manager. */ -export function toQemuBoardType(kind: BoardKind): 'esp32' | 'esp32-s3' | 'esp32-c3' { +export function toQemuBoardType(kind: BoardKind): 'esp32' | 'esp32-s3' | 'esp32-c3' | 'esp32-c6' { + // Overlay-registered boards carry their base chip in the registry. + const proFam = getProBoard(kind)?.esp32Family; + if (proFam) return proFam; if (kind === 'esp32-s3' || kind === 'xiao-esp32-s3' || kind === 'arduino-nano-esp32') return 'esp32-s3'; if (kind === 'esp32-c3' || kind === 'xiao-esp32-c3' || kind === 'aitewinrobot-esp32c3-supermini') @@ -118,6 +122,11 @@ export class Esp32Bridge { */ sdImageB64: string | undefined = undefined; + /** SD chip-select GPIO for a board with a BUILT-IN SD sharing the SPI bus — + * the worker CS-gates the slave so it doesn't consume the display stream. + * Undefined for a standalone microsd-card component (owns the bus). */ + sdCsPin: number | undefined = undefined; + // Callbacks wired up by useSimulatorStore onSerialData: ((char: string, uart?: number) => void) | null = null; onPinChange: ((gpioPin: number, state: boolean) => void) | null = null; @@ -235,6 +244,8 @@ export class Esp32Bridge { case 'xiao-esp32-s3': case 'arduino-nano-esp32': return 43; + case 'esp32-c6': + return 16; // U0TXD default on the C6 (silkscreen TX on the DevKitC-1) case 'esp32-c3': case 'xiao-esp32-c3': case 'aitewinrobot-esp32c3-supermini': @@ -317,7 +328,14 @@ export class Esp32Bridge { ...(this._pendingFirmware ? { firmware_b64: this._pendingFirmware } : {}), sensors: this._pendingSensors, wifi_enabled: this.wifiEnabled, - ...(this.sdImageB64 ? { sd_card: { image_b64: this.sdImageB64 } } : {}), + ...(this.sdImageB64 + ? { + sd_card: { + image_b64: this.sdImageB64, + ...(this.sdCsPin !== undefined ? { cs_pin: this.sdCsPin } : {}), + }, + } + : {}), }, }); }; @@ -880,6 +898,16 @@ export class Esp32Bridge { sendChunk(); } + /** + * Push a key press/release for a board's built-in matrix keyboard. `row`/ + * `col` are the logical grid position dispatched by the board Web Component; + * the worker's keyboard slave encodes it and pulses its interrupt line. + * No-op for boards without a keyboard peripheral configured. + */ + sendKey(row: number, col: number, pressed: boolean): void { + this._send({ type: 'esp32_keyboard_key', data: { row, col, pressed } }); + } + private _send(payload: unknown): void { if (this.socket && this.socket.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify(payload)); diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index 9e30ad78..d5ac1820 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -1,4 +1,5 @@ import { create } from 'zustand'; +import { getProBoard, isProBoardSimulator, type ProBoardSimulator } from '../lib/proBoardRegistry'; import { AVRSimulator } from '../simulation/AVRSimulator'; import { RP2040Simulator } from '../simulation/RP2040Simulator'; import { RiscVSimulator } from '../simulation/RiscVSimulator'; @@ -775,11 +776,14 @@ const ESP32_RISCV_KINDS = new Set([ ]); function isEsp32Kind(kind: BoardKind): boolean { - return ESP32_KINDS.has(kind) || ESP32_RISCV_KINDS.has(kind); + if (ESP32_KINDS.has(kind) || ESP32_RISCV_KINDS.has(kind)) return true; + // Overlay-registered ESP32-class boards route through the same bridge path. + return getProBoard(kind)?.esp32Family !== undefined; } function isRiscVEsp32Kind(kind: BoardKind): boolean { - return ESP32_RISCV_KINDS.has(kind); + const fam = getProBoard(kind)?.esp32Family; + return ESP32_RISCV_KINDS.has(kind) || fam === 'esp32-c3' || fam === 'esp32-c6'; } // ── Component type ──────────────────────────────────────────────────────── @@ -992,9 +996,13 @@ function createSimulator( onSerial: (ch: string) => void, onBaud: (baud: number) => void, onPinTime: (pin: number, state: boolean, t: number) => void, -): AVRSimulator | RP2040Simulator | RiscVSimulator | Esp32C3Simulator { - let sim: AVRSimulator | RP2040Simulator | RiscVSimulator | Esp32C3Simulator; - if (boardKind === 'arduino-mega') { +): AVRSimulator | RP2040Simulator | RiscVSimulator | Esp32C3Simulator | ProBoardSimulator { + let sim: AVRSimulator | RP2040Simulator | RiscVSimulator | Esp32C3Simulator | ProBoardSimulator; + const proDef = getProBoard(boardKind); + if (proDef?.createSimulator) { + // Overlay-provided in-browser simulator (e.g. the RP2350/Hazard3 engine). + sim = proDef.createSimulator(pm); + } else if (boardKind === 'arduino-mega') { sim = new AVRSimulator(pm, 'mega'); } else if (boardKind === 'attiny85') { sim = new AVRSimulator(pm, 'tiny85'); @@ -1295,6 +1303,11 @@ export const useSimulatorStore = create((set, get) => { // surfaces WiFi status via setBoardWifiStatus(). if (sim instanceof RP2040Simulator) { sim.attachPioPeripheral(boardKind, id); + } else if (isProBoardSimulator(sim)) { + (sim as { attachPioPeripheral?: (k: string, i: string) => void }).attachPioPeripheral?.( + boardKind, + id, + ); } } @@ -1381,6 +1394,7 @@ export const useSimulatorStore = create((set, get) => { // Detach the PIO peripheral (it disconnects its own bridge). const rpSim = getBoardSimulator(boardId); if (rpSim instanceof RP2040Simulator) rpSim.detachPioPeripheral(); + else if (isProBoardSimulator(rpSim)) rpSim.detachPioPeripheral?.(); set((s) => { const boards = s.boards.filter((b) => b.id !== boardId); const activeBoardId = @@ -1569,6 +1583,13 @@ export const useSimulatorStore = create((set, get) => { sim.addI2CDevice(new VirtualDS1307() as RP2040I2CDevice); sim.addI2CDevice(new VirtualTempSensor() as RP2040I2CDevice); sim.addI2CDevice(new I2CMemoryDevice(0x50) as RP2040I2CDevice); + } else if (isProBoardSimulator(sim)) { + // Overlay-registered board: the overlay owns the whole load + // sequence (PIO/peripheral attach, binary format, demo devices). + getProBoard(board.boardKind)?.loadFirmware?.(sim, program, { + boardKind: board.boardKind, + boardId, + }); } } catch (err) { console.error(`compileBoardProgram(${boardId}):`, err); @@ -1890,6 +1911,11 @@ export const useSimulatorStore = create((set, get) => { sensors.push(props); } + // Built-in bridge peripherals an overlay-registered board declares + // (e.g. an on-board I2C keyboard) — no canvas wiring involved. + for (const builtIn of getProBoard(board.boardKind)?.builtInSensors ?? []) { + sensors.push({ ...builtIn }); + } esp32Bridge.setSensors(sensors); // Use WiFi flag set by the compiler (most reliable — avoids stale file group issues). @@ -1920,17 +1946,25 @@ export const useSimulatorStore = create((set, get) => { // it to the bridge so the QEMU worker can attach it as an SD-over-SPI // slave. No card -> clear any stale image from a previous run. const sdCard = components.find((c) => c.metadataId === 'microsd-card'); - if (sdCard) { + // Overlay-registered boards can declare a BUILT-IN microSD on a + // shared SPI bus: attach it even without a card component, and tell + // the bridge to CS-gate it so it doesn't eat the display's pixel + // stream. A standalone card owns the bus -> no gating. + const builtInSdCs = getProBoard(board.boardKind)?.builtInSdCsPin; + if (sdCard || builtInSdCs !== undefined) { try { - const uploaded = decodeSdFiles(sdCard.properties.sdFiles); + const uploaded = sdCard ? decodeSdFiles(sdCard.properties.sdFiles) : []; const image = buildProjectSdImage(useEditorStore.getState().files, uploaded); esp32Bridge.sdImageB64 = bytesToB64(image); + esp32Bridge.sdCsPin = sdCard ? undefined : builtInSdCs; } catch (e) { console.warn('[microsd] SD image build failed:', e); esp32Bridge.sdImageB64 = undefined; + esp32Bridge.sdCsPin = undefined; } } else { esp32Bridge.sdImageB64 = undefined; + esp32Bridge.sdCsPin = undefined; } // Ensure firmware is loaded into the bridge (handles page-refresh case diff --git a/frontend/src/utils/boardPinMapping.ts b/frontend/src/utils/boardPinMapping.ts index e89b43d8..dd956088 100644 --- a/frontend/src/utils/boardPinMapping.ts +++ b/frontend/src/utils/boardPinMapping.ts @@ -1,3 +1,4 @@ +import { getProBoard } from '../lib/proBoardRegistry'; /** * Board Pin Mapping Utility * @@ -254,6 +255,12 @@ export function isBoardComponent(componentId: string): boolean { * @returns Numeric pin/GPIO number, or null if unmapped */ export function boardPinToNumber(boardId: string, pinName: string): number | null { + // Overlay-registered boards resolve through their own mapping first. + const proDef = getProBoard(boardId); + if (proDef?.pinToNumber) { + const n = proDef.pinToNumber(pinName); + if (n !== null) return n; + } if (boardId === 'arduino-uno' || boardId === 'arduino-nano') { // Power / GND pins — not real GPIOs, skip silently if (/^(GND|VCC|VIN|IOREF|AREF|RESET|3\.3V|3V3|5V|3V)/.test(pinName)) return -1;