feat(sd): boards with a built-in microSD slot take user uploads

A ProBoardDef can declare builtInSdCsPin, and the store already built a FAT
card from the project files for it - but there was no way to put YOUR files
on that card: the upload panel only existed for the microsd-card component
and for parts with metadata.sdSlot, and boards have no properties bag.

Boards now carry sdFiles (same shape the SD panel persists everywhere else):

- BoardInstance.sdFiles, consumed by the store's Run path (card component
  still wins when one is on the canvas);
- the board inspector shows the SD Card panel whenever the board kind
  declares builtInSdCsPin, and routes the panel's onChange to updateBoard;
- .vlx round-trips it (serialisableBoard + payload type), and gallery
  examples can pre-load the slot via ExampleBoard.sdFiles, applied by
  loadExample the way the panel would.

First user: the XIAO ESP32S3 Sense (CS=GPIO21), whose board def and example
land in the pro overlay.
This commit is contained in:
David Montero Crespo 2026-08-04 05:55:21 +02:00
parent c6256cc49a
commit cfa03aeaf9
6 changed files with 41 additions and 4 deletions

View File

@ -4,7 +4,7 @@ import {
getBoardBridge,
getBoardSimulator,
} from '../../store/useSimulatorStore';
import { getBoardBuiltins } from '../../lib/proBoardRegistry';
import { getBoardBuiltins, getProBoard } from '../../lib/proBoardRegistry';
import { useElectricalStore } from '../../store/useElectricalStore';
import { openDeviceGateway } from '../../lib/openDeviceGateway';
import React, { useEffect, useState, useRef, useCallback } from 'react';
@ -67,7 +67,7 @@ import {
} from '../../utils/wireHitDetection';
import { useIsCoarsePointer } from '../../utils/useTouchDevice';
import type { ComponentMetadata } from '../../types/component-metadata';
import type { BoardKind } from '../../types/board';
import type { BoardKind, BoardInstance } from '../../types/board';
import { BOARD_KIND_FQBN, boardDisplayName } from '../../types/board';
import {
boardGateDecision,
@ -3405,9 +3405,13 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
defaultValues: {},
pinCount: boardPins?.length ?? 0,
tags: [],
// A board with a BUILT-IN microSD slot (XIAO Sense) gets the
// same SD Card upload panel a component slot gets; the files
// live on board.sdFiles and feed the card image on Run.
sdSlot: getProBoard(board.boardKind)?.builtInSdCsPin !== undefined,
} as unknown as ComponentMetadata
}
componentProperties={{}}
componentProperties={{ sdFiles: board.sdFiles }}
position={{ x: boardContextMenu.x, y: boardContextMenu.y }}
pinInfo={boardPins || []}
previewTagName={element?.tagName.toLowerCase()}
@ -3421,6 +3425,14 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
}
wireInProgress={Boolean(wireInProgress)}
onClose={() => setBoardContextMenu(null)}
onPropertyChange={(id, propName, value) => {
// The board dialog's only editable "property" is the SD
// slot's upload list; it lives on the board, not in a
// component's properties bag.
if (propName === 'sdFiles') {
updateBoard(id, { sdFiles: value as BoardInstance['sdFiles'] });
}
}}
onDelete={(id) => {
setBoardContextMenu(null);
setBoardToRemove(id);

View File

@ -32,6 +32,10 @@ export interface ExampleBoard {
code?: string;
/** Files pre-loaded into the Pi VFS (path → content). Only used for raspberry-pi-3. */
vfsFiles?: Record<string, string>;
/** Uploads pre-loaded onto a board's BUILT-IN microSD slot (a ProBoardDef
* with builtInSdCsPin, e.g. the XIAO ESP32S3 Sense) — the same shape the
* SD Card panel persists on board.sdFiles. */
sdFiles?: Array<{ name: string; contentB64: string }>;
}
export interface ExampleProject {

View File

@ -2251,7 +2251,12 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
const builtInSdCs = getProBoard(board.boardKind)?.builtInSdCsPin;
if (sdCard || builtInSdCs !== undefined) {
try {
const uploaded = sdCard ? decodeSdFiles(sdCard.properties.sdFiles) : [];
// Uploads come from the card component when one is on the
// canvas, else from the BOARD's own slot (board.sdFiles - the
// XIAO Sense pattern, same panel, persisted on the board).
const uploaded = sdCard
? decodeSdFiles(sdCard.properties.sdFiles)
: decodeSdFiles(board.sdFiles);
const image = buildProjectSdImage(useEditorStore.getState().files, uploaded);
esp32Bridge.sdImageB64 = bytesToB64(image);
esp32Bridge.sdCsPin = sdCard ? undefined : builtInSdCs;

View File

@ -146,6 +146,12 @@ export interface BoardInstance {
// Types live in `./boardOptions` to avoid a circular import.
boardOptions?: import('./boardOptions').ESP32BoardOptions;
spiffsFiles?: import('./boardOptions').SpiffsFile[];
/** User uploads for a board's BUILT-IN microSD slot (a ProBoardDef with
* builtInSdCsPin, e.g. the XIAO ESP32S3 Sense). Same shape the
* microsd-card component persists in properties.sdFiles, and consumed the
* same way: merged into buildProjectSdImage on Run, overriding same-named
* project files. Undefined for boards without a slot. */
sdFiles?: Array<{ name: string; contentB64: string }>;
// P2.4 — this board's declared library manifest (its velxio.json). The ESP32
// compile scope: each board resolves ONLY its own declared libraries, so two
// boards in the same project can use different (even conflicting) libraries

View File

@ -175,6 +175,12 @@ export async function loadExample(
useEditorStore.getState().loadFiles([{ name: filename, content: eb.code }]);
}
// Built-in SD slot uploads (XIAO Sense): land on the board exactly as
// the SD Card panel would put them, so the Run path finds them.
if (eb.sdFiles?.length) {
useSimulatorStore.getState().updateBoard(boardId, { sdFiles: eb.sdFiles });
}
if (eb.vfsFiles && isPiBoardKind(eb.boardKind)) {
// Pi example scripts go into the board's REGULAR file group — they
// are edited in Monaco like any other board's code, and the run

View File

@ -53,6 +53,7 @@ export interface VlxPayload {
activeFileGroupId: string;
languageMode?: string;
serialBaudRate?: number;
sdFiles?: Array<{ name: string; contentB64: string }>;
}>;
fileGroups: Record<string, Array<{ name: string; content: string }>>;
components: Component[];
@ -70,6 +71,9 @@ function serialisableBoard(b: BoardInstance) {
activeFileGroupId: b.activeFileGroupId,
languageMode: b.languageMode,
serialBaudRate: b.serialBaudRate,
// Built-in SD slot uploads (XIAO Sense etc.) travel with the project,
// exactly like a microsd-card component's properties.sdFiles do.
sdFiles: b.sdFiles,
};
}