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:
parent
9e8381e032
commit
32958640a3
|
|
@ -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 = () => (
|
||||
|
|
|
|||
|
|
@ -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' ? (
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue