velxio/frontend/src/services/romCompileService.ts

148 lines
4.9 KiB
TypeScript
Raw Normal View History

feat(chips): programmable retro CPU chips with external ROM Adds a new way to use the retro CPU chips: write your program in a project file (.s / .asm / .hex / .bin), click Compile, click Run, and the same chip emulates whatever you wrote. Same chip + different ROMs = mini PC, calculator, LED demo, Kill-the-Bit game, etc. SDK: - velxio-chip.h gets two new host imports: uint32_t vx_rom_size(void); void vx_rom_read(uint32_t off, uint8_t* dst, uint32_t len); CPU-emulator chips call these in chip_setup to pull their program out of the host's romBytes property. Frontend runtime: - ChipRuntime accepts opts.romBytes (Uint8Array) and exposes the new imports, copying bytes into chip memory on vx_rom_read. - CustomChipPart pulls component.properties.romBytes (base64) and passes it through. - Component registry declares three new custom-chip properties: romBytes (base64), programFile (matching project filename), and programTarget (cpu name). New programmable bundled chip: - frontend/src/components/customChips/examples/intel/i8080-cpu.{c,chip.json} Same clean-room 8080 emulator as i8080-repl/i8080-counter, but ROM is loaded externally via vx_rom_*. Has 8 LEDs, 8 buttons, UART, 16 KB RAM, 32 KB of external ROM. Backend: - New /api/compile-rom endpoint and rom_compile service that turns chip-program source into ROM bytes. 8080 ASM is assembled by the in-tree two-pass assembler (moved to backend/app/services/asm8080.py). Intel HEX records are parsed; raw .bin is passed through. Future targets (z80, 8086, 4004) are scaffolded but not wired yet. EditorToolbar: - Compile button detects when the active file is .s/.asm/.hex/.bin and routes to compile-rom instead of arduino-cli. The compiled bytes are injected into every custom-chip on the canvas whose programFile property matches the active filename (or is empty). Example: - /examples/i8080-killbits loads Dean McDaniel's 1975 Kill-the-Bit on the programmable i8080-cpu chip. killbits.s is shipped as a project file alongside sketch.ino; the user clicks Compile then Run and the LED walks across 8 outputs, buttons kill it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:38:18 +07:00
/**
* Frontend wrapper for POST /api/compile-rom compiles a chip-program file
* (8080 ASM, Intel HEX, raw .bin) into base64 ROM bytes that get stored on
* a custom-chip component's `romBytes` property. The chip's emulator then
* reads those bytes at chip_setup via vx_rom_size / vx_rom_read.
*/
export type RomTarget = '8080' | 'z80' | '8086' | '4004';
feat(chips): C-to-Z80 compile via SDCC + LED chaser example Adds a third format to /api/compile-rom: `c` (C source compiled by SDCC to Z80 bytes). Same chip-program flow as 8080/Z80 asm — write C in a project file, click Compile, click Run. Backend: - backend/app/services/c_compile.py — async SDCC wrapper. Locates the sdcc binary on PATH (or via SDCC env var, or common Windows install paths) and shells out with target=mz80 + --code-loc 0x100 --data-loc 0x8000. Parses the resulting Intel HEX into raw ROM bytes. Pure 8080 is rejected with a clear error (SDCC has no 8080 backend; Z80 ROMs also run on the i8080-cpu chip if you avoid Z80-only ops). - rom_compile.py: compile_rom is now async; the new c branch delegates to c_compile. compile_rom_endpoint awaits it. Frontend: - romCompileService: RomFormat gains 'c'; formatForFile maps .c/.cpp to 'c'. isChipProgramFile intentionally still excludes .c — disambiguation happens at the EditorToolbar level. - EditorToolbar: the chip-program path also fires when a custom-chip has programFile === activeFile.name (regardless of extension). That lets .c files route to /api/compile-rom (SDCC) when bound to a CPU chip, while .c files NOT bound to any chip continue to route to arduino-cli as before. Docker: - Dockerfile.standalone adds `sdcc` to the apt-get install list, so the prod image ships with SDCC out of the box. Example: - /examples/z80-led-chaser-c — z80-cpu chip + chaser.c (a Larson scanner written in C with __at() MMIO definitions). Compiles cleanly with SDCC's --code-loc 0x100 default crt0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:31:10 +07:00
export type RomFormat = 'asm' | 'hex' | 'bin' | 'c';
feat(chips): programmable retro CPU chips with external ROM Adds a new way to use the retro CPU chips: write your program in a project file (.s / .asm / .hex / .bin), click Compile, click Run, and the same chip emulates whatever you wrote. Same chip + different ROMs = mini PC, calculator, LED demo, Kill-the-Bit game, etc. SDK: - velxio-chip.h gets two new host imports: uint32_t vx_rom_size(void); void vx_rom_read(uint32_t off, uint8_t* dst, uint32_t len); CPU-emulator chips call these in chip_setup to pull their program out of the host's romBytes property. Frontend runtime: - ChipRuntime accepts opts.romBytes (Uint8Array) and exposes the new imports, copying bytes into chip memory on vx_rom_read. - CustomChipPart pulls component.properties.romBytes (base64) and passes it through. - Component registry declares three new custom-chip properties: romBytes (base64), programFile (matching project filename), and programTarget (cpu name). New programmable bundled chip: - frontend/src/components/customChips/examples/intel/i8080-cpu.{c,chip.json} Same clean-room 8080 emulator as i8080-repl/i8080-counter, but ROM is loaded externally via vx_rom_*. Has 8 LEDs, 8 buttons, UART, 16 KB RAM, 32 KB of external ROM. Backend: - New /api/compile-rom endpoint and rom_compile service that turns chip-program source into ROM bytes. 8080 ASM is assembled by the in-tree two-pass assembler (moved to backend/app/services/asm8080.py). Intel HEX records are parsed; raw .bin is passed through. Future targets (z80, 8086, 4004) are scaffolded but not wired yet. EditorToolbar: - Compile button detects when the active file is .s/.asm/.hex/.bin and routes to compile-rom instead of arduino-cli. The compiled bytes are injected into every custom-chip on the canvas whose programFile property matches the active filename (or is empty). Example: - /examples/i8080-killbits loads Dean McDaniel's 1975 Kill-the-Bit on the programmable i8080-cpu chip. killbits.s is shipped as a project file alongside sketch.ino; the user clicks Compile then Run and the LED walks across 8 outputs, buttons kill it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:38:18 +07:00
export interface RomCompileResult {
success: boolean;
rom_base64: string | null;
byte_size: number;
stderr: string;
error: string | null;
}
const BASE = '/api/compile-rom';
export async function compileRom(
source: string,
target: RomTarget,
format: RomFormat,
): Promise<RomCompileResult> {
const res = await fetch(`${BASE}/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ source, target, format }),
});
if (!res.ok) {
const text = await res.text();
return {
success: false,
rom_base64: null,
byte_size: 0,
stderr: '',
error: `HTTP ${res.status}: ${text}`,
};
}
return (await res.json()) as RomCompileResult;
}
feat(chips): C-to-Z80 compile via SDCC + LED chaser example Adds a third format to /api/compile-rom: `c` (C source compiled by SDCC to Z80 bytes). Same chip-program flow as 8080/Z80 asm — write C in a project file, click Compile, click Run. Backend: - backend/app/services/c_compile.py — async SDCC wrapper. Locates the sdcc binary on PATH (or via SDCC env var, or common Windows install paths) and shells out with target=mz80 + --code-loc 0x100 --data-loc 0x8000. Parses the resulting Intel HEX into raw ROM bytes. Pure 8080 is rejected with a clear error (SDCC has no 8080 backend; Z80 ROMs also run on the i8080-cpu chip if you avoid Z80-only ops). - rom_compile.py: compile_rom is now async; the new c branch delegates to c_compile. compile_rom_endpoint awaits it. Frontend: - romCompileService: RomFormat gains 'c'; formatForFile maps .c/.cpp to 'c'. isChipProgramFile intentionally still excludes .c — disambiguation happens at the EditorToolbar level. - EditorToolbar: the chip-program path also fires when a custom-chip has programFile === activeFile.name (regardless of extension). That lets .c files route to /api/compile-rom (SDCC) when bound to a CPU chip, while .c files NOT bound to any chip continue to route to arduino-cli as before. Docker: - Dockerfile.standalone adds `sdcc` to the apt-get install list, so the prod image ships with SDCC out of the box. Example: - /examples/z80-led-chaser-c — z80-cpu chip + chaser.c (a Larson scanner written in C with __at() MMIO definitions). Compiles cleanly with SDCC's --code-loc 0x100 default crt0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:31:10 +07:00
/** Classify a filename as a chip-program file (vs an Arduino sketch).
*
* `.c` is intentionally NOT in the always-list Arduino sketches use .c
* too. The toolbar disambiguates by checking whether a custom-chip on
* the canvas has `programFile === activeFile.name`. If yes, .c is a chip
* program (SDCC route); if no, it's an Arduino sketch (arduino-cli route).
*/
feat(chips): programmable retro CPU chips with external ROM Adds a new way to use the retro CPU chips: write your program in a project file (.s / .asm / .hex / .bin), click Compile, click Run, and the same chip emulates whatever you wrote. Same chip + different ROMs = mini PC, calculator, LED demo, Kill-the-Bit game, etc. SDK: - velxio-chip.h gets two new host imports: uint32_t vx_rom_size(void); void vx_rom_read(uint32_t off, uint8_t* dst, uint32_t len); CPU-emulator chips call these in chip_setup to pull their program out of the host's romBytes property. Frontend runtime: - ChipRuntime accepts opts.romBytes (Uint8Array) and exposes the new imports, copying bytes into chip memory on vx_rom_read. - CustomChipPart pulls component.properties.romBytes (base64) and passes it through. - Component registry declares three new custom-chip properties: romBytes (base64), programFile (matching project filename), and programTarget (cpu name). New programmable bundled chip: - frontend/src/components/customChips/examples/intel/i8080-cpu.{c,chip.json} Same clean-room 8080 emulator as i8080-repl/i8080-counter, but ROM is loaded externally via vx_rom_*. Has 8 LEDs, 8 buttons, UART, 16 KB RAM, 32 KB of external ROM. Backend: - New /api/compile-rom endpoint and rom_compile service that turns chip-program source into ROM bytes. 8080 ASM is assembled by the in-tree two-pass assembler (moved to backend/app/services/asm8080.py). Intel HEX records are parsed; raw .bin is passed through. Future targets (z80, 8086, 4004) are scaffolded but not wired yet. EditorToolbar: - Compile button detects when the active file is .s/.asm/.hex/.bin and routes to compile-rom instead of arduino-cli. The compiled bytes are injected into every custom-chip on the canvas whose programFile property matches the active filename (or is empty). Example: - /examples/i8080-killbits loads Dean McDaniel's 1975 Kill-the-Bit on the programmable i8080-cpu chip. killbits.s is shipped as a project file alongside sketch.ino; the user clicks Compile then Run and the LED walks across 8 outputs, buttons kill it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:38:18 +07:00
export function isChipProgramFile(name: string): boolean {
const lower = name.toLowerCase();
return (
lower.endsWith('.s') ||
lower.endsWith('.asm') ||
lower.endsWith('.hex') ||
lower.endsWith('.bin')
);
}
/** Pick a sensible compile format from the filename extension. */
export function formatForFile(name: string): RomFormat {
const lower = name.toLowerCase();
if (lower.endsWith('.hex')) return 'hex';
if (lower.endsWith('.bin')) return 'bin';
feat(chips): C-to-Z80 compile via SDCC + LED chaser example Adds a third format to /api/compile-rom: `c` (C source compiled by SDCC to Z80 bytes). Same chip-program flow as 8080/Z80 asm — write C in a project file, click Compile, click Run. Backend: - backend/app/services/c_compile.py — async SDCC wrapper. Locates the sdcc binary on PATH (or via SDCC env var, or common Windows install paths) and shells out with target=mz80 + --code-loc 0x100 --data-loc 0x8000. Parses the resulting Intel HEX into raw ROM bytes. Pure 8080 is rejected with a clear error (SDCC has no 8080 backend; Z80 ROMs also run on the i8080-cpu chip if you avoid Z80-only ops). - rom_compile.py: compile_rom is now async; the new c branch delegates to c_compile. compile_rom_endpoint awaits it. Frontend: - romCompileService: RomFormat gains 'c'; formatForFile maps .c/.cpp to 'c'. isChipProgramFile intentionally still excludes .c — disambiguation happens at the EditorToolbar level. - EditorToolbar: the chip-program path also fires when a custom-chip has programFile === activeFile.name (regardless of extension). That lets .c files route to /api/compile-rom (SDCC) when bound to a CPU chip, while .c files NOT bound to any chip continue to route to arduino-cli as before. Docker: - Dockerfile.standalone adds `sdcc` to the apt-get install list, so the prod image ships with SDCC out of the box. Example: - /examples/z80-led-chaser-c — z80-cpu chip + chaser.c (a Larson scanner written in C with __at() MMIO definitions). Compiles cleanly with SDCC's --code-loc 0x100 default crt0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:31:10 +07:00
if (lower.endsWith('.c') || lower.endsWith('.cpp')) return 'c';
feat(chips): programmable retro CPU chips with external ROM Adds a new way to use the retro CPU chips: write your program in a project file (.s / .asm / .hex / .bin), click Compile, click Run, and the same chip emulates whatever you wrote. Same chip + different ROMs = mini PC, calculator, LED demo, Kill-the-Bit game, etc. SDK: - velxio-chip.h gets two new host imports: uint32_t vx_rom_size(void); void vx_rom_read(uint32_t off, uint8_t* dst, uint32_t len); CPU-emulator chips call these in chip_setup to pull their program out of the host's romBytes property. Frontend runtime: - ChipRuntime accepts opts.romBytes (Uint8Array) and exposes the new imports, copying bytes into chip memory on vx_rom_read. - CustomChipPart pulls component.properties.romBytes (base64) and passes it through. - Component registry declares three new custom-chip properties: romBytes (base64), programFile (matching project filename), and programTarget (cpu name). New programmable bundled chip: - frontend/src/components/customChips/examples/intel/i8080-cpu.{c,chip.json} Same clean-room 8080 emulator as i8080-repl/i8080-counter, but ROM is loaded externally via vx_rom_*. Has 8 LEDs, 8 buttons, UART, 16 KB RAM, 32 KB of external ROM. Backend: - New /api/compile-rom endpoint and rom_compile service that turns chip-program source into ROM bytes. 8080 ASM is assembled by the in-tree two-pass assembler (moved to backend/app/services/asm8080.py). Intel HEX records are parsed; raw .bin is passed through. Future targets (z80, 8086, 4004) are scaffolded but not wired yet. EditorToolbar: - Compile button detects when the active file is .s/.asm/.hex/.bin and routes to compile-rom instead of arduino-cli. The compiled bytes are injected into every custom-chip on the canvas whose programFile property matches the active filename (or is empty). Example: - /examples/i8080-killbits loads Dean McDaniel's 1975 Kill-the-Bit on the programmable i8080-cpu chip. killbits.s is shipped as a project file alongside sketch.ino; the user clicks Compile then Run and the LED walks across 8 outputs, buttons kill it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:38:18 +07:00
return 'asm';
}
/** Pick the right target CPU from the chip's chip.json programTargets,
* falling back to 8080 (the only one wired up today). */
export function targetForChip(chipJsonStr: string): RomTarget {
try {
const obj = JSON.parse(chipJsonStr);
if (Array.isArray(obj.programTargets) && obj.programTargets.length > 0) {
const t = String(obj.programTargets[0]).toLowerCase();
if (t === '8080' || t === 'z80' || t === '8086' || t === '4004') return t;
}
} catch { /* ignore */ }
return '8080';
}
feat(custom-chip): newly-added programmable chip auto-gets an editable program; chaser-c goes board-less Two fixes from live testing feedback: 1. Adding a programmable chip (Z80/8080) from the gallery created NO program group — only the chip(s) from the example had one. Root cause: 'programmable' was detected by a non-empty programFile, but a fresh chip's programFile is empty until the user writes one. Now detection uses the canonical signal — chip.json's programTargets — via isProgrammableChip(). When such a chip lands with no program yet, the file explorer seeds an editable program.c (DEFAULT_CHIP_PROGRAM_C, a working walking-LED skeleton) into its own group and stamps programFile/programTarget onto the component so Compile/Run can build it. Behaviour/driver and predefined chips (no programTargets) still get no group — edited in the chip designer. 2. z80-led-chaser-c now runs board-less on a regulated power supply (no Arduino, mirroring z80-larson-no-board) — the Arduino only ever supplied 5V and added confusion. chaser.c stays the chip's editable program in its own section. - romCompileService: isProgrammableChip(), DEFAULT_CHIP_PROGRAM_FILE/_C. - FileExplorer: detect by programTargets; auto-seed program.c + persist programFile/programTarget for fresh chips. - examples-retro-intel: chaser-c -> board-less (psu + 8 resistors + 8 LEDs), drop the now-unused Arduino sketch const; fix a stale sdcc --code-loc comment. - Tests: board+chip case moved to z80-larson-scanner (still board-based); isProgrammableChip unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:52:11 +07:00
/**
* A custom chip is "programmable" it runs a user program / ROM, like a CPU
* emulator when its chip.json declares `programTargets`, or it already
* references a program file. Behaviour / driver chips (a servo driver, a
* sensor) declare no programTargets and are edited only in the chip designer.
*
* This (not `programFile`) is the canonical predicate: a chip dropped fresh
* from the gallery has an empty programFile until we seed one, but its
* chip.json already says it's a CPU.
*/
export function isProgrammableChip(
props: Record<string, unknown> | null | undefined,
): boolean {
if (!props) return false;
if (String(props.programFile ?? '').trim()) return true;
try {
const obj = JSON.parse(String(props.chipJson ?? '{}'));
return Array.isArray(obj.programTargets) && obj.programTargets.length > 0;
} catch {
return false;
}
}
/** Default editable program file name for a freshly-added programmable chip.
* We seed C SDCC compiles it to the chip's CPU (z80 / 8080 / ...). */
export const DEFAULT_CHIP_PROGRAM_FILE = 'program.c';
/**
* Starter C program seeded into a newly-added programmable chip's editor
* group, so the chip has an editable program from the moment it lands on the
* canvas. Walks a single LED across the 8 memory-mapped outputs it compiles
* and does something visible on Run. Mirrors the working chaser.c idiom
* (volatile MMIO pointer + nop-based delay; SDCC treats plain `char` as
* unsigned on these CPUs, so the pattern uses an explicit unsigned byte).
*/
export const DEFAULT_CHIP_PROGRAM_C = `/* Program for the programmable CPU chip — compiled by SDCC and loaded as the
* chip's ROM. Memory-mapped I/O matches the z80-cpu / i8080-cpu map:
*
* 0xC000 LED_OUT write: bit i drives output pin LEDi
* 0xC003 BTN_IN read: bit i reads input pin BTNi
*
* Edit this and click Run. (Rename to .s to write assembly instead.)
*/
#define LED_OUT (*(volatile unsigned char *)0xC000)
#define BTN_IN (*(volatile unsigned char *)0xC003)
static void delay(unsigned int loops) {
while (loops--) {
__asm
nop
__endasm;
}
}
void main(void) {
unsigned char bit = 0x01;
while (1) {
LED_OUT = bit; /* light one LED */
delay(5000);
bit <<= 1; /* walk it left */
if (bit == 0) bit = 0x01; /* wrap around */
}
}
`;