velxio/frontend/src/components/editor/EditorToolbar.tsx

1977 lines
83 KiB
TypeScript
Raw Normal View History

import { useState, useCallback, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
feat(custom-chip): program lives in its own editor group, not the board sketch A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated editor file group — group-chip-<chipId> — rendered as its own collapsible section in the file explorer, exactly like each board owns its sketch group. Behaviour/driver chips and predefined chips carry no programFile and get no group; they stay editable only in the chip designer. Fixes two reported issues on the Z80 examples: - /example/z80-larson-no-board: the board-less chip example now opens its program (larson.s) as the active group, editable on the left — previously the editor showed but no file appeared. - /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as a sibling tab inside the Arduino sketch group; it sits in its own chip section instead. The board group shows only sketch.ino. Details: - useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers. - loadExample: seedChipProgramGroups() routes each chip's programFile into its own group (seeded from the example files), sweeps stale chip groups, keeps the program OUT of the board group, and for a board-less chip example makes the chip group active so the program is the editable file shown. - EditorToolbar.prepareCustomChips: resolves the program from the chip's own group (falls back to board files for older projects) before assembling ROM. - FileExplorer: renders one collapsible section per programmable chip with an IC icon; clicking switches the editor to the chip group. Lazy-creates a group for chips dropped on the canvas. - projectPayload + vlxFile: serialise chip groups alongside board groups and include them in the dirty-check hash, so chip-program edits persist on save / autosave / .vlx export and round-trip via replaceFileGroups on load. - Regression tests for board-less + board+chip routing and stale-group sweep. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
import { useEditorStore, chipFileGroupId } from '../../store/useEditorStore';
fix(simulator): circuitVerifier worst-case GPIO + LED NaN guard Two related correctness fixes that make the simulator's realism match what users actually see. 1. circuitVerifier was running pre-flight against the IDLE circuit (every pin LOW). A Blink sketch is going to write pin 13 HIGH eventually — at which point a missing series resistor produces a ~500 mA spike through the diode. But because pre-flight ran with pin 13 LOW the led-overcurrent rule never fired, and the user sailed through Run only to see the LED stay mysteriously dark on the canvas. The verifier now forces every digital pin connected to a load to HIGH = vcc, the worst case any well-defined sketch will eventually impose. The existing rules (led-overcurrent, resistor-overpower, short-circuit) now fire correctly and the existing CircuitVerificationModal blocks Run until the user adds a proper current limiter or chooses Run Anyway. Pins that are inputs-only (a pull-up + button) get over-driven here too, but the rules tolerate that — a pull-up at 5 V draws ~0.5 mA, well below all thresholds. A circuit that would actually fault under HIGH is flagged. 2. LED simulator was crashing visually on non-finite ngspice branch currents. A degenerate diode (no series R) makes ngspice return NaN, which fell through 'raw !== undefined && current > 1e-6' as false and never triggered the digital fallback. Now we check Number.isFinite(raw) before trusting it — non-finite returns route to the digital fallback so the LED at least lights visually when its driver pin is HIGH (the user still sees the verifier warning that the real-world circuit is wrong, but Run Anyway is not a black screen).
2026-05-18 09:41:19 +07:00
import { useSimulatorStore } from '../../store/useSimulatorStore';
import { useElectricalStore } from '../../store/useElectricalStore';
import { type VerificationResult } from '../../simulation/verify/circuitVerifier';
import { verifyCircuitFromStore } from '../../simulation/verify/verifyFromStore';
import { CircuitVerificationModal } from '../simulator/CircuitVerificationModal';
import type { BoardKind, LanguageMode } from '../../types/board';
feat(esp32): pure ESP-IDF language mode for the ESP32 family (#139) Adds a third entry to the board language selector next to Arduino C++ and MicroPython: ESP-IDF. In this mode the user writes a plain ESP-IDF project — app_main() entry point, FreeRTOS + driver APIs — and the backend compiles it through the same ESP-IDF toolchain it already uses for ESP32 Arduino sketches, just without the arduino-esp32 component. Backend: - CompileRequest.language ('espidf') threaded through the sync + async compile paths and folded into the dedup job key (language='arduino' and omitted hash identically so old clients keep dedupping). - espidf_compiler: pure_idf flag. User files are written into main/ as-is (no Arduino.h wrap, no velxio_compat.h, Arduino library resolution skipped), ARDUINO_ESP32_PATH is dropped from the build env and VELXIO_PURE_SKETCH raised so the template CMake compiles the user's own sources via a glob branch. Pure builds get their own persistent build-dir variant through the eff_hash fold. - QEMU WiFi compat for IDF-style code: esp_wifi.h/esp_wifi_init detection sets has_wifi, and literal #define SSID/PASS plus wifi_config_t designated initializers are normalized to the QEMU AP. - CONFIG_ARDUINO_* lines are stripped from sdkconfig.defaults in pure mode (the symbols don't exist without the arduino component). Frontend: - LanguageMode gains 'espidf'; BOARD_SUPPORTS_ESPIDF covers the ESP32 family (Xtensa, S3, C3). Toolbar shows the option only for those. - Switching modes seeds a main.c blink skeleton (app_main + gpio driver), mirroring the MicroPython main.py flow. - compileCode sends language='espidf'; run/stop paths are unchanged (the QEMU worker consumes the same merged flash image). - New gallery example: esp32-idf-blink (LED + resistor on GPIO 2). Tests: unit coverage for the build-env switch, IDF wifi normalization, job-key variance, file-group seeding and the new example; verified end-to-end in a container from the prod image (pure build produces a bootable flash image; Arduino-mode build unchanged, same variant hash).
2026-07-24 11:37:01 +07:00
import { BOARD_KIND_FQBN, BOARD_SUPPORTS_ESPIDF, BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind, boardDisplayName } from '../../types/board';
import { compileCode } from '../../services/compilation';
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
import {
compileRom,
isChipProgramFile,
formatForFile,
targetForChip,
} from '../../services/romCompileService';
import { compileChip } from '../../services/chipCompileService';
feat(sim): Stop halts custom chips + LEDs go dark; retro chip examples go board-less Phase 1 of the run-system/UX work. Stop bug: a programmable chip kept running after Stop when a board was present. The chip rAF tick gated only on board presence (!boardless), so with a board it ticked forever. Now it gates on the actual run state: board-less -> electrical paused flag; with board(s) -> board.running. handleStop also clears every chip's output drives (clearAllChipDrives) and re-solves so chip-driven LEDs go dark on Stop instead of freezing at their last frame. Examples to board-less (regulated power supply, no Arduino — the Arduino only ever supplied 5V): - z80-larson-scanner -> 'Z80 Comet Scanner': board-less, a faster TWO-LED comet (scanner.s) so it's visually distinct from z80-larson-no-board's single-bit walk; green/blue LEDs. - i8080-killbits -> board-less (psu + resistors), keeps killbits.s as the chip's editable program; buttons re-powered from the supply. - i8080-button-counter -> board-less (psu + resistors); behaviour chip, program baked in, so it shows a note (no editable file) and runs standalone. banner-streamer stays Arduino-based (its TX/RX go through the AVR USART bridge). - CustomChipPart: run-state-aware tick gate. - EditorToolbar: clearAllChipDrives() helper + handleStop clears chip drives. - examples-retro-intel: 3 conversions; drop now-unused sketch consts; add the larsonScannerAsm comet program. - Tests: board+chip routing now uses an inline synthetic example (gallery chip examples are all board-less). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:32:35 +07:00
import { clearChipDrives } from '../../simulation/customChips/chipPinDrives';
import { requestElectricalResolve } from '../../simulation/spice/electricalResolveHook';
2026-04-26 05:46:52 +07:00
import { reportRunEvent } from '../../services/metricsService';
import { useProjectStore } from '../../store/useProjectStore';
import { LibraryManagerModal } from '../simulator/LibraryManagerModal';
import { InstallLibrariesModal } from '../simulator/InstallLibrariesModal';
import { parseCompileResult } from '../../utils/compilationLogger';
import type { CompilationLog, CompileTarget } from '../../utils/compilationLogger';
import { exportToWokwiZip } from '../../utils/wokwiZip';
import { importProjectFile, PROJECT_FILE_ACCEPT } from '../../utils/importProject';
import { readFirmwareFile } from '../../utils/firmwareLoader';
import {
trackCompileCode,
trackRunSimulation,
trackStopSimulation,
trackResetSimulation,
trackOpenLibraryManager,
} from '../../utils/analytics';
import './EditorToolbar.css';
/**
* Output-console group for circuit pre-flight + runtime faults. Routing these
* into the compile console (instead of an inline toolbar toast that overlapped
* the Run/Stop buttons) gives one unified, red-coloured diagnostics log
* Proteus-style. id is matched when clearing so the findings survive an
* auto-compile triggered by the same Run.
*/
const CIRCUIT_CHECK_TARGET: CompileTarget = {
id: 'circuit-check',
label: 'Circuit check',
kind: 'board',
};
feat(sim): Stop halts custom chips + LEDs go dark; retro chip examples go board-less Phase 1 of the run-system/UX work. Stop bug: a programmable chip kept running after Stop when a board was present. The chip rAF tick gated only on board presence (!boardless), so with a board it ticked forever. Now it gates on the actual run state: board-less -> electrical paused flag; with board(s) -> board.running. handleStop also clears every chip's output drives (clearAllChipDrives) and re-solves so chip-driven LEDs go dark on Stop instead of freezing at their last frame. Examples to board-less (regulated power supply, no Arduino — the Arduino only ever supplied 5V): - z80-larson-scanner -> 'Z80 Comet Scanner': board-less, a faster TWO-LED comet (scanner.s) so it's visually distinct from z80-larson-no-board's single-bit walk; green/blue LEDs. - i8080-killbits -> board-less (psu + resistors), keeps killbits.s as the chip's editable program; buttons re-powered from the supply. - i8080-button-counter -> board-less (psu + resistors); behaviour chip, program baked in, so it shows a note (no editable file) and runs standalone. banner-streamer stays Arduino-based (its TX/RX go through the AVR USART bridge). - CustomChipPart: run-state-aware tick gate. - EditorToolbar: clearAllChipDrives() helper + handleStop clears chip drives. - examples-retro-intel: 3 conversions; drop now-unused sketch consts; add the larsonScannerAsm comet program. - Tests: board+chip routing now uses an inline synthetic example (gallery chip examples are all board-less). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:32:35 +07:00
/**
* Clear the output drives of every custom chip on the canvas and re-solve, so
* chip-driven LEDs go dark on Stop. A chip drives its nets via its own SPICE
* voltage sources (registered in chipPinDrives); stopBoard / electrical-pause
* don't touch those, so without this the LEDs would freeze at their last frame.
*/
function clearAllChipDrives(): void {
const comps = useSimulatorStore.getState().components;
let any = false;
for (const c of comps) {
if (c.metadataId === 'custom-chip') {
clearChipDrives(c.id);
any = true;
}
}
if (any) requestElectricalResolve();
}
/**
* Boards whose firmware runs in a QEMU worker rather than a client-side AVR
* core. They can start without a pre-stored `compiledProgram`. Shared by
* handleRun and handleRunAll so the two paths can't drift.
*/
function isQemuBoardKind(kind: BoardKind | undefined): boolean {
if (!kind) return false;
return (
isPiBoardKind(kind) ||
kind === 'esp32' ||
kind === 'esp32-s3' ||
kind === 'esp32-cam' ||
kind === 'esp32-c3' ||
kind === 'esp32-devkit-c-v4' ||
kind === 'wemos-lolin32-lite' ||
kind === 'xiao-esp32-s3' ||
kind === 'arduino-nano-esp32' ||
kind === 'xiao-esp32-c3' ||
kind === 'aitewinrobot-esp32c3-supermini'
);
}
interface EditorToolbarProps {
consoleOpen: boolean;
setConsoleOpen: (open: boolean | ((v: boolean) => boolean)) => void;
compileLogs: CompilationLog[];
setCompileLogs: (logs: CompilationLog[] | ((prev: CompilationLog[]) => CompilationLog[])) => void;
/**
* Optional element rendered between the left action group and the right
* action group. Normally empty (the slot just acts as a flexible spacer
* that keeps the right action icons pinned); private overlays may inject
* deployment-specific content here without forking the toolbar.
*/
centerSlot?: React.ReactNode;
/**
* Optional extra elements rendered after the built-in right-group buttons
* (Libraries / Import-Export / Output Console). Used by private overlays
* to add deployment-specific actions without forking the toolbar.
*/
rightSlot?: React.ReactNode;
}
const BOARD_PILL_ICON: Record<BoardKind, string> = {
'arduino-uno': '⬤',
'arduino-nano': '▪',
'arduino-mega': '▬',
'raspberry-pi-pico': '◆',
'raspberry-pi-3': '⬛',
feat(boards): expose Raspberry Pi 4 and Pi 5 in the picker (UI + pin wiring) The BoardKind type and the QEMU backend already supported raspberry-pi-4 (Cortex-A72) and raspberry-pi-5 (Cortex-A76) by reusing the Pi 3 arm64 image set, but the frontend had no way to actually select either: the board picker, the canvas renderer, the serial monitor, the oscilloscope channel list, and the editor toolbar all hard-coded "raspberry-pi-3" as the only Pi entry. ComponentRegistry even registered Pi 4 / Pi 5 metadata pointing at the velxio-raspberry-pi-3 custom-element tag — a placeholder that meant both boards rendered as a Pi 3 in the picker thumbnail and on the canvas. Add dedicated boards top-to-bottom: * `RaspberryPi4Element.ts` / `RaspberryPi5Element.ts` — Velxio-style schematic SVG (authored from scratch, not traced). Pi 4 is the green PCB with BCM2711 SoC, 4× USB-A, USB-C power, dual µHDMI; Pi 5 is the darker green PCB with BCM2712 + RP1 southbridge, 2.5 GbE, USB-C 5V/5A, PCIe FFC connector, dedicated power button. Both carry a small "velxio" mark in the corner. * `pi40PinHeader.ts` — shared `buildPi40PinHeader()` helper that returns the 40-pin BCM layout. Every Pi from the 1B+ onwards uses the same physical pin positions and same BCM GPIO assignment, so Pi 3 / Pi 4 / Pi 5 elements all consume this helper and example wires drawn against one model transfer to the others without re-routing. * React wrappers `RaspberryPi4.tsx` / `RaspberryPi5.tsx` render the custom elements at absolute positions (mirrors how RaspberryPi3.tsx handles the Pi 3 illustration). * Wire-up across the editor surface: - BoardOnCanvas: BOARD_SIZE entry + switch case. - BoardPickerModal: description, icon, kinds list. - ComponentPickerModal: thumbnails now instantiate the dedicated custom element (was velxio-raspberry-pi-3 fallback). - SerialMonitor / EditorToolbar: pill labels, icons, colours. - Oscilloscope: GPIO channel list (28 BCM pins). - SimulatorCanvas: remote-boards filter for run/stop sync. - SPICE boardPinGroups: same 5V / 3V3 / GND as Pi 3. - boardPinToNumber: accepts physical pin numbers ("1"-"40"), BCM names ("GPIO14") and power labels for any Pi 3/4/5 id. - ComponentRegistry: dedicated tagNames + per-board thumbnails (green for Pi 4, darker green for Pi 5). * EditorToolbar's Pi 3 special cases (Linux/Python compile path, Run/Stop routing) now use `isPiBoardKind()` so Pi 4 and Pi 5 inherit the same behaviour automatically, and any future Pi family member (Zero / 1 / 2) lands in the right code paths the moment its backend boots. QEMU backend was already wired (qemu_manager.py:71/82 + manifest entry 'raspberry-pi-3-virt' shared across arm64 Pis), so this commit makes both boards selectable end-to-end without any backend follow-up.
2026-05-23 09:46:59 +07:00
'raspberry-pi-4': '⬛',
'raspberry-pi-5': '⬛',
esp32: '⬡',
'esp32-s3': '⬡',
'esp32-c3': '⬡',
'stm32-bluepill': '◈',
'stm32-blackpill': '◈',
'stm32-bluepill-f103cb': '◈',
'stm32-blackpill-f401': '◈',
'stm32-f4-discovery': '◈',
'stm32-olimex-h405': '◈',
'stm32-netduino-plus2': '◈',
'stm32-netduino2': '◈',
};
const BOARD_PILL_COLOR: Record<BoardKind, string> = {
'arduino-uno': '#4fc3f7',
'arduino-nano': '#4fc3f7',
'arduino-mega': '#4fc3f7',
'raspberry-pi-pico': '#ce93d8',
'raspberry-pi-3': '#ef9a9a',
feat(boards): expose Raspberry Pi 4 and Pi 5 in the picker (UI + pin wiring) The BoardKind type and the QEMU backend already supported raspberry-pi-4 (Cortex-A72) and raspberry-pi-5 (Cortex-A76) by reusing the Pi 3 arm64 image set, but the frontend had no way to actually select either: the board picker, the canvas renderer, the serial monitor, the oscilloscope channel list, and the editor toolbar all hard-coded "raspberry-pi-3" as the only Pi entry. ComponentRegistry even registered Pi 4 / Pi 5 metadata pointing at the velxio-raspberry-pi-3 custom-element tag — a placeholder that meant both boards rendered as a Pi 3 in the picker thumbnail and on the canvas. Add dedicated boards top-to-bottom: * `RaspberryPi4Element.ts` / `RaspberryPi5Element.ts` — Velxio-style schematic SVG (authored from scratch, not traced). Pi 4 is the green PCB with BCM2711 SoC, 4× USB-A, USB-C power, dual µHDMI; Pi 5 is the darker green PCB with BCM2712 + RP1 southbridge, 2.5 GbE, USB-C 5V/5A, PCIe FFC connector, dedicated power button. Both carry a small "velxio" mark in the corner. * `pi40PinHeader.ts` — shared `buildPi40PinHeader()` helper that returns the 40-pin BCM layout. Every Pi from the 1B+ onwards uses the same physical pin positions and same BCM GPIO assignment, so Pi 3 / Pi 4 / Pi 5 elements all consume this helper and example wires drawn against one model transfer to the others without re-routing. * React wrappers `RaspberryPi4.tsx` / `RaspberryPi5.tsx` render the custom elements at absolute positions (mirrors how RaspberryPi3.tsx handles the Pi 3 illustration). * Wire-up across the editor surface: - BoardOnCanvas: BOARD_SIZE entry + switch case. - BoardPickerModal: description, icon, kinds list. - ComponentPickerModal: thumbnails now instantiate the dedicated custom element (was velxio-raspberry-pi-3 fallback). - SerialMonitor / EditorToolbar: pill labels, icons, colours. - Oscilloscope: GPIO channel list (28 BCM pins). - SimulatorCanvas: remote-boards filter for run/stop sync. - SPICE boardPinGroups: same 5V / 3V3 / GND as Pi 3. - boardPinToNumber: accepts physical pin numbers ("1"-"40"), BCM names ("GPIO14") and power labels for any Pi 3/4/5 id. - ComponentRegistry: dedicated tagNames + per-board thumbnails (green for Pi 4, darker green for Pi 5). * EditorToolbar's Pi 3 special cases (Linux/Python compile path, Run/Stop routing) now use `isPiBoardKind()` so Pi 4 and Pi 5 inherit the same behaviour automatically, and any future Pi family member (Zero / 1 / 2) lands in the right code paths the moment its backend boots. QEMU backend was already wired (qemu_manager.py:71/82 + manifest entry 'raspberry-pi-3-virt' shared across arm64 Pis), so this commit makes both boards selectable end-to-end without any backend follow-up.
2026-05-23 09:46:59 +07:00
'raspberry-pi-4': '#ef9a9a',
'raspberry-pi-5': '#ef9a9a',
esp32: '#a5d6a7',
'esp32-s3': '#a5d6a7',
'esp32-c3': '#a5d6a7',
'stm32-bluepill': '#80cbc4',
'stm32-blackpill': '#b0bec5',
'stm32-bluepill-f103cb': '#80cbc4',
'stm32-blackpill-f401': '#b0bec5',
'stm32-f4-discovery': '#90caf9',
'stm32-olimex-h405': '#a5d6a7',
'stm32-netduino-plus2': '#ce93d8',
'stm32-netduino2': '#ce93d8',
};
export const EditorToolbar = ({
consoleOpen,
setConsoleOpen,
compileLogs: _compileLogs,
setCompileLogs,
centerSlot,
rightSlot,
}: EditorToolbarProps) => {
const { t } = useTranslation();
const { files, codeChangedSinceLastCompile, markCompiled } = useEditorStore();
const {
boards,
activeBoardId,
compileBoardProgram,
loadMicroPythonProgram,
setBoardLanguageMode,
updateBoard,
startBoard,
stopBoard,
resetBoard,
// legacy compat
startSimulation,
stopSimulation,
resetSimulation,
running,
compiledHex,
} = useSimulatorStore();
const activeBoard = boards.find((b) => b.id === activeBoardId) ?? boards[0];
2026-04-26 05:46:52 +07:00
const currentProject = useProjectStore((s) => s.currentProject);
// Board-less mode: digital / analog SPICE-only circuits. The Run / Stop
// buttons toggle the SPICE solver's `paused` flag — pausing freezes every
// LED at its current brightness so the user can inspect the state, and
// resuming flushes the most recent switch toggle through the engine.
const electricalPaused = useElectricalStore((s) => s.paused);
const setElectricalPaused = useElectricalStore((s) => s.setPaused);
const isBoardless = boards.length === 0;
const digitalRunning = isBoardless && !electricalPaused;
// Any board actually running — the correct multi-target signal for the
// Run-All / Stop buttons (the flat `running` flag only tracks the ACTIVE
// board, so it misreports a multi-board or non-active-board run).
const anyBoardRunning = boards.some((b) => b.running);
// Multi-board: the primary Run button runs ALL boards (the whole wired
// project is one system — running a subset is almost never intended), with a
// split-menu to still run just the active board. Single-board is unchanged.
const isMultiBoard = boards.length > 1;
// A "run target" is a board OR a programmable custom-chip (a CPU that runs a
// ROM). When there is more than one target — two boards, a board + a chip, or
// several chips — the unified Compile-All / Run-All buttons appear and act on
// every target, the same way multiple Arduinos behave. Resolved as a number
// so the toolbar only re-renders when the count changes. The predicate is a
// cheap string test (no JSON.parse) since this selector runs on every store
// change, including high-frequency simulation churn. (The compile/run paths
// deliberately act on ALL custom chips, not just programmable ones.)
const targetCount = useSimulatorStore((s) => {
let chips = 0;
for (const c of s.components) {
if (c.metadataId !== 'custom-chip') continue;
const p = c.properties as Record<string, unknown>;
if (String(p?.programFile ?? '').trim() || String(p?.chipJson ?? '').includes('"programTargets"'))
chips++;
}
return s.boards.length + chips;
});
// Circuit-verification modal state. When `pendingRun` is non-null we've
// already paid the cost of solving + analysing — the user can either
// bail out or proceed by running `pendingRun()`.
const [verification, setVerification] = useState<VerificationResult | null>(null);
const pendingRunRef = useRef<(() => void) | null>(null);
2026-04-26 05:46:52 +07:00
// Helper: report a Run event to the backend for analytics. Resolves the
// FQBN from the board kind so the backend can group by family/fqbn.
const reportRun = useCallback(
(boardKind: BoardKind | undefined) => {
const fqbn = boardKind ? BOARD_KIND_FQBN[boardKind] : null;
void reportRunEvent({
project_id: currentProject?.id ?? null,
board_fqbn: fqbn ?? null,
});
},
[currentProject],
);
const [compiling, setCompiling] = useState(false);
// True while the pre-flight circuit verification SPICE solve is running.
// Drives the Run-button spinner so the user gets feedback during the
// (sometimes multi-second, cold-worker) solve instead of a dead button.
const [verifying, setVerifying] = useState(false);
// Synchronous re-entrancy guard: a click while a run/verify is already in
// flight is ignored, so rapid clicks can't stack multiple verifications.
const runInFlightRef = useRef(false);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const [libManagerOpen, setLibManagerOpen] = useState(false);
const [pendingLibraries, setPendingLibraries] = useState<string[]>([]);
const [installModalOpen, setInstallModalOpen] = useState(false);
const importInputRef = useRef<HTMLInputElement>(null);
const firmwareInputRef = useRef<HTMLInputElement>(null);
const toolbarRef = useRef<HTMLDivElement>(null);
const [missingLibHint, setMissingLibHint] = useState(false);
const [moreMenuOpen, setMoreMenuOpen] = useState(false);
const moreMenuRef = useRef<HTMLDivElement>(null);
// Split-button menu for the multi-board Run control ("Run all" / "Run active only").
const [runMenuOpen, setRunMenuOpen] = useState(false);
const runMenuRef = useRef<HTMLDivElement>(null);
// Open the Library Manager when another component (e.g. the velxio.json entry
// in the FileExplorer) asks for it via a window event. Avoids prop-drilling
// the modal state down to the explorer.
useEffect(() => {
const open = () => setLibManagerOpen(true);
window.addEventListener('velxio-open-library-manager', open);
return () => window.removeEventListener('velxio-open-library-manager', open);
}, []);
2026-06-18 01:37:32 +07:00
// Surface a runtime circuit fault (e.g. an LED that burnt out from
// overcurrent during the live SPICE solve) in the output console, in red,
// under the "Circuit check" group — same place as the pre-flight findings.
// (Previously an inline toolbar toast that overlapped the Run/Stop buttons.)
// We do NOT auto-open the console here: the continuous solver can fault on
// load, and popping the console open then would be intrusive. The pre-flight
// (on Run) opens it; this entry then lands in the already-open log.
2026-06-18 01:37:32 +07:00
useEffect(() => {
const onFault = (e: Event) => {
const detail = (e as CustomEvent).detail as { message?: string } | undefined;
if (!detail?.message) return;
const text = detail.message;
setCompileLogs((prev) => [
...prev,
{ timestamp: new Date(), type: 'error', message: text, target: CIRCUIT_CHECK_TARGET },
]);
2026-06-18 01:37:32 +07:00
};
window.addEventListener('velxio-circuit-fault', onFault);
return () => window.removeEventListener('velxio-circuit-fault', onFault);
}, [setCompileLogs]);
2026-06-18 01:37:32 +07:00
useEffect(() => {
if (!moreMenuOpen) return;
const onClickOutside = (e: MouseEvent) => {
if (moreMenuRef.current && !moreMenuRef.current.contains(e.target as Node)) {
setMoreMenuOpen(false);
}
};
const onEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') setMoreMenuOpen(false);
};
document.addEventListener('mousedown', onClickOutside);
document.addEventListener('keydown', onEsc);
return () => {
document.removeEventListener('mousedown', onClickOutside);
document.removeEventListener('keydown', onEsc);
};
}, [moreMenuOpen]);
// Close the Run split-menu on outside click / Escape (mirrors the more-menu).
useEffect(() => {
if (!runMenuOpen) return;
const onClickOutside = (e: MouseEvent) => {
if (runMenuRef.current && !runMenuRef.current.contains(e.target as Node)) {
setRunMenuOpen(false);
}
};
const onEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') setRunMenuOpen(false);
};
document.addEventListener('mousedown', onClickOutside);
document.addEventListener('keydown', onEsc);
return () => {
document.removeEventListener('mousedown', onClickOutside);
document.removeEventListener('keydown', onEsc);
};
}, [runMenuOpen]);
// Compile All / Run All — runs sequentially, logs to console (no dialog)
const [compileAllRunning, setCompileAllRunning] = useState(false);
const addLog = useCallback(
(log: CompilationLog) => {
setCompileLogs((prev: CompilationLog[]) => [...prev, log]);
},
[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;
let failed = 0;
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;
// Stamp every line for this chip with its target so the console groups
// it under its own section (alongside the boards).
const chipTarget: CompileTarget = { id: chip.id, label: chipLabel, kind: 'chip' };
const clog = (type: CompilationLog['type'], message: string) =>
addLog({ timestamp: new Date(), type, message, target: chipTarget });
// 1. C -> WASM. Only when missing — the chip designer fills this too.
if (!String(props.wasmBase64 ?? '') && sourceC) {
clog('info', `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;
clog('success', `Chip "${chipLabel}" compiled (${r.byte_size} B WASM).`);
} else {
clog(
'error',
`Chip "${chipLabel}" WASM compile failed: ${r.error || r.stderr || 'unknown error'}`,
);
failed++;
}
} catch (e) {
clog(
'error',
`Chip "${chipLabel}" WASM compile error: ${e instanceof Error ? e.message : String(e)}`,
);
failed++;
}
}
// 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)) {
feat(custom-chip): program lives in its own editor group, not the board sketch A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated editor file group — group-chip-<chipId> — rendered as its own collapsible section in the file explorer, exactly like each board owns its sketch group. Behaviour/driver chips and predefined chips carry no programFile and get no group; they stay editable only in the chip designer. Fixes two reported issues on the Z80 examples: - /example/z80-larson-no-board: the board-less chip example now opens its program (larson.s) as the active group, editable on the left — previously the editor showed but no file appeared. - /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as a sibling tab inside the Arduino sketch group; it sits in its own chip section instead. The board group shows only sketch.ino. Details: - useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers. - loadExample: seedChipProgramGroups() routes each chip's programFile into its own group (seeded from the example files), sweeps stale chip groups, keeps the program OUT of the board group, and for a board-less chip example makes the chip group active so the program is the editable file shown. - EditorToolbar.prepareCustomChips: resolves the program from the chip's own group (falls back to board files for older projects) before assembling ROM. - FileExplorer: renders one collapsible section per programmable chip with an IC icon; clicking switches the editor to the chip group. Lazy-creates a group for chips dropped on the canvas. - projectPayload + vlxFile: serialise chip groups alongside board groups and include them in the dirty-check hash, so chip-program edits persist on save / autosave / .vlx export and round-trip via replaceFileGroups on load. - Regression tests for board-less + board+chip routing and stale-group sweep. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
// The program lives in the chip's OWN editor group (its collapsible
// section in the file explorer), separate from the board sketch.
// Fall back to the board files for older projects that still carried
// the program alongside sketch.ino in the board group.
const chipGroupFiles = useEditorStore
.getState()
.getGroupFiles(chipFileGroupId(chip.id));
const file =
chipGroupFiles.find((f) => f.name === programFile) ??
boardFiles.find((f) => f.name === programFile);
if (!file) {
clog('error', `Chip "${chipLabel}": program file "${programFile}" not found in the chip's files.`);
failed++;
} else {
const target = targetForChip(chipJson);
const fmt = formatForFile(programFile);
clog(
'info',
`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;
clog('success', `ROM ready: ${rr.byte_size} B injected into "${chipLabel}".`);
} else {
clog(
'error',
`ROM compile failed for "${programFile}": ${rr.error || rr.stderr || 'unknown error'}`,
);
failed++;
}
} catch (e) {
clog(
'error',
`ROM compile error for "${programFile}": ${e instanceof Error ? e.message : String(e)}`,
);
failed++;
}
}
}
if (changed) {
updateComponent(chip.id, { properties: props } as any);
}
}
return { failed };
},
[addLog],
);
const handleCompile = async () => {
setCompiling(true);
setMessage(null);
setConsoleOpen(true);
fix: 5 user-reported issues (#208 #209 #210 #211 #212) #208 — stale binary executes after compile error EditorToolbar.handleCompile: on failed compile, clear the active board's compiledProgram so a subsequent Run can't silently execute the previous successful build (which doesn't match the editor any more). The Run gate already short-circuits on !compiledProgram and forces a fresh compile. #209 — compile terminal kept stale messages across runs EditorToolbar.handleCompile: setCompileLogs([]) at the top of the handler. Previously logs from the prior compile lingered, making it hard to tell new errors / warnings apart from old ones. #210 — desktop File > New Project did nothing desktop/menu.ts: the menu action used to dispatch a CustomEvent nobody listened to. Replaced with a real `newProject()` function that stops the running simulation, removes every board (also drops the bridges + wires touching them), clears components / wires, loads the default Blink sketch into the editor, clears project metadata, and wipes the compile output. Confirms first if there's unsaved work on the canvas. #211 — deleting the only board made every other component unresponsive (wires still worked) SimulatorCanvas.tsx::interactionRunning: the old expression treated boards.length === 0 as "boardless electrical mode is running" — which suppressed the property dialog on click and made non-sensor components look frozen. Fixed by also requiring useElectricalStore.submittedNetlist !== '' before flipping to the boardless-running branch. SPICE has to have actually solved at least once for the mode to engage. #212 — ESP32 Support 404 with no actionable message desktop/Esp32QemuPrompt.tsx: catch the raw "download HTTP 404" / "not found" upstream error and reword it to "ESP32 support is not yet available for your platform. The Velxio team is preparing this build - try again in a few days, or use Arduino/RP2040 boards in the meantime." The real fix is server-side (the velxio team needs to publish a qemu-xtensa.tar.gz for the user's platform into the asset bucket and update esp32-qemu/latest.json). Tracked in project/desktop-agent-v040/ follow-ups. All five fixes verified with `tsc --noEmit` clean and the existing 25-test vitest suite green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:06:44 +07:00
// Wipe the previous build's output before we append anything new.
// Issue #209: lingering logs from prior compiles made it impossible
// to tell the latest errors / warnings apart from stale ones.
// Keep the "Circuit check" findings, though: a Run auto-compiles right
// after the pre-flight verification logs them, and clearing here would
// wipe a circuit warning the user just triggered.
setCompileLogs((prev) => prev.filter((l) => l.target?.id === CIRCUIT_CHECK_TARGET.id));
trackCompileCode();
// ── 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.
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
//
// 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.
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
const componentsForCompile = useSimulatorStore.getState().components;
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);
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
}
if (customChips.length > 0) {
const boardFiles = activeBoard?.activeFileGroupId
? useEditorStore.getState().getGroupFiles(activeBoard.activeFileGroupId)
: files;
await prepareCustomChips(customChips, boardFiles);
}
// ── End custom-chip preparation ─────────────────────────────────────
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
const kind = activeBoard?.boardKind;
// The active board's console target, defined up front so EVERY board path
// (Pi, MicroPython, arduino-cli, errors) groups its lines under one section.
const boardLabel = activeBoard ? boardDisplayName(activeBoard) : 'Unknown';
const boardTarget: CompileTarget | undefined = activeBoardId
? { id: activeBoardId, label: boardLabel, kind: 'board' }
: undefined;
const blog = (type: CompilationLog['type'], message: string) =>
addLog({ timestamp: new Date(), type, message, target: boardTarget });
// Raspberry Pi 3B doesn't need arduino-cli compilation
feat(boards): expose Raspberry Pi 4 and Pi 5 in the picker (UI + pin wiring) The BoardKind type and the QEMU backend already supported raspberry-pi-4 (Cortex-A72) and raspberry-pi-5 (Cortex-A76) by reusing the Pi 3 arm64 image set, but the frontend had no way to actually select either: the board picker, the canvas renderer, the serial monitor, the oscilloscope channel list, and the editor toolbar all hard-coded "raspberry-pi-3" as the only Pi entry. ComponentRegistry even registered Pi 4 / Pi 5 metadata pointing at the velxio-raspberry-pi-3 custom-element tag — a placeholder that meant both boards rendered as a Pi 3 in the picker thumbnail and on the canvas. Add dedicated boards top-to-bottom: * `RaspberryPi4Element.ts` / `RaspberryPi5Element.ts` — Velxio-style schematic SVG (authored from scratch, not traced). Pi 4 is the green PCB with BCM2711 SoC, 4× USB-A, USB-C power, dual µHDMI; Pi 5 is the darker green PCB with BCM2712 + RP1 southbridge, 2.5 GbE, USB-C 5V/5A, PCIe FFC connector, dedicated power button. Both carry a small "velxio" mark in the corner. * `pi40PinHeader.ts` — shared `buildPi40PinHeader()` helper that returns the 40-pin BCM layout. Every Pi from the 1B+ onwards uses the same physical pin positions and same BCM GPIO assignment, so Pi 3 / Pi 4 / Pi 5 elements all consume this helper and example wires drawn against one model transfer to the others without re-routing. * React wrappers `RaspberryPi4.tsx` / `RaspberryPi5.tsx` render the custom elements at absolute positions (mirrors how RaspberryPi3.tsx handles the Pi 3 illustration). * Wire-up across the editor surface: - BoardOnCanvas: BOARD_SIZE entry + switch case. - BoardPickerModal: description, icon, kinds list. - ComponentPickerModal: thumbnails now instantiate the dedicated custom element (was velxio-raspberry-pi-3 fallback). - SerialMonitor / EditorToolbar: pill labels, icons, colours. - Oscilloscope: GPIO channel list (28 BCM pins). - SimulatorCanvas: remote-boards filter for run/stop sync. - SPICE boardPinGroups: same 5V / 3V3 / GND as Pi 3. - boardPinToNumber: accepts physical pin numbers ("1"-"40"), BCM names ("GPIO14") and power labels for any Pi 3/4/5 id. - ComponentRegistry: dedicated tagNames + per-board thumbnails (green for Pi 4, darker green for Pi 5). * EditorToolbar's Pi 3 special cases (Linux/Python compile path, Run/Stop routing) now use `isPiBoardKind()` so Pi 4 and Pi 5 inherit the same behaviour automatically, and any future Pi family member (Zero / 1 / 2) lands in the right code paths the moment its backend boots. QEMU backend was already wired (qemu_manager.py:71/82 + manifest entry 'raspberry-pi-3-virt' shared across arm64 Pis), so this commit makes both boards selectable end-to-end without any backend follow-up.
2026-05-23 09:46:59 +07:00
if (isPiBoardKind(kind)) {
blog('info', 'Raspberry Pi 3B: no compilation needed — run Python scripts directly.');
setMessage({ type: 'success', text: 'Ready (no compilation needed)' });
setCompiling(false);
return;
}
// MicroPython mode — no backend compilation needed
if (activeBoard?.languageMode === 'micropython' && activeBoardId) {
blog('info', 'MicroPython: loading firmware and user files...');
try {
const groupFiles = useEditorStore.getState().getGroupFiles(activeBoard.activeFileGroupId);
const pyFiles = groupFiles.map((f) => ({ name: f.name, content: f.content }));
await loadMicroPythonProgram(activeBoardId, pyFiles);
blog('success', 'MicroPython firmware loaded successfully');
setMessage({ type: 'success', text: 'MicroPython ready' });
} catch (err) {
const errMsg = err instanceof Error ? err.message : 'Failed to load MicroPython';
blog('error', errMsg);
setMessage({ type: 'error', text: errMsg });
} finally {
setCompiling(false);
}
return;
}
const fqbn = kind ? BOARD_KIND_FQBN[kind] : null;
if (!fqbn) {
blog('error', `No FQBN for board kind: ${kind}`);
setMessage({ type: 'error', text: 'Unknown board' });
setCompiling(false);
return;
}
blog('info', `Starting compilation for ${boardLabel} (${fqbn})...`);
try {
const groupFiles = activeBoard?.activeFileGroupId
? useEditorStore.getState().getGroupFiles(activeBoard.activeFileGroupId)
: files;
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,
}));
feat(compile): stream live ESP-IDF cmake + ninja output to the console A user reported on Discord: "the Velxio Console doesn't update anything, it just waits until the very end and displays everything in one go". True for the async compile path — /compile/status only carried `state` and the final `result`, so the editor's CompilationConsole stayed empty during the 5-7 minute cold ESP-IDF builds and dumped 1500 lines at once when the build finished. This wires live build output through the whole stack. Backend (espidf_compiler.py) - New _run_with_streaming() helper. When a progress_callback is provided it spawns the subprocess via Popen + stdout/stderr drain threads and invokes the callback line-by-line. When None it falls back to the existing subprocess.run(capture_output=True) one-shot path so the unit-test code that doesn't care about live output is unaffected. - compile() and _compile_in_dir() take an optional ProgressCallback. - _run_cmake / _run_ninja closures now go through _run_with_streaming with that callback. cmake configure (~2-5 s) + ninja (~5-300+ s) both stream now; the ninja output is the one users actually want to watch. Backend (compile.py) - _compile_job seeds COMPILE_JOBS[id]['stdout_buffer'] = '' and defines on_progress_line(line) which appends to it. Buffer capped at 256 KB (tail kept) so a runaway build can't OOM the FastAPI process. - The buffer is preserved on both the success and the error path so late polls still see the log even after state transitions to done/error. - /compile/status now returns the buffer as a `stdout` field. CompileStatusResponse gains the field with default '' so old clients that don't read it still work. Frontend (compilation.ts) - compileCode() takes a 4th argument: optional CompileProgress callback fired every poll while state ∈ {pending, running}. Carries the cumulative stdout (caller computes deltas) plus elapsed seconds. - Surfaces the new `stdout` field of /compile/status and forwards it to the callback. Errors thrown from the callback are swallowed — a faulty UI hook must never break the polling loop. Frontend (EditorToolbar.tsx) - Both compileCode() call sites (Run and Compile-All) now pass an onProgress callback. It tracks `lastStreamedLen` per-compile, splits each new delta on newlines, and appends them as `info`-typed CompilationLog entries via setCompileLogs. The Compile-All flow prefixes each line with the board label so multi-board builds stay readable. - After the build settles, the existing parseCompileResult call still runs and appends the structured analysis on top of the live stream — that's where FAILED-block detection + the `error`-typed entries that drive the auto-switch-to-errors filter live. Net effect on the user complaint: cold ESP-IDF builds now show the ninja [N/1483] progress lines streaming into the console as they happen, instead of staring at an empty panel for 5-7 minutes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 04:36:58 +07:00
// Stream live cmake + ninja output into the compilation console as
// it arrives, instead of waiting for the whole build to finish.
// Each poll the backend returns the cumulative stdout buffer; we
// append only the delta since the previous call as 'info' lines.
let lastStreamedLen = 0;
const result = await compileCode(
sketchFiles,
fqbn,
currentProject?.id ?? null,
({ stdout }) => {
if (stdout.length <= lastStreamedLen) return;
const delta = stdout.slice(lastStreamedLen);
lastStreamedLen = stdout.length;
const newLines = delta.split('\n').filter((s) => s.trim());
if (!newLines.length) return;
const now = new Date();
setCompileLogs((prev: CompilationLog[]) => [
...prev,
...newLines.map((line) => ({
timestamp: now,
type: 'info' as const,
message: line,
target: boardTarget,
feat(compile): stream live ESP-IDF cmake + ninja output to the console A user reported on Discord: "the Velxio Console doesn't update anything, it just waits until the very end and displays everything in one go". True for the async compile path — /compile/status only carried `state` and the final `result`, so the editor's CompilationConsole stayed empty during the 5-7 minute cold ESP-IDF builds and dumped 1500 lines at once when the build finished. This wires live build output through the whole stack. Backend (espidf_compiler.py) - New _run_with_streaming() helper. When a progress_callback is provided it spawns the subprocess via Popen + stdout/stderr drain threads and invokes the callback line-by-line. When None it falls back to the existing subprocess.run(capture_output=True) one-shot path so the unit-test code that doesn't care about live output is unaffected. - compile() and _compile_in_dir() take an optional ProgressCallback. - _run_cmake / _run_ninja closures now go through _run_with_streaming with that callback. cmake configure (~2-5 s) + ninja (~5-300+ s) both stream now; the ninja output is the one users actually want to watch. Backend (compile.py) - _compile_job seeds COMPILE_JOBS[id]['stdout_buffer'] = '' and defines on_progress_line(line) which appends to it. Buffer capped at 256 KB (tail kept) so a runaway build can't OOM the FastAPI process. - The buffer is preserved on both the success and the error path so late polls still see the log even after state transitions to done/error. - /compile/status now returns the buffer as a `stdout` field. CompileStatusResponse gains the field with default '' so old clients that don't read it still work. Frontend (compilation.ts) - compileCode() takes a 4th argument: optional CompileProgress callback fired every poll while state ∈ {pending, running}. Carries the cumulative stdout (caller computes deltas) plus elapsed seconds. - Surfaces the new `stdout` field of /compile/status and forwards it to the callback. Errors thrown from the callback are swallowed — a faulty UI hook must never break the polling loop. Frontend (EditorToolbar.tsx) - Both compileCode() call sites (Run and Compile-All) now pass an onProgress callback. It tracks `lastStreamedLen` per-compile, splits each new delta on newlines, and appends them as `info`-typed CompilationLog entries via setCompileLogs. The Compile-All flow prefixes each line with the board label so multi-board builds stay readable. - After the build settles, the existing parseCompileResult call still runs and appends the structured analysis on top of the live stream — that's where FAILED-block detection + the `error`-typed entries that drive the auto-switch-to-errors filter live. Net effect on the user complaint: cold ESP-IDF builds now show the ninja [N/1483] progress lines streaming into the console as they happen, instead of staring at an empty panel for 5-7 minutes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 04:36:58 +07:00
})),
]);
},
// Per-board ESP32 build options + SPIFFS uploads. Undefined for AVR
// / RP2040 boards (ignored on those paths by the backend).
{
boardOptions: activeBoard?.boardOptions,
spiffsFiles: activeBoard?.spiffsFiles,
// P2.4 — THIS board's declared manifest (compile scope). Per-board so
// two boards can use different libraries without clashing.
libraries: activeBoard?.libraries?.length ? activeBoard.libraries : null,
feat(esp32): pure ESP-IDF language mode for the ESP32 family (#139) Adds a third entry to the board language selector next to Arduino C++ and MicroPython: ESP-IDF. In this mode the user writes a plain ESP-IDF project — app_main() entry point, FreeRTOS + driver APIs — and the backend compiles it through the same ESP-IDF toolchain it already uses for ESP32 Arduino sketches, just without the arduino-esp32 component. Backend: - CompileRequest.language ('espidf') threaded through the sync + async compile paths and folded into the dedup job key (language='arduino' and omitted hash identically so old clients keep dedupping). - espidf_compiler: pure_idf flag. User files are written into main/ as-is (no Arduino.h wrap, no velxio_compat.h, Arduino library resolution skipped), ARDUINO_ESP32_PATH is dropped from the build env and VELXIO_PURE_SKETCH raised so the template CMake compiles the user's own sources via a glob branch. Pure builds get their own persistent build-dir variant through the eff_hash fold. - QEMU WiFi compat for IDF-style code: esp_wifi.h/esp_wifi_init detection sets has_wifi, and literal #define SSID/PASS plus wifi_config_t designated initializers are normalized to the QEMU AP. - CONFIG_ARDUINO_* lines are stripped from sdkconfig.defaults in pure mode (the symbols don't exist without the arduino component). Frontend: - LanguageMode gains 'espidf'; BOARD_SUPPORTS_ESPIDF covers the ESP32 family (Xtensa, S3, C3). Toolbar shows the option only for those. - Switching modes seeds a main.c blink skeleton (app_main + gpio driver), mirroring the MicroPython main.py flow. - compileCode sends language='espidf'; run/stop paths are unchanged (the QEMU worker consumes the same merged flash image). - New gallery example: esp32-idf-blink (LED + resistor on GPIO 2). Tests: unit coverage for the build-env switch, IDF wifi normalization, job-key variance, file-group seeding and the new example; verified end-to-end in a container from the prod image (pure build produces a bootable flash image; Arduino-mode build unchanged, same variant hash).
2026-07-24 11:37:01 +07:00
// Pure ESP-IDF mode (issue #139): tell the backend to compile the
// user's app_main() sources without the arduino-esp32 component.
language: activeBoard?.languageMode === 'espidf' ? 'espidf' : undefined,
},
feat(compile): stream live ESP-IDF cmake + ninja output to the console A user reported on Discord: "the Velxio Console doesn't update anything, it just waits until the very end and displays everything in one go". True for the async compile path — /compile/status only carried `state` and the final `result`, so the editor's CompilationConsole stayed empty during the 5-7 minute cold ESP-IDF builds and dumped 1500 lines at once when the build finished. This wires live build output through the whole stack. Backend (espidf_compiler.py) - New _run_with_streaming() helper. When a progress_callback is provided it spawns the subprocess via Popen + stdout/stderr drain threads and invokes the callback line-by-line. When None it falls back to the existing subprocess.run(capture_output=True) one-shot path so the unit-test code that doesn't care about live output is unaffected. - compile() and _compile_in_dir() take an optional ProgressCallback. - _run_cmake / _run_ninja closures now go through _run_with_streaming with that callback. cmake configure (~2-5 s) + ninja (~5-300+ s) both stream now; the ninja output is the one users actually want to watch. Backend (compile.py) - _compile_job seeds COMPILE_JOBS[id]['stdout_buffer'] = '' and defines on_progress_line(line) which appends to it. Buffer capped at 256 KB (tail kept) so a runaway build can't OOM the FastAPI process. - The buffer is preserved on both the success and the error path so late polls still see the log even after state transitions to done/error. - /compile/status now returns the buffer as a `stdout` field. CompileStatusResponse gains the field with default '' so old clients that don't read it still work. Frontend (compilation.ts) - compileCode() takes a 4th argument: optional CompileProgress callback fired every poll while state ∈ {pending, running}. Carries the cumulative stdout (caller computes deltas) plus elapsed seconds. - Surfaces the new `stdout` field of /compile/status and forwards it to the callback. Errors thrown from the callback are swallowed — a faulty UI hook must never break the polling loop. Frontend (EditorToolbar.tsx) - Both compileCode() call sites (Run and Compile-All) now pass an onProgress callback. It tracks `lastStreamedLen` per-compile, splits each new delta on newlines, and appends them as `info`-typed CompilationLog entries via setCompileLogs. The Compile-All flow prefixes each line with the board label so multi-board builds stay readable. - After the build settles, the existing parseCompileResult call still runs and appends the structured analysis on top of the live stream — that's where FAILED-block detection + the `error`-typed entries that drive the auto-switch-to-errors filter live. Net effect on the user complaint: cold ESP-IDF builds now show the ninja [N/1483] progress lines streaming into the console as they happen, instead of staring at an empty panel for 5-7 minutes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 04:36:58 +07:00
);
// After the build settles, append the structured analysis on top of
// the live stream — parseCompileResult highlights FAILED blocks and
// tags compiler errors with type='error', which the console uses for
// colour + the auto-switch-to-errors filter.
const resultLogs = parseCompileResult(result, boardLabel, boardTarget);
setCompileLogs((prev: CompilationLog[]) => [...prev, ...resultLogs]);
if (result.success) {
const program = result.hex_content ?? result.binary_content ?? null;
if (program && activeBoardId) {
compileBoardProgram(activeBoardId, program);
if (result.has_wifi !== undefined) {
updateBoard(activeBoardId, { hasWifi: result.has_wifi });
}
}
setMessage({ type: 'success', text: 'Compiled successfully' });
markCompiled();
setMissingLibHint(false);
} else {
const errText = result.error || result.stderr || 'Compile failed';
setMessage({ type: 'error', text: errText });
fix: 5 user-reported issues (#208 #209 #210 #211 #212) #208 — stale binary executes after compile error EditorToolbar.handleCompile: on failed compile, clear the active board's compiledProgram so a subsequent Run can't silently execute the previous successful build (which doesn't match the editor any more). The Run gate already short-circuits on !compiledProgram and forces a fresh compile. #209 — compile terminal kept stale messages across runs EditorToolbar.handleCompile: setCompileLogs([]) at the top of the handler. Previously logs from the prior compile lingered, making it hard to tell new errors / warnings apart from old ones. #210 — desktop File > New Project did nothing desktop/menu.ts: the menu action used to dispatch a CustomEvent nobody listened to. Replaced with a real `newProject()` function that stops the running simulation, removes every board (also drops the bridges + wires touching them), clears components / wires, loads the default Blink sketch into the editor, clears project metadata, and wipes the compile output. Confirms first if there's unsaved work on the canvas. #211 — deleting the only board made every other component unresponsive (wires still worked) SimulatorCanvas.tsx::interactionRunning: the old expression treated boards.length === 0 as "boardless electrical mode is running" — which suppressed the property dialog on click and made non-sensor components look frozen. Fixed by also requiring useElectricalStore.submittedNetlist !== '' before flipping to the boardless-running branch. SPICE has to have actually solved at least once for the mode to engage. #212 — ESP32 Support 404 with no actionable message desktop/Esp32QemuPrompt.tsx: catch the raw "download HTTP 404" / "not found" upstream error and reword it to "ESP32 support is not yet available for your platform. The Velxio team is preparing this build - try again in a few days, or use Arduino/RP2040 boards in the meantime." The real fix is server-side (the velxio team needs to publish a qemu-xtensa.tar.gz for the user's platform into the asset bucket and update esp32-qemu/latest.json). Tracked in project/desktop-agent-v040/ follow-ups. All five fixes verified with `tsc --noEmit` clean and the existing 25-test vitest suite green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:06:44 +07:00
// Issue #208: drop the previous successful program from this
// board so a subsequent Run cannot silently execute stale code
// that doesn't match the editor any more. The Run button gates
// on `!compiledProgram` and will refuse + force a re-compile.
if (activeBoardId) {
updateBoard(activeBoardId, { compiledProgram: null });
}
// Detect missing library errors — common patterns:
// "No such file or directory" for #include, "fatal error: XXX.h"
const looksLikeMissingLib =
/No such file or directory|fatal error:.*\.h|library not found/i.test(errText);
setMissingLibHint(looksLikeMissingLib);
}
} catch (err) {
const errMsg = err instanceof Error ? err.message : 'Compile failed';
blog('error', errMsg);
setMessage({ type: 'error', text: errMsg });
} finally {
setCompiling(false);
}
};
// Track whether we should auto-run after compilation completes
const autoRunAfterCompile = useRef(false);
/**
* Pre-flight safety check: solves the current circuit and flags shorts,
* LED over-current and resistor over-power. Returns the result. When the
* solver fails to converge (degenerate netlist, no power source, ) we
* silently report a clean result so the user isn't blocked on circuits
* that aren't physically meaningful yet.
*/
const runVerification = useCallback(
(): Promise<VerificationResult | null> => verifyCircuitFromStore(),
[],
);
/**
* Returns true if the caller should proceed inline. All findings are written
* to the output console (red errors / orange warnings, "Circuit check"
* group). If the verifier finds errors we also stash a resume callback in
* `pendingRunRef` and pop the verification modal; the resume callback
* re-enters `handleRun` with `skipVerify = true` so we don't loop.
* Warnings-only results don't block the console entry is enough.
*/
const checkOrBlock = useCallback(
async (resume: () => void): Promise<boolean> => {
const result = await runVerification();
if (!result) return true;
if (result.errors.length === 0 && result.warnings.length === 0) return true;
// Write every finding to the output console under "Circuit check" — red
// for errors, orange for warnings — so there's one persistent, unified
// diagnostics log next to the compiler output (Proteus-style). Replace
// any prior circuit-check entries so repeated runs stay clean, and open
// the console so the findings are visible.
const now = new Date();
setCompileLogs((prev) => [
...prev.filter((l) => l.target?.id !== CIRCUIT_CHECK_TARGET.id),
...result.errors.map((e) => ({
timestamp: now,
type: 'error' as const,
message: e.message,
target: CIRCUIT_CHECK_TARGET,
})),
...result.warnings.map((w) => ({
timestamp: now,
type: 'warning' as const,
message: w.message,
target: CIRCUIT_CHECK_TARGET,
})),
]);
setConsoleOpen(true);
// Warnings only — non-blocking; the console entry is enough, run continues.
if (result.errors.length === 0) return true;
// Errors → also pop the modal so the user makes an explicit Run-anyway /
// Cancel decision; the console keeps the persistent red record.
pendingRunRef.current = resume;
setVerification(result);
return false;
},
[runVerification, setCompileLogs, setConsoleOpen],
);
const handleRun = async (skipVerify = false) => {
feat: ESP32-CAM emulation with webcam frame bridge First open-source end-to-end emulation of the AI-Thinker ESP32-CAM in QEMU, paired with a browser webcam → firmware bridge so users can develop camera sketches without hardware. Status: esp_camera_init() returns ESP_OK; OV2640 chip-id verifies (PID/VER/MIDH/MIDL exactly match the datasheet); GPIO 25 VSYNC NEGEDGE interrupt enabled by the upstream driver. Final piece (cam_task accepting frames) is in progress — descriptor walker fix landed in this commit. Backend (Python/FastAPI): - simulation.py: camera_attach/frame/detach WS handlers - esp32_worker.py: ctypes binding to velxio_push_camera_frame + feature-detection fallback for older DLLs - esp32_lib_manager.py: forward camera commands to the worker stdin - esp-idf-template/main/CMakeLists.txt: esp32-camera headers added via add_prebuilt_library + REQUIRES driver (resolves i2c_master_* symbols). LED_BUILTIN=2 fallback for sketches that hardcode it. Frontend (React/TS): - EditorToolbar.tsx: ESP32-CAM (and the rest of the ESP32 family) added to isQemuBoard list — Run button now starts the QEMU bridge for these boards instead of falling through to the AVR path - useWebcamFrames.ts: getUserMedia → OffscreenCanvas → toBlob('image/jpeg') → base64 → WS at ~10 fps - CameraToggle.tsx: header button with status colors + frame counter - SimulatorCanvas.tsx: render CameraToggle for esp32-cam boards - Esp32Bridge.ts: sendCameraAttach/Frame/Detach + chunked btoa - useSimulatorStore.ts: diagnostic log on compileBoardProgram - components-metadata.json: regen including esp32-cam component Submodule pointer: - wokwi-libs/qemu-lcgamboa → ff8eee0 (camera devices commit on davidmonterocrespo24/qemu-lcgamboa branch picsimlab-esp32) Investigation + tests in test/test-esp32-cam/: - 13 autosearch markdown docs (overview, SOTA, OV2640 spec, DVP/I2S spec, build blueprint, blockers resolved, descriptor walker fix) - 5 sketches (camera_init, sccb_probe, dma_smoke, frame_roundtrip, webcam_demo) + 8 live + WS regression tests - README with the user-facing flow .gitignore: - libqemu-*.dll.{pre-camera,new,bak} (rollback points, regenerated) - wokwi-libs/esp32-camera/ (clone consumed by arduino-esp32 path, not part of this repo) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 05:28:55 +07:00
console.log('[handleRun] click', { activeBoardId, running, codeChangedSinceLastCompile });
// Pre-flight: solve the circuit and check for shorts / overcurrent /
// overpower. If anything trips we hand control to the modal, which
// resumes by calling `handleRun(true)` for "Run anyway".
if (!skipVerify) {
// The verification solve can take a second or two (cold ngspice worker).
// Show the Run-button spinner and ignore re-clicks while it runs — the
// button otherwise looks idle and gets clicked repeatedly, stacking
// multiple verifications.
if (runInFlightRef.current) return;
runInFlightRef.current = true;
setVerifying(true);
let ok = false;
try {
ok = await checkOrBlock(() => handleRun(true));
} finally {
setVerifying(false);
runInFlightRef.current = false;
}
if (!ok) return;
}
// Board-less circuits have no MCU to start. If there are custom-chip CPUs
// on the canvas, compile them (WASM + ROM) and re-attach so they pick up
// the fresh WASM — Velxio runs custom chips with no Arduino/ESP32 board,
// as a general-purpose electronics simulator. Then resume the electrical
// solver (replays any switch toggles captured while paused).
if (isBoardless) {
const customChips = useSimulatorStore
.getState()
.components.filter((c) => c.metadataId === 'custom-chip');
if (customChips.length > 0) {
setCompiling(true);
setConsoleOpen(true);
// Fresh chip output, but keep the circuit pre-flight findings just
// logged by checkOrBlock so they survive a "Run anyway".
setCompileLogs((prev) => prev.filter((l) => l.target?.id === CIRCUIT_CHECK_TARGET.id));
try {
await prepareCustomChips(customChips, files);
} catch (e) {
addLog({
timestamp: new Date(),
type: 'error',
message: e instanceof Error ? e.message : String(e),
});
}
setCompiling(false);
// Force the chip parts to re-attach with their freshly compiled WASM.
useSimulatorStore.getState().restartParts();
}
setElectricalPaused(false);
setMessage(null);
return;
}
if (activeBoardId) {
const board = boards.find((b) => b.id === activeBoardId);
feat: ESP32-CAM emulation with webcam frame bridge First open-source end-to-end emulation of the AI-Thinker ESP32-CAM in QEMU, paired with a browser webcam → firmware bridge so users can develop camera sketches without hardware. Status: esp_camera_init() returns ESP_OK; OV2640 chip-id verifies (PID/VER/MIDH/MIDL exactly match the datasheet); GPIO 25 VSYNC NEGEDGE interrupt enabled by the upstream driver. Final piece (cam_task accepting frames) is in progress — descriptor walker fix landed in this commit. Backend (Python/FastAPI): - simulation.py: camera_attach/frame/detach WS handlers - esp32_worker.py: ctypes binding to velxio_push_camera_frame + feature-detection fallback for older DLLs - esp32_lib_manager.py: forward camera commands to the worker stdin - esp-idf-template/main/CMakeLists.txt: esp32-camera headers added via add_prebuilt_library + REQUIRES driver (resolves i2c_master_* symbols). LED_BUILTIN=2 fallback for sketches that hardcode it. Frontend (React/TS): - EditorToolbar.tsx: ESP32-CAM (and the rest of the ESP32 family) added to isQemuBoard list — Run button now starts the QEMU bridge for these boards instead of falling through to the AVR path - useWebcamFrames.ts: getUserMedia → OffscreenCanvas → toBlob('image/jpeg') → base64 → WS at ~10 fps - CameraToggle.tsx: header button with status colors + frame counter - SimulatorCanvas.tsx: render CameraToggle for esp32-cam boards - Esp32Bridge.ts: sendCameraAttach/Frame/Detach + chunked btoa - useSimulatorStore.ts: diagnostic log on compileBoardProgram - components-metadata.json: regen including esp32-cam component Submodule pointer: - wokwi-libs/qemu-lcgamboa → ff8eee0 (camera devices commit on davidmonterocrespo24/qemu-lcgamboa branch picsimlab-esp32) Investigation + tests in test/test-esp32-cam/: - 13 autosearch markdown docs (overview, SOTA, OV2640 spec, DVP/I2S spec, build blueprint, blockers resolved, descriptor walker fix) - 5 sketches (camera_init, sccb_probe, dma_smoke, frame_roundtrip, webcam_demo) + 8 live + WS regression tests - README with the user-facing flow .gitignore: - libqemu-*.dll.{pre-camera,new,bak} (rollback points, regenerated) - wokwi-libs/esp32-camera/ (clone consumed by arduino-esp32 path, not part of this repo) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 05:28:55 +07:00
console.log('[handleRun] active board', {
id: board?.id,
kind: board?.boardKind,
hasCompiledProgram: !!board?.compiledProgram,
compiledProgramLen: board?.compiledProgram?.length ?? 0,
});
// MicroPython mode: stop any running session first, then reload firmware + start
if (board?.languageMode === 'micropython') {
trackRunSimulation(board.boardKind);
2026-04-26 05:46:52 +07:00
reportRun(board.boardKind);
// Always stop the current session so the new run gets a clean QEMU boot.
// This also prevents the double start_esp32 that occurs when the bridge
// is already connected and startBoard() is called again.
if (board.running) {
stopBoard(activeBoardId);
// Give the WebSocket a moment to close cleanly before reconnecting.
await new Promise((resolve) => setTimeout(resolve, 300));
}
setCompiling(true);
setMessage(null);
const mpyTarget: CompileTarget = {
id: activeBoardId,
label: boardDisplayName(board),
kind: 'board',
};
const mlog = (type: CompilationLog['type'], message: string) =>
addLog({ timestamp: new Date(), type, message, target: mpyTarget });
mlog('info', 'MicroPython: loading firmware and user files...');
try {
const groupFiles = useEditorStore.getState().getGroupFiles(board.activeFileGroupId);
const pyFiles = groupFiles.map((f) => ({ name: f.name, content: f.content }));
await loadMicroPythonProgram(activeBoardId, pyFiles);
mlog('success', 'MicroPython firmware loaded');
} catch (err) {
const errMsg = err instanceof Error ? err.message : 'Failed to load MicroPython';
mlog('error', errMsg);
setMessage({ type: 'error', text: errMsg });
setCompiling(false);
return;
}
setCompiling(false);
startBoard(activeBoardId);
setMessage(null);
return;
}
const isQemuBoard = isQemuBoardKind(board?.boardKind);
// QEMU boards: auto-compile if no firmware available yet
if (isQemuBoard) {
feat: ESP32-CAM emulation with webcam frame bridge First open-source end-to-end emulation of the AI-Thinker ESP32-CAM in QEMU, paired with a browser webcam → firmware bridge so users can develop camera sketches without hardware. Status: esp_camera_init() returns ESP_OK; OV2640 chip-id verifies (PID/VER/MIDH/MIDL exactly match the datasheet); GPIO 25 VSYNC NEGEDGE interrupt enabled by the upstream driver. Final piece (cam_task accepting frames) is in progress — descriptor walker fix landed in this commit. Backend (Python/FastAPI): - simulation.py: camera_attach/frame/detach WS handlers - esp32_worker.py: ctypes binding to velxio_push_camera_frame + feature-detection fallback for older DLLs - esp32_lib_manager.py: forward camera commands to the worker stdin - esp-idf-template/main/CMakeLists.txt: esp32-camera headers added via add_prebuilt_library + REQUIRES driver (resolves i2c_master_* symbols). LED_BUILTIN=2 fallback for sketches that hardcode it. Frontend (React/TS): - EditorToolbar.tsx: ESP32-CAM (and the rest of the ESP32 family) added to isQemuBoard list — Run button now starts the QEMU bridge for these boards instead of falling through to the AVR path - useWebcamFrames.ts: getUserMedia → OffscreenCanvas → toBlob('image/jpeg') → base64 → WS at ~10 fps - CameraToggle.tsx: header button with status colors + frame counter - SimulatorCanvas.tsx: render CameraToggle for esp32-cam boards - Esp32Bridge.ts: sendCameraAttach/Frame/Detach + chunked btoa - useSimulatorStore.ts: diagnostic log on compileBoardProgram - components-metadata.json: regen including esp32-cam component Submodule pointer: - wokwi-libs/qemu-lcgamboa → ff8eee0 (camera devices commit on davidmonterocrespo24/qemu-lcgamboa branch picsimlab-esp32) Investigation + tests in test/test-esp32-cam/: - 13 autosearch markdown docs (overview, SOTA, OV2640 spec, DVP/I2S spec, build blueprint, blockers resolved, descriptor walker fix) - 5 sketches (camera_init, sccb_probe, dma_smoke, frame_roundtrip, webcam_demo) + 8 live + WS regression tests - README with the user-facing flow .gitignore: - libqemu-*.dll.{pre-camera,new,bak} (rollback points, regenerated) - wokwi-libs/esp32-camera/ (clone consumed by arduino-esp32 path, not part of this repo) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 05:28:55 +07:00
console.log('[handleRun] QEMU path');
fix(sim): clean restart on Run after agent + display body occludes crossing wires Two issues from a real ESP32 7-segment clock the agent built. Run after the agent didn't work until a page reload --------------------------------------------------- The agent's run_simulation leaves the ESP32 board RUNNING (live QEMU WebSocket). Esp32Bridge.connect() is a no-op while the socket is non-CLOSED, so the user's subsequent Run click called startBoard() → connect() → did NOTHING. And if the backend QEMU session had since died while the frontend socket lingered (CONNECTING/OPEN/CLOSING), the user saw a dead sim that only a reload cleared — exactly the "di Run y no funcionó; recargué y sí" report. The Arduino/C++ QEMU path now stops a running board first (closing the WS), waits for it to settle, then boots fresh — the MicroPython path already did this for the same reason. Wires painted over the 7-segment digits ---------------------------------------- The agent bridges each segment strip to its resistor from a breadboard hole that is physically UNDER the seated display; on the flat canvas those wires (wire layer z 35) painted over the digits (component z 1) — "casi ni se ven los dígitos". A large-bodied display seated on a breadboard now renders ABOVE the wire layer, so its face occludes the wires crossing it exactly as the real part's body would (the wire passes behind it to reach the hole). Scoped to display bodies (7segment, matrix, oled, lcd, ili9341, led-ring…) and only when actually seated; thin parts and free-floating displays are untouched. The pin overlay + seated-pin markers share the display's stacking group, so they rise with it and wiring still works. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 11:46:57 +07:00
// Clean restart when the board is already running. Esp32Bridge.connect()
// is a no-op while the socket is non-CLOSED, so startBoard() on a live
// session does NOTHING — and if the backend QEMU session has since died
// but the frontend socket is still zombie (CONNECTING/OPEN/CLOSING), the
// user sees a dead sim that only a page reload fixes. This is the exact
// "el agente terminó, di Run y no funcionó; recargué y sí" report: the
// agent's run_simulation left the board running, so the user's Run
// no-op'd. Stop first (closes the WS), let it settle, then boot fresh —
// mirrors what the MicroPython branch above already does.
if (board?.running) {
stopBoard(activeBoardId);
await new Promise((resolve) => setTimeout(resolve, 300));
}
// QEMU-Linux boards (Raspberry Pi family + overlay piFamily kinds)
// boot straight from the rootfs — there is no firmware to compile,
// and handleCompile's Pi early-return never sets compiledProgram, so
// the gate below would surface a bogus "Compilation produced no
// firmware" error. Power the board on directly instead.
if (isPiBoardKind(board?.boardKind ?? '')) {
trackRunSimulation(board?.boardKind);
reportRun(board?.boardKind);
console.log('[handleRun] → startBoard (QEMU-Linux, no firmware)', activeBoardId);
startBoard(activeBoardId);
setMessage(null);
return;
}
if (!board?.compiledProgram || codeChangedSinceLastCompile) {
feat: ESP32-CAM emulation with webcam frame bridge First open-source end-to-end emulation of the AI-Thinker ESP32-CAM in QEMU, paired with a browser webcam → firmware bridge so users can develop camera sketches without hardware. Status: esp_camera_init() returns ESP_OK; OV2640 chip-id verifies (PID/VER/MIDH/MIDL exactly match the datasheet); GPIO 25 VSYNC NEGEDGE interrupt enabled by the upstream driver. Final piece (cam_task accepting frames) is in progress — descriptor walker fix landed in this commit. Backend (Python/FastAPI): - simulation.py: camera_attach/frame/detach WS handlers - esp32_worker.py: ctypes binding to velxio_push_camera_frame + feature-detection fallback for older DLLs - esp32_lib_manager.py: forward camera commands to the worker stdin - esp-idf-template/main/CMakeLists.txt: esp32-camera headers added via add_prebuilt_library + REQUIRES driver (resolves i2c_master_* symbols). LED_BUILTIN=2 fallback for sketches that hardcode it. Frontend (React/TS): - EditorToolbar.tsx: ESP32-CAM (and the rest of the ESP32 family) added to isQemuBoard list — Run button now starts the QEMU bridge for these boards instead of falling through to the AVR path - useWebcamFrames.ts: getUserMedia → OffscreenCanvas → toBlob('image/jpeg') → base64 → WS at ~10 fps - CameraToggle.tsx: header button with status colors + frame counter - SimulatorCanvas.tsx: render CameraToggle for esp32-cam boards - Esp32Bridge.ts: sendCameraAttach/Frame/Detach + chunked btoa - useSimulatorStore.ts: diagnostic log on compileBoardProgram - components-metadata.json: regen including esp32-cam component Submodule pointer: - wokwi-libs/qemu-lcgamboa → ff8eee0 (camera devices commit on davidmonterocrespo24/qemu-lcgamboa branch picsimlab-esp32) Investigation + tests in test/test-esp32-cam/: - 13 autosearch markdown docs (overview, SOTA, OV2640 spec, DVP/I2S spec, build blueprint, blockers resolved, descriptor walker fix) - 5 sketches (camera_init, sccb_probe, dma_smoke, frame_roundtrip, webcam_demo) + 8 live + WS regression tests - README with the user-facing flow .gitignore: - libqemu-*.dll.{pre-camera,new,bak} (rollback points, regenerated) - wokwi-libs/esp32-camera/ (clone consumed by arduino-esp32 path, not part of this repo) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 05:28:55 +07:00
console.log('[handleRun] auto-compile + run');
autoRunAfterCompile.current = true;
await handleCompile();
const updatedBoard = useSimulatorStore
.getState()
.boards.find((b) => b.id === activeBoardId);
feat: ESP32-CAM emulation with webcam frame bridge First open-source end-to-end emulation of the AI-Thinker ESP32-CAM in QEMU, paired with a browser webcam → firmware bridge so users can develop camera sketches without hardware. Status: esp_camera_init() returns ESP_OK; OV2640 chip-id verifies (PID/VER/MIDH/MIDL exactly match the datasheet); GPIO 25 VSYNC NEGEDGE interrupt enabled by the upstream driver. Final piece (cam_task accepting frames) is in progress — descriptor walker fix landed in this commit. Backend (Python/FastAPI): - simulation.py: camera_attach/frame/detach WS handlers - esp32_worker.py: ctypes binding to velxio_push_camera_frame + feature-detection fallback for older DLLs - esp32_lib_manager.py: forward camera commands to the worker stdin - esp-idf-template/main/CMakeLists.txt: esp32-camera headers added via add_prebuilt_library + REQUIRES driver (resolves i2c_master_* symbols). LED_BUILTIN=2 fallback for sketches that hardcode it. Frontend (React/TS): - EditorToolbar.tsx: ESP32-CAM (and the rest of the ESP32 family) added to isQemuBoard list — Run button now starts the QEMU bridge for these boards instead of falling through to the AVR path - useWebcamFrames.ts: getUserMedia → OffscreenCanvas → toBlob('image/jpeg') → base64 → WS at ~10 fps - CameraToggle.tsx: header button with status colors + frame counter - SimulatorCanvas.tsx: render CameraToggle for esp32-cam boards - Esp32Bridge.ts: sendCameraAttach/Frame/Detach + chunked btoa - useSimulatorStore.ts: diagnostic log on compileBoardProgram - components-metadata.json: regen including esp32-cam component Submodule pointer: - wokwi-libs/qemu-lcgamboa → ff8eee0 (camera devices commit on davidmonterocrespo24/qemu-lcgamboa branch picsimlab-esp32) Investigation + tests in test/test-esp32-cam/: - 13 autosearch markdown docs (overview, SOTA, OV2640 spec, DVP/I2S spec, build blueprint, blockers resolved, descriptor walker fix) - 5 sketches (camera_init, sccb_probe, dma_smoke, frame_roundtrip, webcam_demo) + 8 live + WS regression tests - README with the user-facing flow .gitignore: - libqemu-*.dll.{pre-camera,new,bak} (rollback points, regenerated) - wokwi-libs/esp32-camera/ (clone consumed by arduino-esp32 path, not part of this repo) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 05:28:55 +07:00
console.log('[handleRun] after compile', {
hasCompiledProgram: !!updatedBoard?.compiledProgram,
compiledProgramLen: updatedBoard?.compiledProgram?.length ?? 0,
autoRunFlag: autoRunAfterCompile.current,
});
if (autoRunAfterCompile.current) {
autoRunAfterCompile.current = false;
feat: ESP32-CAM emulation with webcam frame bridge First open-source end-to-end emulation of the AI-Thinker ESP32-CAM in QEMU, paired with a browser webcam → firmware bridge so users can develop camera sketches without hardware. Status: esp_camera_init() returns ESP_OK; OV2640 chip-id verifies (PID/VER/MIDH/MIDL exactly match the datasheet); GPIO 25 VSYNC NEGEDGE interrupt enabled by the upstream driver. Final piece (cam_task accepting frames) is in progress — descriptor walker fix landed in this commit. Backend (Python/FastAPI): - simulation.py: camera_attach/frame/detach WS handlers - esp32_worker.py: ctypes binding to velxio_push_camera_frame + feature-detection fallback for older DLLs - esp32_lib_manager.py: forward camera commands to the worker stdin - esp-idf-template/main/CMakeLists.txt: esp32-camera headers added via add_prebuilt_library + REQUIRES driver (resolves i2c_master_* symbols). LED_BUILTIN=2 fallback for sketches that hardcode it. Frontend (React/TS): - EditorToolbar.tsx: ESP32-CAM (and the rest of the ESP32 family) added to isQemuBoard list — Run button now starts the QEMU bridge for these boards instead of falling through to the AVR path - useWebcamFrames.ts: getUserMedia → OffscreenCanvas → toBlob('image/jpeg') → base64 → WS at ~10 fps - CameraToggle.tsx: header button with status colors + frame counter - SimulatorCanvas.tsx: render CameraToggle for esp32-cam boards - Esp32Bridge.ts: sendCameraAttach/Frame/Detach + chunked btoa - useSimulatorStore.ts: diagnostic log on compileBoardProgram - components-metadata.json: regen including esp32-cam component Submodule pointer: - wokwi-libs/qemu-lcgamboa → ff8eee0 (camera devices commit on davidmonterocrespo24/qemu-lcgamboa branch picsimlab-esp32) Investigation + tests in test/test-esp32-cam/: - 13 autosearch markdown docs (overview, SOTA, OV2640 spec, DVP/I2S spec, build blueprint, blockers resolved, descriptor walker fix) - 5 sketches (camera_init, sccb_probe, dma_smoke, frame_roundtrip, webcam_demo) + 8 live + WS regression tests - README with the user-facing flow .gitignore: - libqemu-*.dll.{pre-camera,new,bak} (rollback points, regenerated) - wokwi-libs/esp32-camera/ (clone consumed by arduino-esp32 path, not part of this repo) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 05:28:55 +07:00
if (updatedBoard?.compiledProgram) {
trackRunSimulation(updatedBoard.boardKind);
reportRun(updatedBoard.boardKind);
console.log('[handleRun] → startBoard', activeBoardId);
startBoard(activeBoardId);
setMessage(null);
} else {
// handleCompile returned without producing a firmware/program.
// Most common causes: arduino-cli unreachable, ESP-IDF compile
// error in the user's sketch, MicroPython firmware download
// failed, or the bridge rejected the load. handleCompile has
// already addLog'd the underlying error — surface a top-level
// toast too so the user knows their Run click didn't silently
// succeed.
const isMicropython = updatedBoard?.languageMode === 'micropython';
const errText = isMicropython
? 'MicroPython firmware did not load. Click "Load MicroPython" to retry, or check the console for the underlying error.'
: 'Compilation produced no firmware. Check the output console for the underlying error.';
feat: ESP32-CAM emulation with webcam frame bridge First open-source end-to-end emulation of the AI-Thinker ESP32-CAM in QEMU, paired with a browser webcam → firmware bridge so users can develop camera sketches without hardware. Status: esp_camera_init() returns ESP_OK; OV2640 chip-id verifies (PID/VER/MIDH/MIDL exactly match the datasheet); GPIO 25 VSYNC NEGEDGE interrupt enabled by the upstream driver. Final piece (cam_task accepting frames) is in progress — descriptor walker fix landed in this commit. Backend (Python/FastAPI): - simulation.py: camera_attach/frame/detach WS handlers - esp32_worker.py: ctypes binding to velxio_push_camera_frame + feature-detection fallback for older DLLs - esp32_lib_manager.py: forward camera commands to the worker stdin - esp-idf-template/main/CMakeLists.txt: esp32-camera headers added via add_prebuilt_library + REQUIRES driver (resolves i2c_master_* symbols). LED_BUILTIN=2 fallback for sketches that hardcode it. Frontend (React/TS): - EditorToolbar.tsx: ESP32-CAM (and the rest of the ESP32 family) added to isQemuBoard list — Run button now starts the QEMU bridge for these boards instead of falling through to the AVR path - useWebcamFrames.ts: getUserMedia → OffscreenCanvas → toBlob('image/jpeg') → base64 → WS at ~10 fps - CameraToggle.tsx: header button with status colors + frame counter - SimulatorCanvas.tsx: render CameraToggle for esp32-cam boards - Esp32Bridge.ts: sendCameraAttach/Frame/Detach + chunked btoa - useSimulatorStore.ts: diagnostic log on compileBoardProgram - components-metadata.json: regen including esp32-cam component Submodule pointer: - wokwi-libs/qemu-lcgamboa → ff8eee0 (camera devices commit on davidmonterocrespo24/qemu-lcgamboa branch picsimlab-esp32) Investigation + tests in test/test-esp32-cam/: - 13 autosearch markdown docs (overview, SOTA, OV2640 spec, DVP/I2S spec, build blueprint, blockers resolved, descriptor walker fix) - 5 sketches (camera_init, sccb_probe, dma_smoke, frame_roundtrip, webcam_demo) + 8 live + WS regression tests - README with the user-facing flow .gitignore: - libqemu-*.dll.{pre-camera,new,bak} (rollback points, regenerated) - wokwi-libs/esp32-camera/ (clone consumed by arduino-esp32 path, not part of this repo) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 05:28:55 +07:00
console.warn('[handleRun] compile finished but no compiledProgram — not starting');
setMessage({ type: 'error', text: errText });
addLog({ timestamp: new Date(), type: 'error', message: errText });
feat: ESP32-CAM emulation with webcam frame bridge First open-source end-to-end emulation of the AI-Thinker ESP32-CAM in QEMU, paired with a browser webcam → firmware bridge so users can develop camera sketches without hardware. Status: esp_camera_init() returns ESP_OK; OV2640 chip-id verifies (PID/VER/MIDH/MIDL exactly match the datasheet); GPIO 25 VSYNC NEGEDGE interrupt enabled by the upstream driver. Final piece (cam_task accepting frames) is in progress — descriptor walker fix landed in this commit. Backend (Python/FastAPI): - simulation.py: camera_attach/frame/detach WS handlers - esp32_worker.py: ctypes binding to velxio_push_camera_frame + feature-detection fallback for older DLLs - esp32_lib_manager.py: forward camera commands to the worker stdin - esp-idf-template/main/CMakeLists.txt: esp32-camera headers added via add_prebuilt_library + REQUIRES driver (resolves i2c_master_* symbols). LED_BUILTIN=2 fallback for sketches that hardcode it. Frontend (React/TS): - EditorToolbar.tsx: ESP32-CAM (and the rest of the ESP32 family) added to isQemuBoard list — Run button now starts the QEMU bridge for these boards instead of falling through to the AVR path - useWebcamFrames.ts: getUserMedia → OffscreenCanvas → toBlob('image/jpeg') → base64 → WS at ~10 fps - CameraToggle.tsx: header button with status colors + frame counter - SimulatorCanvas.tsx: render CameraToggle for esp32-cam boards - Esp32Bridge.ts: sendCameraAttach/Frame/Detach + chunked btoa - useSimulatorStore.ts: diagnostic log on compileBoardProgram - components-metadata.json: regen including esp32-cam component Submodule pointer: - wokwi-libs/qemu-lcgamboa → ff8eee0 (camera devices commit on davidmonterocrespo24/qemu-lcgamboa branch picsimlab-esp32) Investigation + tests in test/test-esp32-cam/: - 13 autosearch markdown docs (overview, SOTA, OV2640 spec, DVP/I2S spec, build blueprint, blockers resolved, descriptor walker fix) - 5 sketches (camera_init, sccb_probe, dma_smoke, frame_roundtrip, webcam_demo) + 8 live + WS regression tests - README with the user-facing flow .gitignore: - libqemu-*.dll.{pre-camera,new,bak} (rollback points, regenerated) - wokwi-libs/esp32-camera/ (clone consumed by arduino-esp32 path, not part of this repo) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 05:28:55 +07:00
}
}
return;
}
trackRunSimulation(board?.boardKind);
2026-04-26 05:46:52 +07:00
reportRun(board?.boardKind);
feat: ESP32-CAM emulation with webcam frame bridge First open-source end-to-end emulation of the AI-Thinker ESP32-CAM in QEMU, paired with a browser webcam → firmware bridge so users can develop camera sketches without hardware. Status: esp_camera_init() returns ESP_OK; OV2640 chip-id verifies (PID/VER/MIDH/MIDL exactly match the datasheet); GPIO 25 VSYNC NEGEDGE interrupt enabled by the upstream driver. Final piece (cam_task accepting frames) is in progress — descriptor walker fix landed in this commit. Backend (Python/FastAPI): - simulation.py: camera_attach/frame/detach WS handlers - esp32_worker.py: ctypes binding to velxio_push_camera_frame + feature-detection fallback for older DLLs - esp32_lib_manager.py: forward camera commands to the worker stdin - esp-idf-template/main/CMakeLists.txt: esp32-camera headers added via add_prebuilt_library + REQUIRES driver (resolves i2c_master_* symbols). LED_BUILTIN=2 fallback for sketches that hardcode it. Frontend (React/TS): - EditorToolbar.tsx: ESP32-CAM (and the rest of the ESP32 family) added to isQemuBoard list — Run button now starts the QEMU bridge for these boards instead of falling through to the AVR path - useWebcamFrames.ts: getUserMedia → OffscreenCanvas → toBlob('image/jpeg') → base64 → WS at ~10 fps - CameraToggle.tsx: header button with status colors + frame counter - SimulatorCanvas.tsx: render CameraToggle for esp32-cam boards - Esp32Bridge.ts: sendCameraAttach/Frame/Detach + chunked btoa - useSimulatorStore.ts: diagnostic log on compileBoardProgram - components-metadata.json: regen including esp32-cam component Submodule pointer: - wokwi-libs/qemu-lcgamboa → ff8eee0 (camera devices commit on davidmonterocrespo24/qemu-lcgamboa branch picsimlab-esp32) Investigation + tests in test/test-esp32-cam/: - 13 autosearch markdown docs (overview, SOTA, OV2640 spec, DVP/I2S spec, build blueprint, blockers resolved, descriptor walker fix) - 5 sketches (camera_init, sccb_probe, dma_smoke, frame_roundtrip, webcam_demo) + 8 live + WS regression tests - README with the user-facing flow .gitignore: - libqemu-*.dll.{pre-camera,new,bak} (rollback points, regenerated) - wokwi-libs/esp32-camera/ (clone consumed by arduino-esp32 path, not part of this repo) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 05:28:55 +07:00
console.log('[handleRun] → startBoard (already compiled)', activeBoardId);
startBoard(activeBoardId);
setMessage(null);
return;
}
// Auto-compile if no program or code changed since last compile
if (!board?.compiledProgram || codeChangedSinceLastCompile) {
autoRunAfterCompile.current = true;
await handleCompile();
// After compile, check if it succeeded and run
const updatedBoard = useSimulatorStore
.getState()
.boards.find((b) => b.id === activeBoardId);
if (autoRunAfterCompile.current && updatedBoard?.compiledProgram) {
autoRunAfterCompile.current = false;
trackRunSimulation(updatedBoard.boardKind);
2026-04-26 05:46:52 +07:00
reportRun(updatedBoard.boardKind);
startBoard(activeBoardId);
setMessage(null);
} else {
autoRunAfterCompile.current = false;
}
return;
}
trackRunSimulation(board?.boardKind);
2026-04-26 05:46:52 +07:00
reportRun(board?.boardKind);
startBoard(activeBoardId);
setMessage(null);
return;
}
// Legacy fallback
if (!compiledHex || codeChangedSinceLastCompile) {
autoRunAfterCompile.current = true;
await handleCompile();
const hex = useSimulatorStore.getState().compiledHex;
if (autoRunAfterCompile.current && hex) {
autoRunAfterCompile.current = false;
trackRunSimulation();
2026-04-26 05:46:52 +07:00
reportRun(undefined);
startSimulation();
setMessage(null);
} else {
autoRunAfterCompile.current = false;
}
} else {
trackRunSimulation();
2026-04-26 05:46:52 +07:00
reportRun(undefined);
startSimulation();
setMessage(null);
}
};
const handleStop = () => {
trackStopSimulation();
if (isBoardless) {
feat(sim): Stop halts custom chips + LEDs go dark; retro chip examples go board-less Phase 1 of the run-system/UX work. Stop bug: a programmable chip kept running after Stop when a board was present. The chip rAF tick gated only on board presence (!boardless), so with a board it ticked forever. Now it gates on the actual run state: board-less -> electrical paused flag; with board(s) -> board.running. handleStop also clears every chip's output drives (clearAllChipDrives) and re-solves so chip-driven LEDs go dark on Stop instead of freezing at their last frame. Examples to board-less (regulated power supply, no Arduino — the Arduino only ever supplied 5V): - z80-larson-scanner -> 'Z80 Comet Scanner': board-less, a faster TWO-LED comet (scanner.s) so it's visually distinct from z80-larson-no-board's single-bit walk; green/blue LEDs. - i8080-killbits -> board-less (psu + resistors), keeps killbits.s as the chip's editable program; buttons re-powered from the supply. - i8080-button-counter -> board-less (psu + resistors); behaviour chip, program baked in, so it shows a note (no editable file) and runs standalone. banner-streamer stays Arduino-based (its TX/RX go through the AVR USART bridge). - CustomChipPart: run-state-aware tick gate. - EditorToolbar: clearAllChipDrives() helper + handleStop clears chip drives. - examples-retro-intel: 3 conversions; drop now-unused sketch consts; add the larsonScannerAsm comet program. - Tests: board+chip routing now uses an inline synthetic example (gallery chip examples are all board-less). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:32:35 +07:00
// Freeze the chip tick (the paused flag) AND clear the chip's output
// drives so its LEDs go dark on Stop — not frozen at their last frame.
setElectricalPaused(true);
feat(sim): Stop halts custom chips + LEDs go dark; retro chip examples go board-less Phase 1 of the run-system/UX work. Stop bug: a programmable chip kept running after Stop when a board was present. The chip rAF tick gated only on board presence (!boardless), so with a board it ticked forever. Now it gates on the actual run state: board-less -> electrical paused flag; with board(s) -> board.running. handleStop also clears every chip's output drives (clearAllChipDrives) and re-solves so chip-driven LEDs go dark on Stop instead of freezing at their last frame. Examples to board-less (regulated power supply, no Arduino — the Arduino only ever supplied 5V): - z80-larson-scanner -> 'Z80 Comet Scanner': board-less, a faster TWO-LED comet (scanner.s) so it's visually distinct from z80-larson-no-board's single-bit walk; green/blue LEDs. - i8080-killbits -> board-less (psu + resistors), keeps killbits.s as the chip's editable program; buttons re-powered from the supply. - i8080-button-counter -> board-less (psu + resistors); behaviour chip, program baked in, so it shows a note (no editable file) and runs standalone. banner-streamer stays Arduino-based (its TX/RX go through the AVR USART bridge). - CustomChipPart: run-state-aware tick gate. - EditorToolbar: clearAllChipDrives() helper + handleStop clears chip drives. - examples-retro-intel: 3 conversions; drop now-unused sketch consts; add the larsonScannerAsm comet program. - Tests: board+chip routing now uses an inline synthetic example (gallery chip examples are all board-less). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:32:35 +07:00
clearAllChipDrives();
setMessage(null);
return;
}
// Stop EVERY running board — Run-All can start several, and leaving any
// running keeps chips ticking (their gate is boards.some(running)).
const runningBoards = useSimulatorStore.getState().boards.filter((b) => b.running);
if (runningBoards.length > 0) runningBoards.forEach((b) => stopBoard(b.id));
else if (activeBoardId) stopBoard(activeBoardId);
else stopSimulation();
feat(sim): Stop halts custom chips + LEDs go dark; retro chip examples go board-less Phase 1 of the run-system/UX work. Stop bug: a programmable chip kept running after Stop when a board was present. The chip rAF tick gated only on board presence (!boardless), so with a board it ticked forever. Now it gates on the actual run state: board-less -> electrical paused flag; with board(s) -> board.running. handleStop also clears every chip's output drives (clearAllChipDrives) and re-solves so chip-driven LEDs go dark on Stop instead of freezing at their last frame. Examples to board-less (regulated power supply, no Arduino — the Arduino only ever supplied 5V): - z80-larson-scanner -> 'Z80 Comet Scanner': board-less, a faster TWO-LED comet (scanner.s) so it's visually distinct from z80-larson-no-board's single-bit walk; green/blue LEDs. - i8080-killbits -> board-less (psu + resistors), keeps killbits.s as the chip's editable program; buttons re-powered from the supply. - i8080-button-counter -> board-less (psu + resistors); behaviour chip, program baked in, so it shows a note (no editable file) and runs standalone. banner-streamer stays Arduino-based (its TX/RX go through the AVR USART bridge). - CustomChipPart: run-state-aware tick gate. - EditorToolbar: clearAllChipDrives() helper + handleStop clears chip drives. - examples-retro-intel: 3 conversions; drop now-unused sketch consts; add the larsonScannerAsm comet program. - Tests: board+chip routing now uses an inline synthetic example (gallery chip examples are all board-less). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:32:35 +07:00
// A chip wired to a board drives its LEDs via its own SPICE sources, which
// stopBoard doesn't touch — clear them so those LEDs also go dark.
clearAllChipDrives();
setMessage(null);
};
const handleReset = () => {
trackResetSimulation();
if (activeBoardId) resetBoard(activeBoardId);
else resetSimulation();
setMessage(null);
};
/**
* Compile every board on the canvas sequentially. Progress + per-board
* results stream to the existing compilation console no separate dialog.
* Returns the count of boards that ended up with a runnable program (so
* Run All can use it to decide whether to proceed to start them).
*/
const compileAllBoards = async (): Promise<{ ok: number; failed: number }> => {
const boardsList = useSimulatorStore.getState().boards;
// Every custom-chip is a target too — Compile-All / Run-All build chips
// (WASM + ROM) alongside boards, so the flow works for a board + chip, for
// several chips with no board, etc.
const allCustomChips = useSimulatorStore
.getState()
.components.filter((c) => c.metadataId === 'custom-chip');
if (boardsList.length === 0 && allCustomChips.length === 0) return { ok: 0, failed: 0 };
setCompileAllRunning(true);
setConsoleOpen(true);
const targetSummary = [
boardsList.length ? `${boardsList.length} board${boardsList.length === 1 ? '' : 's'}` : '',
allCustomChips.length ? `${allCustomChips.length} chip${allCustomChips.length === 1 ? '' : 's'}` : '',
]
.filter(Boolean)
.join(' + ');
addLog({
timestamp: new Date(),
type: 'info',
message: `Compiling all targets (${targetSummary})...`,
});
// 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 chipProgramFiles = new Set<string>();
for (const chip of allCustomChips) {
const pf = String((chip.properties as any)?.programFile ?? '').trim();
if (pf) chipProgramFiles.add(pf);
}
let chipFailed = 0;
if (allCustomChips.length > 0) {
const everyFile = boardsList.flatMap((b) =>
useEditorStore.getState().getGroupFiles(b.activeFileGroupId),
);
chipFailed = (await prepareCustomChips(allCustomChips, everyFile)).failed;
}
let ok = 0;
let boardFailed = 0;
for (const board of boardsList) {
feat(editor): rename boards & custom chips; show which target owns each file Phase 2 of the run-system/UX work. - BoardInstance gains an optional user ; boardDisplayName(board) resolver (name || kind label) routes every INSTANCE-label surface: file-explorer section header, compile console (EditorToolbar), canvas selector/tooltip/ context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options subtitle. Board/component pickers keep the KIND label (they pick new boards). - Inline rename on board AND chip section headers (double-click the name, or a hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in properties. Enter commits, Escape cancels (cancel-flag ref guards the unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'. - FileTabs shows an owner badge naming the board/chip whose files are shown (resolved as a selector so it doesn't re-render on every sim pin toggle). - CustomChipDialog no longer clobbers a user-given chipName: chip.json's name only seeds the blank defaults (My Chip / Custom Chip); loading an example relabels explicitly. - Persistence: board name round-trips via projectPayload (+ dirty hash), vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json. - Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal (early return before a useCallback). Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness); all major findings folded in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:22:25 +07:00
const label = boardDisplayName(board);
// Stamp this board's lines so the console groups them under its section.
const boardTarget: CompileTarget = { id: board.id, label, kind: 'board' };
const blog = (type: CompilationLog['type'], message: string) =>
addLog({ timestamp: new Date(), type, message, target: boardTarget });
feat(boards): expose Raspberry Pi 4 and Pi 5 in the picker (UI + pin wiring) The BoardKind type and the QEMU backend already supported raspberry-pi-4 (Cortex-A72) and raspberry-pi-5 (Cortex-A76) by reusing the Pi 3 arm64 image set, but the frontend had no way to actually select either: the board picker, the canvas renderer, the serial monitor, the oscilloscope channel list, and the editor toolbar all hard-coded "raspberry-pi-3" as the only Pi entry. ComponentRegistry even registered Pi 4 / Pi 5 metadata pointing at the velxio-raspberry-pi-3 custom-element tag — a placeholder that meant both boards rendered as a Pi 3 in the picker thumbnail and on the canvas. Add dedicated boards top-to-bottom: * `RaspberryPi4Element.ts` / `RaspberryPi5Element.ts` — Velxio-style schematic SVG (authored from scratch, not traced). Pi 4 is the green PCB with BCM2711 SoC, 4× USB-A, USB-C power, dual µHDMI; Pi 5 is the darker green PCB with BCM2712 + RP1 southbridge, 2.5 GbE, USB-C 5V/5A, PCIe FFC connector, dedicated power button. Both carry a small "velxio" mark in the corner. * `pi40PinHeader.ts` — shared `buildPi40PinHeader()` helper that returns the 40-pin BCM layout. Every Pi from the 1B+ onwards uses the same physical pin positions and same BCM GPIO assignment, so Pi 3 / Pi 4 / Pi 5 elements all consume this helper and example wires drawn against one model transfer to the others without re-routing. * React wrappers `RaspberryPi4.tsx` / `RaspberryPi5.tsx` render the custom elements at absolute positions (mirrors how RaspberryPi3.tsx handles the Pi 3 illustration). * Wire-up across the editor surface: - BoardOnCanvas: BOARD_SIZE entry + switch case. - BoardPickerModal: description, icon, kinds list. - ComponentPickerModal: thumbnails now instantiate the dedicated custom element (was velxio-raspberry-pi-3 fallback). - SerialMonitor / EditorToolbar: pill labels, icons, colours. - Oscilloscope: GPIO channel list (28 BCM pins). - SimulatorCanvas: remote-boards filter for run/stop sync. - SPICE boardPinGroups: same 5V / 3V3 / GND as Pi 3. - boardPinToNumber: accepts physical pin numbers ("1"-"40"), BCM names ("GPIO14") and power labels for any Pi 3/4/5 id. - ComponentRegistry: dedicated tagNames + per-board thumbnails (green for Pi 4, darker green for Pi 5). * EditorToolbar's Pi 3 special cases (Linux/Python compile path, Run/Stop routing) now use `isPiBoardKind()` so Pi 4 and Pi 5 inherit the same behaviour automatically, and any future Pi family member (Zero / 1 / 2) lands in the right code paths the moment its backend boots. QEMU backend was already wired (qemu_manager.py:71/82 + manifest entry 'raspberry-pi-3-virt' shared across arm64 Pis), so this commit makes both boards selectable end-to-end without any backend follow-up.
2026-05-23 09:46:59 +07:00
if (isPiBoardKind(board.boardKind)) {
blog('info', 'skipped (no compilation needed)');
ok++;
continue;
}
const fqbn = BOARD_KIND_FQBN[board.boardKind];
if (!fqbn) {
blog('error', 'no FQBN configured');
boardFailed++;
continue;
}
blog('info', 'compiling...');
try {
const groupFiles = useEditorStore.getState().getGroupFiles(board.activeFileGroupId);
const sketchFiles = groupFiles
.filter((f) => !chipProgramFiles.has(f.name) && !isChipProgramFile(f.name))
.map((f) => ({ name: f.name, content: f.content }));
feat(compile): stream live ESP-IDF cmake + ninja output to the console A user reported on Discord: "the Velxio Console doesn't update anything, it just waits until the very end and displays everything in one go". True for the async compile path — /compile/status only carried `state` and the final `result`, so the editor's CompilationConsole stayed empty during the 5-7 minute cold ESP-IDF builds and dumped 1500 lines at once when the build finished. This wires live build output through the whole stack. Backend (espidf_compiler.py) - New _run_with_streaming() helper. When a progress_callback is provided it spawns the subprocess via Popen + stdout/stderr drain threads and invokes the callback line-by-line. When None it falls back to the existing subprocess.run(capture_output=True) one-shot path so the unit-test code that doesn't care about live output is unaffected. - compile() and _compile_in_dir() take an optional ProgressCallback. - _run_cmake / _run_ninja closures now go through _run_with_streaming with that callback. cmake configure (~2-5 s) + ninja (~5-300+ s) both stream now; the ninja output is the one users actually want to watch. Backend (compile.py) - _compile_job seeds COMPILE_JOBS[id]['stdout_buffer'] = '' and defines on_progress_line(line) which appends to it. Buffer capped at 256 KB (tail kept) so a runaway build can't OOM the FastAPI process. - The buffer is preserved on both the success and the error path so late polls still see the log even after state transitions to done/error. - /compile/status now returns the buffer as a `stdout` field. CompileStatusResponse gains the field with default '' so old clients that don't read it still work. Frontend (compilation.ts) - compileCode() takes a 4th argument: optional CompileProgress callback fired every poll while state ∈ {pending, running}. Carries the cumulative stdout (caller computes deltas) plus elapsed seconds. - Surfaces the new `stdout` field of /compile/status and forwards it to the callback. Errors thrown from the callback are swallowed — a faulty UI hook must never break the polling loop. Frontend (EditorToolbar.tsx) - Both compileCode() call sites (Run and Compile-All) now pass an onProgress callback. It tracks `lastStreamedLen` per-compile, splits each new delta on newlines, and appends them as `info`-typed CompilationLog entries via setCompileLogs. The Compile-All flow prefixes each line with the board label so multi-board builds stay readable. - After the build settles, the existing parseCompileResult call still runs and appends the structured analysis on top of the live stream — that's where FAILED-block detection + the `error`-typed entries that drive the auto-switch-to-errors filter live. Net effect on the user complaint: cold ESP-IDF builds now show the ninja [N/1483] progress lines streaming into the console as they happen, instead of staring at an empty panel for 5-7 minutes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 04:36:58 +07:00
// Stream live cmake + ninja output per-board (Compile-All flow).
let lastStreamedLen = 0;
const result = await compileCode(
sketchFiles,
fqbn,
currentProject?.id ?? null,
({ stdout }) => {
if (stdout.length <= lastStreamedLen) return;
const delta = stdout.slice(lastStreamedLen);
lastStreamedLen = stdout.length;
const newLines = delta.split('\n').filter((s) => s.trim());
if (!newLines.length) return;
const now = new Date();
setCompileLogs((prev: CompilationLog[]) => [
...prev,
// No `${label}: ` prefix — the target section header carries it.
feat(compile): stream live ESP-IDF cmake + ninja output to the console A user reported on Discord: "the Velxio Console doesn't update anything, it just waits until the very end and displays everything in one go". True for the async compile path — /compile/status only carried `state` and the final `result`, so the editor's CompilationConsole stayed empty during the 5-7 minute cold ESP-IDF builds and dumped 1500 lines at once when the build finished. This wires live build output through the whole stack. Backend (espidf_compiler.py) - New _run_with_streaming() helper. When a progress_callback is provided it spawns the subprocess via Popen + stdout/stderr drain threads and invokes the callback line-by-line. When None it falls back to the existing subprocess.run(capture_output=True) one-shot path so the unit-test code that doesn't care about live output is unaffected. - compile() and _compile_in_dir() take an optional ProgressCallback. - _run_cmake / _run_ninja closures now go through _run_with_streaming with that callback. cmake configure (~2-5 s) + ninja (~5-300+ s) both stream now; the ninja output is the one users actually want to watch. Backend (compile.py) - _compile_job seeds COMPILE_JOBS[id]['stdout_buffer'] = '' and defines on_progress_line(line) which appends to it. Buffer capped at 256 KB (tail kept) so a runaway build can't OOM the FastAPI process. - The buffer is preserved on both the success and the error path so late polls still see the log even after state transitions to done/error. - /compile/status now returns the buffer as a `stdout` field. CompileStatusResponse gains the field with default '' so old clients that don't read it still work. Frontend (compilation.ts) - compileCode() takes a 4th argument: optional CompileProgress callback fired every poll while state ∈ {pending, running}. Carries the cumulative stdout (caller computes deltas) plus elapsed seconds. - Surfaces the new `stdout` field of /compile/status and forwards it to the callback. Errors thrown from the callback are swallowed — a faulty UI hook must never break the polling loop. Frontend (EditorToolbar.tsx) - Both compileCode() call sites (Run and Compile-All) now pass an onProgress callback. It tracks `lastStreamedLen` per-compile, splits each new delta on newlines, and appends them as `info`-typed CompilationLog entries via setCompileLogs. The Compile-All flow prefixes each line with the board label so multi-board builds stay readable. - After the build settles, the existing parseCompileResult call still runs and appends the structured analysis on top of the live stream — that's where FAILED-block detection + the `error`-typed entries that drive the auto-switch-to-errors filter live. Net effect on the user complaint: cold ESP-IDF builds now show the ninja [N/1483] progress lines streaming into the console as they happen, instead of staring at an empty panel for 5-7 minutes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 04:36:58 +07:00
...newLines.map((line) => ({
timestamp: now,
type: 'info' as const,
message: line,
target: boardTarget,
feat(compile): stream live ESP-IDF cmake + ninja output to the console A user reported on Discord: "the Velxio Console doesn't update anything, it just waits until the very end and displays everything in one go". True for the async compile path — /compile/status only carried `state` and the final `result`, so the editor's CompilationConsole stayed empty during the 5-7 minute cold ESP-IDF builds and dumped 1500 lines at once when the build finished. This wires live build output through the whole stack. Backend (espidf_compiler.py) - New _run_with_streaming() helper. When a progress_callback is provided it spawns the subprocess via Popen + stdout/stderr drain threads and invokes the callback line-by-line. When None it falls back to the existing subprocess.run(capture_output=True) one-shot path so the unit-test code that doesn't care about live output is unaffected. - compile() and _compile_in_dir() take an optional ProgressCallback. - _run_cmake / _run_ninja closures now go through _run_with_streaming with that callback. cmake configure (~2-5 s) + ninja (~5-300+ s) both stream now; the ninja output is the one users actually want to watch. Backend (compile.py) - _compile_job seeds COMPILE_JOBS[id]['stdout_buffer'] = '' and defines on_progress_line(line) which appends to it. Buffer capped at 256 KB (tail kept) so a runaway build can't OOM the FastAPI process. - The buffer is preserved on both the success and the error path so late polls still see the log even after state transitions to done/error. - /compile/status now returns the buffer as a `stdout` field. CompileStatusResponse gains the field with default '' so old clients that don't read it still work. Frontend (compilation.ts) - compileCode() takes a 4th argument: optional CompileProgress callback fired every poll while state ∈ {pending, running}. Carries the cumulative stdout (caller computes deltas) plus elapsed seconds. - Surfaces the new `stdout` field of /compile/status and forwards it to the callback. Errors thrown from the callback are swallowed — a faulty UI hook must never break the polling loop. Frontend (EditorToolbar.tsx) - Both compileCode() call sites (Run and Compile-All) now pass an onProgress callback. It tracks `lastStreamedLen` per-compile, splits each new delta on newlines, and appends them as `info`-typed CompilationLog entries via setCompileLogs. The Compile-All flow prefixes each line with the board label so multi-board builds stay readable. - After the build settles, the existing parseCompileResult call still runs and appends the structured analysis on top of the live stream — that's where FAILED-block detection + the `error`-typed entries that drive the auto-switch-to-errors filter live. Net effect on the user complaint: cold ESP-IDF builds now show the ninja [N/1483] progress lines streaming into the console as they happen, instead of staring at an empty panel for 5-7 minutes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 04:36:58 +07:00
})),
]);
},
feat(esp32): pure ESP-IDF language mode for the ESP32 family (#139) Adds a third entry to the board language selector next to Arduino C++ and MicroPython: ESP-IDF. In this mode the user writes a plain ESP-IDF project — app_main() entry point, FreeRTOS + driver APIs — and the backend compiles it through the same ESP-IDF toolchain it already uses for ESP32 Arduino sketches, just without the arduino-esp32 component. Backend: - CompileRequest.language ('espidf') threaded through the sync + async compile paths and folded into the dedup job key (language='arduino' and omitted hash identically so old clients keep dedupping). - espidf_compiler: pure_idf flag. User files are written into main/ as-is (no Arduino.h wrap, no velxio_compat.h, Arduino library resolution skipped), ARDUINO_ESP32_PATH is dropped from the build env and VELXIO_PURE_SKETCH raised so the template CMake compiles the user's own sources via a glob branch. Pure builds get their own persistent build-dir variant through the eff_hash fold. - QEMU WiFi compat for IDF-style code: esp_wifi.h/esp_wifi_init detection sets has_wifi, and literal #define SSID/PASS plus wifi_config_t designated initializers are normalized to the QEMU AP. - CONFIG_ARDUINO_* lines are stripped from sdkconfig.defaults in pure mode (the symbols don't exist without the arduino component). Frontend: - LanguageMode gains 'espidf'; BOARD_SUPPORTS_ESPIDF covers the ESP32 family (Xtensa, S3, C3). Toolbar shows the option only for those. - Switching modes seeds a main.c blink skeleton (app_main + gpio driver), mirroring the MicroPython main.py flow. - compileCode sends language='espidf'; run/stop paths are unchanged (the QEMU worker consumes the same merged flash image). - New gallery example: esp32-idf-blink (LED + resistor on GPIO 2). Tests: unit coverage for the build-env switch, IDF wifi normalization, job-key variance, file-group seeding and the new example; verified end-to-end in a container from the prod image (pure build produces a bootable flash image; Arduino-mode build unchanged, same variant hash).
2026-07-24 11:37:01 +07:00
{ boardOptions: board.boardOptions, spiffsFiles: board.spiffsFiles, libraries: board.libraries?.length ? board.libraries : null, language: board.languageMode === 'espidf' ? 'espidf' : undefined },
feat(compile): stream live ESP-IDF cmake + ninja output to the console A user reported on Discord: "the Velxio Console doesn't update anything, it just waits until the very end and displays everything in one go". True for the async compile path — /compile/status only carried `state` and the final `result`, so the editor's CompilationConsole stayed empty during the 5-7 minute cold ESP-IDF builds and dumped 1500 lines at once when the build finished. This wires live build output through the whole stack. Backend (espidf_compiler.py) - New _run_with_streaming() helper. When a progress_callback is provided it spawns the subprocess via Popen + stdout/stderr drain threads and invokes the callback line-by-line. When None it falls back to the existing subprocess.run(capture_output=True) one-shot path so the unit-test code that doesn't care about live output is unaffected. - compile() and _compile_in_dir() take an optional ProgressCallback. - _run_cmake / _run_ninja closures now go through _run_with_streaming with that callback. cmake configure (~2-5 s) + ninja (~5-300+ s) both stream now; the ninja output is the one users actually want to watch. Backend (compile.py) - _compile_job seeds COMPILE_JOBS[id]['stdout_buffer'] = '' and defines on_progress_line(line) which appends to it. Buffer capped at 256 KB (tail kept) so a runaway build can't OOM the FastAPI process. - The buffer is preserved on both the success and the error path so late polls still see the log even after state transitions to done/error. - /compile/status now returns the buffer as a `stdout` field. CompileStatusResponse gains the field with default '' so old clients that don't read it still work. Frontend (compilation.ts) - compileCode() takes a 4th argument: optional CompileProgress callback fired every poll while state ∈ {pending, running}. Carries the cumulative stdout (caller computes deltas) plus elapsed seconds. - Surfaces the new `stdout` field of /compile/status and forwards it to the callback. Errors thrown from the callback are swallowed — a faulty UI hook must never break the polling loop. Frontend (EditorToolbar.tsx) - Both compileCode() call sites (Run and Compile-All) now pass an onProgress callback. It tracks `lastStreamedLen` per-compile, splits each new delta on newlines, and appends them as `info`-typed CompilationLog entries via setCompileLogs. The Compile-All flow prefixes each line with the board label so multi-board builds stay readable. - After the build settles, the existing parseCompileResult call still runs and appends the structured analysis on top of the live stream — that's where FAILED-block detection + the `error`-typed entries that drive the auto-switch-to-errors filter live. Net effect on the user complaint: cold ESP-IDF builds now show the ninja [N/1483] progress lines streaming into the console as they happen, instead of staring at an empty panel for 5-7 minutes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 04:36:58 +07:00
);
const resultLogs = parseCompileResult(result, label, boardTarget);
setCompileLogs((prev: CompilationLog[]) => [...prev, ...resultLogs]);
if (result.success) {
const program = result.hex_content ?? result.binary_content ?? null;
if (program) {
compileBoardProgram(board.id, program);
if (result.has_wifi !== undefined) {
updateBoard(board.id, { hasWifi: result.has_wifi });
}
}
ok++;
} else {
boardFailed++;
}
} catch (err) {
blog('error', err instanceof Error ? err.message : String(err));
boardFailed++;
}
}
const failed = boardFailed + chipFailed;
const chipOk = allCustomChips.length - chipFailed;
const doneParts = [];
if (boardsList.length)
doneParts.push(`${ok} board${ok === 1 ? '' : 's'} ok${boardFailed > 0 ? `, ${boardFailed} failed` : ''}`);
if (allCustomChips.length)
doneParts.push(`${chipOk} chip${chipOk === 1 ? '' : 's'} ok${chipFailed > 0 ? `, ${chipFailed} failed` : ''}`);
addLog({
timestamp: new Date(),
type: failed > 0 ? 'error' : 'success',
message: `Done — ${doneParts.join('; ')}`,
});
if (failed === 0) markCompiled();
setCompileAllRunning(false);
return { ok, failed };
};
const handleCompileAll = () => {
trackCompileCode();
void compileAllBoards();
};
/**
* Run All = compile every target (boards + chips) if needed, then start every
* one: boards via startBoard, chips via restartParts (re-attach with the
* fresh WASM/ROM) + resuming the electrical solver when there's no board.
* Mirrors single Run, generalised across all targets.
*/
const handleRunAll = async (skipVerify = false) => {
const sim = useSimulatorStore.getState();
const boardsList = sim.boards;
const chips = sim.components.filter((c) => c.metadataId === 'custom-chip');
if (boardsList.length === 0 && chips.length === 0) return;
// Same pre-flight safety check as handleRun — block on shorts / overcurrent
// before starting every board, with a "Run anyway" escape.
if (!skipVerify) {
const ok = await checkOrBlock(() => handleRunAll(true));
if (!ok) return;
}
// A chip needs compiling when it has no WASM yet, or it references a program
// file but hasn't been assembled to ROM.
const chipNeedsCompile = chips.some((c) => {
const p = c.properties as Record<string, unknown>;
const programFile = String(p?.programFile ?? '').trim();
return !String(p?.wasmBase64 ?? '') || (programFile && !String(p?.romBytes ?? ''));
});
const needsCompile =
codeChangedSinceLastCompile ||
chipNeedsCompile ||
boardsList.some(
(b) =>
feat(boards): expose Raspberry Pi 4 and Pi 5 in the picker (UI + pin wiring) The BoardKind type and the QEMU backend already supported raspberry-pi-4 (Cortex-A72) and raspberry-pi-5 (Cortex-A76) by reusing the Pi 3 arm64 image set, but the frontend had no way to actually select either: the board picker, the canvas renderer, the serial monitor, the oscilloscope channel list, and the editor toolbar all hard-coded "raspberry-pi-3" as the only Pi entry. ComponentRegistry even registered Pi 4 / Pi 5 metadata pointing at the velxio-raspberry-pi-3 custom-element tag — a placeholder that meant both boards rendered as a Pi 3 in the picker thumbnail and on the canvas. Add dedicated boards top-to-bottom: * `RaspberryPi4Element.ts` / `RaspberryPi5Element.ts` — Velxio-style schematic SVG (authored from scratch, not traced). Pi 4 is the green PCB with BCM2711 SoC, 4× USB-A, USB-C power, dual µHDMI; Pi 5 is the darker green PCB with BCM2712 + RP1 southbridge, 2.5 GbE, USB-C 5V/5A, PCIe FFC connector, dedicated power button. Both carry a small "velxio" mark in the corner. * `pi40PinHeader.ts` — shared `buildPi40PinHeader()` helper that returns the 40-pin BCM layout. Every Pi from the 1B+ onwards uses the same physical pin positions and same BCM GPIO assignment, so Pi 3 / Pi 4 / Pi 5 elements all consume this helper and example wires drawn against one model transfer to the others without re-routing. * React wrappers `RaspberryPi4.tsx` / `RaspberryPi5.tsx` render the custom elements at absolute positions (mirrors how RaspberryPi3.tsx handles the Pi 3 illustration). * Wire-up across the editor surface: - BoardOnCanvas: BOARD_SIZE entry + switch case. - BoardPickerModal: description, icon, kinds list. - ComponentPickerModal: thumbnails now instantiate the dedicated custom element (was velxio-raspberry-pi-3 fallback). - SerialMonitor / EditorToolbar: pill labels, icons, colours. - Oscilloscope: GPIO channel list (28 BCM pins). - SimulatorCanvas: remote-boards filter for run/stop sync. - SPICE boardPinGroups: same 5V / 3V3 / GND as Pi 3. - boardPinToNumber: accepts physical pin numbers ("1"-"40"), BCM names ("GPIO14") and power labels for any Pi 3/4/5 id. - ComponentRegistry: dedicated tagNames + per-board thumbnails (green for Pi 4, darker green for Pi 5). * EditorToolbar's Pi 3 special cases (Linux/Python compile path, Run/Stop routing) now use `isPiBoardKind()` so Pi 4 and Pi 5 inherit the same behaviour automatically, and any future Pi family member (Zero / 1 / 2) lands in the right code paths the moment its backend boots. QEMU backend was already wired (qemu_manager.py:71/82 + manifest entry 'raspberry-pi-3-virt' shared across arm64 Pis), so this commit makes both boards selectable end-to-end without any backend follow-up.
2026-05-23 09:46:59 +07:00
!isPiBoardKind(b.boardKind) &&
b.languageMode !== 'micropython' &&
!b.compiledProgram,
);
if (needsCompile) {
const { failed } = await compileAllBoards();
if (failed > 0) return; // a board failed — don't start anything
}
// Start every board (compiledProgram may have changed during compile).
const refreshed = useSimulatorStore.getState().boards;
for (const board of refreshed) {
if (board.running) continue;
if (isQemuBoardKind(board.boardKind) || board.compiledProgram || board.languageMode === 'micropython') {
trackRunSimulation(board.boardKind);
2026-04-26 05:46:52 +07:00
reportRun(board.boardKind);
startBoard(board.id);
}
}
// Run the chips: re-attach so they pick up the freshly compiled WASM/ROM.
// The chip tick gates on a running board, so when NO board actually started
// (board-less, or a board that compiled to nothing) resume the electrical
// solver instead, otherwise the chips would stay frozen.
if (chips.length > 0) {
useSimulatorStore.getState().restartParts();
const anyBoardRunning = useSimulatorStore.getState().boards.some((b) => b.running);
if (!anyBoardRunning) setElectricalPaused(false);
}
};
const handleExport = async () => {
try {
const {
components,
wires,
boardPosition,
boardType: legacyBoardType,
} = useSimulatorStore.getState();
const projectName =
files.find((f) => f.name.endsWith('.ino'))?.name.replace('.ino', '') || 'velxio-project';
await exportToWokwiZip(files, components, wires, legacyBoardType, projectName, boardPosition);
} catch (err) {
setMessage({ type: 'error', text: 'Export failed.' });
}
};
// Phase 3 D3.2 — Schematic screenshot. Pro-tier-gated by the backend.
// Same UX pattern as BOM export: everyone can click; 402 redirects to
// /pricing. The server-side headless chromium renders the canvas and
// returns a PNG, which we trigger a download for.
const handleExportScreenshot = async () => {
const projectId = currentProject?.id;
if (!projectId) {
setMessage({ type: 'error', text: 'Save the project before exporting an image.' });
return;
}
setMessage({ type: 'info', text: 'Rendering screenshot — may take 5-10 seconds…' });
try {
const resp = await fetch(`/api/pro/projects/${projectId}/screenshot.png`, {
credentials: 'include',
});
if (resp.status === 402) {
// Fire the in-place upgrade modal instead of bouncing to /pricing —
// keeps the user in the editor with full context. The pro overlay's
// UpgradeGate listens for this event and opens UpgradePromptModal.
window.dispatchEvent(new CustomEvent('velxio-pro-upgrade-prompt', {
detail: { componentName: 'Schematic screenshot export' },
}));
return;
}
if (resp.status === 401) {
window.location.href = `/login?redirect=${encodeURIComponent(window.location.pathname)}`;
return;
}
if (resp.status === 422) {
setMessage({ type: 'error', text: 'Add at least one component to export an image.' });
return;
}
if (!resp.ok) {
setMessage({ type: 'error', text: 'Screenshot export failed.' });
return;
}
const blob = await resp.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const cd = resp.headers.get('Content-Disposition') || '';
const m = /filename="?([^"]+)"?/.exec(cd);
a.download = m ? m[1] : `velxio-${projectId}.png`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
setMessage({ type: 'success', text: 'Screenshot downloaded.' });
} catch {
setMessage({ type: 'error', text: 'Screenshot export failed.' });
}
};
// Phase 3 D3.1 — BOM export. Pro-tier-gated by the backend (402 if not pro).
// We let everyone click; the 402 response feeds the upgrade prompt below
// so free/maker users hit the funnel naturally instead of an obviously-
// locked button (which they'd just dismiss).
const handleExportBom = async () => {
const projectId = currentProject?.id;
if (!projectId) {
setMessage({ type: 'error', text: 'Save the project before exporting a BOM.' });
return;
}
try {
const resp = await fetch(`/api/pro/projects/${projectId}/bom.csv`, {
credentials: 'include',
});
if (resp.status === 402) {
// Fire the in-place upgrade modal instead of bouncing to /pricing —
// keeps the user in the editor with full context. The pro overlay's
// UpgradeGate listens for this event and opens UpgradePromptModal.
window.dispatchEvent(new CustomEvent('velxio-pro-upgrade-prompt', {
detail: { componentName: 'BOM export' },
}));
return;
}
if (resp.status === 401) {
window.location.href = `/login?redirect=${encodeURIComponent(window.location.pathname)}`;
return;
}
if (!resp.ok) {
setMessage({ type: 'error', text: 'BOM export failed.' });
return;
}
const blob = await resp.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
// Filename comes from Content-Disposition; pick a fallback.
const cd = resp.headers.get('Content-Disposition') || '';
const m = /filename="?([^"]+)"?/.exec(cd);
a.download = m ? m[1] : `bom-${projectId}.csv`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
} catch {
setMessage({ type: 'error', text: 'BOM export failed.' });
}
};
const handleFirmwareUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (firmwareInputRef.current) firmwareInputRef.current.value = '';
if (!file) return;
setConsoleOpen(true);
addLog({ timestamp: new Date(), type: 'info', message: `Loading firmware: ${file.name}...` });
try {
const boardKind = activeBoard?.boardKind;
if (!boardKind) {
setMessage({ type: 'error', text: 'No board selected' });
return;
}
const result = await readFirmwareFile(file, boardKind);
// Architecture mismatch warning for ELF files
if (result.elfInfo?.suggestedBoard && result.elfInfo.suggestedBoard !== boardKind) {
const detected = result.elfInfo.architectureName;
feat(editor): rename boards & custom chips; show which target owns each file Phase 2 of the run-system/UX work. - BoardInstance gains an optional user ; boardDisplayName(board) resolver (name || kind label) routes every INSTANCE-label surface: file-explorer section header, compile console (EditorToolbar), canvas selector/tooltip/ context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options subtitle. Board/component pickers keep the KIND label (they pick new boards). - Inline rename on board AND chip section headers (double-click the name, or a hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in properties. Enter commits, Escape cancels (cancel-flag ref guards the unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'. - FileTabs shows an owner badge naming the board/chip whose files are shown (resolved as a selector so it doesn't re-render on every sim pin toggle). - CustomChipDialog no longer clobbers a user-given chipName: chip.json's name only seeds the blank defaults (My Chip / Custom Chip); loading an example relabels explicitly. - Persistence: board name round-trips via projectPayload (+ dirty hash), vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json. - Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal (early return before a useCallback). Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness); all major findings folded in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:22:25 +07:00
const current = activeBoard ? boardDisplayName(activeBoard) : boardKind;
addLog({
timestamp: new Date(),
type: 'info',
message: `Note: Detected ${detected} architecture, but current board is ${current}. Loading anyway.`,
});
}
if (activeBoardId) {
compileBoardProgram(activeBoardId, result.program);
markCompiled();
addLog({ timestamp: new Date(), type: 'info', message: result.message });
setMessage({ type: 'success', text: `Firmware loaded: ${file.name}` });
}
} catch (err) {
const errMsg = err instanceof Error ? err.message : 'Failed to load firmware';
addLog({ timestamp: new Date(), type: 'error', message: errMsg });
setMessage({ type: 'error', text: errMsg });
}
};
const handleImportFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!importInputRef.current) return;
importInputRef.current.value = '';
if (!file) return;
try {
const result = await importProjectFile(file);
if (result.kind === 'vlx') {
// importVlxFile already wrote into the stores.
setMessage({ type: 'success', text: `Imported ${file.name}` });
return;
}
// .zip path: apply the parsed payload to the stores ourselves, then
// surface any missing libraries via the existing install modal.
const { loadFiles } = useEditorStore.getState();
const { setComponents, setWires, setBoardType, setBoardPosition, stopSimulation } =
useSimulatorStore.getState();
stopSimulation();
if (result.boardType) setBoardType(result.boardType);
setBoardPosition(result.boardPosition);
setComponents(result.components);
setWires(result.wires);
if (result.files.length > 0) loadFiles(result.files);
setMessage({ type: 'success', text: `Imported ${file.name}` });
if (result.libraries.length > 0) {
setPendingLibraries(result.libraries);
setInstallModalOpen(true);
}
} catch (err: any) {
setMessage({ type: 'error', text: err?.message || 'Import failed.' });
}
};
return (
<>
<div className="editor-toolbar-wrapper" style={{ position: 'relative' }}>
<div className="editor-toolbar" ref={toolbarRef}>
feat(esp32): pure ESP-IDF language mode for the ESP32 family (#139) Adds a third entry to the board language selector next to Arduino C++ and MicroPython: ESP-IDF. In this mode the user writes a plain ESP-IDF project — app_main() entry point, FreeRTOS + driver APIs — and the backend compiles it through the same ESP-IDF toolchain it already uses for ESP32 Arduino sketches, just without the arduino-esp32 component. Backend: - CompileRequest.language ('espidf') threaded through the sync + async compile paths and folded into the dedup job key (language='arduino' and omitted hash identically so old clients keep dedupping). - espidf_compiler: pure_idf flag. User files are written into main/ as-is (no Arduino.h wrap, no velxio_compat.h, Arduino library resolution skipped), ARDUINO_ESP32_PATH is dropped from the build env and VELXIO_PURE_SKETCH raised so the template CMake compiles the user's own sources via a glob branch. Pure builds get their own persistent build-dir variant through the eff_hash fold. - QEMU WiFi compat for IDF-style code: esp_wifi.h/esp_wifi_init detection sets has_wifi, and literal #define SSID/PASS plus wifi_config_t designated initializers are normalized to the QEMU AP. - CONFIG_ARDUINO_* lines are stripped from sdkconfig.defaults in pure mode (the symbols don't exist without the arduino component). Frontend: - LanguageMode gains 'espidf'; BOARD_SUPPORTS_ESPIDF covers the ESP32 family (Xtensa, S3, C3). Toolbar shows the option only for those. - Switching modes seeds a main.c blink skeleton (app_main + gpio driver), mirroring the MicroPython main.py flow. - compileCode sends language='espidf'; run/stop paths are unchanged (the QEMU worker consumes the same merged flash image). - New gallery example: esp32-idf-blink (LED + resistor on GPIO 2). Tests: unit coverage for the build-env switch, IDF wifi normalization, job-key variance, file-group seeding and the new example; verified end-to-end in a container from the prod image (pure build produces a bootable flash image; Arduino-mode build unchanged, same variant hash).
2026-07-24 11:37:01 +07:00
{/* Language selector only when active board supports an
alternative to Arduino C++ (MicroPython on Pico/ESP32 boards,
pure ESP-IDF on the ESP32 family issue #139). The board
context pill that used to live here was removed: it duplicated
the BoardSelector dropdown elsewhere in the toolbar. */}
feat(editor): view-mode toggle, agent-chat slot, toolbar polish Changes that ship to OSS — all benign for self-hosters, but most are extension points the velxio-prod overlay (and any private fork) needs to plug an in-editor AI chat into the page. Editor: - 3-way view-mode toggle (code / both / circuit) in the unified toolbar. Lets users hide a pane to give a right-docked sidebar (e.g. the AI chat overlay) more breathing room. Persisted in useEditorStore. - Default file explorer narrower (210 → 165 px); min 110. - Removed the redundant `tb-board-pill` (icon + "Editing: X" tooltip); the BoardSelector dropdown elsewhere already shows the active board. - Inlined Import/Export/Upload-firmware buttons; the 3-dot overflow menu gave up too much discoverability. Removed dead overflow state. Simulator: - Fix: global Delete/Backspace handler in SimulatorCanvas no longer fires when the event target is an INPUT/TEXTAREA/SELECT/contentEditable — affected any in-page text field, not just the chat overlay. Overlay extensibility: - New `data-velxio-slot="agent-chat"` at the bottom of EditorPage so pro overlays can portal a chat panel into the editor without forking the page. - vite.config.ts: preserveSymlinks=true when VITE_PRO_BUILD is set. Lets local-dev junctions (overlay tree → frontend/src/pro) resolve bare imports back to the OSS node_modules without resolving symlinks. Deps: - Added react-markdown + remark-gfm (rendered chat output) and @google/genai + zod (overlay agent loop). Tree-shaken from the OSS bundle when no pro code imports them. gitignore: - Ignore backend/app/pro/ and frontend/src/pro/ junctions used by developers running a private overlay against the OSS dev server. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:53:27 +07:00
{activeBoard && BOARD_SUPPORTS_MICROPYTHON.has(activeBoard.boardKind) && (
<select
className="tb-lang-select"
value={activeBoard.languageMode ?? 'arduino'}
onChange={(e) => {
if (activeBoardId)
setBoardLanguageMode(activeBoardId, e.target.value as LanguageMode);
}}
title={t('editor.toolbar.languageMode')}
feat(editor): view-mode toggle, agent-chat slot, toolbar polish Changes that ship to OSS — all benign for self-hosters, but most are extension points the velxio-prod overlay (and any private fork) needs to plug an in-editor AI chat into the page. Editor: - 3-way view-mode toggle (code / both / circuit) in the unified toolbar. Lets users hide a pane to give a right-docked sidebar (e.g. the AI chat overlay) more breathing room. Persisted in useEditorStore. - Default file explorer narrower (210 → 165 px); min 110. - Removed the redundant `tb-board-pill` (icon + "Editing: X" tooltip); the BoardSelector dropdown elsewhere already shows the active board. - Inlined Import/Export/Upload-firmware buttons; the 3-dot overflow menu gave up too much discoverability. Removed dead overflow state. Simulator: - Fix: global Delete/Backspace handler in SimulatorCanvas no longer fires when the event target is an INPUT/TEXTAREA/SELECT/contentEditable — affected any in-page text field, not just the chat overlay. Overlay extensibility: - New `data-velxio-slot="agent-chat"` at the bottom of EditorPage so pro overlays can portal a chat panel into the editor without forking the page. - vite.config.ts: preserveSymlinks=true when VITE_PRO_BUILD is set. Lets local-dev junctions (overlay tree → frontend/src/pro) resolve bare imports back to the OSS node_modules without resolving symlinks. Deps: - Added react-markdown + remark-gfm (rendered chat output) and @google/genai + zod (overlay agent loop). Tree-shaken from the OSS bundle when no pro code imports them. gitignore: - Ignore backend/app/pro/ and frontend/src/pro/ junctions used by developers running a private overlay against the OSS dev server. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:53:27 +07:00
style={{
background: '#2d2d2d',
color: '#ccc',
border: '1px solid #444',
borderRadius: 4,
padding: '2px 4px',
fontSize: 11,
cursor: 'pointer',
outline: 'none',
marginRight: 4,
}}
>
<option value="arduino">Arduino C++</option>
<option value="micropython">MicroPython</option>
feat(esp32): pure ESP-IDF language mode for the ESP32 family (#139) Adds a third entry to the board language selector next to Arduino C++ and MicroPython: ESP-IDF. In this mode the user writes a plain ESP-IDF project — app_main() entry point, FreeRTOS + driver APIs — and the backend compiles it through the same ESP-IDF toolchain it already uses for ESP32 Arduino sketches, just without the arduino-esp32 component. Backend: - CompileRequest.language ('espidf') threaded through the sync + async compile paths and folded into the dedup job key (language='arduino' and omitted hash identically so old clients keep dedupping). - espidf_compiler: pure_idf flag. User files are written into main/ as-is (no Arduino.h wrap, no velxio_compat.h, Arduino library resolution skipped), ARDUINO_ESP32_PATH is dropped from the build env and VELXIO_PURE_SKETCH raised so the template CMake compiles the user's own sources via a glob branch. Pure builds get their own persistent build-dir variant through the eff_hash fold. - QEMU WiFi compat for IDF-style code: esp_wifi.h/esp_wifi_init detection sets has_wifi, and literal #define SSID/PASS plus wifi_config_t designated initializers are normalized to the QEMU AP. - CONFIG_ARDUINO_* lines are stripped from sdkconfig.defaults in pure mode (the symbols don't exist without the arduino component). Frontend: - LanguageMode gains 'espidf'; BOARD_SUPPORTS_ESPIDF covers the ESP32 family (Xtensa, S3, C3). Toolbar shows the option only for those. - Switching modes seeds a main.c blink skeleton (app_main + gpio driver), mirroring the MicroPython main.py flow. - compileCode sends language='espidf'; run/stop paths are unchanged (the QEMU worker consumes the same merged flash image). - New gallery example: esp32-idf-blink (LED + resistor on GPIO 2). Tests: unit coverage for the build-env switch, IDF wifi normalization, job-key variance, file-group seeding and the new example; verified end-to-end in a container from the prod image (pure build produces a bootable flash image; Arduino-mode build unchanged, same variant hash).
2026-07-24 11:37:01 +07:00
{BOARD_SUPPORTS_ESPIDF.has(activeBoard.boardKind) && (
<option value="espidf">ESP-IDF</option>
)}
feat(editor): view-mode toggle, agent-chat slot, toolbar polish Changes that ship to OSS — all benign for self-hosters, but most are extension points the velxio-prod overlay (and any private fork) needs to plug an in-editor AI chat into the page. Editor: - 3-way view-mode toggle (code / both / circuit) in the unified toolbar. Lets users hide a pane to give a right-docked sidebar (e.g. the AI chat overlay) more breathing room. Persisted in useEditorStore. - Default file explorer narrower (210 → 165 px); min 110. - Removed the redundant `tb-board-pill` (icon + "Editing: X" tooltip); the BoardSelector dropdown elsewhere already shows the active board. - Inlined Import/Export/Upload-firmware buttons; the 3-dot overflow menu gave up too much discoverability. Removed dead overflow state. Simulator: - Fix: global Delete/Backspace handler in SimulatorCanvas no longer fires when the event target is an INPUT/TEXTAREA/SELECT/contentEditable — affected any in-page text field, not just the chat overlay. Overlay extensibility: - New `data-velxio-slot="agent-chat"` at the bottom of EditorPage so pro overlays can portal a chat panel into the editor without forking the page. - vite.config.ts: preserveSymlinks=true when VITE_PRO_BUILD is set. Lets local-dev junctions (overlay tree → frontend/src/pro) resolve bare imports back to the OSS node_modules without resolving symlinks. Deps: - Added react-markdown + remark-gfm (rendered chat output) and @google/genai + zod (overlay agent loop). Tree-shaken from the OSS bundle when no pro code imports them. gitignore: - Ignore backend/app/pro/ and frontend/src/pro/ junctions used by developers running a private overlay against the OSS dev server. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:53:27 +07:00
</select>
)}
<div className="toolbar-group">
{/* Compile */}
<button
onClick={handleCompile}
disabled={compiling || !activeBoard}
className="tb-btn tb-btn-compile"
title={
!activeBoard
? t('editor.toolbar.compile.addBoard')
: compiling
? t('editor.toolbar.compile.loading')
: activeBoard?.languageMode === 'micropython'
? t('editor.toolbar.compile.loadMicropython')
: t('editor.toolbar.compile.compile')
}
>
{compiling ? (
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="spin"
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
) : (
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
</svg>
)}
</button>
<div className="tb-divider" />
{/* Run in a multi-board project this runs ALL boards (the wired
boards are one system; running a subset is almost never
intended), with a split-menu to still run only the active board.
Single-board / board-less behaviour is unchanged. */}
<div className="tb-run-split" ref={runMenuRef}>
<button
onClick={() => (isMultiBoard ? handleRunAll() : handleRun())}
disabled={
isBoardless
? digitalRunning || verifying
: isMultiBoard
? compileAllRunning || anyBoardRunning || verifying
: running || compiling || verifying || !activeBoard
}
className="tb-btn tb-btn-run"
title={
verifying
? t('editor.toolbar.run.verifying', 'Checking circuit...')
: isBoardless
? digitalRunning
? 'Digital simulation running'
: 'Resume digital simulation'
: isMultiBoard
? t('editor.toolbar.runAll')
: !activeBoard
? t('editor.toolbar.run.addBoard')
: activeBoard?.languageMode === 'micropython'
? t('editor.toolbar.run.runMicropython')
: t('editor.toolbar.run.run')
}
>
{verifying || compiling ? (
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="spin"
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
) : (
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<polygon points="5,3 19,12 5,21" />
</svg>
)}
</button>
{isMultiBoard && (
<button
className="tb-btn tb-btn-run-caret"
onClick={() => setRunMenuOpen((o) => !o)}
disabled={compileAllRunning || anyBoardRunning || verifying}
title={t('editor.toolbar.run.options', 'Run options')}
aria-haspopup="true"
aria-expanded={runMenuOpen}
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
)}
{isMultiBoard && runMenuOpen && (
<div className="tb-run-menu" role="menu">
<button
role="menuitem"
className="tb-run-menu-item"
onClick={() => {
setRunMenuOpen(false);
handleRunAll();
}}
>
{t('editor.toolbar.runAll')}
</button>
<button
role="menuitem"
className="tb-run-menu-item"
disabled={!activeBoard}
onClick={() => {
setRunMenuOpen(false);
handleRun();
}}
>
{t('editor.toolbar.run.runActiveOnly', {
name: activeBoard ? boardDisplayName(activeBoard) : '',
defaultValue: `Run only ${activeBoard ? boardDisplayName(activeBoard) : ''}`,
})}
</button>
</div>
)}
</div>
{/* Stop */}
<button
onClick={handleStop}
disabled={isBoardless ? !digitalRunning : !anyBoardRunning}
className="tb-btn tb-btn-stop"
title={isBoardless ? 'Freeze digital simulation' : t('editor.toolbar.stop')}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<rect x="3" y="3" width="18" height="18" rx="2" />
</svg>
</button>
{/* Reset */}
<button
onClick={handleReset}
disabled={!compiledHex && !activeBoard?.compiledProgram}
className="tb-btn tb-btn-reset"
title={t('editor.toolbar.reset')}
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
<path d="M3 3v5h5" />
</svg>
</button>
{targetCount > 1 && (
<>
<div className="tb-divider" />
{/* Compile All — boards + programmable chips */}
<button
onClick={handleCompileAll}
disabled={compileAllRunning}
className="tb-btn tb-btn-compile-all"
title={t('editor.toolbar.compileAll')}
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
<path d="M6 20h4M14 4l4 4" strokeDasharray="2 2" />
</svg>
</button>
{/* Run All only when the primary Run isn't already the
"run all boards" action (i.e. board + chip or chips-only
projects). For 2+ boards the split Run button covers it. */}
{!isMultiBoard && (
<button
onClick={() => handleRunAll()}
disabled={compileAllRunning || anyBoardRunning || digitalRunning}
className="tb-btn tb-btn-run-all"
title={t('editor.toolbar.runAll')}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<polygon points="3,3 11,12 3,21" />
<polygon points="13,3 21,12 13,21" />
</svg>
</button>
)}
</>
)}
</div>
{/* Center slot a flexible spacer that keeps the right action group
pinned to the far right. Rendered unconditionally so the layout
holds even when no overlay supplies content here. */}
<div className="toolbar-center-slot">{centerSlot}</div>
<div className="toolbar-group toolbar-group-right">
{/* Hidden file input for project import. Accepts both .vlx
(Velxio native) and .zip (Wokwi bundle); the dispatcher in
utils/importProject.ts picks the right loader by extension. */}
<input
ref={importInputRef}
type="file"
accept={PROJECT_FILE_ACCEPT}
style={{ display: 'none' }}
onChange={handleImportFile}
/>
{/* Hidden file input for firmware upload */}
<input
ref={firmwareInputRef}
type="file"
accept=".hex,.bin,.elf,.ihex"
style={{ display: 'none' }}
onChange={handleFirmwareUpload}
/>
{/* Library Manager — always visible with label */}
<button
onClick={() => {
trackOpenLibraryManager();
setLibManagerOpen(true);
}}
className="tb-btn-libraries"
title={t('editor.toolbar.libraries.title')}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" />
<path d="m3.3 7 8.7 5 8.7-5" />
<path d="M12 22V12" />
</svg>
<span className="tb-libraries-label">{t('editor.toolbar.libraries.label')}</span>
</button>
{/* Import zip inline by default; container query at narrow
widths swaps this for the corresponding overflow-menu item. */}
feat(editor): view-mode toggle, agent-chat slot, toolbar polish Changes that ship to OSS — all benign for self-hosters, but most are extension points the velxio-prod overlay (and any private fork) needs to plug an in-editor AI chat into the page. Editor: - 3-way view-mode toggle (code / both / circuit) in the unified toolbar. Lets users hide a pane to give a right-docked sidebar (e.g. the AI chat overlay) more breathing room. Persisted in useEditorStore. - Default file explorer narrower (210 → 165 px); min 110. - Removed the redundant `tb-board-pill` (icon + "Editing: X" tooltip); the BoardSelector dropdown elsewhere already shows the active board. - Inlined Import/Export/Upload-firmware buttons; the 3-dot overflow menu gave up too much discoverability. Removed dead overflow state. Simulator: - Fix: global Delete/Backspace handler in SimulatorCanvas no longer fires when the event target is an INPUT/TEXTAREA/SELECT/contentEditable — affected any in-page text field, not just the chat overlay. Overlay extensibility: - New `data-velxio-slot="agent-chat"` at the bottom of EditorPage so pro overlays can portal a chat panel into the editor without forking the page. - vite.config.ts: preserveSymlinks=true when VITE_PRO_BUILD is set. Lets local-dev junctions (overlay tree → frontend/src/pro) resolve bare imports back to the OSS node_modules without resolving symlinks. Deps: - Added react-markdown + remark-gfm (rendered chat output) and @google/genai + zod (overlay agent loop). Tree-shaken from the OSS bundle when no pro code imports them. gitignore: - Ignore backend/app/pro/ and frontend/src/pro/ junctions used by developers running a private overlay against the OSS dev server. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:53:27 +07:00
<button
onClick={() => importInputRef.current?.click()}
className="tb-btn tb-btn-import-inline"
title={t('editor.toolbar.import')}
feat(editor): view-mode toggle, agent-chat slot, toolbar polish Changes that ship to OSS — all benign for self-hosters, but most are extension points the velxio-prod overlay (and any private fork) needs to plug an in-editor AI chat into the page. Editor: - 3-way view-mode toggle (code / both / circuit) in the unified toolbar. Lets users hide a pane to give a right-docked sidebar (e.g. the AI chat overlay) more breathing room. Persisted in useEditorStore. - Default file explorer narrower (210 → 165 px); min 110. - Removed the redundant `tb-board-pill` (icon + "Editing: X" tooltip); the BoardSelector dropdown elsewhere already shows the active board. - Inlined Import/Export/Upload-firmware buttons; the 3-dot overflow menu gave up too much discoverability. Removed dead overflow state. Simulator: - Fix: global Delete/Backspace handler in SimulatorCanvas no longer fires when the event target is an INPUT/TEXTAREA/SELECT/contentEditable — affected any in-page text field, not just the chat overlay. Overlay extensibility: - New `data-velxio-slot="agent-chat"` at the bottom of EditorPage so pro overlays can portal a chat panel into the editor without forking the page. - vite.config.ts: preserveSymlinks=true when VITE_PRO_BUILD is set. Lets local-dev junctions (overlay tree → frontend/src/pro) resolve bare imports back to the OSS node_modules without resolving symlinks. Deps: - Added react-markdown + remark-gfm (rendered chat output) and @google/genai + zod (overlay agent loop). Tree-shaken from the OSS bundle when no pro code imports them. gitignore: - Ignore backend/app/pro/ and frontend/src/pro/ junctions used by developers running a private overlay against the OSS dev server. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:53:27 +07:00
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
</button>
<button
onClick={() => handleExport()}
className="tb-btn tb-btn-export-inline"
title={t('editor.toolbar.export')}
feat(editor): view-mode toggle, agent-chat slot, toolbar polish Changes that ship to OSS — all benign for self-hosters, but most are extension points the velxio-prod overlay (and any private fork) needs to plug an in-editor AI chat into the page. Editor: - 3-way view-mode toggle (code / both / circuit) in the unified toolbar. Lets users hide a pane to give a right-docked sidebar (e.g. the AI chat overlay) more breathing room. Persisted in useEditorStore. - Default file explorer narrower (210 → 165 px); min 110. - Removed the redundant `tb-board-pill` (icon + "Editing: X" tooltip); the BoardSelector dropdown elsewhere already shows the active board. - Inlined Import/Export/Upload-firmware buttons; the 3-dot overflow menu gave up too much discoverability. Removed dead overflow state. Simulator: - Fix: global Delete/Backspace handler in SimulatorCanvas no longer fires when the event target is an INPUT/TEXTAREA/SELECT/contentEditable — affected any in-page text field, not just the chat overlay. Overlay extensibility: - New `data-velxio-slot="agent-chat"` at the bottom of EditorPage so pro overlays can portal a chat panel into the editor without forking the page. - vite.config.ts: preserveSymlinks=true when VITE_PRO_BUILD is set. Lets local-dev junctions (overlay tree → frontend/src/pro) resolve bare imports back to the OSS node_modules without resolving symlinks. Deps: - Added react-markdown + remark-gfm (rendered chat output) and @google/genai + zod (overlay agent loop). Tree-shaken from the OSS bundle when no pro code imports them. gitignore: - Ignore backend/app/pro/ and frontend/src/pro/ junctions used by developers running a private overlay against the OSS dev server. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:53:27 +07:00
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
</button>
{/* Overflow "More" menu collects the secondary actions
(BOM, Schematic image, Upload firmware) so the toolbar no
longer overflows on narrow widths. The two Pro items show
a small "PRO" pill in the menu so users know they're
premium BEFORE clicking, instead of being surprised by an
upgrade prompt. */}
<div className="tb-overflow-wrap" ref={moreMenuRef}>
<button
onClick={() => setMoreMenuOpen((v) => !v)}
className={`tb-btn tb-btn-overflow${moreMenuOpen ? ' tb-btn-overflow-active' : ''}`}
title={t('editor.toolbar.more', 'More')}
aria-haspopup="true"
aria-expanded={moreMenuOpen}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<circle cx="5" cy="12" r="1.8" />
<circle cx="12" cy="12" r="1.8" />
<circle cx="19" cy="12" r="1.8" />
</svg>
</button>
{moreMenuOpen && (
<div className="tb-overflow-menu" role="menu">
{/* Responsive items hidden by default, shown via
container query when the toolbar is too narrow to
keep their inline twins. Keeps mobile users from
losing access to Import / Export entirely. */}
<button
className="tb-overflow-item tb-overflow-import"
role="menuitem"
onClick={() => {
setMoreMenuOpen(false);
importInputRef.current?.click();
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
<span className="tb-overflow-label">{t('editor.toolbar.importLabel', 'Import project')}</span>
</button>
<button
className="tb-overflow-item tb-overflow-export"
role="menuitem"
onClick={() => {
setMoreMenuOpen(false);
handleExport();
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
<span className="tb-overflow-label">{t('editor.toolbar.exportLabel', 'Export project (.zip)')}</span>
</button>
<button
className="tb-overflow-item"
role="menuitem"
onClick={() => {
setMoreMenuOpen(false);
handleExportBom();
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="4" width="18" height="16" rx="2" />
<line x1="3" y1="10" x2="21" y2="10" />
<line x1="9" y1="4" x2="9" y2="20" />
</svg>
<span className="tb-overflow-label">{t('editor.toolbar.exportBomLabel', 'Bill of Materials (CSV)')}</span>
<span className="tb-overflow-pro">PRO</span>
</button>
<button
className="tb-overflow-item"
role="menuitem"
onClick={() => {
setMoreMenuOpen(false);
handleExportScreenshot();
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
<circle cx="12" cy="13" r="4" />
</svg>
<span className="tb-overflow-label">{t('editor.toolbar.exportScreenshotLabel', 'Schematic image (PNG)')}</span>
<span className="tb-overflow-pro">PRO</span>
</button>
<button
className="tb-overflow-item"
role="menuitem"
onClick={() => {
setMoreMenuOpen(false);
firmwareInputRef.current?.click();
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
<line x1="12" y1="15" x2="12" y2="22" />
<polyline points="8 18 12 22 16 18" />
</svg>
<span className="tb-overflow-label">{t('editor.toolbar.uploadFirmwareLabel', 'Upload firmware')}</span>
</button>
{/* Sync to GitHub Pro feature. Fires a window event the
pro overlay listens for; if no overlay is loaded (OSS
build) the click is a silent no-op which is fine
OSS users can't have linked repos anyway. */}
<button
className="tb-overflow-item"
role="menuitem"
onClick={() => {
setMoreMenuOpen(false);
window.dispatchEvent(new CustomEvent('velxio-pro-github-sync-prompt', {
detail: { projectId: currentProject?.id ?? null },
}));
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.44 9.8 8.21 11.39.6.11.82-.26.82-.58 0-.29-.01-1.05-.02-2.06-3.34.72-4.04-1.61-4.04-1.61-.55-1.38-1.33-1.75-1.33-1.75-1.09-.74.08-.72.08-.72 1.2.08 1.84 1.24 1.84 1.24 1.07 1.84 2.81 1.31 3.5 1 .11-.78.42-1.31.76-1.62-2.66-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.12-.3-.54-1.52.11-3.18 0 0 1.01-.32 3.3 1.23A11.5 11.5 0 0 1 12 5.8c1.02.01 2.05.14 3.01.4 2.29-1.55 3.3-1.23 3.3-1.23.65 1.66.24 2.88.12 3.18.77.84 1.24 1.91 1.24 3.22 0 4.61-2.81 5.62-5.49 5.92.43.37.82 1.1.82 2.22 0 1.6-.02 2.89-.02 3.29 0 .32.22.7.83.58A12 12 0 0 0 24 12c0-6.63-5.37-12-12-12z" />
</svg>
<span className="tb-overflow-label">{t('editor.toolbar.githubSyncLabel', 'Sync to GitHub')}</span>
<span className="tb-overflow-pro">PRO</span>
</button>
{/* Share / Embed free for all users with a public project.
Watermark removal on the embed is the Pro perk; the
Share modal itself is open to everyone so they can
copy the link / iframe snippet. */}
<button
className="tb-overflow-item"
role="menuitem"
onClick={() => {
setMoreMenuOpen(false);
window.dispatchEvent(new CustomEvent('velxio-pro-share-prompt', {
detail: { projectId: currentProject?.id ?? null },
}));
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="18" cy="5" r="3" />
<circle cx="6" cy="12" r="3" />
<circle cx="18" cy="19" r="3" />
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
</svg>
<span className="tb-overflow-label">{t('editor.toolbar.shareLabel', 'Share / Embed')}</span>
</button>
{/* Record simulation Pro feature. Dispatches a toggle the
pro overlay handles (plan check, board-type check,
start/stop the recorder). OSS build no listener
silent no-op. */}
<button
className="tb-overflow-item"
role="menuitem"
onClick={() => {
setMoreMenuOpen(false);
window.dispatchEvent(new CustomEvent('velxio-pro-replay-record-toggle', {
detail: { projectId: currentProject?.id ?? null },
}));
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="7" />
</svg>
<span className="tb-overflow-label">{t('editor.toolbar.recordLabel', 'Record simulation')}</span>
<span className="tb-overflow-pro">PRO</span>
</button>
</div>
)}
</div>
<div className="tb-divider" />
{/* Output Console toggle */}
<button
onClick={() => setConsoleOpen((v) => !v)}
className={`tb-btn tb-btn-output${consoleOpen ? ' tb-btn-output-active' : ''}`}
title={t('editor.toolbar.toggleConsole')}
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="4 17 10 11 4 5" />
<line x1="12" y1="19" x2="20" y2="19" />
</svg>
</button>
{rightSlot}
</div>
</div>
</div>
{/* Error detail bar */}
{message?.type === 'error' && message.text.length > 40 && !consoleOpen && (
<div className="toolbar-error-detail">{message.text}</div>
)}
{/* Missing library hint */}
{missingLibHint && (
<div className="tb-lib-hint">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
<span>{t('editor.toolbar.libHint.message')}</span>
<button
className="tb-lib-hint-btn"
onClick={() => {
trackOpenLibraryManager();
setLibManagerOpen(true);
setMissingLibHint(false);
}}
>
{t('editor.toolbar.libHint.cta')}
</button>
<button
className="tb-lib-hint-close"
onClick={() => setMissingLibHint(false)}
title={t('editor.toolbar.libHint.dismiss')}
>
&times;
</button>
</div>
)}
<LibraryManagerModal isOpen={libManagerOpen} onClose={() => setLibManagerOpen(false)} />
<InstallLibrariesModal
isOpen={installModalOpen}
onClose={() => setInstallModalOpen(false)}
libraries={pendingLibraries}
/>
{verification && (
<CircuitVerificationModal
result={verification}
onCancel={() => {
pendingRunRef.current = null;
setVerification(null);
}}
onRunAnyway={() => {
const resume = pendingRunRef.current;
pendingRunRef.current = null;
setVerification(null);
resume?.();
}}
/>
)}
</>
);
};