From cfa03aeaf951028017873ae7b7aab4b5c31adca3 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Tue, 4 Aug 2026 05:55:21 +0200 Subject: [PATCH] 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. --- .../components/simulator/SimulatorCanvas.tsx | 18 +++++++++++++++--- frontend/src/data/examples.ts | 4 ++++ frontend/src/store/useSimulatorStore.ts | 7 ++++++- frontend/src/types/board.ts | 6 ++++++ frontend/src/utils/loadExample.ts | 6 ++++++ frontend/src/utils/vlxFile.ts | 4 ++++ 6 files changed, 41 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index fc2cdda0..b9087d7b 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -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); diff --git a/frontend/src/data/examples.ts b/frontend/src/data/examples.ts index 7d2c2612..aa5d8aa5 100644 --- a/frontend/src/data/examples.ts +++ b/frontend/src/data/examples.ts @@ -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; + /** 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 { diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index 24ba269f..0996df49 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -2251,7 +2251,12 @@ export const useSimulatorStore = create((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; diff --git a/frontend/src/types/board.ts b/frontend/src/types/board.ts index 381c4fbc..a2e14038 100644 --- a/frontend/src/types/board.ts +++ b/frontend/src/types/board.ts @@ -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 diff --git a/frontend/src/utils/loadExample.ts b/frontend/src/utils/loadExample.ts index ca08061f..0b1420bf 100644 --- a/frontend/src/utils/loadExample.ts +++ b/frontend/src/utils/loadExample.ts @@ -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 diff --git a/frontend/src/utils/vlxFile.ts b/frontend/src/utils/vlxFile.ts index c6cf5f92..bc2112dc 100644 --- a/frontend/src/utils/vlxFile.ts +++ b/frontend/src/utils/vlxFile.ts @@ -53,6 +53,7 @@ export interface VlxPayload { activeFileGroupId: string; languageMode?: string; serialBaudRate?: number; + sdFiles?: Array<{ name: string; contentB64: string }>; }>; fileGroups: Record>; 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, }; }