feat(boards): runtime board-registration seam for private overlays

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.
This commit is contained in:
David Montero Crespo 2026-07-22 21:21:21 +02:00
parent 9e8381e032
commit 32958640a3
9 changed files with 252 additions and 22 deletions

View File

@ -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<ComponentPickerModalProps> = ({
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<ComponentPickerModalProps> = ({
{/* Boards Panel */}
{selectedCategory === 'boards' ? (
<div className="components-grid" onScroll={clearHover}>
{ALL_BOARDS.map((kind) => (
{allBoards.map((kind) => (
<BoardCard
key={kind}
kind={kind}
@ -328,7 +334,7 @@ export const ComponentPickerModal: React.FC<ComponentPickerModalProps> = ({
hoverApi={hoverApi}
/>
))}
{VISIBLE_BOARD_ADS.map((ad) => (
{visibleBoardAds().map((ad) => (
<OnlineOnlyBoardCard key={ad.id} ad={ad} />
))}
</div>
@ -343,7 +349,7 @@ export const ComponentPickerModal: React.FC<ComponentPickerModalProps> = ({
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<ComponentPickerModalProps> = ({
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<BoardCardProps> = ({ 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<BoardCardProps> = ({ 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<BoardCardProps> = ({ kind, onSelect, hoverApi }) => {
</div>
<div className="card-content">
<div className="card-name">{BOARD_KIND_LABELS[kind]}</div>
<div className="card-description">{BOARD_DESCRIPTIONS[kind]}</div>
<div className="card-description">{BOARD_DESCRIPTIONS[kind] ?? getProBoard(kind)?.description}</div>
</div>
</button>
);
@ -759,7 +765,9 @@ const BoardCard: React.FC<BoardCardProps> = ({ 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 = () => (

View File

@ -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 width="16" height="16" viewBox="0 0 16 16" fill="none">
<rect x="3" y="3" width="10" height="10" rx="2" fill="#8b5cf6" />
<rect x="5.5" y="5.5" width="5" height="5" rx="1" fill="#1e1b2e" />
</svg>
);
// SVG icons — same style as EditorToolbar (stroke-based, 16x16)
const IcoFile = () => (
<svg
@ -582,7 +591,7 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ 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<FileExplorerProps> = ({ onSaveClick, onNewCl
</button>
<span className="fe-board-icon" style={{ color }}>
{BOARD_ICON[board.boardKind]}
{BOARD_ICON[board.boardKind] ?? PRO_FALLBACK_ICON}
</span>
{renamingSection?.id === board.id && renamingSection.kind === 'board' ? (

View File

@ -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 <ArduinoUno id={id} x={x} y={y} led13={led13} />;

View File

@ -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 = (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<rect x="3" y="3" width="10" height="10" rx="2" fill="#8b5cf6" />
<rect x="5.5" y="5.5" width="5" height="5" rx="1" fill="#1e1b2e" />
</svg>
);
const BOARD_DESCRIPTIONS: Record<BoardKind, string> = {
'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}
</span>
<div>
<div style={{ fontWeight: 600, fontSize: 14 }}>{BOARD_KIND_LABELS[kind]}</div>

View File

@ -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'];

View File

@ -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 `<tag id=... style=absolute@x,y>`. */
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<string, ProBoardDef>();
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<string, string>)[kind] = def.label;
(BOARD_KIND_FQBN as Record<string, string | null>)[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;
}

View File

@ -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));

View File

@ -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<BoardKind>([
]);
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<SimulatorState>((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<SimulatorState>((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<SimulatorState>((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<SimulatorState>((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<SimulatorState>((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

View File

@ -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;