2026-03-16 10:12:33 +07:00
|
|
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 13:05:51 +07:00
|
|
|
import { useTranslation } from 'react-i18next';
|
2026-03-03 10:20:49 +07:00
|
|
|
import { useEditorStore } from '../../store/useEditorStore';
|
2026-05-18 09:41:19 +07:00
|
|
|
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
2026-05-13 00:26:33 +07:00
|
|
|
import { useElectricalStore } from '../../store/useElectricalStore';
|
2026-05-13 02:55:15 +07:00
|
|
|
import { verifyCircuit, type VerificationResult } from '../../simulation/verify/circuitVerifier';
|
|
|
|
|
import { buildInputFromStore } from '../../simulation/spice/storeAdapter';
|
|
|
|
|
import { BOARD_PIN_GROUPS } from '../../simulation/spice/boardPinGroups';
|
|
|
|
|
import { CircuitVerificationModal } from '../simulator/CircuitVerificationModal';
|
|
|
|
|
import type { PinSourceState } from '../../simulation/spice/types';
|
2026-03-30 07:27:41 +07:00
|
|
|
import type { BoardKind, LanguageMode } from '../../types/board';
|
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
|
|
|
import { BOARD_KIND_FQBN, BOARD_KIND_LABELS, BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind } from '../../types/board';
|
2026-03-03 10:20:49 +07:00
|
|
|
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';
|
feat(metrics): add usage analytics dashboard with country tracking
Track per-user, per-project, and per-board usage to inform pricing tier
decisions. Adds an admin dashboard with KPIs (DAU/WAU/MAU, totals,
success rate), time-series charts for compiles/runs, board family +
FQBN breakdowns, "board diversity" pie chart (key freemium signal),
top users/projects, and per-country breakdown via Cloudflare's
CF-IPCountry header. Admin can now also view private projects.
Backend:
- New UsageEvent table (append-only event log with user_id, project_id,
event_type, board_fqbn/family, country, error_kind, duration_ms)
- Aggregate counters on User (total_compiles/runs/errors, last_active,
signup_country, last_country) and Project (compile/run/update counts,
last_compiled/run timestamps) kept in sync by MetricsService for O(1)
dashboard reads
- 10 admin endpoints under /api/admin/metrics/{overview, timeseries,
boards, board-diversity, top-users, top-projects, countries,
users/{id}, projects/{id}}
- POST /api/metrics/run for client-side run telemetry
- Country detection via cf-ipcountry header (no DB / no API calls)
- Auto-migrations in lifespan for legacy DBs
Frontend:
- recharts-powered Dashboard tab with KPI cards and 4 charts
- New Boards tab with per-family + per-FQBN breakdown
- Country column with flag emoji on Users tab
- Top countries card on Dashboard
- compileCode now forwards project_id; Run button reports via WS
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 05:46:52 +07:00
|
|
|
import { reportRunEvent } from '../../services/metricsService';
|
|
|
|
|
import { useProjectStore } from '../../store/useProjectStore';
|
2026-03-05 08:05:23 +07:00
|
|
|
import { LibraryManagerModal } from '../simulator/LibraryManagerModal';
|
2026-03-09 22:53:24 +07:00
|
|
|
import { InstallLibrariesModal } from '../simulator/InstallLibrariesModal';
|
2026-03-06 07:07:03 +07:00
|
|
|
import { parseCompileResult } from '../../utils/compilationLogger';
|
|
|
|
|
import type { CompilationLog } from '../../utils/compilationLogger';
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
import { exportToWokwiZip } from '../../utils/wokwiZip';
|
|
|
|
|
import { importProjectFile, PROJECT_FILE_ACCEPT } from '../../utils/importProject';
|
feat: load precompiled firmware files (.hex, .bin, .elf) directly
Closes #1
Add the ability to upload precompiled firmware files directly into the
emulator, bypassing the built-in compilation step. This enables users
with custom toolchains (ESP-IDF, PlatformIO, ASM workflows) to use
Velxio purely as an emulation/debugging environment.
New features:
- "Upload firmware" option in the overflow menu (accepts .hex, .bin, .elf)
- Automatic format detection from file extension and magic bytes
- ELF parser extracts PT_LOAD segments and detects target architecture
(AVR, ARM, RISC-V, Xtensa) from ELF e_machine header
- Architecture mismatch warnings logged when ELF target differs from
current board (non-blocking — upload proceeds anyway)
- Firmware routed to the correct simulator loader via existing
compileBoardProgram() — no simulator changes needed
Supported formats per board:
- AVR (Uno/Nano/Mega/ATtiny85): .hex (direct), .elf (parsed → HEX)
- RP2040 (Pico): .bin (direct), .elf (parsed → binary)
- ESP32-C3: .bin (direct), .hex, .elf (parsed → binary)
- ESP32/S3 (QEMU): .bin (direct), .elf (parsed → binary)
New file: frontend/src/utils/firmwareLoader.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 12:08:11 +07:00
|
|
|
import { readFirmwareFile } from '../../utils/firmwareLoader';
|
2026-04-22 02:45:45 +07:00
|
|
|
import {
|
|
|
|
|
trackCompileCode,
|
|
|
|
|
trackRunSimulation,
|
|
|
|
|
trackStopSimulation,
|
|
|
|
|
trackResetSimulation,
|
|
|
|
|
trackOpenLibraryManager,
|
|
|
|
|
} from '../../utils/analytics';
|
2026-03-03 10:20:49 +07:00
|
|
|
import './EditorToolbar.css';
|
|
|
|
|
|
2026-03-06 07:07:10 +07:00
|
|
|
interface EditorToolbarProps {
|
|
|
|
|
consoleOpen: boolean;
|
|
|
|
|
setConsoleOpen: (open: boolean | ((v: boolean) => boolean)) => void;
|
|
|
|
|
compileLogs: CompilationLog[];
|
|
|
|
|
setCompileLogs: (logs: CompilationLog[] | ((prev: CompilationLog[]) => CompilationLog[])) => void;
|
2026-04-29 09:23:20 +07:00
|
|
|
/**
|
|
|
|
|
* Optional element rendered between the left action group and the right
|
|
|
|
|
* action group. The editor passes <FileTabs /> here so the tabs share the
|
|
|
|
|
* same row as the toolbar — keeping every action icon pinned and visible
|
|
|
|
|
* regardless of how narrow the editor pane gets.
|
|
|
|
|
*/
|
|
|
|
|
centerSlot?: React.ReactNode;
|
2026-05-05 00:03:20 +07:00
|
|
|
/**
|
|
|
|
|
* 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;
|
2026-03-06 07:07:10 +07:00
|
|
|
}
|
|
|
|
|
|
2026-03-16 00:04:01 +07:00
|
|
|
const BOARD_PILL_ICON: Record<BoardKind, string> = {
|
2026-04-22 02:45:45 +07:00
|
|
|
'arduino-uno': '⬤',
|
|
|
|
|
'arduino-nano': '▪',
|
|
|
|
|
'arduino-mega': '▬',
|
2026-03-16 00:04:01 +07:00
|
|
|
'raspberry-pi-pico': '◆',
|
2026-04-22 02:45:45 +07:00
|
|
|
'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': '⬛',
|
2026-04-22 02:45:45 +07:00
|
|
|
esp32: '⬡',
|
2026-03-16 00:04:01 +07:00
|
|
|
'esp32-s3': '⬡',
|
|
|
|
|
'esp32-c3': '⬡',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const BOARD_PILL_COLOR: Record<BoardKind, string> = {
|
2026-04-22 02:45:45 +07:00
|
|
|
'arduino-uno': '#4fc3f7',
|
|
|
|
|
'arduino-nano': '#4fc3f7',
|
|
|
|
|
'arduino-mega': '#4fc3f7',
|
2026-03-16 00:04:01 +07:00
|
|
|
'raspberry-pi-pico': '#ce93d8',
|
2026-04-22 02:45:45 +07:00
|
|
|
'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',
|
2026-04-22 02:45:45 +07:00
|
|
|
esp32: '#a5d6a7',
|
2026-03-16 00:04:01 +07:00
|
|
|
'esp32-s3': '#a5d6a7',
|
|
|
|
|
'esp32-c3': '#a5d6a7',
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
export const EditorToolbar = ({
|
|
|
|
|
consoleOpen,
|
|
|
|
|
setConsoleOpen,
|
|
|
|
|
compileLogs: _compileLogs,
|
|
|
|
|
setCompileLogs,
|
2026-04-29 09:23:20 +07:00
|
|
|
centerSlot,
|
2026-05-05 00:03:20 +07:00
|
|
|
rightSlot,
|
2026-04-22 02:45:45 +07:00
|
|
|
}: EditorToolbarProps) => {
|
feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 13:05:51 +07:00
|
|
|
const { t } = useTranslation();
|
2026-03-29 11:46:07 +07:00
|
|
|
const { files, codeChangedSinceLastCompile, markCompiled } = useEditorStore();
|
2026-03-05 05:28:33 +07:00
|
|
|
const {
|
2026-03-13 09:39:04 +07:00
|
|
|
boards,
|
|
|
|
|
activeBoardId,
|
|
|
|
|
compileBoardProgram,
|
2026-03-30 07:27:41 +07:00
|
|
|
loadMicroPythonProgram,
|
|
|
|
|
setBoardLanguageMode,
|
2026-04-06 03:14:40 +07:00
|
|
|
updateBoard,
|
2026-03-13 09:39:04 +07:00
|
|
|
startBoard,
|
|
|
|
|
stopBoard,
|
|
|
|
|
resetBoard,
|
|
|
|
|
// legacy compat
|
2026-03-05 05:28:33 +07:00
|
|
|
startSimulation,
|
|
|
|
|
stopSimulation,
|
|
|
|
|
resetSimulation,
|
|
|
|
|
running,
|
|
|
|
|
compiledHex,
|
|
|
|
|
} = useSimulatorStore();
|
2026-03-13 09:39:04 +07:00
|
|
|
|
|
|
|
|
const activeBoard = boards.find((b) => b.id === activeBoardId) ?? boards[0];
|
feat(metrics): add usage analytics dashboard with country tracking
Track per-user, per-project, and per-board usage to inform pricing tier
decisions. Adds an admin dashboard with KPIs (DAU/WAU/MAU, totals,
success rate), time-series charts for compiles/runs, board family +
FQBN breakdowns, "board diversity" pie chart (key freemium signal),
top users/projects, and per-country breakdown via Cloudflare's
CF-IPCountry header. Admin can now also view private projects.
Backend:
- New UsageEvent table (append-only event log with user_id, project_id,
event_type, board_fqbn/family, country, error_kind, duration_ms)
- Aggregate counters on User (total_compiles/runs/errors, last_active,
signup_country, last_country) and Project (compile/run/update counts,
last_compiled/run timestamps) kept in sync by MetricsService for O(1)
dashboard reads
- 10 admin endpoints under /api/admin/metrics/{overview, timeseries,
boards, board-diversity, top-users, top-projects, countries,
users/{id}, projects/{id}}
- POST /api/metrics/run for client-side run telemetry
- Country detection via cf-ipcountry header (no DB / no API calls)
- Auto-migrations in lifespan for legacy DBs
Frontend:
- recharts-powered Dashboard tab with KPI cards and 4 charts
- New Boards tab with per-family + per-FQBN breakdown
- Country column with flag emoji on Users tab
- Top countries card on Dashboard
- compileCode now forwards project_id; Run button reports via WS
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 05:46:52 +07:00
|
|
|
const currentProject = useProjectStore((s) => s.currentProject);
|
|
|
|
|
|
2026-05-13 00:26:33 +07:00
|
|
|
// 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;
|
|
|
|
|
|
2026-05-13 02:55:15 +07:00
|
|
|
// 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);
|
|
|
|
|
|
feat(metrics): add usage analytics dashboard with country tracking
Track per-user, per-project, and per-board usage to inform pricing tier
decisions. Adds an admin dashboard with KPIs (DAU/WAU/MAU, totals,
success rate), time-series charts for compiles/runs, board family +
FQBN breakdowns, "board diversity" pie chart (key freemium signal),
top users/projects, and per-country breakdown via Cloudflare's
CF-IPCountry header. Admin can now also view private projects.
Backend:
- New UsageEvent table (append-only event log with user_id, project_id,
event_type, board_fqbn/family, country, error_kind, duration_ms)
- Aggregate counters on User (total_compiles/runs/errors, last_active,
signup_country, last_country) and Project (compile/run/update counts,
last_compiled/run timestamps) kept in sync by MetricsService for O(1)
dashboard reads
- 10 admin endpoints under /api/admin/metrics/{overview, timeseries,
boards, board-diversity, top-users, top-projects, countries,
users/{id}, projects/{id}}
- POST /api/metrics/run for client-side run telemetry
- Country detection via cf-ipcountry header (no DB / no API calls)
- Auto-migrations in lifespan for legacy DBs
Frontend:
- recharts-powered Dashboard tab with KPI cards and 4 charts
- New Boards tab with per-family + per-FQBN breakdown
- Country column with flag emoji on Users tab
- Top countries card on Dashboard
- compileCode now forwards project_id; Run button reports via WS
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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],
|
|
|
|
|
);
|
2026-03-03 10:20:49 +07:00
|
|
|
const [compiling, setCompiling] = useState(false);
|
|
|
|
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
2026-03-05 08:05:23 +07:00
|
|
|
const [libManagerOpen, setLibManagerOpen] = useState(false);
|
2026-03-09 22:53:24 +07:00
|
|
|
const [pendingLibraries, setPendingLibraries] = useState<string[]>([]);
|
|
|
|
|
const [installModalOpen, setInstallModalOpen] = useState(false);
|
2026-03-07 10:14:35 +07:00
|
|
|
const importInputRef = useRef<HTMLInputElement>(null);
|
feat: load precompiled firmware files (.hex, .bin, .elf) directly
Closes #1
Add the ability to upload precompiled firmware files directly into the
emulator, bypassing the built-in compilation step. This enables users
with custom toolchains (ESP-IDF, PlatformIO, ASM workflows) to use
Velxio purely as an emulation/debugging environment.
New features:
- "Upload firmware" option in the overflow menu (accepts .hex, .bin, .elf)
- Automatic format detection from file extension and magic bytes
- ELF parser extracts PT_LOAD segments and detects target architecture
(AVR, ARM, RISC-V, Xtensa) from ELF e_machine header
- Architecture mismatch warnings logged when ELF target differs from
current board (non-blocking — upload proceeds anyway)
- Firmware routed to the correct simulator loader via existing
compileBoardProgram() — no simulator changes needed
Supported formats per board:
- AVR (Uno/Nano/Mega/ATtiny85): .hex (direct), .elf (parsed → HEX)
- RP2040 (Pico): .bin (direct), .elf (parsed → binary)
- ESP32-C3: .bin (direct), .hex, .elf (parsed → binary)
- ESP32/S3 (QEMU): .bin (direct), .elf (parsed → binary)
New file: frontend/src/utils/firmwareLoader.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 12:08:11 +07:00
|
|
|
const firmwareInputRef = useRef<HTMLInputElement>(null);
|
2026-03-16 10:12:33 +07:00
|
|
|
const toolbarRef = useRef<HTMLDivElement>(null);
|
2026-03-25 02:52:01 +07:00
|
|
|
const [missingLibHint, setMissingLibHint] = useState(false);
|
2026-03-16 10:12:33 +07:00
|
|
|
|
2026-04-29 12:33:59 +07:00
|
|
|
// Compile All / Run All — runs sequentially, logs to console (no dialog)
|
2026-03-16 00:04:01 +07:00
|
|
|
const [compileAllRunning, setCompileAllRunning] = useState(false);
|
2026-03-06 07:07:03 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
const addLog = useCallback(
|
|
|
|
|
(log: CompilationLog) => {
|
|
|
|
|
setCompileLogs((prev: CompilationLog[]) => [...prev, log]);
|
|
|
|
|
},
|
|
|
|
|
[setCompileLogs],
|
|
|
|
|
);
|
2026-03-03 10:20:49 +07:00
|
|
|
|
|
|
|
|
const handleCompile = async () => {
|
|
|
|
|
setCompiling(true);
|
|
|
|
|
setMessage(null);
|
2026-03-06 07:07:03 +07:00
|
|
|
setConsoleOpen(true);
|
2026-03-13 05:02:24 +07:00
|
|
|
trackCompileCode();
|
2026-03-06 07:07:03 +07:00
|
|
|
|
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
|
|
|
// ── Chip-program path ───────────────────────────────────────────────
|
2026-05-19 10:31:10 +07:00
|
|
|
// If the editor's active file is a chip-program file we don't compile
|
|
|
|
|
// Arduino code — we assemble/compile it into ROM bytes via
|
|
|
|
|
// /api/compile-rom and stash the result on every custom-chip component
|
|
|
|
|
// that points at this filename through its `programFile` property. The
|
|
|
|
|
// chip's emulator then reads the bytes on chip_setup via vx_rom_size /
|
|
|
|
|
// vx_rom_read.
|
|
|
|
|
//
|
|
|
|
|
// A file is "chip program" when EITHER its extension is unambiguous
|
|
|
|
|
// (.s/.asm/.hex/.bin) OR some custom-chip on the canvas has
|
|
|
|
|
// programFile === activeFile.name. The latter lets .c files route to
|
|
|
|
|
// SDCC instead of arduino-cli when wired to a CPU chip.
|
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 activeFile = files.find((f) => f.id === useEditorStore.getState().activeFileId);
|
2026-05-19 10:31:10 +07:00
|
|
|
const componentsForCompile = useSimulatorStore.getState().components;
|
|
|
|
|
const chipsBoundToFile = activeFile
|
|
|
|
|
? componentsForCompile.filter((c) => {
|
feat(chips): programmable retro CPU chips with external ROM
Adds a new way to use the retro CPU chips: write your program in a
project file (.s / .asm / .hex / .bin), click Compile, click Run, and
the same chip emulates whatever you wrote. Same chip + different ROMs =
mini PC, calculator, LED demo, Kill-the-Bit game, etc.
SDK:
- velxio-chip.h gets two new host imports:
uint32_t vx_rom_size(void);
void vx_rom_read(uint32_t off, uint8_t* dst, uint32_t len);
CPU-emulator chips call these in chip_setup to pull their program out
of the host's romBytes property.
Frontend runtime:
- ChipRuntime accepts opts.romBytes (Uint8Array) and exposes the new
imports, copying bytes into chip memory on vx_rom_read.
- CustomChipPart pulls component.properties.romBytes (base64) and passes
it through.
- Component registry declares three new custom-chip properties:
romBytes (base64), programFile (matching project filename), and
programTarget (cpu name).
New programmable bundled chip:
- frontend/src/components/customChips/examples/intel/i8080-cpu.{c,chip.json}
Same clean-room 8080 emulator as i8080-repl/i8080-counter, but ROM is
loaded externally via vx_rom_*. Has 8 LEDs, 8 buttons, UART, 16 KB RAM,
32 KB of external ROM.
Backend:
- New /api/compile-rom endpoint and rom_compile service that turns
chip-program source into ROM bytes. 8080 ASM is assembled by the
in-tree two-pass assembler (moved to backend/app/services/asm8080.py).
Intel HEX records are parsed; raw .bin is passed through. Future targets
(z80, 8086, 4004) are scaffolded but not wired yet.
EditorToolbar:
- Compile button detects when the active file is .s/.asm/.hex/.bin and
routes to compile-rom instead of arduino-cli. The compiled bytes are
injected into every custom-chip on the canvas whose programFile property
matches the active filename (or is empty).
Example:
- /examples/i8080-killbits loads Dean McDaniel's 1975 Kill-the-Bit on
the programmable i8080-cpu chip. killbits.s is shipped as a project
file alongside sketch.ino; the user clicks Compile then Run and the
LED walks across 8 outputs, buttons kill it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:38:18 +07:00
|
|
|
if (c.metadataId !== 'custom-chip') return false;
|
|
|
|
|
const prog = String((c.properties as any)?.programFile ?? '').trim();
|
2026-05-19 10:31:10 +07:00
|
|
|
return prog === activeFile.name;
|
|
|
|
|
})
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
if (activeFile && (isChipProgramFile(activeFile.name) || chipsBoundToFile.length > 0)) {
|
|
|
|
|
try {
|
|
|
|
|
const chips = chipsBoundToFile.length > 0
|
|
|
|
|
? chipsBoundToFile
|
|
|
|
|
: componentsForCompile.filter((c) => {
|
|
|
|
|
if (c.metadataId !== 'custom-chip') return false;
|
|
|
|
|
const prog = String((c.properties as any)?.programFile ?? '').trim();
|
|
|
|
|
return prog === '' || prog === activeFile.name;
|
|
|
|
|
});
|
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 (chips.length === 0) {
|
|
|
|
|
addLog({
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
type: 'error',
|
|
|
|
|
message: `No custom-chip on the canvas references ${activeFile.name}. Drop an "i8080 CPU" chip, or set its programFile property.`,
|
|
|
|
|
});
|
|
|
|
|
setMessage({ type: 'error', text: 'No matching custom-chip on canvas' });
|
|
|
|
|
setCompiling(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Resolve target from the first matching chip's chip.json.
|
|
|
|
|
const firstChipJson = String((chips[0].properties as any)?.chipJson ?? '{}');
|
|
|
|
|
const target = targetForChip(firstChipJson);
|
|
|
|
|
const fmt = formatForFile(activeFile.name);
|
|
|
|
|
addLog({
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
type: 'info',
|
|
|
|
|
message: `Assembling ${activeFile.name} (target=${target}, format=${fmt}) for ${chips.length} chip(s)...`,
|
|
|
|
|
});
|
|
|
|
|
const result = await compileRom(activeFile.content, target, fmt);
|
|
|
|
|
if (!result.success || !result.rom_base64) {
|
|
|
|
|
addLog({
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
type: 'error',
|
|
|
|
|
message: result.error || 'ROM compile failed',
|
|
|
|
|
});
|
|
|
|
|
if (result.stderr) {
|
|
|
|
|
addLog({ timestamp: new Date(), type: 'error', message: result.stderr });
|
|
|
|
|
}
|
|
|
|
|
setMessage({ type: 'error', text: result.error || 'ROM compile failed' });
|
|
|
|
|
setCompiling(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Inject into every matching chip's romBytes property.
|
|
|
|
|
const updateComponent = useSimulatorStore.getState().updateComponent;
|
|
|
|
|
for (const chip of chips) {
|
|
|
|
|
updateComponent(chip.id, {
|
|
|
|
|
properties: {
|
|
|
|
|
...(chip.properties as Record<string, unknown>),
|
|
|
|
|
romBytes: result.rom_base64,
|
|
|
|
|
programFile: activeFile.name,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
addLog({
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
type: 'success',
|
|
|
|
|
message: `ROM compiled: ${result.byte_size} bytes injected into ${chips.length} chip(s).`,
|
|
|
|
|
});
|
|
|
|
|
setMessage({
|
|
|
|
|
type: 'success',
|
|
|
|
|
text: `ROM ready (${result.byte_size} B). Hit Run.`,
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
const errMsg = e instanceof Error ? e.message : String(e);
|
|
|
|
|
addLog({ timestamp: new Date(), type: 'error', message: errMsg });
|
|
|
|
|
setMessage({ type: 'error', text: errMsg });
|
|
|
|
|
} finally {
|
|
|
|
|
setCompiling(false);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// ── End chip-program path ───────────────────────────────────────────
|
|
|
|
|
|
2026-03-13 09:39:04 +07:00
|
|
|
const kind = activeBoard?.boardKind;
|
|
|
|
|
|
|
|
|
|
// 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)) {
|
2026-04-22 02:45:45 +07:00
|
|
|
addLog({
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
type: 'info',
|
|
|
|
|
message: 'Raspberry Pi 3B: no compilation needed — run Python scripts directly.',
|
|
|
|
|
});
|
2026-03-13 09:39:04 +07:00
|
|
|
setMessage({ type: 'success', text: 'Ready (no compilation needed)' });
|
|
|
|
|
setCompiling(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 07:27:41 +07:00
|
|
|
// MicroPython mode — no backend compilation needed
|
|
|
|
|
if (activeBoard?.languageMode === 'micropython' && activeBoardId) {
|
2026-04-22 02:45:45 +07:00
|
|
|
addLog({
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
type: 'info',
|
|
|
|
|
message: 'MicroPython: loading firmware and user files...',
|
|
|
|
|
});
|
2026-03-30 07:27:41 +07:00
|
|
|
try {
|
|
|
|
|
const groupFiles = useEditorStore.getState().getGroupFiles(activeBoard.activeFileGroupId);
|
|
|
|
|
const pyFiles = groupFiles.map((f) => ({ name: f.name, content: f.content }));
|
|
|
|
|
await loadMicroPythonProgram(activeBoardId, pyFiles);
|
2026-04-22 02:45:45 +07:00
|
|
|
addLog({
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
type: 'success',
|
|
|
|
|
message: 'MicroPython firmware loaded successfully',
|
|
|
|
|
});
|
2026-03-30 07:27:41 +07:00
|
|
|
setMessage({ type: 'success', text: 'MicroPython ready' });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
const errMsg = err instanceof Error ? err.message : 'Failed to load MicroPython';
|
|
|
|
|
addLog({ timestamp: new Date(), type: 'error', message: errMsg });
|
|
|
|
|
setMessage({ type: 'error', text: errMsg });
|
|
|
|
|
} finally {
|
|
|
|
|
setCompiling(false);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 09:39:04 +07:00
|
|
|
const fqbn = kind ? BOARD_KIND_FQBN[kind] : null;
|
|
|
|
|
const boardLabel = kind ? BOARD_KIND_LABELS[kind] : 'Unknown';
|
|
|
|
|
|
|
|
|
|
if (!fqbn) {
|
|
|
|
|
addLog({ timestamp: new Date(), type: 'error', message: `No FQBN for board kind: ${kind}` });
|
|
|
|
|
setMessage({ type: 'error', text: 'Unknown board' });
|
|
|
|
|
setCompiling(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-06 07:07:03 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
addLog({
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
type: 'info',
|
|
|
|
|
message: `Starting compilation for ${boardLabel} (${fqbn})...`,
|
|
|
|
|
});
|
2026-03-06 07:07:03 +07:00
|
|
|
|
2026-03-03 10:20:49 +07:00
|
|
|
try {
|
2026-04-16 18:39:47 +07:00
|
|
|
const groupFiles = activeBoard?.activeFileGroupId
|
|
|
|
|
? useEditorStore.getState().getGroupFiles(activeBoard.activeFileGroupId)
|
|
|
|
|
: files;
|
2026-04-22 02:45:45 +07:00
|
|
|
const sketchFiles = (groupFiles.length > 0 ? groupFiles : files).map((f) => ({
|
|
|
|
|
name: f.name,
|
|
|
|
|
content: f.content,
|
|
|
|
|
}));
|
2026-03-06 07:07:03 +07:00
|
|
|
|
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,
|
|
|
|
|
})),
|
|
|
|
|
]);
|
|
|
|
|
},
|
2026-05-18 12:38:51 +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,
|
|
|
|
|
},
|
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.
|
2026-03-06 07:07:03 +07:00
|
|
|
const resultLogs = parseCompileResult(result, boardLabel);
|
2026-03-06 07:07:10 +07:00
|
|
|
setCompileLogs((prev: CompilationLog[]) => [...prev, ...resultLogs]);
|
2026-03-06 07:07:03 +07:00
|
|
|
|
2026-03-05 05:28:33 +07:00
|
|
|
if (result.success) {
|
2026-03-13 09:39:04 +07:00
|
|
|
const program = result.hex_content ?? result.binary_content ?? null;
|
|
|
|
|
if (program && activeBoardId) {
|
|
|
|
|
compileBoardProgram(activeBoardId, program);
|
2026-04-06 03:14:40 +07:00
|
|
|
if (result.has_wifi !== undefined) {
|
|
|
|
|
updateBoard(activeBoardId, { hasWifi: result.has_wifi });
|
|
|
|
|
}
|
2026-03-05 05:28:33 +07:00
|
|
|
}
|
2026-03-13 09:39:04 +07:00
|
|
|
setMessage({ type: 'success', text: 'Compiled successfully' });
|
2026-03-29 11:46:07 +07:00
|
|
|
markCompiled();
|
2026-03-25 02:52:01 +07:00
|
|
|
setMissingLibHint(false);
|
2026-03-03 10:20:49 +07:00
|
|
|
} else {
|
2026-03-25 02:52:01 +07:00
|
|
|
const errText = result.error || result.stderr || 'Compile failed';
|
|
|
|
|
setMessage({ type: 'error', text: errText });
|
|
|
|
|
// Detect missing library errors — common patterns:
|
|
|
|
|
// "No such file or directory" for #include, "fatal error: XXX.h"
|
2026-04-22 02:45:45 +07:00
|
|
|
const looksLikeMissingLib =
|
|
|
|
|
/No such file or directory|fatal error:.*\.h|library not found/i.test(errText);
|
2026-03-25 02:52:01 +07:00
|
|
|
setMissingLibHint(looksLikeMissingLib);
|
2026-03-03 10:20:49 +07:00
|
|
|
}
|
|
|
|
|
} catch (err) {
|
2026-03-06 07:07:03 +07:00
|
|
|
const errMsg = err instanceof Error ? err.message : 'Compile failed';
|
|
|
|
|
addLog({ timestamp: new Date(), type: 'error', message: errMsg });
|
|
|
|
|
setMessage({ type: 'error', text: errMsg });
|
2026-03-03 10:20:49 +07:00
|
|
|
} finally {
|
|
|
|
|
setCompiling(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-29 11:46:07 +07:00
|
|
|
// Track whether we should auto-run after compilation completes
|
|
|
|
|
const autoRunAfterCompile = useRef(false);
|
|
|
|
|
|
2026-05-13 02:55:15 +07:00
|
|
|
/**
|
|
|
|
|
* 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(async (): Promise<VerificationResult | null> => {
|
|
|
|
|
try {
|
|
|
|
|
const sim = useSimulatorStore.getState();
|
|
|
|
|
// Skip if the circuit hasn't got anything analysable on it yet.
|
|
|
|
|
const hasSource = sim.components.some(
|
|
|
|
|
(c) => c.metadataId.startsWith('signal-generator') || c.metadataId.startsWith('battery'),
|
|
|
|
|
);
|
|
|
|
|
if (!hasSource && sim.boards.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
const snap = {
|
|
|
|
|
components: sim.components.map((c) => ({
|
|
|
|
|
id: c.id,
|
|
|
|
|
metadataId: c.metadataId,
|
|
|
|
|
properties: c.properties,
|
|
|
|
|
})),
|
|
|
|
|
wires: sim.wires,
|
|
|
|
|
boards: sim.boards.map((b) => {
|
2026-05-18 09:41:19 +07:00
|
|
|
// Realistic pre-flight: simulate the WORST CASE — every digital
|
|
|
|
|
// pin connected to a load is forced HIGH at the board's vcc.
|
|
|
|
|
// This is what we want because the user's sketch WILL eventually
|
|
|
|
|
// do `digitalWrite(pin, HIGH)` (otherwise why is the LED wired?).
|
|
|
|
|
// Testing idle state would never flag a missing series resistor
|
|
|
|
|
// because the LED draws zero current when its pin is LOW.
|
|
|
|
|
//
|
|
|
|
|
// Caveat: pins wired only to inputs (e.g. a pull-up resistor +
|
|
|
|
|
// button) get over-driven here too. The verifier rules are
|
|
|
|
|
// already tolerant — a properly-spec'd pull-up sees minimal
|
|
|
|
|
// current and doesn't trip overcurrent / overpower. A circuit
|
|
|
|
|
// that would actually fault under HIGH is flagged correctly.
|
2026-05-13 02:55:15 +07:00
|
|
|
const pinStates: Record<string, PinSourceState> = {};
|
|
|
|
|
const group = BOARD_PIN_GROUPS[b.boardKind] ?? BOARD_PIN_GROUPS.default;
|
2026-05-18 09:41:19 +07:00
|
|
|
const wiredPinNames = new Set<string>();
|
|
|
|
|
for (const w of sim.wires) {
|
|
|
|
|
if (w.start.componentId === b.id) wiredPinNames.add(w.start.pinName);
|
|
|
|
|
if (w.end.componentId === b.id) wiredPinNames.add(w.end.pinName);
|
|
|
|
|
}
|
|
|
|
|
for (const pinName of wiredPinNames) {
|
|
|
|
|
// Skip GND / power-rail pin names — they belong to the rail
|
|
|
|
|
// groups and don't need to be re-asserted as digital sources.
|
|
|
|
|
if (group.gnd.includes(pinName)) continue;
|
|
|
|
|
if (group.vcc_pins.includes(pinName)) continue;
|
|
|
|
|
const arduinoPin = Number.parseInt(pinName, 10);
|
|
|
|
|
// Skip pins we can't identify as a digital GPIO (e.g.
|
|
|
|
|
// 'AREF', 'RESET', 'TX', 'RX' on some boards). Those are
|
|
|
|
|
// either rail-ish or non-driven by the sketch.
|
|
|
|
|
if (Number.isNaN(arduinoPin)) continue;
|
|
|
|
|
pinStates[pinName] = { type: 'digital', v: group.vcc };
|
2026-05-13 02:55:15 +07:00
|
|
|
}
|
|
|
|
|
return { id: b.id, boardKind: b.boardKind, pinStates };
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
const input = buildInputFromStore(snap);
|
|
|
|
|
return await verifyCircuit(input);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.warn('[verifyCircuit] failed', err);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns true if the caller should proceed inline. If the verifier finds
|
|
|
|
|
* errors we 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 — they surface inline via `setMessage` and the run continues.
|
|
|
|
|
*/
|
|
|
|
|
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;
|
|
|
|
|
if (result.errors.length === 0) {
|
|
|
|
|
// Warnings only — non-blocking. Surface inline and continue.
|
|
|
|
|
const summary = result.warnings
|
|
|
|
|
.slice(0, 3)
|
|
|
|
|
.map((w) => w.message)
|
|
|
|
|
.join(' • ');
|
|
|
|
|
const more = result.warnings.length > 3 ? ` (+${result.warnings.length - 3} more)` : '';
|
|
|
|
|
setMessage({
|
|
|
|
|
type: 'error',
|
|
|
|
|
text: `${result.warnings.length} circuit warning${result.warnings.length === 1 ? '' : 's'}: ${summary}${more}`,
|
|
|
|
|
});
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
// Errors → block until the user explicitly chooses Run Anyway.
|
|
|
|
|
pendingRunRef.current = resume;
|
|
|
|
|
setVerification(result);
|
|
|
|
|
return false;
|
|
|
|
|
},
|
|
|
|
|
[runVerification],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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 });
|
2026-05-13 00:26:33 +07:00
|
|
|
|
2026-05-13 02:55:15 +07:00
|
|
|
// 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) {
|
|
|
|
|
const ok = await checkOrBlock(() => handleRun(true));
|
|
|
|
|
if (!ok) return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 00:26:33 +07:00
|
|
|
// Board-less circuits (SPICE-only digital / analog gallery) have no MCU
|
|
|
|
|
// to start. Resuming the electrical solver replays any switch toggles
|
|
|
|
|
// captured while paused so the canvas catches up instantly.
|
|
|
|
|
if (isBoardless) {
|
|
|
|
|
setElectricalPaused(false);
|
|
|
|
|
setMessage(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 09:39:04 +07:00
|
|
|
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,
|
|
|
|
|
});
|
2026-03-30 07:27:41 +07:00
|
|
|
|
2026-04-13 09:55:11 +07:00
|
|
|
// MicroPython mode: stop any running session first, then reload firmware + start
|
2026-03-30 07:27:41 +07:00
|
|
|
if (board?.languageMode === 'micropython') {
|
|
|
|
|
trackRunSimulation(board.boardKind);
|
feat(metrics): add usage analytics dashboard with country tracking
Track per-user, per-project, and per-board usage to inform pricing tier
decisions. Adds an admin dashboard with KPIs (DAU/WAU/MAU, totals,
success rate), time-series charts for compiles/runs, board family +
FQBN breakdowns, "board diversity" pie chart (key freemium signal),
top users/projects, and per-country breakdown via Cloudflare's
CF-IPCountry header. Admin can now also view private projects.
Backend:
- New UsageEvent table (append-only event log with user_id, project_id,
event_type, board_fqbn/family, country, error_kind, duration_ms)
- Aggregate counters on User (total_compiles/runs/errors, last_active,
signup_country, last_country) and Project (compile/run/update counts,
last_compiled/run timestamps) kept in sync by MetricsService for O(1)
dashboard reads
- 10 admin endpoints under /api/admin/metrics/{overview, timeseries,
boards, board-diversity, top-users, top-projects, countries,
users/{id}, projects/{id}}
- POST /api/metrics/run for client-side run telemetry
- Country detection via cf-ipcountry header (no DB / no API calls)
- Auto-migrations in lifespan for legacy DBs
Frontend:
- recharts-powered Dashboard tab with KPI cards and 4 charts
- New Boards tab with per-family + per-FQBN breakdown
- Country column with flag emoji on Users tab
- Top countries card on Dashboard
- compileCode now forwards project_id; Run button reports via WS
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 05:46:52 +07:00
|
|
|
reportRun(board.boardKind);
|
2026-04-13 09:55:11 +07:00
|
|
|
|
|
|
|
|
// 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.
|
2026-04-22 02:45:45 +07:00
|
|
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
2026-04-13 09:55:11 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setCompiling(true);
|
|
|
|
|
setMessage(null);
|
2026-04-22 02:45:45 +07:00
|
|
|
addLog({
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
type: 'info',
|
|
|
|
|
message: 'MicroPython: loading firmware and user files...',
|
|
|
|
|
});
|
2026-04-13 09:55:11 +07:00
|
|
|
try {
|
|
|
|
|
const groupFiles = useEditorStore.getState().getGroupFiles(board.activeFileGroupId);
|
|
|
|
|
const pyFiles = groupFiles.map((f) => ({ name: f.name, content: f.content }));
|
|
|
|
|
await loadMicroPythonProgram(activeBoardId, pyFiles);
|
2026-04-22 02:45:45 +07:00
|
|
|
addLog({
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
type: 'success',
|
|
|
|
|
message: 'MicroPython firmware loaded',
|
|
|
|
|
});
|
2026-04-13 09:55:11 +07:00
|
|
|
} catch (err) {
|
|
|
|
|
const errMsg = err instanceof Error ? err.message : 'Failed to load MicroPython';
|
|
|
|
|
addLog({ timestamp: new Date(), type: 'error', message: errMsg });
|
|
|
|
|
setMessage({ type: 'error', text: errMsg });
|
2026-03-30 07:27:41 +07:00
|
|
|
setCompiling(false);
|
2026-04-13 09:55:11 +07:00
|
|
|
return;
|
2026-03-30 07:27:41 +07:00
|
|
|
}
|
2026-04-13 09:55:11 +07:00
|
|
|
setCompiling(false);
|
2026-03-30 07:27:41 +07:00
|
|
|
startBoard(activeBoardId);
|
|
|
|
|
setMessage(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
const isQemuBoard =
|
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
|
|
|
board?.boardKind && isPiBoardKind(board.boardKind) ||
|
2026-04-22 02:45:45 +07:00
|
|
|
board?.boardKind === 'esp32' ||
|
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
|
|
|
board?.boardKind === 'esp32-s3' ||
|
|
|
|
|
board?.boardKind === 'esp32-cam' ||
|
|
|
|
|
board?.boardKind === 'esp32-c3' ||
|
|
|
|
|
board?.boardKind === 'esp32-devkit-c-v4' ||
|
|
|
|
|
board?.boardKind === 'wemos-lolin32-lite' ||
|
|
|
|
|
board?.boardKind === 'xiao-esp32-s3' ||
|
|
|
|
|
board?.boardKind === 'arduino-nano-esp32' ||
|
|
|
|
|
board?.boardKind === 'xiao-esp32-c3' ||
|
|
|
|
|
board?.boardKind === 'aitewinrobot-esp32c3-supermini';
|
2026-03-29 11:46:07 +07:00
|
|
|
|
2026-04-01 23:47:15 +07:00
|
|
|
// QEMU boards: auto-compile if no firmware available yet
|
2026-03-29 11:46:07 +07:00
|
|
|
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');
|
2026-04-01 23:47:15 +07:00
|
|
|
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');
|
2026-04-01 23:47:15 +07:00
|
|
|
autoRunAfterCompile.current = true;
|
|
|
|
|
await handleCompile();
|
2026-04-22 02:45:45 +07:00
|
|
|
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) {
|
2026-04-01 23:47:15 +07:00
|
|
|
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 {
|
2026-05-23 13:39:26 +07:00
|
|
|
// 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');
|
2026-05-23 13:39:26 +07:00
|
|
|
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
|
|
|
}
|
2026-04-01 23:47:15 +07:00
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-25 13:06:38 +07:00
|
|
|
trackRunSimulation(board?.boardKind);
|
feat(metrics): add usage analytics dashboard with country tracking
Track per-user, per-project, and per-board usage to inform pricing tier
decisions. Adds an admin dashboard with KPIs (DAU/WAU/MAU, totals,
success rate), time-series charts for compiles/runs, board family +
FQBN breakdowns, "board diversity" pie chart (key freemium signal),
top users/projects, and per-country breakdown via Cloudflare's
CF-IPCountry header. Admin can now also view private projects.
Backend:
- New UsageEvent table (append-only event log with user_id, project_id,
event_type, board_fqbn/family, country, error_kind, duration_ms)
- Aggregate counters on User (total_compiles/runs/errors, last_active,
signup_country, last_country) and Project (compile/run/update counts,
last_compiled/run timestamps) kept in sync by MetricsService for O(1)
dashboard reads
- 10 admin endpoints under /api/admin/metrics/{overview, timeseries,
boards, board-diversity, top-users, top-projects, countries,
users/{id}, projects/{id}}
- POST /api/metrics/run for client-side run telemetry
- Country detection via cf-ipcountry header (no DB / no API calls)
- Auto-migrations in lifespan for legacy DBs
Frontend:
- recharts-powered Dashboard tab with KPI cards and 4 charts
- New Boards tab with per-family + per-FQBN breakdown
- Country column with flag emoji on Users tab
- Top countries card on Dashboard
- compileCode now forwards project_id; Run button reports via WS
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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);
|
2026-03-13 09:39:04 +07:00
|
|
|
startBoard(activeBoardId);
|
|
|
|
|
setMessage(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-29 11:46:07 +07:00
|
|
|
|
|
|
|
|
// 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
|
2026-04-22 02:45:45 +07:00
|
|
|
const updatedBoard = useSimulatorStore
|
|
|
|
|
.getState()
|
|
|
|
|
.boards.find((b) => b.id === activeBoardId);
|
2026-03-29 11:46:07 +07:00
|
|
|
if (autoRunAfterCompile.current && updatedBoard?.compiledProgram) {
|
|
|
|
|
autoRunAfterCompile.current = false;
|
|
|
|
|
trackRunSimulation(updatedBoard.boardKind);
|
feat(metrics): add usage analytics dashboard with country tracking
Track per-user, per-project, and per-board usage to inform pricing tier
decisions. Adds an admin dashboard with KPIs (DAU/WAU/MAU, totals,
success rate), time-series charts for compiles/runs, board family +
FQBN breakdowns, "board diversity" pie chart (key freemium signal),
top users/projects, and per-country breakdown via Cloudflare's
CF-IPCountry header. Admin can now also view private projects.
Backend:
- New UsageEvent table (append-only event log with user_id, project_id,
event_type, board_fqbn/family, country, error_kind, duration_ms)
- Aggregate counters on User (total_compiles/runs/errors, last_active,
signup_country, last_country) and Project (compile/run/update counts,
last_compiled/run timestamps) kept in sync by MetricsService for O(1)
dashboard reads
- 10 admin endpoints under /api/admin/metrics/{overview, timeseries,
boards, board-diversity, top-users, top-projects, countries,
users/{id}, projects/{id}}
- POST /api/metrics/run for client-side run telemetry
- Country detection via cf-ipcountry header (no DB / no API calls)
- Auto-migrations in lifespan for legacy DBs
Frontend:
- recharts-powered Dashboard tab with KPI cards and 4 charts
- New Boards tab with per-family + per-FQBN breakdown
- Country column with flag emoji on Users tab
- Top countries card on Dashboard
- compileCode now forwards project_id; Run button reports via WS
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 05:46:52 +07:00
|
|
|
reportRun(updatedBoard.boardKind);
|
2026-03-29 11:46:07 +07:00
|
|
|
startBoard(activeBoardId);
|
|
|
|
|
setMessage(null);
|
|
|
|
|
} else {
|
|
|
|
|
autoRunAfterCompile.current = false;
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trackRunSimulation(board?.boardKind);
|
feat(metrics): add usage analytics dashboard with country tracking
Track per-user, per-project, and per-board usage to inform pricing tier
decisions. Adds an admin dashboard with KPIs (DAU/WAU/MAU, totals,
success rate), time-series charts for compiles/runs, board family +
FQBN breakdowns, "board diversity" pie chart (key freemium signal),
top users/projects, and per-country breakdown via Cloudflare's
CF-IPCountry header. Admin can now also view private projects.
Backend:
- New UsageEvent table (append-only event log with user_id, project_id,
event_type, board_fqbn/family, country, error_kind, duration_ms)
- Aggregate counters on User (total_compiles/runs/errors, last_active,
signup_country, last_country) and Project (compile/run/update counts,
last_compiled/run timestamps) kept in sync by MetricsService for O(1)
dashboard reads
- 10 admin endpoints under /api/admin/metrics/{overview, timeseries,
boards, board-diversity, top-users, top-projects, countries,
users/{id}, projects/{id}}
- POST /api/metrics/run for client-side run telemetry
- Country detection via cf-ipcountry header (no DB / no API calls)
- Auto-migrations in lifespan for legacy DBs
Frontend:
- recharts-powered Dashboard tab with KPI cards and 4 charts
- New Boards tab with per-family + per-FQBN breakdown
- Country column with flag emoji on Users tab
- Top countries card on Dashboard
- compileCode now forwards project_id; Run button reports via WS
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 05:46:52 +07:00
|
|
|
reportRun(board?.boardKind);
|
2026-03-29 11:46:07 +07:00
|
|
|
startBoard(activeBoardId);
|
|
|
|
|
setMessage(null);
|
|
|
|
|
return;
|
2026-03-13 09:39:04 +07:00
|
|
|
}
|
2026-03-29 11:46:07 +07:00
|
|
|
|
|
|
|
|
// Legacy fallback
|
|
|
|
|
if (!compiledHex || codeChangedSinceLastCompile) {
|
|
|
|
|
autoRunAfterCompile.current = true;
|
|
|
|
|
await handleCompile();
|
|
|
|
|
const hex = useSimulatorStore.getState().compiledHex;
|
|
|
|
|
if (autoRunAfterCompile.current && hex) {
|
|
|
|
|
autoRunAfterCompile.current = false;
|
|
|
|
|
trackRunSimulation();
|
feat(metrics): add usage analytics dashboard with country tracking
Track per-user, per-project, and per-board usage to inform pricing tier
decisions. Adds an admin dashboard with KPIs (DAU/WAU/MAU, totals,
success rate), time-series charts for compiles/runs, board family +
FQBN breakdowns, "board diversity" pie chart (key freemium signal),
top users/projects, and per-country breakdown via Cloudflare's
CF-IPCountry header. Admin can now also view private projects.
Backend:
- New UsageEvent table (append-only event log with user_id, project_id,
event_type, board_fqbn/family, country, error_kind, duration_ms)
- Aggregate counters on User (total_compiles/runs/errors, last_active,
signup_country, last_country) and Project (compile/run/update counts,
last_compiled/run timestamps) kept in sync by MetricsService for O(1)
dashboard reads
- 10 admin endpoints under /api/admin/metrics/{overview, timeseries,
boards, board-diversity, top-users, top-projects, countries,
users/{id}, projects/{id}}
- POST /api/metrics/run for client-side run telemetry
- Country detection via cf-ipcountry header (no DB / no API calls)
- Auto-migrations in lifespan for legacy DBs
Frontend:
- recharts-powered Dashboard tab with KPI cards and 4 charts
- New Boards tab with per-family + per-FQBN breakdown
- Country column with flag emoji on Users tab
- Top countries card on Dashboard
- compileCode now forwards project_id; Run button reports via WS
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 05:46:52 +07:00
|
|
|
reportRun(undefined);
|
2026-03-29 11:46:07 +07:00
|
|
|
startSimulation();
|
|
|
|
|
setMessage(null);
|
|
|
|
|
} else {
|
|
|
|
|
autoRunAfterCompile.current = false;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2026-03-13 05:02:24 +07:00
|
|
|
trackRunSimulation();
|
feat(metrics): add usage analytics dashboard with country tracking
Track per-user, per-project, and per-board usage to inform pricing tier
decisions. Adds an admin dashboard with KPIs (DAU/WAU/MAU, totals,
success rate), time-series charts for compiles/runs, board family +
FQBN breakdowns, "board diversity" pie chart (key freemium signal),
top users/projects, and per-country breakdown via Cloudflare's
CF-IPCountry header. Admin can now also view private projects.
Backend:
- New UsageEvent table (append-only event log with user_id, project_id,
event_type, board_fqbn/family, country, error_kind, duration_ms)
- Aggregate counters on User (total_compiles/runs/errors, last_active,
signup_country, last_country) and Project (compile/run/update counts,
last_compiled/run timestamps) kept in sync by MetricsService for O(1)
dashboard reads
- 10 admin endpoints under /api/admin/metrics/{overview, timeseries,
boards, board-diversity, top-users, top-projects, countries,
users/{id}, projects/{id}}
- POST /api/metrics/run for client-side run telemetry
- Country detection via cf-ipcountry header (no DB / no API calls)
- Auto-migrations in lifespan for legacy DBs
Frontend:
- recharts-powered Dashboard tab with KPI cards and 4 charts
- New Boards tab with per-family + per-FQBN breakdown
- Country column with flag emoji on Users tab
- Top countries card on Dashboard
- compileCode now forwards project_id; Run button reports via WS
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 05:46:52 +07:00
|
|
|
reportRun(undefined);
|
2026-03-03 10:20:49 +07:00
|
|
|
startSimulation();
|
2026-03-05 08:05:23 +07:00
|
|
|
setMessage(null);
|
2026-03-03 10:20:49 +07:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleStop = () => {
|
2026-03-25 13:06:38 +07:00
|
|
|
trackStopSimulation();
|
2026-05-13 00:26:33 +07:00
|
|
|
if (isBoardless) {
|
|
|
|
|
// Freeze the SPICE solver — every LED stays at its current brightness
|
|
|
|
|
// and switch clicks stop re-triggering ngspice until the user hits Run.
|
|
|
|
|
setElectricalPaused(true);
|
|
|
|
|
setMessage(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-13 09:39:04 +07:00
|
|
|
if (activeBoardId) stopBoard(activeBoardId);
|
|
|
|
|
else stopSimulation();
|
2026-03-05 08:05:23 +07:00
|
|
|
setMessage(null);
|
2026-03-03 10:20:49 +07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleReset = () => {
|
2026-03-25 13:06:38 +07:00
|
|
|
trackResetSimulation();
|
2026-03-13 09:39:04 +07:00
|
|
|
if (activeBoardId) resetBoard(activeBoardId);
|
|
|
|
|
else resetSimulation();
|
2026-03-05 08:05:23 +07:00
|
|
|
setMessage(null);
|
2026-03-03 10:20:49 +07:00
|
|
|
};
|
|
|
|
|
|
2026-04-29 12:33:59 +07:00
|
|
|
/**
|
|
|
|
|
* 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 }> => {
|
2026-03-16 00:04:01 +07:00
|
|
|
const boardsList = useSimulatorStore.getState().boards;
|
2026-04-29 12:33:59 +07:00
|
|
|
if (boardsList.length === 0) return { ok: 0, failed: 0 };
|
|
|
|
|
|
2026-03-16 00:04:01 +07:00
|
|
|
setCompileAllRunning(true);
|
2026-04-29 12:33:59 +07:00
|
|
|
setConsoleOpen(true);
|
|
|
|
|
addLog({
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
type: 'info',
|
|
|
|
|
message: `Compiling all ${boardsList.length} board${boardsList.length === 1 ? '' : 's'}...`,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let ok = 0;
|
|
|
|
|
let failed = 0;
|
2026-03-16 00:04:01 +07:00
|
|
|
|
|
|
|
|
for (const board of boardsList) {
|
2026-04-29 12:33:59 +07:00
|
|
|
const label = BOARD_KIND_LABELS[board.boardKind] ?? board.boardKind;
|
2026-03-16 00:04:01 +07:00
|
|
|
|
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)) {
|
2026-04-29 12:33:59 +07:00
|
|
|
addLog({
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
type: 'info',
|
|
|
|
|
message: `${label}: skipped (no compilation needed)`,
|
|
|
|
|
});
|
|
|
|
|
ok++;
|
2026-03-16 00:04:01 +07:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fqbn = BOARD_KIND_FQBN[board.boardKind];
|
|
|
|
|
if (!fqbn) {
|
2026-04-29 12:33:59 +07:00
|
|
|
addLog({
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
type: 'error',
|
|
|
|
|
message: `${label}: no FQBN configured`,
|
|
|
|
|
});
|
|
|
|
|
failed++;
|
2026-03-16 00:04:01 +07:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 12:33:59 +07:00
|
|
|
addLog({ timestamp: new Date(), type: 'info', message: `${label}: compiling...` });
|
2026-03-16 00:04:01 +07:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const groupFiles = useEditorStore.getState().getGroupFiles(board.activeFileGroupId);
|
|
|
|
|
const sketchFiles = groupFiles.map((f) => ({ name: f.name, content: f.content }));
|
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,
|
|
|
|
|
...newLines.map((line) => ({
|
|
|
|
|
timestamp: now,
|
|
|
|
|
type: 'info' as const,
|
|
|
|
|
message: `${label}: ${line}`,
|
|
|
|
|
})),
|
|
|
|
|
]);
|
|
|
|
|
},
|
2026-05-18 12:38:51 +07:00
|
|
|
{ boardOptions: board.boardOptions, spiffsFiles: board.spiffsFiles },
|
2026-05-10 04:36:58 +07:00
|
|
|
);
|
|
|
|
|
|
2026-04-29 12:33:59 +07:00
|
|
|
const resultLogs = parseCompileResult(result, label);
|
|
|
|
|
setCompileLogs((prev: CompilationLog[]) => [...prev, ...resultLogs]);
|
2026-03-16 00:04:01 +07:00
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
const program = result.hex_content ?? result.binary_content ?? null;
|
2026-04-06 03:14:40 +07:00
|
|
|
if (program) {
|
|
|
|
|
compileBoardProgram(board.id, program);
|
|
|
|
|
if (result.has_wifi !== undefined) {
|
|
|
|
|
updateBoard(board.id, { hasWifi: result.has_wifi });
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-29 12:33:59 +07:00
|
|
|
ok++;
|
2026-03-16 00:04:01 +07:00
|
|
|
} else {
|
2026-04-29 12:33:59 +07:00
|
|
|
failed++;
|
2026-03-16 00:04:01 +07:00
|
|
|
}
|
|
|
|
|
} catch (err) {
|
2026-04-29 12:33:59 +07:00
|
|
|
addLog({
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
type: 'error',
|
|
|
|
|
message: `${label}: ${err instanceof Error ? err.message : String(err)}`,
|
|
|
|
|
});
|
|
|
|
|
failed++;
|
2026-03-16 00:04:01 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 12:33:59 +07:00
|
|
|
addLog({
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
type: ok > 0 && failed === 0 ? 'success' : failed > 0 ? 'error' : 'info',
|
|
|
|
|
message: `Done — ${ok} succeeded, ${failed} failed`,
|
|
|
|
|
});
|
|
|
|
|
if (ok > 0 && failed === 0) markCompiled();
|
2026-03-16 00:04:01 +07:00
|
|
|
setCompileAllRunning(false);
|
2026-04-29 12:33:59 +07:00
|
|
|
return { ok, failed };
|
2026-03-16 00:04:01 +07:00
|
|
|
};
|
|
|
|
|
|
2026-04-29 12:33:59 +07:00
|
|
|
const handleCompileAll = () => {
|
|
|
|
|
trackCompileCode();
|
|
|
|
|
void compileAllBoards();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** Run All = compile all (if needed) + start every board, mirroring single Run. */
|
|
|
|
|
const handleRunAll = async () => {
|
2026-03-16 00:04:01 +07:00
|
|
|
const boardsList = useSimulatorStore.getState().boards;
|
2026-04-29 12:33:59 +07:00
|
|
|
if (boardsList.length === 0) return;
|
|
|
|
|
|
|
|
|
|
// Compile if anything is missing a program or code changed since last compile
|
|
|
|
|
const needsCompile =
|
|
|
|
|
codeChangedSinceLastCompile ||
|
|
|
|
|
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) &&
|
2026-04-29 12:33:59 +07:00
|
|
|
b.languageMode !== 'micropython' &&
|
|
|
|
|
!b.compiledProgram,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (needsCompile) {
|
|
|
|
|
const { failed } = await compileAllBoards();
|
|
|
|
|
if (failed > 0) return; // Don't start anything if any board failed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Refresh list after compile (compiledProgram may have changed)
|
|
|
|
|
const refreshed = useSimulatorStore.getState().boards;
|
|
|
|
|
for (const board of refreshed) {
|
|
|
|
|
if (board.running) continue;
|
2026-04-22 02:45:45 +07:00
|
|
|
const isQemu =
|
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(board.boardKind) ||
|
2026-04-22 02:45:45 +07:00
|
|
|
board.boardKind === 'esp32' ||
|
|
|
|
|
board.boardKind === 'esp32-s3';
|
2026-04-29 12:33:59 +07:00
|
|
|
if (isQemu || board.compiledProgram || board.languageMode === 'micropython') {
|
|
|
|
|
trackRunSimulation(board.boardKind);
|
feat(metrics): add usage analytics dashboard with country tracking
Track per-user, per-project, and per-board usage to inform pricing tier
decisions. Adds an admin dashboard with KPIs (DAU/WAU/MAU, totals,
success rate), time-series charts for compiles/runs, board family +
FQBN breakdowns, "board diversity" pie chart (key freemium signal),
top users/projects, and per-country breakdown via Cloudflare's
CF-IPCountry header. Admin can now also view private projects.
Backend:
- New UsageEvent table (append-only event log with user_id, project_id,
event_type, board_fqbn/family, country, error_kind, duration_ms)
- Aggregate counters on User (total_compiles/runs/errors, last_active,
signup_country, last_country) and Project (compile/run/update counts,
last_compiled/run timestamps) kept in sync by MetricsService for O(1)
dashboard reads
- 10 admin endpoints under /api/admin/metrics/{overview, timeseries,
boards, board-diversity, top-users, top-projects, countries,
users/{id}, projects/{id}}
- POST /api/metrics/run for client-side run telemetry
- Country detection via cf-ipcountry header (no DB / no API calls)
- Auto-migrations in lifespan for legacy DBs
Frontend:
- recharts-powered Dashboard tab with KPI cards and 4 charts
- New Boards tab with per-family + per-FQBN breakdown
- Country column with flag emoji on Users tab
- Top countries card on Dashboard
- compileCode now forwards project_id; Run button reports via WS
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 05:46:52 +07:00
|
|
|
reportRun(board.boardKind);
|
2026-03-16 00:04:01 +07:00
|
|
|
startBoard(board.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-07 10:14:35 +07:00
|
|
|
const handleExport = async () => {
|
|
|
|
|
try {
|
2026-04-22 02:45:45 +07:00
|
|
|
const {
|
|
|
|
|
components,
|
|
|
|
|
wires,
|
|
|
|
|
boardPosition,
|
|
|
|
|
boardType: legacyBoardType,
|
|
|
|
|
} = useSimulatorStore.getState();
|
|
|
|
|
const projectName =
|
|
|
|
|
files.find((f) => f.name.endsWith('.ino'))?.name.replace('.ino', '') || 'velxio-project';
|
2026-03-13 09:39:04 +07:00
|
|
|
await exportToWokwiZip(files, components, wires, legacyBoardType, projectName, boardPosition);
|
2026-03-07 10:14:35 +07:00
|
|
|
} catch (err) {
|
|
|
|
|
setMessage({ type: 'error', text: 'Export failed.' });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
feat: load precompiled firmware files (.hex, .bin, .elf) directly
Closes #1
Add the ability to upload precompiled firmware files directly into the
emulator, bypassing the built-in compilation step. This enables users
with custom toolchains (ESP-IDF, PlatformIO, ASM workflows) to use
Velxio purely as an emulation/debugging environment.
New features:
- "Upload firmware" option in the overflow menu (accepts .hex, .bin, .elf)
- Automatic format detection from file extension and magic bytes
- ELF parser extracts PT_LOAD segments and detects target architecture
(AVR, ARM, RISC-V, Xtensa) from ELF e_machine header
- Architecture mismatch warnings logged when ELF target differs from
current board (non-blocking — upload proceeds anyway)
- Firmware routed to the correct simulator loader via existing
compileBoardProgram() — no simulator changes needed
Supported formats per board:
- AVR (Uno/Nano/Mega/ATtiny85): .hex (direct), .elf (parsed → HEX)
- RP2040 (Pico): .bin (direct), .elf (parsed → binary)
- ESP32-C3: .bin (direct), .hex, .elf (parsed → binary)
- ESP32/S3 (QEMU): .bin (direct), .elf (parsed → binary)
New file: frontend/src/utils/firmwareLoader.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 12:08:11 +07:00
|
|
|
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;
|
|
|
|
|
const current = activeBoard ? BOARD_KIND_LABELS[activeBoard.boardKind] : 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 });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-07 10:14:35 +07:00
|
|
|
const handleImportFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
const file = e.target.files?.[0];
|
|
|
|
|
if (!importInputRef.current) return;
|
|
|
|
|
importInputRef.current.value = '';
|
|
|
|
|
if (!file) return;
|
|
|
|
|
try {
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
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.
|
2026-03-07 10:14:35 +07:00
|
|
|
const { loadFiles } = useEditorStore.getState();
|
2026-04-22 02:45:45 +07:00
|
|
|
const { setComponents, setWires, setBoardType, setBoardPosition, stopSimulation } =
|
|
|
|
|
useSimulatorStore.getState();
|
2026-03-07 10:14:35 +07:00
|
|
|
stopSimulation();
|
|
|
|
|
if (result.boardType) setBoardType(result.boardType);
|
2026-03-08 05:29:23 +07:00
|
|
|
setBoardPosition(result.boardPosition);
|
2026-03-07 10:14:35 +07:00
|
|
|
setComponents(result.components);
|
|
|
|
|
setWires(result.wires);
|
|
|
|
|
if (result.files.length > 0) loadFiles(result.files);
|
|
|
|
|
setMessage({ type: 'success', text: `Imported ${file.name}` });
|
2026-03-09 22:53:24 +07:00
|
|
|
if (result.libraries.length > 0) {
|
|
|
|
|
setPendingLibraries(result.libraries);
|
|
|
|
|
setInstallModalOpen(true);
|
|
|
|
|
}
|
2026-03-07 10:14:35 +07:00
|
|
|
} catch (err: any) {
|
|
|
|
|
setMessage({ type: 'error', text: err?.message || 'Import failed.' });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-03 10:20:49 +07:00
|
|
|
return (
|
2026-03-05 08:05:23 +07:00
|
|
|
<>
|
2026-03-16 00:04:01 +07:00
|
|
|
<div className="editor-toolbar-wrapper" style={{ position: 'relative' }}>
|
2026-04-22 02:45:45 +07:00
|
|
|
<div className="editor-toolbar" ref={toolbarRef}>
|
2026-05-08 10:53:27 +07:00
|
|
|
{/* MicroPython language selector — only when active board supports it.
|
|
|
|
|
The board context pill that used to live here was removed: it
|
|
|
|
|
duplicated the BoardSelector dropdown elsewhere in the toolbar. */}
|
|
|
|
|
{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);
|
|
|
|
|
}}
|
feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 13:05:51 +07:00
|
|
|
title={t('editor.toolbar.languageMode')}
|
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>
|
|
|
|
|
</select>
|
2026-04-22 02:45:45 +07:00
|
|
|
)}
|
2026-03-16 00:04:01 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
<div className="toolbar-group">
|
|
|
|
|
{/* Compile */}
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleCompile}
|
|
|
|
|
disabled={compiling || !activeBoard}
|
|
|
|
|
className="tb-btn tb-btn-compile"
|
|
|
|
|
title={
|
|
|
|
|
!activeBoard
|
feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 13:05:51 +07:00
|
|
|
? t('editor.toolbar.compile.addBoard')
|
2026-04-22 02:45:45 +07:00
|
|
|
: compiling
|
feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 13:05:51 +07:00
|
|
|
? t('editor.toolbar.compile.loading')
|
2026-04-22 02:45:45 +07:00
|
|
|
: activeBoard?.languageMode === 'micropython'
|
feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 13:05:51 +07:00
|
|
|
? t('editor.toolbar.compile.loadMicropython')
|
|
|
|
|
: t('editor.toolbar.compile.compile')
|
2026-04-22 02:45:45 +07:00
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{compiling ? (
|
|
|
|
|
<svg
|
2026-04-29 09:23:20 +07:00
|
|
|
width="18"
|
|
|
|
|
height="18"
|
2026-04-22 02:45:45 +07:00
|
|
|
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" />
|
2026-03-16 00:04:01 +07:00
|
|
|
</svg>
|
2026-04-22 02:45:45 +07:00
|
|
|
) : (
|
|
|
|
|
<svg
|
2026-04-29 09:23:20 +07:00
|
|
|
width="18"
|
|
|
|
|
height="18"
|
2026-04-22 02:45:45 +07:00
|
|
|
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" />
|
2026-03-16 00:04:01 +07:00
|
|
|
</svg>
|
2026-04-22 02:45:45 +07:00
|
|
|
)}
|
|
|
|
|
</button>
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
<div className="tb-divider" />
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
{/* Run */}
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleRun}
|
2026-05-13 00:26:33 +07:00
|
|
|
disabled={
|
|
|
|
|
isBoardless
|
|
|
|
|
? digitalRunning
|
|
|
|
|
: running || compiling || !activeBoard
|
|
|
|
|
}
|
2026-04-22 02:45:45 +07:00
|
|
|
className="tb-btn tb-btn-run"
|
|
|
|
|
title={
|
2026-05-13 00:26:33 +07:00
|
|
|
isBoardless
|
|
|
|
|
? digitalRunning
|
|
|
|
|
? 'Digital simulation running'
|
|
|
|
|
: 'Resume digital simulation'
|
|
|
|
|
: !activeBoard
|
|
|
|
|
? t('editor.toolbar.run.addBoard')
|
|
|
|
|
: activeBoard?.languageMode === 'micropython'
|
|
|
|
|
? t('editor.toolbar.run.runMicropython')
|
|
|
|
|
: t('editor.toolbar.run.run')
|
2026-04-22 02:45:45 +07:00
|
|
|
}
|
|
|
|
|
>
|
2026-04-29 09:23:20 +07:00
|
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
2026-04-22 02:45:45 +07:00
|
|
|
<polygon points="5,3 19,12 5,21" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
2026-03-07 10:14:35 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
{/* Stop */}
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleStop}
|
2026-05-13 00:26:33 +07:00
|
|
|
disabled={isBoardless ? !digitalRunning : !running}
|
2026-04-22 02:45:45 +07:00
|
|
|
className="tb-btn tb-btn-stop"
|
2026-05-13 00:26:33 +07:00
|
|
|
title={isBoardless ? 'Freeze digital simulation' : t('editor.toolbar.stop')}
|
2026-04-22 02:45:45 +07:00
|
|
|
>
|
2026-04-29 09:23:20 +07:00
|
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
2026-04-22 02:45:45 +07:00
|
|
|
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
2026-03-06 07:07:03 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
{/* Reset */}
|
2026-03-25 02:52:01 +07:00
|
|
|
<button
|
2026-04-22 02:45:45 +07:00
|
|
|
onClick={handleReset}
|
|
|
|
|
disabled={!compiledHex && !activeBoard?.compiledProgram}
|
|
|
|
|
className="tb-btn tb-btn-reset"
|
feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 13:05:51 +07:00
|
|
|
title={t('editor.toolbar.reset')}
|
2026-03-25 02:52:01 +07:00
|
|
|
>
|
2026-04-22 02:45:45 +07:00
|
|
|
<svg
|
2026-04-29 09:23:20 +07:00
|
|
|
width="18"
|
|
|
|
|
height="18"
|
2026-04-22 02:45:45 +07:00
|
|
|
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" />
|
2026-03-25 02:52:01 +07:00
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
{boards.length > 1 && (
|
|
|
|
|
<>
|
|
|
|
|
<div className="tb-divider" />
|
|
|
|
|
|
|
|
|
|
{/* Compile All */}
|
2026-03-25 02:52:01 +07:00
|
|
|
<button
|
2026-04-22 02:45:45 +07:00
|
|
|
onClick={handleCompileAll}
|
|
|
|
|
disabled={compileAllRunning}
|
|
|
|
|
className="tb-btn tb-btn-compile-all"
|
feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 13:05:51 +07:00
|
|
|
title={t('editor.toolbar.compileAll')}
|
2026-03-25 02:52:01 +07:00
|
|
|
>
|
2026-04-22 02:45:45 +07:00
|
|
|
<svg
|
2026-04-29 09:23:20 +07:00
|
|
|
width="18"
|
|
|
|
|
height="18"
|
2026-04-22 02:45:45 +07:00
|
|
|
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" />
|
2026-03-25 02:52:01 +07:00
|
|
|
</svg>
|
|
|
|
|
</button>
|
2026-04-22 02:45:45 +07:00
|
|
|
|
|
|
|
|
{/* Run All */}
|
feat: load precompiled firmware files (.hex, .bin, .elf) directly
Closes #1
Add the ability to upload precompiled firmware files directly into the
emulator, bypassing the built-in compilation step. This enables users
with custom toolchains (ESP-IDF, PlatformIO, ASM workflows) to use
Velxio purely as an emulation/debugging environment.
New features:
- "Upload firmware" option in the overflow menu (accepts .hex, .bin, .elf)
- Automatic format detection from file extension and magic bytes
- ELF parser extracts PT_LOAD segments and detects target architecture
(AVR, ARM, RISC-V, Xtensa) from ELF e_machine header
- Architecture mismatch warnings logged when ELF target differs from
current board (non-blocking — upload proceeds anyway)
- Firmware routed to the correct simulator loader via existing
compileBoardProgram() — no simulator changes needed
Supported formats per board:
- AVR (Uno/Nano/Mega/ATtiny85): .hex (direct), .elf (parsed → HEX)
- RP2040 (Pico): .bin (direct), .elf (parsed → binary)
- ESP32-C3: .bin (direct), .hex, .elf (parsed → binary)
- ESP32/S3 (QEMU): .bin (direct), .elf (parsed → binary)
New file: frontend/src/utils/firmwareLoader.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 12:08:11 +07:00
|
|
|
<button
|
2026-04-22 02:45:45 +07:00
|
|
|
onClick={handleRunAll}
|
|
|
|
|
disabled={running}
|
|
|
|
|
className="tb-btn tb-btn-run-all"
|
feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 13:05:51 +07:00
|
|
|
title={t('editor.toolbar.runAll')}
|
feat: load precompiled firmware files (.hex, .bin, .elf) directly
Closes #1
Add the ability to upload precompiled firmware files directly into the
emulator, bypassing the built-in compilation step. This enables users
with custom toolchains (ESP-IDF, PlatformIO, ASM workflows) to use
Velxio purely as an emulation/debugging environment.
New features:
- "Upload firmware" option in the overflow menu (accepts .hex, .bin, .elf)
- Automatic format detection from file extension and magic bytes
- ELF parser extracts PT_LOAD segments and detects target architecture
(AVR, ARM, RISC-V, Xtensa) from ELF e_machine header
- Architecture mismatch warnings logged when ELF target differs from
current board (non-blocking — upload proceeds anyway)
- Firmware routed to the correct simulator loader via existing
compileBoardProgram() — no simulator changes needed
Supported formats per board:
- AVR (Uno/Nano/Mega/ATtiny85): .hex (direct), .elf (parsed → HEX)
- RP2040 (Pico): .bin (direct), .elf (parsed → binary)
- ESP32-C3: .bin (direct), .hex, .elf (parsed → binary)
- ESP32/S3 (QEMU): .bin (direct), .elf (parsed → binary)
New file: frontend/src/utils/firmwareLoader.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 12:08:11 +07:00
|
|
|
>
|
2026-04-29 09:23:20 +07:00
|
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
2026-04-22 02:45:45 +07:00
|
|
|
<polygon points="3,3 11,12 3,21" />
|
|
|
|
|
<polygon points="13,3 21,12 13,21" />
|
feat: load precompiled firmware files (.hex, .bin, .elf) directly
Closes #1
Add the ability to upload precompiled firmware files directly into the
emulator, bypassing the built-in compilation step. This enables users
with custom toolchains (ESP-IDF, PlatformIO, ASM workflows) to use
Velxio purely as an emulation/debugging environment.
New features:
- "Upload firmware" option in the overflow menu (accepts .hex, .bin, .elf)
- Automatic format detection from file extension and magic bytes
- ELF parser extracts PT_LOAD segments and detects target architecture
(AVR, ARM, RISC-V, Xtensa) from ELF e_machine header
- Architecture mismatch warnings logged when ELF target differs from
current board (non-blocking — upload proceeds anyway)
- Firmware routed to the correct simulator loader via existing
compileBoardProgram() — no simulator changes needed
Supported formats per board:
- AVR (Uno/Nano/Mega/ATtiny85): .hex (direct), .elf (parsed → HEX)
- RP2040 (Pico): .bin (direct), .elf (parsed → binary)
- ESP32-C3: .bin (direct), .hex, .elf (parsed → binary)
- ESP32/S3 (QEMU): .bin (direct), .elf (parsed → binary)
New file: frontend/src/utils/firmwareLoader.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 12:08:11 +07:00
|
|
|
</svg>
|
|
|
|
|
</button>
|
2026-04-22 02:45:45 +07:00
|
|
|
</>
|
2026-03-25 02:52:01 +07:00
|
|
|
)}
|
|
|
|
|
</div>
|
2026-03-06 07:07:03 +07:00
|
|
|
|
2026-04-29 09:23:20 +07:00
|
|
|
{/* Center slot — file tabs share the row so action icons stay pinned. */}
|
|
|
|
|
{centerSlot && <div className="toolbar-center-slot">{centerSlot}</div>}
|
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
<div className="toolbar-group toolbar-group-right">
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
{/* 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. */}
|
2026-04-22 02:45:45 +07:00
|
|
|
<input
|
|
|
|
|
ref={importInputRef}
|
|
|
|
|
type="file"
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
accept={PROJECT_FILE_ACCEPT}
|
2026-04-22 02:45:45 +07:00
|
|
|
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"
|
feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 13:05:51 +07:00
|
|
|
title={t('editor.toolbar.libraries.title')}
|
2026-04-22 02:45:45 +07:00
|
|
|
>
|
|
|
|
|
<svg
|
2026-04-29 09:23:20 +07:00
|
|
|
width="16"
|
|
|
|
|
height="16"
|
2026-04-22 02:45:45 +07:00
|
|
|
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>
|
feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 13:05:51 +07:00
|
|
|
<span className="tb-libraries-label">{t('editor.toolbar.libraries.label')}</span>
|
2026-04-22 02:45:45 +07:00
|
|
|
</button>
|
2026-03-06 07:07:03 +07:00
|
|
|
|
2026-05-08 10:53:27 +07:00
|
|
|
{/* Import zip — was previously hidden in a 3-dot overflow menu;
|
|
|
|
|
inlined since there's space and the discoverability cost
|
|
|
|
|
outweighed the toolbar savings. */}
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => importInputRef.current?.click()}
|
|
|
|
|
className="tb-btn"
|
feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 13:05:51 +07:00
|
|
|
title={t('editor.toolbar.import')}
|
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"
|
feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 13:05:51 +07:00
|
|
|
title={t('editor.toolbar.export')}
|
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>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => firmwareInputRef.current?.click()}
|
|
|
|
|
className="tb-btn"
|
feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 13:05:51 +07:00
|
|
|
title={t('editor.toolbar.uploadFirmware')}
|
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="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>
|
|
|
|
|
</button>
|
2026-04-22 02:45:45 +07:00
|
|
|
|
|
|
|
|
<div className="tb-divider" />
|
|
|
|
|
|
|
|
|
|
{/* Output Console toggle */}
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setConsoleOpen((v) => !v)}
|
|
|
|
|
className={`tb-btn tb-btn-output${consoleOpen ? ' tb-btn-output-active' : ''}`}
|
feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 13:05:51 +07:00
|
|
|
title={t('editor.toolbar.toggleConsole')}
|
2026-04-22 02:45:45 +07:00
|
|
|
>
|
|
|
|
|
<svg
|
2026-04-29 09:23:20 +07:00
|
|
|
width="18"
|
|
|
|
|
height="18"
|
2026-04-22 02:45:45 +07:00
|
|
|
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>
|
2026-05-05 00:03:20 +07:00
|
|
|
{rightSlot}
|
2026-04-22 02:45:45 +07:00
|
|
|
</div>
|
2026-03-05 08:05:23 +07:00
|
|
|
</div>
|
2026-03-03 10:20:49 +07:00
|
|
|
</div>
|
2026-03-05 08:05:23 +07:00
|
|
|
|
|
|
|
|
{/* Error detail bar */}
|
2026-03-06 07:07:03 +07:00
|
|
|
{message?.type === 'error' && message.text.length > 40 && !consoleOpen && (
|
2026-03-05 08:05:23 +07:00
|
|
|
<div className="toolbar-error-detail">{message.text}</div>
|
2026-03-03 10:20:49 +07:00
|
|
|
)}
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-03-25 02:52:01 +07:00
|
|
|
{/* Missing library hint */}
|
|
|
|
|
{missingLibHint && (
|
|
|
|
|
<div className="tb-lib-hint">
|
2026-04-22 02:45:45 +07:00
|
|
|
<svg
|
|
|
|
|
width="16"
|
|
|
|
|
height="16"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
2026-03-25 02:52:01 +07:00
|
|
|
<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>
|
feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 13:05:51 +07:00
|
|
|
<span>{t('editor.toolbar.libHint.message')}</span>
|
2026-04-22 02:45:45 +07:00
|
|
|
<button
|
|
|
|
|
className="tb-lib-hint-btn"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
trackOpenLibraryManager();
|
|
|
|
|
setLibManagerOpen(true);
|
|
|
|
|
setMissingLibHint(false);
|
|
|
|
|
}}
|
|
|
|
|
>
|
feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 13:05:51 +07:00
|
|
|
{t('editor.toolbar.libHint.cta')}
|
2026-03-25 02:52:01 +07:00
|
|
|
</button>
|
2026-04-22 02:45:45 +07:00
|
|
|
<button
|
|
|
|
|
className="tb-lib-hint-close"
|
|
|
|
|
onClick={() => setMissingLibHint(false)}
|
feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 13:05:51 +07:00
|
|
|
title={t('editor.toolbar.libHint.dismiss')}
|
2026-04-22 02:45:45 +07:00
|
|
|
>
|
2026-03-25 02:52:01 +07:00
|
|
|
×
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-03-05 08:05:23 +07:00
|
|
|
<LibraryManagerModal isOpen={libManagerOpen} onClose={() => setLibManagerOpen(false)} />
|
2026-03-09 22:53:24 +07:00
|
|
|
<InstallLibrariesModal
|
|
|
|
|
isOpen={installModalOpen}
|
|
|
|
|
onClose={() => setInstallModalOpen(false)}
|
|
|
|
|
libraries={pendingLibraries}
|
|
|
|
|
/>
|
2026-05-13 02:55:15 +07:00
|
|
|
{verification && (
|
|
|
|
|
<CircuitVerificationModal
|
|
|
|
|
result={verification}
|
|
|
|
|
onCancel={() => {
|
|
|
|
|
pendingRunRef.current = null;
|
|
|
|
|
setVerification(null);
|
|
|
|
|
}}
|
|
|
|
|
onRunAnyway={() => {
|
|
|
|
|
const resume = pendingRunRef.current;
|
|
|
|
|
pendingRunRef.current = null;
|
|
|
|
|
setVerification(null);
|
|
|
|
|
resume?.();
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2026-03-05 08:05:23 +07:00
|
|
|
</>
|
2026-03-03 10:20:49 +07:00
|
|
|
);
|
|
|
|
|
};
|