feat(custom-chip): one-click Run for programmable CPU chips
Compile/Run now makes every custom-chip on the canvas live in a single click instead of requiring a manual trip through the chip designer plus a separate ROM compile: - Each custom-chip's C source is auto-compiled to WASM when it has none yet (via /api/compile-chip), and programmable CPU chips get their program file (larson.s, chaser.c, ...) assembled/compiled to ROM bytes (via /api/compile-rom) and injected, all before the board starts. - Chip-program files are excluded from the arduino-cli sketch build, so SDCC-only syntax such as __at(0xC000) no longer breaks the Arduino compile (this is what made the Z80 LED-chaser-C example error out). Fixes the Z80 examples that either errored on Run (z80-led-chaser-c) or compiled but did nothing (z80-larson-scanner, whose chip never had WASM or ROM). Works for any circuit built from scratch with a programmable CPU chip, not just the bundled examples. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
083823d945
commit
65b2c02f9b
|
|
@ -17,6 +17,7 @@ import {
|
|||
formatForFile,
|
||||
targetForChip,
|
||||
} from '../../services/romCompileService';
|
||||
import { compileChip } from '../../services/chipCompileService';
|
||||
import { reportRunEvent } from '../../services/metricsService';
|
||||
import { useProjectStore } from '../../store/useProjectStore';
|
||||
import { LibraryManagerModal } from '../simulator/LibraryManagerModal';
|
||||
|
|
@ -195,6 +196,120 @@ export const EditorToolbar = ({
|
|||
[setCompileLogs],
|
||||
);
|
||||
|
||||
/**
|
||||
* Make every custom-chip on the canvas runnable: compile its C source to
|
||||
* WASM (when it has none yet) and, for programmable CPU chips, assemble or
|
||||
* compile the program file it references into ROM bytes — stashing both on
|
||||
* the chip component's `properties` so the next simulation start picks them
|
||||
* up. Non-fatal by design: a chip that fails to compile is logged and
|
||||
* skipped so the board itself still runs.
|
||||
*/
|
||||
const prepareCustomChips = useCallback(
|
||||
async (
|
||||
chips: { id: string; properties: Record<string, unknown> }[],
|
||||
boardFiles: { name: string; content: string }[],
|
||||
) => {
|
||||
const codeChanged = useEditorStore.getState().codeChangedSinceLastCompile;
|
||||
const updateComponent = useSimulatorStore.getState().updateComponent;
|
||||
|
||||
for (const chip of chips) {
|
||||
// Re-read the freshest properties each iteration (an earlier chip's
|
||||
// update doesn't touch this one, but be defensive).
|
||||
const live = useSimulatorStore.getState().components.find((c) => c.id === chip.id);
|
||||
const props = { ...(live?.properties ?? chip.properties) } as Record<string, unknown>;
|
||||
const chipLabel = String(props.chipName ?? 'custom chip');
|
||||
const sourceC = String(props.sourceC ?? '');
|
||||
const chipJson = String(props.chipJson ?? '{}');
|
||||
let changed = false;
|
||||
|
||||
// 1. C -> WASM. Only when missing — the chip designer fills this too.
|
||||
if (!String(props.wasmBase64 ?? '') && sourceC) {
|
||||
addLog({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Compiling chip "${chipLabel}" to WASM...`,
|
||||
});
|
||||
try {
|
||||
const r = await compileChip(sourceC, chipJson);
|
||||
if (r.success && r.wasm_base64) {
|
||||
props.wasmBase64 = r.wasm_base64;
|
||||
changed = true;
|
||||
addLog({
|
||||
timestamp: new Date(),
|
||||
type: 'success',
|
||||
message: `Chip "${chipLabel}" compiled (${r.byte_size} B WASM).`,
|
||||
});
|
||||
} else {
|
||||
addLog({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: `Chip "${chipLabel}" WASM compile failed: ${r.error || r.stderr || 'unknown error'}`,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
addLog({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: `Chip "${chipLabel}" WASM compile error: ${e instanceof Error ? e.message : String(e)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. program file -> ROM bytes (programmable CPU chips). Recompile
|
||||
// when there's no ROM yet or the user edited code since last build.
|
||||
const programFile = String(props.programFile ?? '').trim();
|
||||
if (programFile && (!String(props.romBytes ?? '') || codeChanged)) {
|
||||
const file = boardFiles.find((f) => f.name === programFile);
|
||||
if (!file) {
|
||||
addLog({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: `Chip "${chipLabel}": program file "${programFile}" not found in this board's files.`,
|
||||
});
|
||||
} else {
|
||||
const target = targetForChip(chipJson);
|
||||
const fmt = formatForFile(programFile);
|
||||
addLog({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Assembling "${programFile}" (target=${target}, format=${fmt}) for chip "${chipLabel}"...`,
|
||||
});
|
||||
try {
|
||||
const rr = await compileRom(file.content, target, fmt);
|
||||
if (rr.success && rr.rom_base64) {
|
||||
props.romBytes = rr.rom_base64;
|
||||
props.programFile = programFile;
|
||||
changed = true;
|
||||
addLog({
|
||||
timestamp: new Date(),
|
||||
type: 'success',
|
||||
message: `ROM ready: ${rr.byte_size} B injected into "${chipLabel}".`,
|
||||
});
|
||||
} else {
|
||||
addLog({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: `ROM compile failed for "${programFile}": ${rr.error || rr.stderr || 'unknown error'}`,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
addLog({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: `ROM compile error for "${programFile}": ${e instanceof Error ? e.message : String(e)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
updateComponent(chip.id, { properties: props } as any);
|
||||
}
|
||||
}
|
||||
},
|
||||
[addLog],
|
||||
);
|
||||
|
||||
const handleCompile = async () => {
|
||||
setCompiling(true);
|
||||
setMessage(null);
|
||||
|
|
@ -205,100 +320,35 @@ export const EditorToolbar = ({
|
|||
setCompileLogs([]);
|
||||
trackCompileCode();
|
||||
|
||||
// ── Chip-program path ───────────────────────────────────────────────
|
||||
// If the editor's active file is a chip-program file we don't compile
|
||||
// Arduino code — we assemble/compile it into ROM bytes via
|
||||
// /api/compile-rom and stash the result on every custom-chip component
|
||||
// that points at this filename through its `programFile` property. The
|
||||
// chip's emulator then reads the bytes on chip_setup via vx_rom_size /
|
||||
// vx_rom_read.
|
||||
// ── Custom-chip preparation ─────────────────────────────────────────
|
||||
// Any custom-chip on the canvas is made "live" here so a single
|
||||
// Compile / Run is enough — no separate trip through the chip designer
|
||||
// or a manual ROM compile. For every custom-chip we:
|
||||
// 1. compile its C source to WASM (when it has none yet), and
|
||||
// 2. for programmable CPU chips, assemble/compile the program file it
|
||||
// points at (larson.s, chaser.c, …) into ROM bytes.
|
||||
// Both artefacts are stashed on the chip component's `properties`;
|
||||
// CustomChipPart reads wasmBase64 + romBytes at simulation start.
|
||||
//
|
||||
// A file is "chip program" when EITHER its extension is unambiguous
|
||||
// (.s/.asm/.hex/.bin) OR some custom-chip on the canvas has
|
||||
// programFile === activeFile.name. The latter lets .c files route to
|
||||
// SDCC instead of arduino-cli when wired to a CPU chip.
|
||||
const activeFile = files.find((f) => f.id === useEditorStore.getState().activeFileId);
|
||||
// The chip program files are ALSO kept out of the Arduino sketch compile
|
||||
// below (see `chipProgramFiles`) — otherwise arduino-cli/avr-gcc would
|
||||
// try to build e.g. chaser.c and choke on SDCC-only syntax such as
|
||||
// `__at(0xC000)`, which is exactly what broke the Z80 examples.
|
||||
const componentsForCompile = useSimulatorStore.getState().components;
|
||||
const chipsBoundToFile = activeFile
|
||||
? componentsForCompile.filter((c) => {
|
||||
if (c.metadataId !== 'custom-chip') return false;
|
||||
const prog = String((c.properties as any)?.programFile ?? '').trim();
|
||||
return prog === activeFile.name;
|
||||
})
|
||||
: [];
|
||||
|
||||
if (activeFile && (isChipProgramFile(activeFile.name) || chipsBoundToFile.length > 0)) {
|
||||
try {
|
||||
const chips = chipsBoundToFile.length > 0
|
||||
? chipsBoundToFile
|
||||
: componentsForCompile.filter((c) => {
|
||||
if (c.metadataId !== 'custom-chip') return false;
|
||||
const prog = String((c.properties as any)?.programFile ?? '').trim();
|
||||
return prog === '' || prog === activeFile.name;
|
||||
});
|
||||
if (chips.length === 0) {
|
||||
addLog({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: `No custom-chip on the canvas references ${activeFile.name}. Drop an "i8080 CPU" chip, or set its programFile property.`,
|
||||
});
|
||||
setMessage({ type: 'error', text: 'No matching custom-chip on canvas' });
|
||||
setCompiling(false);
|
||||
return;
|
||||
}
|
||||
// Resolve target from the first matching chip's chip.json.
|
||||
const firstChipJson = String((chips[0].properties as any)?.chipJson ?? '{}');
|
||||
const target = targetForChip(firstChipJson);
|
||||
const fmt = formatForFile(activeFile.name);
|
||||
addLog({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Assembling ${activeFile.name} (target=${target}, format=${fmt}) for ${chips.length} chip(s)...`,
|
||||
});
|
||||
const result = await compileRom(activeFile.content, target, fmt);
|
||||
if (!result.success || !result.rom_base64) {
|
||||
addLog({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: result.error || 'ROM compile failed',
|
||||
});
|
||||
if (result.stderr) {
|
||||
addLog({ timestamp: new Date(), type: 'error', message: result.stderr });
|
||||
}
|
||||
setMessage({ type: 'error', text: result.error || 'ROM compile failed' });
|
||||
setCompiling(false);
|
||||
return;
|
||||
}
|
||||
// Inject into every matching chip's romBytes property.
|
||||
const updateComponent = useSimulatorStore.getState().updateComponent;
|
||||
for (const chip of chips) {
|
||||
updateComponent(chip.id, {
|
||||
properties: {
|
||||
...(chip.properties as Record<string, unknown>),
|
||||
romBytes: result.rom_base64,
|
||||
programFile: activeFile.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
addLog({
|
||||
timestamp: new Date(),
|
||||
type: 'success',
|
||||
message: `ROM compiled: ${result.byte_size} bytes injected into ${chips.length} chip(s).`,
|
||||
});
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text: `ROM ready (${result.byte_size} B). Hit Run.`,
|
||||
});
|
||||
} catch (e) {
|
||||
const errMsg = e instanceof Error ? e.message : String(e);
|
||||
addLog({ timestamp: new Date(), type: 'error', message: errMsg });
|
||||
setMessage({ type: 'error', text: errMsg });
|
||||
} finally {
|
||||
setCompiling(false);
|
||||
}
|
||||
return;
|
||||
const customChips = componentsForCompile.filter((c) => c.metadataId === 'custom-chip');
|
||||
const chipProgramFiles = new Set<string>();
|
||||
for (const chip of customChips) {
|
||||
const pf = String((chip.properties as any)?.programFile ?? '').trim();
|
||||
if (pf) chipProgramFiles.add(pf);
|
||||
}
|
||||
// ── End chip-program path ───────────────────────────────────────────
|
||||
|
||||
if (customChips.length > 0) {
|
||||
const boardFiles = activeBoard?.activeFileGroupId
|
||||
? useEditorStore.getState().getGroupFiles(activeBoard.activeFileGroupId)
|
||||
: files;
|
||||
await prepareCustomChips(customChips, boardFiles);
|
||||
}
|
||||
// ── End custom-chip preparation ─────────────────────────────────────
|
||||
|
||||
const kind = activeBoard?.boardKind;
|
||||
|
||||
|
|
@ -361,10 +411,15 @@ export const EditorToolbar = ({
|
|||
const groupFiles = activeBoard?.activeFileGroupId
|
||||
? useEditorStore.getState().getGroupFiles(activeBoard.activeFileGroupId)
|
||||
: files;
|
||||
const sketchFiles = (groupFiles.length > 0 ? groupFiles : files).map((f) => ({
|
||||
name: f.name,
|
||||
content: f.content,
|
||||
}));
|
||||
const sketchFiles = (groupFiles.length > 0 ? groupFiles : files)
|
||||
// Keep chip-program files (a chip's programFile, or .s/.asm/.hex/.bin)
|
||||
// out of the arduino-cli build — they're compiled to ROM above, not
|
||||
// Arduino sources, and avr-gcc chokes on e.g. SDCC's __at().
|
||||
.filter((f) => !chipProgramFiles.has(f.name) && !isChipProgramFile(f.name))
|
||||
.map((f) => ({
|
||||
name: f.name,
|
||||
content: f.content,
|
||||
}));
|
||||
|
||||
// Stream live cmake + ninja output into the compilation console as
|
||||
// it arrives, instead of waiting for the whole build to finish.
|
||||
|
|
@ -767,6 +822,24 @@ export const EditorToolbar = ({
|
|||
message: `Compiling all ${boardsList.length} board${boardsList.length === 1 ? '' : 's'}...`,
|
||||
});
|
||||
|
||||
// Make every custom-chip live (WASM + ROM) before compiling the boards,
|
||||
// mirroring the single-board Compile path, and collect their program file
|
||||
// names so they stay out of the arduino-cli builds below.
|
||||
const allCustomChips = useSimulatorStore
|
||||
.getState()
|
||||
.components.filter((c) => c.metadataId === 'custom-chip');
|
||||
const chipProgramFiles = new Set<string>();
|
||||
for (const chip of allCustomChips) {
|
||||
const pf = String((chip.properties as any)?.programFile ?? '').trim();
|
||||
if (pf) chipProgramFiles.add(pf);
|
||||
}
|
||||
if (allCustomChips.length > 0) {
|
||||
const everyFile = boardsList.flatMap((b) =>
|
||||
useEditorStore.getState().getGroupFiles(b.activeFileGroupId),
|
||||
);
|
||||
await prepareCustomChips(allCustomChips, everyFile);
|
||||
}
|
||||
|
||||
let ok = 0;
|
||||
let failed = 0;
|
||||
|
||||
|
|
@ -798,7 +871,9 @@ export const EditorToolbar = ({
|
|||
|
||||
try {
|
||||
const groupFiles = useEditorStore.getState().getGroupFiles(board.activeFileGroupId);
|
||||
const sketchFiles = groupFiles.map((f) => ({ name: f.name, content: f.content }));
|
||||
const sketchFiles = groupFiles
|
||||
.filter((f) => !chipProgramFiles.has(f.name) && !isChipProgramFile(f.name))
|
||||
.map((f) => ({ name: f.name, content: f.content }));
|
||||
|
||||
// Stream live cmake + ninja output per-board (Compile-All flow).
|
||||
let lastStreamedLen = 0;
|
||||
|
|
|
|||
|
|
@ -74,19 +74,16 @@ void main(void) {
|
|||
}
|
||||
`;
|
||||
|
||||
const chaserZ80CSketch = `// Z80 LED chaser — C source compiled by SDCC.
|
||||
const chaserZ80CSketch = `// Z80 LED chaser — the program is written in C (chaser.c) and compiled to
|
||||
// the Z80 by SDCC on the backend.
|
||||
//
|
||||
// The companion file is chaser.c. With sdcc installed on the backend,
|
||||
// clicking Compile shells out to:
|
||||
// sdcc -mz80 --code-loc 0x100 --data-loc 0x8000 chaser.c
|
||||
// and the resulting Intel HEX is loaded into the z80-cpu chip's ROM.
|
||||
// Just click Run. Velxio does the rest automatically:
|
||||
// 1. compiles the z80-cpu chip's C source to WASM,
|
||||
// 2. compiles chaser.c to a Z80 ROM (sdcc -mz80) and loads it into the chip,
|
||||
// 3. compiles this (empty) Arduino sketch and starts the simulation.
|
||||
// A single LED then walks back and forth across the 8 outputs.
|
||||
//
|
||||
// Steps:
|
||||
// 1. Click chaser.c in the file explorer.
|
||||
// 2. Click Compile. If SDCC isn't installed yet the toolbar will
|
||||
// say so — install with \`apt-get install sdcc\` (Linux) or
|
||||
// \`winget install SDCC.sdcc\` (Windows), then restart the backend.
|
||||
// 3. Click Run. A single LED bounces back and forth across the 8 outputs.
|
||||
// Want to change the animation? Edit chaser.c and hit Run again.
|
||||
|
||||
void setup() {}
|
||||
void loop() {}
|
||||
|
|
@ -128,12 +125,12 @@ const larsonZ80Sketch = `// Z80 Larson Scanner -- Arduino Uno companion sketch.
|
|||
// The Z80 chip on the canvas runs the larson.s program. This Arduino
|
||||
// sketch just keeps Serial alive in case you wire UART later.
|
||||
//
|
||||
// Steps:
|
||||
// 1. Open larson.s and click Compile.
|
||||
// 2. Click Run. A bit will walk across the 8 LEDs.
|
||||
// Just click Run. Velxio automatically compiles the z80-cpu chip to WASM,
|
||||
// assembles larson.s into a Z80 ROM, loads it into the chip, and starts the
|
||||
// simulation. A single bit then walks across the 8 LEDs.
|
||||
//
|
||||
// To slow it down or speed it up: change the "LD C, 80" line in larson.s
|
||||
// (higher number = slower).
|
||||
// (higher number = slower), then hit Run again.
|
||||
|
||||
void setup() {}
|
||||
void loop() {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue