feat(boards): a board can ship the code it opens with
The editor seeded a freshly placed board from its FAMILY: the Arduino LED_BUILTIN blink, the Pico Pin(25) MicroPython blink, or the Raspberry Pi RPi.GPIO script. None of those run on a board that has no such LED, no RPi.GPIO and its own vendor library, so the first Run failed on a board the user had not touched yet. ProBoardDef gains defaultFiles (per language mode, plus 'python' for the QEMU-Linux guests) and defaultLibraries; addBoard and setBoardLanguageMode prefer the board's own seed over the family default, and the seeded libraries land in the board's compile manifest. Also fix the MicroPython family fallback: it recognised an ESP32 by 'esp32' appearing in the group id, so an overlay ESP32 whose kind carries no such substring (m5stack-core, cardputer-adv) was seeded the Pico's Pin(25) blink - a dead pin on both. It now consults the board's declared esp32Family too.
This commit is contained in:
parent
0a0badd10f
commit
189ba355a9
|
|
@ -0,0 +1,140 @@
|
|||
/**
|
||||
* Per-board seed code — what a freshly placed board starts with in the editor.
|
||||
*
|
||||
* The editor's family defaults (Arduino `LED_BUILTIN` blink, Pico `Pin(25)`
|
||||
* blink, Raspberry Pi `RPi.GPIO` script) do not run on a board that has no
|
||||
* such LED and its own vendor library: a placed M5Stack / UNIHIKER / Badger
|
||||
* used to open with code that could not work on it. A board declares its own
|
||||
* seed through ProBoardDef.defaultFiles, and addBoard / setBoardLanguageMode
|
||||
* must prefer it over the family default.
|
||||
*
|
||||
* Pure Vitest unit tests — no QEMU, no network, no DOM.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { registerProBoards, getBoardSeedFiles } from '../lib/proBoardRegistry';
|
||||
import { useEditorStore } from '../store/useEditorStore';
|
||||
import { useSimulatorStore } from '../store/useSimulatorStore';
|
||||
import type { BoardKind } from '../types/board';
|
||||
|
||||
const SEEDED_ARDUINO = '// seeded arduino sketch';
|
||||
const SEEDED_MPY = '# seeded micropython';
|
||||
const SEEDED_PY = '# seeded guest script';
|
||||
|
||||
registerProBoards([
|
||||
{
|
||||
kind: 'seed-test-esp32',
|
||||
label: 'Seed Test ESP32',
|
||||
fqbn: 'esp32:esp32:esp32',
|
||||
description: 'test board that ships its own seed code',
|
||||
tag: 'velxio-seed-test-esp32',
|
||||
size: { w: 100, h: 100 },
|
||||
supportsMicroPython: true,
|
||||
esp32Family: 'esp32',
|
||||
defaultLibraries: ['SeedLib'],
|
||||
defaultFiles: {
|
||||
arduino: [{ name: 'sketch.ino', content: SEEDED_ARDUINO }],
|
||||
micropython: [{ name: 'main.py', content: SEEDED_MPY }],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'seed-test-linux',
|
||||
label: 'Seed Test Linux SBC',
|
||||
fqbn: null,
|
||||
description: 'test QEMU-Linux board that ships its own guest script',
|
||||
tag: 'velxio-seed-test-linux',
|
||||
size: { w: 100, h: 100 },
|
||||
piFamily: true,
|
||||
defaultFiles: { python: [{ name: 'script.py', content: SEEDED_PY }] },
|
||||
},
|
||||
{
|
||||
kind: 'seed-test-plain',
|
||||
label: 'Seed Test Plain',
|
||||
fqbn: 'esp32:esp32:esp32',
|
||||
description: 'test board with no seed of its own',
|
||||
tag: 'velxio-seed-test-plain',
|
||||
size: { w: 100, h: 100 },
|
||||
supportsMicroPython: true,
|
||||
esp32Family: 'esp32',
|
||||
},
|
||||
]);
|
||||
|
||||
const groupFiles = (groupId: string) => useEditorStore.getState().getGroupFiles(groupId);
|
||||
|
||||
describe('getBoardSeedFiles', () => {
|
||||
it('returns the board’s own files for a declared mode', () => {
|
||||
expect(getBoardSeedFiles('seed-test-esp32', 'arduino')?.[0].content).toBe(SEEDED_ARDUINO);
|
||||
expect(getBoardSeedFiles('seed-test-esp32', 'micropython')?.[0].content).toBe(SEEDED_MPY);
|
||||
expect(getBoardSeedFiles('seed-test-linux', 'python')?.[0].content).toBe(SEEDED_PY);
|
||||
});
|
||||
|
||||
it('returns undefined for an undeclared mode or an unknown board', () => {
|
||||
expect(getBoardSeedFiles('seed-test-esp32', 'espidf')).toBeUndefined();
|
||||
expect(getBoardSeedFiles('seed-test-plain', 'arduino')).toBeUndefined();
|
||||
expect(getBoardSeedFiles('arduino-uno', 'arduino')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('addBoard seeds the board’s own code', () => {
|
||||
beforeEach(() => {
|
||||
for (const b of [...useSimulatorStore.getState().boards]) {
|
||||
useSimulatorStore.getState().removeBoard(b.id);
|
||||
}
|
||||
});
|
||||
|
||||
it('a placed board with a seed opens on its own sketch, not the blink default', () => {
|
||||
const id = useSimulatorStore.getState().addBoard('seed-test-esp32' as BoardKind, 0, 0);
|
||||
const files = groupFiles(`group-${id}`);
|
||||
expect(files).toHaveLength(1);
|
||||
expect(files[0].name).toBe('sketch.ino');
|
||||
expect(files[0].content).toBe(SEEDED_ARDUINO);
|
||||
});
|
||||
|
||||
it('the seed’s vendor libraries land in the board’s compile manifest', () => {
|
||||
const id = useSimulatorStore.getState().addBoard('seed-test-esp32' as BoardKind, 0, 0);
|
||||
const board = useSimulatorStore.getState().boards.find((b) => b.id === id);
|
||||
expect(board?.libraries).toEqual(['SeedLib']);
|
||||
});
|
||||
|
||||
it('a QEMU-Linux board seeds its guest script instead of the RPi.GPIO default', () => {
|
||||
const id = useSimulatorStore.getState().addBoard('seed-test-linux' as BoardKind, 0, 0);
|
||||
const files = groupFiles(`group-${id}`);
|
||||
expect(files[0].name).toBe('script.py');
|
||||
expect(files[0].content).toBe(SEEDED_PY);
|
||||
expect(files[0].content).not.toContain('RPi.GPIO');
|
||||
});
|
||||
|
||||
it('a board without a seed keeps the family default', () => {
|
||||
const id = useSimulatorStore.getState().addBoard('seed-test-plain' as BoardKind, 0, 0);
|
||||
const files = groupFiles(`group-${id}`);
|
||||
expect(files[0].name).toBe('sketch.ino');
|
||||
expect(files[0].content).toContain('LED_BUILTIN');
|
||||
});
|
||||
});
|
||||
|
||||
describe('switching language mode re-seeds from the board', () => {
|
||||
beforeEach(() => {
|
||||
for (const b of [...useSimulatorStore.getState().boards]) {
|
||||
useSimulatorStore.getState().removeBoard(b.id);
|
||||
}
|
||||
});
|
||||
|
||||
it('MicroPython mode uses the board’s own main.py', () => {
|
||||
const id = useSimulatorStore.getState().addBoard('seed-test-esp32' as BoardKind, 0, 0);
|
||||
useSimulatorStore.getState().setBoardLanguageMode(id, 'micropython');
|
||||
const files = groupFiles(`group-${id}`);
|
||||
expect(files[0].name).toBe('main.py');
|
||||
expect(files[0].content).toBe(SEEDED_MPY);
|
||||
});
|
||||
|
||||
it('a seedless ESP32-family overlay board gets the ESP32 blink, not the Pico one', () => {
|
||||
// The fallback used to key off 'esp32' appearing in the group id, so an
|
||||
// overlay ESP32 whose kind has no 'esp32' in its name (m5stack-core,
|
||||
// cardputer-adv) was seeded the Pico's Pin(25) blink — a dead pin there.
|
||||
const id = useSimulatorStore.getState().addBoard('seed-test-plain' as BoardKind, 0, 0);
|
||||
useSimulatorStore.getState().setBoardLanguageMode(id, 'micropython');
|
||||
const files = groupFiles(`group-${id}`);
|
||||
expect(files[0].name).toBe('main.py');
|
||||
expect(files[0].content).toContain('Pin(2, Pin.OUT)');
|
||||
});
|
||||
});
|
||||
|
|
@ -138,6 +138,30 @@ export interface ProBoardDef {
|
|||
bridge: unknown;
|
||||
}) => () => void;
|
||||
|
||||
/**
|
||||
* Seed code a freshly placed board starts with, per language mode.
|
||||
*
|
||||
* Without this the editor falls back to the family default — the Arduino
|
||||
* `LED_BUILTIN` blink, the Pico `Pin(25)` blink, or the Raspberry Pi
|
||||
* `RPi.GPIO` script — and NONE of those run on a board that has no such
|
||||
* LED, no RPi.GPIO and its own vendor library (an M5Stack needs
|
||||
* `M5.begin()`, the UNIHIKER needs pinpong/unihiker). The user's very
|
||||
* first Run then fails on a board they have not touched yet. Keep each
|
||||
* entry as close to the vendor's own first example as the emulator
|
||||
* allows, and self-contained: a freshly placed board has nothing wired
|
||||
* to it, so the code must do something visible on the board itself.
|
||||
*
|
||||
* Keys are the board's language modes plus 'python' for QEMU-Linux
|
||||
* (piFamily) boards, whose seed is the guest script.
|
||||
*/
|
||||
defaultFiles?: Partial<
|
||||
Record<'arduino' | 'micropython' | 'espidf' | 'python', Array<{ name: string; content: string }>>
|
||||
>;
|
||||
/** Library manifest seeded together with `defaultFiles.arduino` — the seed
|
||||
* sketch includes the vendor library, so the board must declare it or the
|
||||
* first compile resolves against nothing. */
|
||||
defaultLibraries?: string[];
|
||||
|
||||
/** Sidebar / toolbar accents (fall back to a neutral chip icon). */
|
||||
icon?: string;
|
||||
color?: string;
|
||||
|
|
@ -226,6 +250,19 @@ export function getBoardBuiltins(kind: string): BuiltinsAttach | undefined {
|
|||
return registry.get(kind)?.attachBuiltins ?? boardBuiltins.get(kind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed files for a board kind in a given language mode, or undefined when the
|
||||
* board carries no seed of its own (the editor's family default applies).
|
||||
* `mode` is 'python' for QEMU-Linux boards — see ProBoardDef.defaultFiles.
|
||||
*/
|
||||
export function getBoardSeedFiles(
|
||||
kind: string,
|
||||
mode: 'arduino' | 'micropython' | 'espidf' | 'python',
|
||||
): Array<{ name: string; content: string }> | undefined {
|
||||
const files = registry.get(kind)?.defaultFiles?.[mode];
|
||||
return files && files.length ? files : undefined;
|
||||
}
|
||||
|
||||
export function listProBoards(): ProBoardDef[] {
|
||||
return Array.from(registry.values());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { create } from 'zustand';
|
||||
import { generateUUID } from '../utils/uuid';
|
||||
import { isPiBoardKind } from '../types/board';
|
||||
import { getProBoard } from '../lib/proBoardRegistry';
|
||||
|
||||
export interface WorkspaceFile {
|
||||
id: string;
|
||||
|
|
@ -491,7 +492,14 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||
const mainId = `${groupId}-main`;
|
||||
let fileName: string;
|
||||
let content: string;
|
||||
const isEsp32 = groupId.includes('esp32');
|
||||
// ESP32 by id ('group-esp32-c3') or by the overlay board's declared
|
||||
// family — an overlay kind can be an ESP32 with no 'esp32' in its name
|
||||
// (m5stack-core, cardputer-adv), and those used to be seeded the Pico's
|
||||
// `Pin(25)` blink, a dead pin on both.
|
||||
const isEsp32 =
|
||||
groupId.includes('esp32') ||
|
||||
getProBoard(boardIdPart)?.esp32Family !== undefined ||
|
||||
getProBoard(boardIdPart.replace(/-\d+$/, ''))?.esp32Family !== undefined;
|
||||
if (languageMode === 'espidf') {
|
||||
fileName = 'main.c';
|
||||
content = DEFAULT_ESPIDF_CONTENT;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { create } from 'zustand';
|
||||
import { decideEngine, getInstantEngine } from '../lib/instantEngine';
|
||||
import {
|
||||
getBoardSeedFiles,
|
||||
getProBoard,
|
||||
getGuestSetup,
|
||||
isProBoardSimulator,
|
||||
|
|
@ -1681,8 +1682,24 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
simulator: simulatorMap.get(nextActive) ?? s.simulator,
|
||||
};
|
||||
});
|
||||
// Create the editor file group for this board
|
||||
useEditorStore.getState().createFileGroup(`group-${id}`);
|
||||
// Create the editor file group for this board. A board that ships its
|
||||
// own seed code (vendor library boards: M5Stack, UNIHIKER, ...) gets it
|
||||
// here — the editor's family default is an LED_BUILTIN blink / an
|
||||
// RPi.GPIO script, neither of which runs on such a board, so the very
|
||||
// first Run on a freshly placed board would fail. QEMU-Linux boards seed
|
||||
// their guest script ('python'); everything else starts in Arduino mode.
|
||||
const seedMode = isPiBoardKind(boardKind) ? 'python' : 'arduino';
|
||||
useEditorStore
|
||||
.getState()
|
||||
.createFileGroup(`group-${id}`, getBoardSeedFiles(boardKind, seedMode));
|
||||
// The seed sketch includes the board's vendor library, so declare it in
|
||||
// the board's manifest (= the compile resolution scope) right away.
|
||||
const seedLibs = getProBoard(boardKind)?.defaultLibraries;
|
||||
if (seedLibs?.length) {
|
||||
set((s) => ({
|
||||
boards: s.boards.map((b) => (b.id === id ? { ...b, libraries: [...seedLibs] } : b)),
|
||||
}));
|
||||
}
|
||||
// If this board is now the active one (it's the first board, or the
|
||||
// previously-active board was removed), point the editor at its file
|
||||
// group too. The canvas board picker calls addBoard directly WITHOUT
|
||||
|
|
@ -2236,10 +2253,17 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
),
|
||||
}));
|
||||
|
||||
// Replace file group with appropriate default files and activate it
|
||||
// Replace file group with appropriate default files and activate it.
|
||||
// A board's own seed for the target mode wins over the family default:
|
||||
// the MicroPython fallback is a Pico `Pin(25)` blink, which is a dead
|
||||
// pin on an M5Stack.
|
||||
const editorStore = useEditorStore.getState();
|
||||
editorStore.deleteFileGroup(board.activeFileGroupId);
|
||||
editorStore.createFileGroup(board.activeFileGroupId, mode);
|
||||
const modeSeed = getBoardSeedFiles(
|
||||
board.boardKind,
|
||||
mode === 'micropython' || mode === 'espidf' ? mode : 'arduino',
|
||||
);
|
||||
editorStore.createFileGroup(board.activeFileGroupId, modeSeed ?? mode);
|
||||
editorStore.setActiveGroup(board.activeFileGroupId);
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue