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';
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
import { useEditorStore, chipFileGroupId } from '../../store/useEditorStore';
|
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(editor): rename boards & custom chips; show which target owns each file
Phase 2 of the run-system/UX work.
- BoardInstance gains an optional user ; boardDisplayName(board) resolver
(name || kind label) routes every INSTANCE-label surface: file-explorer
section header, compile console (EditorToolbar), canvas selector/tooltip/
context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options
subtitle. Board/component pickers keep the KIND label (they pick new boards).
- Inline rename on board AND chip section headers (double-click the name, or a
hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in
properties. Enter commits, Escape cancels (cancel-flag ref guards the
unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'.
- FileTabs shows an owner badge naming the board/chip whose files are shown
(resolved as a selector so it doesn't re-render on every sim pin toggle).
- CustomChipDialog no longer clobbers a user-given chipName: chip.json's name
only seeds the blank defaults (My Chip / Custom Chip); loading an example
relabels explicitly.
- Persistence: board name round-trips via projectPayload (+ dirty hash),
vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json.
- Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal
(early return before a useCallback).
Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness);
all major findings folded in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:22:25 +07:00
|
|
|
import { BOARD_KIND_FQBN, BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind, boardDisplayName } 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';
|
2026-06-03 09:47:26 +07:00
|
|
|
import { compileChip } from '../../services/chipCompileService';
|
2026-06-04 09:32:35 +07:00
|
|
|
import { clearChipDrives } from '../../simulation/customChips/chipPinDrives';
|
|
|
|
|
import { requestElectricalResolve } from '../../simulation/spice/electricalResolveHook';
|
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';
|
2026-06-04 12:56:54 +07:00
|
|
|
import type { CompilationLog, CompileTarget } 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-06-04 09:32:35 +07:00
|
|
|
/**
|
|
|
|
|
* Clear the output drives of every custom chip on the canvas and re-solve, so
|
|
|
|
|
* chip-driven LEDs go dark on Stop. A chip drives its nets via its own SPICE
|
|
|
|
|
* voltage sources (registered in chipPinDrives); stopBoard / electrical-pause
|
|
|
|
|
* don't touch those, so without this the LEDs would freeze at their last frame.
|
|
|
|
|
*/
|
|
|
|
|
function clearAllChipDrives(): void {
|
|
|
|
|
const comps = useSimulatorStore.getState().components;
|
|
|
|
|
let any = false;
|
|
|
|
|
for (const c of comps) {
|
|
|
|
|
if (c.metadataId === 'custom-chip') {
|
|
|
|
|
clearChipDrives(c.id);
|
|
|
|
|
any = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (any) requestElectricalResolve();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 12:21:39 +07:00
|
|
|
/**
|
|
|
|
|
* Boards whose firmware runs in a QEMU worker rather than a client-side AVR
|
|
|
|
|
* core. They can start without a pre-stored `compiledProgram`. Shared by
|
|
|
|
|
* handleRun and handleRunAll so the two paths can't drift.
|
|
|
|
|
*/
|
|
|
|
|
function isQemuBoardKind(kind: BoardKind | undefined): boolean {
|
|
|
|
|
if (!kind) return false;
|
|
|
|
|
return (
|
|
|
|
|
isPiBoardKind(kind) ||
|
|
|
|
|
kind === 'esp32' ||
|
|
|
|
|
kind === 'esp32-s3' ||
|
|
|
|
|
kind === 'esp32-cam' ||
|
|
|
|
|
kind === 'esp32-c3' ||
|
|
|
|
|
kind === 'esp32-devkit-c-v4' ||
|
|
|
|
|
kind === 'wemos-lolin32-lite' ||
|
|
|
|
|
kind === 'xiao-esp32-s3' ||
|
|
|
|
|
kind === 'arduino-nano-esp32' ||
|
|
|
|
|
kind === 'xiao-esp32-c3' ||
|
|
|
|
|
kind === 'aitewinrobot-esp32c3-supermini'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
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': '⬡',
|
feat: STM32 (Blue Pill / Black Pill) QEMU emulation + Pro board gating
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.
Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:06:14 +07:00
|
|
|
'stm32-bluepill': '◈',
|
|
|
|
|
'stm32-blackpill': '◈',
|
feat(boards): add 6 STM32 boards (F4 Discovery, Olimex H405, Netduino 2/+2, Pill variants)
Adds stm32-f4-discovery, stm32-olimex-h405, stm32-netduino-plus2, stm32-netduino2, stm32-blackpill-f401 and stm32-bluepill-f103cb, mapped to existing qemu-lcgamboa machines (netduinoplus2, olimex-stm32-h405, netduino2, stm32vldiscovery). A generic inline board renderer (no SVG) draws the Discovery/Olimex/Netduino boards from a header pin layout; the Pill variants reuse the Blue/Black Pill SVGs. Per-board onboard-LED pin and polarity via STM32_LED. One blink+serial example per board.
tsc --noEmit clean; all new FQBN pnum variants present in STM32 core 2.12.0; worker smoke tests pass for the new machines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 10:59:04 +07:00
|
|
|
'stm32-bluepill-f103cb': '◈',
|
|
|
|
|
'stm32-blackpill-f401': '◈',
|
|
|
|
|
'stm32-f4-discovery': '◈',
|
|
|
|
|
'stm32-olimex-h405': '◈',
|
|
|
|
|
'stm32-netduino-plus2': '◈',
|
|
|
|
|
'stm32-netduino2': '◈',
|
2026-03-16 00:04:01 +07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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',
|
feat: STM32 (Blue Pill / Black Pill) QEMU emulation + Pro board gating
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.
Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:06:14 +07:00
|
|
|
'stm32-bluepill': '#80cbc4',
|
|
|
|
|
'stm32-blackpill': '#b0bec5',
|
feat(boards): add 6 STM32 boards (F4 Discovery, Olimex H405, Netduino 2/+2, Pill variants)
Adds stm32-f4-discovery, stm32-olimex-h405, stm32-netduino-plus2, stm32-netduino2, stm32-blackpill-f401 and stm32-bluepill-f103cb, mapped to existing qemu-lcgamboa machines (netduinoplus2, olimex-stm32-h405, netduino2, stm32vldiscovery). A generic inline board renderer (no SVG) draws the Discovery/Olimex/Netduino boards from a header pin layout; the Pill variants reuse the Blue/Black Pill SVGs. Per-board onboard-LED pin and polarity via STM32_LED. One blink+serial example per board.
tsc --noEmit clean; all new FQBN pnum variants present in STM32 core 2.12.0; worker smoke tests pass for the new machines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 10:59:04 +07:00
|
|
|
'stm32-bluepill-f103cb': '#80cbc4',
|
|
|
|
|
'stm32-blackpill-f401': '#b0bec5',
|
|
|
|
|
'stm32-f4-discovery': '#90caf9',
|
|
|
|
|
'stm32-olimex-h405': '#a5d6a7',
|
|
|
|
|
'stm32-netduino-plus2': '#ce93d8',
|
|
|
|
|
'stm32-netduino2': '#ce93d8',
|
2026-03-16 00:04:01 +07:00
|
|
|
};
|
|
|
|
|
|
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-06-04 12:21:39 +07:00
|
|
|
// Any board actually running — the correct multi-target signal for the
|
|
|
|
|
// Run-All / Stop buttons (the flat `running` flag only tracks the ACTIVE
|
|
|
|
|
// board, so it misreports a multi-board or non-active-board run).
|
|
|
|
|
const anyBoardRunning = boards.some((b) => b.running);
|
|
|
|
|
|
|
|
|
|
// A "run target" is a board OR a programmable custom-chip (a CPU that runs a
|
|
|
|
|
// ROM). When there is more than one target — two boards, a board + a chip, or
|
|
|
|
|
// several chips — the unified Compile-All / Run-All buttons appear and act on
|
|
|
|
|
// every target, the same way multiple Arduinos behave. Resolved as a number
|
|
|
|
|
// so the toolbar only re-renders when the count changes. The predicate is a
|
|
|
|
|
// cheap string test (no JSON.parse) since this selector runs on every store
|
|
|
|
|
// change, including high-frequency simulation churn. (The compile/run paths
|
|
|
|
|
// deliberately act on ALL custom chips, not just programmable ones.)
|
|
|
|
|
const targetCount = useSimulatorStore((s) => {
|
|
|
|
|
let chips = 0;
|
|
|
|
|
for (const c of s.components) {
|
|
|
|
|
if (c.metadataId !== 'custom-chip') continue;
|
|
|
|
|
const p = c.properties as Record<string, unknown>;
|
|
|
|
|
if (String(p?.programFile ?? '').trim() || String(p?.chipJson ?? '').includes('"programTargets"'))
|
|
|
|
|
chips++;
|
|
|
|
|
}
|
|
|
|
|
return s.boards.length + chips;
|
|
|
|
|
});
|
2026-05-13 00:26:33 +07:00
|
|
|
|
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-05-29 12:41:05 +07:00
|
|
|
const [moreMenuOpen, setMoreMenuOpen] = useState(false);
|
|
|
|
|
const moreMenuRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
2026-06-07 10:08:34 +07:00
|
|
|
// Open the Library Manager when another component (e.g. the velxio.json entry
|
|
|
|
|
// in the FileExplorer) asks for it via a window event. Avoids prop-drilling
|
|
|
|
|
// the modal state down to the explorer.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const open = () => setLibManagerOpen(true);
|
|
|
|
|
window.addEventListener('velxio-open-library-manager', open);
|
|
|
|
|
return () => window.removeEventListener('velxio-open-library-manager', open);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-05-29 12:41:05 +07:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!moreMenuOpen) return;
|
|
|
|
|
const onClickOutside = (e: MouseEvent) => {
|
|
|
|
|
if (moreMenuRef.current && !moreMenuRef.current.contains(e.target as Node)) {
|
|
|
|
|
setMoreMenuOpen(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const onEsc = (e: KeyboardEvent) => {
|
|
|
|
|
if (e.key === 'Escape') setMoreMenuOpen(false);
|
|
|
|
|
};
|
|
|
|
|
document.addEventListener('mousedown', onClickOutside);
|
|
|
|
|
document.addEventListener('keydown', onEsc);
|
|
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener('mousedown', onClickOutside);
|
|
|
|
|
document.removeEventListener('keydown', onEsc);
|
|
|
|
|
};
|
|
|
|
|
}, [moreMenuOpen]);
|
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
|
|
|
|
2026-06-03 09:47:26 +07:00
|
|
|
/**
|
|
|
|
|
* Make every custom-chip on the canvas runnable: compile its C source to
|
|
|
|
|
* WASM (when it has none yet) and, for programmable CPU chips, assemble or
|
|
|
|
|
* compile the program file it references into ROM bytes — stashing both on
|
|
|
|
|
* the chip component's `properties` so the next simulation start picks them
|
|
|
|
|
* up. Non-fatal by design: a chip that fails to compile is logged and
|
|
|
|
|
* skipped so the board itself still runs.
|
|
|
|
|
*/
|
|
|
|
|
const prepareCustomChips = useCallback(
|
|
|
|
|
async (
|
|
|
|
|
chips: { id: string; properties: Record<string, unknown> }[],
|
|
|
|
|
boardFiles: { name: string; content: string }[],
|
|
|
|
|
) => {
|
|
|
|
|
const codeChanged = useEditorStore.getState().codeChangedSinceLastCompile;
|
|
|
|
|
const updateComponent = useSimulatorStore.getState().updateComponent;
|
2026-06-04 12:21:39 +07:00
|
|
|
let failed = 0;
|
2026-06-03 09:47:26 +07:00
|
|
|
|
|
|
|
|
for (const chip of chips) {
|
|
|
|
|
// Re-read the freshest properties each iteration (an earlier chip's
|
|
|
|
|
// update doesn't touch this one, but be defensive).
|
|
|
|
|
const live = useSimulatorStore.getState().components.find((c) => c.id === chip.id);
|
|
|
|
|
const props = { ...(live?.properties ?? chip.properties) } as Record<string, unknown>;
|
|
|
|
|
const chipLabel = String(props.chipName ?? 'custom chip');
|
|
|
|
|
const sourceC = String(props.sourceC ?? '');
|
|
|
|
|
const chipJson = String(props.chipJson ?? '{}');
|
|
|
|
|
let changed = false;
|
2026-06-04 12:56:54 +07:00
|
|
|
// Stamp every line for this chip with its target so the console groups
|
|
|
|
|
// it under its own section (alongside the boards).
|
|
|
|
|
const chipTarget: CompileTarget = { id: chip.id, label: chipLabel, kind: 'chip' };
|
|
|
|
|
const clog = (type: CompilationLog['type'], message: string) =>
|
|
|
|
|
addLog({ timestamp: new Date(), type, message, target: chipTarget });
|
2026-06-03 09:47:26 +07:00
|
|
|
|
|
|
|
|
// 1. C -> WASM. Only when missing — the chip designer fills this too.
|
|
|
|
|
if (!String(props.wasmBase64 ?? '') && sourceC) {
|
2026-06-04 12:56:54 +07:00
|
|
|
clog('info', `Compiling chip "${chipLabel}" to WASM...`);
|
2026-06-03 09:47:26 +07:00
|
|
|
try {
|
|
|
|
|
const r = await compileChip(sourceC, chipJson);
|
|
|
|
|
if (r.success && r.wasm_base64) {
|
|
|
|
|
props.wasmBase64 = r.wasm_base64;
|
|
|
|
|
changed = true;
|
2026-06-04 12:56:54 +07:00
|
|
|
clog('success', `Chip "${chipLabel}" compiled (${r.byte_size} B WASM).`);
|
2026-06-03 09:47:26 +07:00
|
|
|
} else {
|
2026-06-04 12:56:54 +07:00
|
|
|
clog(
|
|
|
|
|
'error',
|
|
|
|
|
`Chip "${chipLabel}" WASM compile failed: ${r.error || r.stderr || 'unknown error'}`,
|
|
|
|
|
);
|
2026-06-04 12:21:39 +07:00
|
|
|
failed++;
|
2026-06-03 09:47:26 +07:00
|
|
|
}
|
|
|
|
|
} catch (e) {
|
2026-06-04 12:56:54 +07:00
|
|
|
clog(
|
|
|
|
|
'error',
|
|
|
|
|
`Chip "${chipLabel}" WASM compile error: ${e instanceof Error ? e.message : String(e)}`,
|
|
|
|
|
);
|
2026-06-04 12:21:39 +07:00
|
|
|
failed++;
|
2026-06-03 09:47:26 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. program file -> ROM bytes (programmable CPU chips). Recompile
|
|
|
|
|
// when there's no ROM yet or the user edited code since last build.
|
|
|
|
|
const programFile = String(props.programFile ?? '').trim();
|
|
|
|
|
if (programFile && (!String(props.romBytes ?? '') || codeChanged)) {
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
// The program lives in the chip's OWN editor group (its collapsible
|
|
|
|
|
// section in the file explorer), separate from the board sketch.
|
|
|
|
|
// Fall back to the board files for older projects that still carried
|
|
|
|
|
// the program alongside sketch.ino in the board group.
|
|
|
|
|
const chipGroupFiles = useEditorStore
|
|
|
|
|
.getState()
|
|
|
|
|
.getGroupFiles(chipFileGroupId(chip.id));
|
|
|
|
|
const file =
|
|
|
|
|
chipGroupFiles.find((f) => f.name === programFile) ??
|
|
|
|
|
boardFiles.find((f) => f.name === programFile);
|
2026-06-03 09:47:26 +07:00
|
|
|
if (!file) {
|
2026-06-04 12:56:54 +07:00
|
|
|
clog('error', `Chip "${chipLabel}": program file "${programFile}" not found in the chip's files.`);
|
2026-06-04 12:21:39 +07:00
|
|
|
failed++;
|
2026-06-03 09:47:26 +07:00
|
|
|
} else {
|
|
|
|
|
const target = targetForChip(chipJson);
|
|
|
|
|
const fmt = formatForFile(programFile);
|
2026-06-04 12:56:54 +07:00
|
|
|
clog(
|
|
|
|
|
'info',
|
|
|
|
|
`Assembling "${programFile}" (target=${target}, format=${fmt}) for chip "${chipLabel}"...`,
|
|
|
|
|
);
|
2026-06-03 09:47:26 +07:00
|
|
|
try {
|
|
|
|
|
const rr = await compileRom(file.content, target, fmt);
|
|
|
|
|
if (rr.success && rr.rom_base64) {
|
|
|
|
|
props.romBytes = rr.rom_base64;
|
|
|
|
|
props.programFile = programFile;
|
|
|
|
|
changed = true;
|
2026-06-04 12:56:54 +07:00
|
|
|
clog('success', `ROM ready: ${rr.byte_size} B injected into "${chipLabel}".`);
|
2026-06-03 09:47:26 +07:00
|
|
|
} else {
|
2026-06-04 12:56:54 +07:00
|
|
|
clog(
|
|
|
|
|
'error',
|
|
|
|
|
`ROM compile failed for "${programFile}": ${rr.error || rr.stderr || 'unknown error'}`,
|
|
|
|
|
);
|
2026-06-04 12:21:39 +07:00
|
|
|
failed++;
|
2026-06-03 09:47:26 +07:00
|
|
|
}
|
|
|
|
|
} catch (e) {
|
2026-06-04 12:56:54 +07:00
|
|
|
clog(
|
|
|
|
|
'error',
|
|
|
|
|
`ROM compile error for "${programFile}": ${e instanceof Error ? e.message : String(e)}`,
|
|
|
|
|
);
|
2026-06-04 12:21:39 +07:00
|
|
|
failed++;
|
2026-06-03 09:47:26 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (changed) {
|
|
|
|
|
updateComponent(chip.id, { properties: props } as any);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-04 12:21:39 +07:00
|
|
|
return { failed };
|
2026-06-03 09:47:26 +07:00
|
|
|
},
|
|
|
|
|
[addLog],
|
|
|
|
|
);
|
|
|
|
|
|
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-05-27 08:06:44 +07:00
|
|
|
// Wipe the previous build's output before we append anything new.
|
|
|
|
|
// Issue #209: lingering logs from prior compiles made it impossible
|
|
|
|
|
// to tell the latest errors / warnings apart from stale ones.
|
|
|
|
|
setCompileLogs([]);
|
2026-03-13 05:02:24 +07:00
|
|
|
trackCompileCode();
|
2026-03-06 07:07:03 +07:00
|
|
|
|
2026-06-03 09:47:26 +07:00
|
|
|
// ── Custom-chip preparation ─────────────────────────────────────────
|
|
|
|
|
// Any custom-chip on the canvas is made "live" here so a single
|
|
|
|
|
// Compile / Run is enough — no separate trip through the chip designer
|
|
|
|
|
// or a manual ROM compile. For every custom-chip we:
|
|
|
|
|
// 1. compile its C source to WASM (when it has none yet), and
|
|
|
|
|
// 2. for programmable CPU chips, assemble/compile the program file it
|
|
|
|
|
// points at (larson.s, chaser.c, …) into ROM bytes.
|
|
|
|
|
// Both artefacts are stashed on the chip component's `properties`;
|
|
|
|
|
// CustomChipPart reads wasmBase64 + romBytes at simulation start.
|
2026-05-19 10:31:10 +07:00
|
|
|
//
|
2026-06-03 09:47:26 +07:00
|
|
|
// The chip program files are ALSO kept out of the Arduino sketch compile
|
|
|
|
|
// below (see `chipProgramFiles`) — otherwise arduino-cli/avr-gcc would
|
|
|
|
|
// try to build e.g. chaser.c and choke on SDCC-only syntax such as
|
|
|
|
|
// `__at(0xC000)`, which is exactly what broke the Z80 examples.
|
2026-05-19 10:31:10 +07:00
|
|
|
const componentsForCompile = useSimulatorStore.getState().components;
|
2026-06-03 09:47:26 +07:00
|
|
|
const customChips = componentsForCompile.filter((c) => c.metadataId === 'custom-chip');
|
|
|
|
|
const chipProgramFiles = new Set<string>();
|
|
|
|
|
for (const chip of customChips) {
|
|
|
|
|
const pf = String((chip.properties as any)?.programFile ?? '').trim();
|
|
|
|
|
if (pf) chipProgramFiles.add(pf);
|
feat(chips): programmable retro CPU chips with external ROM
Adds a new way to use the retro CPU chips: write your program in a
project file (.s / .asm / .hex / .bin), click Compile, click Run, and
the same chip emulates whatever you wrote. Same chip + different ROMs =
mini PC, calculator, LED demo, Kill-the-Bit game, etc.
SDK:
- velxio-chip.h gets two new host imports:
uint32_t vx_rom_size(void);
void vx_rom_read(uint32_t off, uint8_t* dst, uint32_t len);
CPU-emulator chips call these in chip_setup to pull their program out
of the host's romBytes property.
Frontend runtime:
- ChipRuntime accepts opts.romBytes (Uint8Array) and exposes the new
imports, copying bytes into chip memory on vx_rom_read.
- CustomChipPart pulls component.properties.romBytes (base64) and passes
it through.
- Component registry declares three new custom-chip properties:
romBytes (base64), programFile (matching project filename), and
programTarget (cpu name).
New programmable bundled chip:
- frontend/src/components/customChips/examples/intel/i8080-cpu.{c,chip.json}
Same clean-room 8080 emulator as i8080-repl/i8080-counter, but ROM is
loaded externally via vx_rom_*. Has 8 LEDs, 8 buttons, UART, 16 KB RAM,
32 KB of external ROM.
Backend:
- New /api/compile-rom endpoint and rom_compile service that turns
chip-program source into ROM bytes. 8080 ASM is assembled by the
in-tree two-pass assembler (moved to backend/app/services/asm8080.py).
Intel HEX records are parsed; raw .bin is passed through. Future targets
(z80, 8086, 4004) are scaffolded but not wired yet.
EditorToolbar:
- Compile button detects when the active file is .s/.asm/.hex/.bin and
routes to compile-rom instead of arduino-cli. The compiled bytes are
injected into every custom-chip on the canvas whose programFile property
matches the active filename (or is empty).
Example:
- /examples/i8080-killbits loads Dean McDaniel's 1975 Kill-the-Bit on
the programmable i8080-cpu chip. killbits.s is shipped as a project
file alongside sketch.ino; the user clicks Compile then Run and the
LED walks across 8 outputs, buttons kill it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:38:18 +07:00
|
|
|
}
|
2026-06-03 09:47:26 +07:00
|
|
|
|
|
|
|
|
if (customChips.length > 0) {
|
|
|
|
|
const boardFiles = activeBoard?.activeFileGroupId
|
|
|
|
|
? useEditorStore.getState().getGroupFiles(activeBoard.activeFileGroupId)
|
|
|
|
|
: files;
|
|
|
|
|
await prepareCustomChips(customChips, boardFiles);
|
|
|
|
|
}
|
|
|
|
|
// ── End custom-chip preparation ─────────────────────────────────────
|
feat(chips): programmable retro CPU chips with external ROM
Adds a new way to use the retro CPU chips: write your program in a
project file (.s / .asm / .hex / .bin), click Compile, click Run, and
the same chip emulates whatever you wrote. Same chip + different ROMs =
mini PC, calculator, LED demo, Kill-the-Bit game, etc.
SDK:
- velxio-chip.h gets two new host imports:
uint32_t vx_rom_size(void);
void vx_rom_read(uint32_t off, uint8_t* dst, uint32_t len);
CPU-emulator chips call these in chip_setup to pull their program out
of the host's romBytes property.
Frontend runtime:
- ChipRuntime accepts opts.romBytes (Uint8Array) and exposes the new
imports, copying bytes into chip memory on vx_rom_read.
- CustomChipPart pulls component.properties.romBytes (base64) and passes
it through.
- Component registry declares three new custom-chip properties:
romBytes (base64), programFile (matching project filename), and
programTarget (cpu name).
New programmable bundled chip:
- frontend/src/components/customChips/examples/intel/i8080-cpu.{c,chip.json}
Same clean-room 8080 emulator as i8080-repl/i8080-counter, but ROM is
loaded externally via vx_rom_*. Has 8 LEDs, 8 buttons, UART, 16 KB RAM,
32 KB of external ROM.
Backend:
- New /api/compile-rom endpoint and rom_compile service that turns
chip-program source into ROM bytes. 8080 ASM is assembled by the
in-tree two-pass assembler (moved to backend/app/services/asm8080.py).
Intel HEX records are parsed; raw .bin is passed through. Future targets
(z80, 8086, 4004) are scaffolded but not wired yet.
EditorToolbar:
- Compile button detects when the active file is .s/.asm/.hex/.bin and
routes to compile-rom instead of arduino-cli. The compiled bytes are
injected into every custom-chip on the canvas whose programFile property
matches the active filename (or is empty).
Example:
- /examples/i8080-killbits loads Dean McDaniel's 1975 Kill-the-Bit on
the programmable i8080-cpu chip. killbits.s is shipped as a project
file alongside sketch.ino; the user clicks Compile then Run and the
LED walks across 8 outputs, buttons kill it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:38:18 +07:00
|
|
|
|
2026-03-13 09:39:04 +07:00
|
|
|
const kind = activeBoard?.boardKind;
|
2026-06-04 12:56:54 +07:00
|
|
|
// The active board's console target, defined up front so EVERY board path
|
|
|
|
|
// (Pi, MicroPython, arduino-cli, errors) groups its lines under one section.
|
|
|
|
|
const boardLabel = activeBoard ? boardDisplayName(activeBoard) : 'Unknown';
|
|
|
|
|
const boardTarget: CompileTarget | undefined = activeBoardId
|
|
|
|
|
? { id: activeBoardId, label: boardLabel, kind: 'board' }
|
|
|
|
|
: undefined;
|
|
|
|
|
const blog = (type: CompilationLog['type'], message: string) =>
|
|
|
|
|
addLog({ timestamp: new Date(), type, message, target: boardTarget });
|
2026-03-13 09:39:04 +07:00
|
|
|
|
|
|
|
|
// 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-06-04 12:56:54 +07:00
|
|
|
blog('info', '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-06-04 12:56:54 +07:00
|
|
|
blog('info', '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-06-04 12:56:54 +07:00
|
|
|
blog('success', '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';
|
2026-06-04 12:56:54 +07:00
|
|
|
blog('error', errMsg);
|
2026-03-30 07:27:41 +07:00
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
if (!fqbn) {
|
2026-06-04 12:56:54 +07:00
|
|
|
blog('error', `No FQBN for board kind: ${kind}`);
|
2026-03-13 09:39:04 +07:00
|
|
|
setMessage({ type: 'error', text: 'Unknown board' });
|
|
|
|
|
setCompiling(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-06 07:07:03 +07:00
|
|
|
|
2026-06-04 12:56:54 +07:00
|
|
|
blog('info', `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-06-03 09:47:26 +07:00
|
|
|
const sketchFiles = (groupFiles.length > 0 ? groupFiles : files)
|
|
|
|
|
// Keep chip-program files (a chip's programFile, or .s/.asm/.hex/.bin)
|
|
|
|
|
// out of the arduino-cli build — they're compiled to ROM above, not
|
|
|
|
|
// Arduino sources, and avr-gcc chokes on e.g. SDCC's __at().
|
|
|
|
|
.filter((f) => !chipProgramFiles.has(f.name) && !isChipProgramFile(f.name))
|
|
|
|
|
.map((f) => ({
|
|
|
|
|
name: f.name,
|
|
|
|
|
content: f.content,
|
|
|
|
|
}));
|
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-06-04 12:56:54 +07:00
|
|
|
target: boardTarget,
|
2026-05-10 04:36:58 +07:00
|
|
|
})),
|
|
|
|
|
]);
|
|
|
|
|
},
|
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-06-07 11:07:48 +07:00
|
|
|
// P2.4 — THIS board's declared manifest (compile scope). Per-board so
|
|
|
|
|
// two boards can use different libraries without clashing.
|
|
|
|
|
libraries: activeBoard?.libraries?.length ? activeBoard.libraries : null,
|
2026-05-18 12:38:51 +07:00
|
|
|
},
|
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-06-04 12:56:54 +07:00
|
|
|
const resultLogs = parseCompileResult(result, boardLabel, boardTarget);
|
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 });
|
2026-05-27 08:06:44 +07:00
|
|
|
// Issue #208: drop the previous successful program from this
|
|
|
|
|
// board so a subsequent Run cannot silently execute stale code
|
|
|
|
|
// that doesn't match the editor any more. The Run button gates
|
|
|
|
|
// on `!compiledProgram` and will refuse + force a re-compile.
|
|
|
|
|
if (activeBoardId) {
|
|
|
|
|
updateBoard(activeBoardId, { compiledProgram: null });
|
|
|
|
|
}
|
2026-03-25 02:52:01 +07:00
|
|
|
// 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';
|
2026-06-04 12:56:54 +07:00
|
|
|
blog('error', errMsg);
|
2026-03-06 07:07:03 +07:00
|
|
|
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-06-04 02:04:17 +07:00
|
|
|
// Board-less circuits have no MCU to start. If there are custom-chip CPUs
|
|
|
|
|
// on the canvas, compile them (WASM + ROM) and re-attach so they pick up
|
|
|
|
|
// the fresh WASM — Velxio runs custom chips with no Arduino/ESP32 board,
|
|
|
|
|
// as a general-purpose electronics simulator. Then resume the electrical
|
|
|
|
|
// solver (replays any switch toggles captured while paused).
|
2026-05-13 00:26:33 +07:00
|
|
|
if (isBoardless) {
|
2026-06-04 02:04:17 +07:00
|
|
|
const customChips = useSimulatorStore
|
|
|
|
|
.getState()
|
|
|
|
|
.components.filter((c) => c.metadataId === 'custom-chip');
|
|
|
|
|
if (customChips.length > 0) {
|
|
|
|
|
setCompiling(true);
|
|
|
|
|
setConsoleOpen(true);
|
|
|
|
|
setCompileLogs([]);
|
|
|
|
|
try {
|
|
|
|
|
await prepareCustomChips(customChips, files);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
addLog({
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
type: 'error',
|
|
|
|
|
message: e instanceof Error ? e.message : String(e),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
setCompiling(false);
|
|
|
|
|
// Force the chip parts to re-attach with their freshly compiled WASM.
|
|
|
|
|
useSimulatorStore.getState().restartParts();
|
|
|
|
|
}
|
2026-05-13 00:26:33 +07:00
|
|
|
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-06-04 12:56:54 +07:00
|
|
|
const mpyTarget: CompileTarget = {
|
|
|
|
|
id: activeBoardId,
|
|
|
|
|
label: boardDisplayName(board),
|
|
|
|
|
kind: 'board',
|
|
|
|
|
};
|
|
|
|
|
const mlog = (type: CompilationLog['type'], message: string) =>
|
|
|
|
|
addLog({ timestamp: new Date(), type, message, target: mpyTarget });
|
|
|
|
|
mlog('info', 'MicroPython: loading firmware and user files...');
|
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-06-04 12:56:54 +07:00
|
|
|
mlog('success', 'MicroPython firmware loaded');
|
2026-04-13 09:55:11 +07:00
|
|
|
} catch (err) {
|
|
|
|
|
const errMsg = err instanceof Error ? err.message : 'Failed to load MicroPython';
|
2026-06-04 12:56:54 +07:00
|
|
|
mlog('error', errMsg);
|
2026-04-13 09:55:11 +07:00
|
|
|
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-06-04 12:21:39 +07:00
|
|
|
const isQemuBoard = isQemuBoardKind(board?.boardKind);
|
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) {
|
2026-06-04 09:32:35 +07:00
|
|
|
// Freeze the chip tick (the paused flag) AND clear the chip's output
|
|
|
|
|
// drives so its LEDs go dark on Stop — not frozen at their last frame.
|
2026-05-13 00:26:33 +07:00
|
|
|
setElectricalPaused(true);
|
2026-06-04 09:32:35 +07:00
|
|
|
clearAllChipDrives();
|
2026-05-13 00:26:33 +07:00
|
|
|
setMessage(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-04 12:21:39 +07:00
|
|
|
// Stop EVERY running board — Run-All can start several, and leaving any
|
|
|
|
|
// running keeps chips ticking (their gate is boards.some(running)).
|
|
|
|
|
const runningBoards = useSimulatorStore.getState().boards.filter((b) => b.running);
|
|
|
|
|
if (runningBoards.length > 0) runningBoards.forEach((b) => stopBoard(b.id));
|
|
|
|
|
else if (activeBoardId) stopBoard(activeBoardId);
|
2026-03-13 09:39:04 +07:00
|
|
|
else stopSimulation();
|
2026-06-04 09:32:35 +07:00
|
|
|
// A chip wired to a board drives its LEDs via its own SPICE sources, which
|
|
|
|
|
// stopBoard doesn't touch — clear them so those LEDs also go dark.
|
|
|
|
|
clearAllChipDrives();
|
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-06-04 12:21:39 +07:00
|
|
|
// Every custom-chip is a target too — Compile-All / Run-All build chips
|
|
|
|
|
// (WASM + ROM) alongside boards, so the flow works for a board + chip, for
|
|
|
|
|
// several chips with no board, etc.
|
|
|
|
|
const allCustomChips = useSimulatorStore
|
|
|
|
|
.getState()
|
|
|
|
|
.components.filter((c) => c.metadataId === 'custom-chip');
|
|
|
|
|
if (boardsList.length === 0 && allCustomChips.length === 0) return { ok: 0, failed: 0 };
|
2026-04-29 12:33:59 +07:00
|
|
|
|
2026-03-16 00:04:01 +07:00
|
|
|
setCompileAllRunning(true);
|
2026-04-29 12:33:59 +07:00
|
|
|
setConsoleOpen(true);
|
2026-06-04 12:21:39 +07:00
|
|
|
const targetSummary = [
|
|
|
|
|
boardsList.length ? `${boardsList.length} board${boardsList.length === 1 ? '' : 's'}` : '',
|
|
|
|
|
allCustomChips.length ? `${allCustomChips.length} chip${allCustomChips.length === 1 ? '' : 's'}` : '',
|
|
|
|
|
]
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join(' + ');
|
2026-04-29 12:33:59 +07:00
|
|
|
addLog({
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
type: 'info',
|
2026-06-04 12:21:39 +07:00
|
|
|
message: `Compiling all targets (${targetSummary})...`,
|
2026-04-29 12:33:59 +07:00
|
|
|
});
|
|
|
|
|
|
2026-06-03 09:47:26 +07:00
|
|
|
// Make every custom-chip live (WASM + ROM) before compiling the boards,
|
|
|
|
|
// mirroring the single-board Compile path, and collect their program file
|
|
|
|
|
// names so they stay out of the arduino-cli builds below.
|
|
|
|
|
const chipProgramFiles = new Set<string>();
|
|
|
|
|
for (const chip of allCustomChips) {
|
|
|
|
|
const pf = String((chip.properties as any)?.programFile ?? '').trim();
|
|
|
|
|
if (pf) chipProgramFiles.add(pf);
|
|
|
|
|
}
|
2026-06-04 12:21:39 +07:00
|
|
|
let chipFailed = 0;
|
2026-06-03 09:47:26 +07:00
|
|
|
if (allCustomChips.length > 0) {
|
|
|
|
|
const everyFile = boardsList.flatMap((b) =>
|
|
|
|
|
useEditorStore.getState().getGroupFiles(b.activeFileGroupId),
|
|
|
|
|
);
|
2026-06-04 12:21:39 +07:00
|
|
|
chipFailed = (await prepareCustomChips(allCustomChips, everyFile)).failed;
|
2026-06-03 09:47:26 +07:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 12:33:59 +07:00
|
|
|
let ok = 0;
|
2026-06-04 12:21:39 +07:00
|
|
|
let boardFailed = 0;
|
2026-03-16 00:04:01 +07:00
|
|
|
|
|
|
|
|
for (const board of boardsList) {
|
feat(editor): rename boards & custom chips; show which target owns each file
Phase 2 of the run-system/UX work.
- BoardInstance gains an optional user ; boardDisplayName(board) resolver
(name || kind label) routes every INSTANCE-label surface: file-explorer
section header, compile console (EditorToolbar), canvas selector/tooltip/
context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options
subtitle. Board/component pickers keep the KIND label (they pick new boards).
- Inline rename on board AND chip section headers (double-click the name, or a
hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in
properties. Enter commits, Escape cancels (cancel-flag ref guards the
unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'.
- FileTabs shows an owner badge naming the board/chip whose files are shown
(resolved as a selector so it doesn't re-render on every sim pin toggle).
- CustomChipDialog no longer clobbers a user-given chipName: chip.json's name
only seeds the blank defaults (My Chip / Custom Chip); loading an example
relabels explicitly.
- Persistence: board name round-trips via projectPayload (+ dirty hash),
vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json.
- Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal
(early return before a useCallback).
Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness);
all major findings folded in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:22:25 +07:00
|
|
|
const label = boardDisplayName(board);
|
2026-06-04 12:56:54 +07:00
|
|
|
// Stamp this board's lines so the console groups them under its section.
|
|
|
|
|
const boardTarget: CompileTarget = { id: board.id, label, kind: 'board' };
|
|
|
|
|
const blog = (type: CompilationLog['type'], message: string) =>
|
|
|
|
|
addLog({ timestamp: new Date(), type, message, target: boardTarget });
|
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-06-04 12:56:54 +07:00
|
|
|
blog('info', 'skipped (no compilation needed)');
|
2026-04-29 12:33:59 +07:00
|
|
|
ok++;
|
2026-03-16 00:04:01 +07:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fqbn = BOARD_KIND_FQBN[board.boardKind];
|
|
|
|
|
if (!fqbn) {
|
2026-06-04 12:56:54 +07:00
|
|
|
blog('error', 'no FQBN configured');
|
2026-06-04 12:21:39 +07:00
|
|
|
boardFailed++;
|
2026-03-16 00:04:01 +07:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 12:56:54 +07:00
|
|
|
blog('info', 'compiling...');
|
2026-03-16 00:04:01 +07:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const groupFiles = useEditorStore.getState().getGroupFiles(board.activeFileGroupId);
|
2026-06-03 09:47:26 +07:00
|
|
|
const sketchFiles = groupFiles
|
|
|
|
|
.filter((f) => !chipProgramFiles.has(f.name) && !isChipProgramFile(f.name))
|
|
|
|
|
.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,
|
2026-06-04 12:56:54 +07:00
|
|
|
// No `${label}: ` prefix — the target section header carries it.
|
2026-05-10 04:36:58 +07:00
|
|
|
...newLines.map((line) => ({
|
|
|
|
|
timestamp: now,
|
|
|
|
|
type: 'info' as const,
|
2026-06-04 12:56:54 +07:00
|
|
|
message: line,
|
|
|
|
|
target: boardTarget,
|
2026-05-10 04:36:58 +07:00
|
|
|
})),
|
|
|
|
|
]);
|
|
|
|
|
},
|
2026-06-07 11:07:48 +07:00
|
|
|
{ boardOptions: board.boardOptions, spiffsFiles: board.spiffsFiles, libraries: board.libraries?.length ? board.libraries : null },
|
2026-05-10 04:36:58 +07:00
|
|
|
);
|
|
|
|
|
|
2026-06-04 12:56:54 +07:00
|
|
|
const resultLogs = parseCompileResult(result, label, boardTarget);
|
2026-04-29 12:33:59 +07:00
|
|
|
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-06-04 12:21:39 +07:00
|
|
|
boardFailed++;
|
2026-03-16 00:04:01 +07:00
|
|
|
}
|
|
|
|
|
} catch (err) {
|
2026-06-04 12:56:54 +07:00
|
|
|
blog('error', err instanceof Error ? err.message : String(err));
|
2026-06-04 12:21:39 +07:00
|
|
|
boardFailed++;
|
2026-03-16 00:04:01 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 12:21:39 +07:00
|
|
|
const failed = boardFailed + chipFailed;
|
|
|
|
|
const chipOk = allCustomChips.length - chipFailed;
|
|
|
|
|
const doneParts = [];
|
|
|
|
|
if (boardsList.length)
|
|
|
|
|
doneParts.push(`${ok} board${ok === 1 ? '' : 's'} ok${boardFailed > 0 ? `, ${boardFailed} failed` : ''}`);
|
|
|
|
|
if (allCustomChips.length)
|
|
|
|
|
doneParts.push(`${chipOk} chip${chipOk === 1 ? '' : 's'} ok${chipFailed > 0 ? `, ${chipFailed} failed` : ''}`);
|
2026-04-29 12:33:59 +07:00
|
|
|
addLog({
|
|
|
|
|
timestamp: new Date(),
|
2026-06-04 12:21:39 +07:00
|
|
|
type: failed > 0 ? 'error' : 'success',
|
|
|
|
|
message: `Done — ${doneParts.join('; ')}`,
|
2026-04-29 12:33:59 +07:00
|
|
|
});
|
2026-06-04 12:21:39 +07:00
|
|
|
if (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();
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-04 12:21:39 +07:00
|
|
|
/**
|
|
|
|
|
* Run All = compile every target (boards + chips) if needed, then start every
|
|
|
|
|
* one: boards via startBoard, chips via restartParts (re-attach with the
|
|
|
|
|
* fresh WASM/ROM) + resuming the electrical solver when there's no board.
|
|
|
|
|
* Mirrors single Run, generalised across all targets.
|
|
|
|
|
*/
|
2026-04-29 12:33:59 +07:00
|
|
|
const handleRunAll = async () => {
|
2026-06-04 12:21:39 +07:00
|
|
|
const sim = useSimulatorStore.getState();
|
|
|
|
|
const boardsList = sim.boards;
|
|
|
|
|
const chips = sim.components.filter((c) => c.metadataId === 'custom-chip');
|
|
|
|
|
if (boardsList.length === 0 && chips.length === 0) return;
|
|
|
|
|
|
|
|
|
|
// A chip needs compiling when it has no WASM yet, or it references a program
|
|
|
|
|
// file but hasn't been assembled to ROM.
|
|
|
|
|
const chipNeedsCompile = chips.some((c) => {
|
|
|
|
|
const p = c.properties as Record<string, unknown>;
|
|
|
|
|
const programFile = String(p?.programFile ?? '').trim();
|
|
|
|
|
return !String(p?.wasmBase64 ?? '') || (programFile && !String(p?.romBytes ?? ''));
|
|
|
|
|
});
|
2026-04-29 12:33:59 +07:00
|
|
|
const needsCompile =
|
|
|
|
|
codeChangedSinceLastCompile ||
|
2026-06-04 12:21:39 +07:00
|
|
|
chipNeedsCompile ||
|
2026-04-29 12:33:59 +07:00
|
|
|
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();
|
2026-06-04 12:21:39 +07:00
|
|
|
if (failed > 0) return; // a board failed — don't start anything
|
2026-04-29 12:33:59 +07:00
|
|
|
}
|
|
|
|
|
|
2026-06-04 12:21:39 +07:00
|
|
|
// Start every board (compiledProgram may have changed during compile).
|
2026-04-29 12:33:59 +07:00
|
|
|
const refreshed = useSimulatorStore.getState().boards;
|
|
|
|
|
for (const board of refreshed) {
|
|
|
|
|
if (board.running) continue;
|
2026-06-04 12:21:39 +07:00
|
|
|
if (isQemuBoardKind(board.boardKind) || board.compiledProgram || board.languageMode === 'micropython') {
|
2026-04-29 12:33:59 +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);
|
2026-03-16 00:04:01 +07:00
|
|
|
startBoard(board.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-04 12:21:39 +07:00
|
|
|
|
|
|
|
|
// Run the chips: re-attach so they pick up the freshly compiled WASM/ROM.
|
|
|
|
|
// The chip tick gates on a running board, so when NO board actually started
|
|
|
|
|
// (board-less, or a board that compiled to nothing) resume the electrical
|
|
|
|
|
// solver instead, otherwise the chips would stay frozen.
|
|
|
|
|
if (chips.length > 0) {
|
|
|
|
|
useSimulatorStore.getState().restartParts();
|
|
|
|
|
const anyBoardRunning = useSimulatorStore.getState().boards.some((b) => b.running);
|
|
|
|
|
if (!anyBoardRunning) setElectricalPaused(false);
|
|
|
|
|
}
|
2026-03-16 00:04:01 +07:00
|
|
|
};
|
|
|
|
|
|
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.' });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-29 09:18:43 +07:00
|
|
|
// Phase 3 D3.2 — Schematic screenshot. Pro-tier-gated by the backend.
|
|
|
|
|
// Same UX pattern as BOM export: everyone can click; 402 redirects to
|
|
|
|
|
// /pricing. The server-side headless chromium renders the canvas and
|
|
|
|
|
// returns a PNG, which we trigger a download for.
|
|
|
|
|
const handleExportScreenshot = async () => {
|
|
|
|
|
const projectId = currentProject?.id;
|
|
|
|
|
if (!projectId) {
|
|
|
|
|
setMessage({ type: 'error', text: 'Save the project before exporting an image.' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setMessage({ type: 'info', text: 'Rendering screenshot — may take 5-10 seconds…' });
|
|
|
|
|
try {
|
|
|
|
|
const resp = await fetch(`/api/pro/projects/${projectId}/screenshot.png`, {
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
});
|
|
|
|
|
if (resp.status === 402) {
|
2026-05-29 12:41:05 +07:00
|
|
|
// Fire the in-place upgrade modal instead of bouncing to /pricing —
|
|
|
|
|
// keeps the user in the editor with full context. The pro overlay's
|
|
|
|
|
// UpgradeGate listens for this event and opens UpgradePromptModal.
|
|
|
|
|
window.dispatchEvent(new CustomEvent('velxio-pro-upgrade-prompt', {
|
|
|
|
|
detail: { componentName: 'Schematic screenshot export' },
|
|
|
|
|
}));
|
2026-05-29 09:18:43 +07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (resp.status === 401) {
|
|
|
|
|
window.location.href = `/login?redirect=${encodeURIComponent(window.location.pathname)}`;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (resp.status === 422) {
|
|
|
|
|
setMessage({ type: 'error', text: 'Add at least one component to export an image.' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!resp.ok) {
|
|
|
|
|
setMessage({ type: 'error', text: 'Screenshot export failed.' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const blob = await resp.blob();
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
a.href = url;
|
|
|
|
|
const cd = resp.headers.get('Content-Disposition') || '';
|
|
|
|
|
const m = /filename="?([^"]+)"?/.exec(cd);
|
|
|
|
|
a.download = m ? m[1] : `velxio-${projectId}.png`;
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
a.click();
|
|
|
|
|
a.remove();
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
setMessage({ type: 'success', text: 'Screenshot downloaded.' });
|
|
|
|
|
} catch {
|
|
|
|
|
setMessage({ type: 'error', text: 'Screenshot export failed.' });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-29 08:12:52 +07:00
|
|
|
// Phase 3 D3.1 — BOM export. Pro-tier-gated by the backend (402 if not pro).
|
|
|
|
|
// We let everyone click; the 402 response feeds the upgrade prompt below
|
|
|
|
|
// so free/maker users hit the funnel naturally instead of an obviously-
|
|
|
|
|
// locked button (which they'd just dismiss).
|
|
|
|
|
const handleExportBom = async () => {
|
|
|
|
|
const projectId = currentProject?.id;
|
|
|
|
|
if (!projectId) {
|
|
|
|
|
setMessage({ type: 'error', text: 'Save the project before exporting a BOM.' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const resp = await fetch(`/api/pro/projects/${projectId}/bom.csv`, {
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
});
|
|
|
|
|
if (resp.status === 402) {
|
2026-05-29 12:41:05 +07:00
|
|
|
// Fire the in-place upgrade modal instead of bouncing to /pricing —
|
|
|
|
|
// keeps the user in the editor with full context. The pro overlay's
|
|
|
|
|
// UpgradeGate listens for this event and opens UpgradePromptModal.
|
|
|
|
|
window.dispatchEvent(new CustomEvent('velxio-pro-upgrade-prompt', {
|
|
|
|
|
detail: { componentName: 'BOM export' },
|
|
|
|
|
}));
|
2026-05-29 08:12:52 +07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (resp.status === 401) {
|
|
|
|
|
window.location.href = `/login?redirect=${encodeURIComponent(window.location.pathname)}`;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!resp.ok) {
|
|
|
|
|
setMessage({ type: 'error', text: 'BOM export failed.' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const blob = await resp.blob();
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
a.href = url;
|
|
|
|
|
// Filename comes from Content-Disposition; pick a fallback.
|
|
|
|
|
const cd = resp.headers.get('Content-Disposition') || '';
|
|
|
|
|
const m = /filename="?([^"]+)"?/.exec(cd);
|
|
|
|
|
a.download = m ? m[1] : `bom-${projectId}.csv`;
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
a.click();
|
|
|
|
|
a.remove();
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
} catch {
|
|
|
|
|
setMessage({ type: 'error', text: 'BOM export failed.' });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
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;
|
feat(editor): rename boards & custom chips; show which target owns each file
Phase 2 of the run-system/UX work.
- BoardInstance gains an optional user ; boardDisplayName(board) resolver
(name || kind label) routes every INSTANCE-label surface: file-explorer
section header, compile console (EditorToolbar), canvas selector/tooltip/
context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options
subtitle. Board/component pickers keep the KIND label (they pick new boards).
- Inline rename on board AND chip section headers (double-click the name, or a
hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in
properties. Enter commits, Escape cancels (cancel-flag ref guards the
unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'.
- FileTabs shows an owner badge naming the board/chip whose files are shown
(resolved as a selector so it doesn't re-render on every sim pin toggle).
- CustomChipDialog no longer clobbers a user-given chipName: chip.json's name
only seeds the blank defaults (My Chip / Custom Chip); loading an example
relabels explicitly.
- Persistence: board name round-trips via projectPayload (+ dirty hash),
vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json.
- Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal
(early return before a useCallback).
Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness);
all major findings folded in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:22:25 +07:00
|
|
|
const current = activeBoard ? boardDisplayName(activeBoard) : boardKind;
|
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
|
|
|
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-06-04 12:21:39 +07:00
|
|
|
disabled={isBoardless ? !digitalRunning : !anyBoardRunning}
|
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-06-04 12:21:39 +07:00
|
|
|
{targetCount > 1 && (
|
2026-04-22 02:45:45 +07:00
|
|
|
<>
|
|
|
|
|
<div className="tb-divider" />
|
|
|
|
|
|
2026-06-04 12:21:39 +07:00
|
|
|
{/* Compile All — boards + programmable chips */}
|
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}
|
2026-06-04 12:21:39 +07:00
|
|
|
disabled={compileAllRunning || anyBoardRunning || digitalRunning}
|
2026-04-22 02:45:45 +07:00
|
|
|
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-29 12:41:05 +07:00
|
|
|
{/* Import zip — inline by default; container query at narrow
|
|
|
|
|
widths swaps this for the corresponding overflow-menu item. */}
|
2026-05-08 10:53:27 +07:00
|
|
|
<button
|
|
|
|
|
onClick={() => importInputRef.current?.click()}
|
2026-05-29 12:41:05 +07:00
|
|
|
className="tb-btn tb-btn-import-inline"
|
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()}
|
2026-05-29 12:41:05 +07:00
|
|
|
className="tb-btn tb-btn-export-inline"
|
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>
|
2026-05-29 12:41:05 +07:00
|
|
|
{/* Overflow "More" menu — collects the secondary actions
|
|
|
|
|
(BOM, Schematic image, Upload firmware) so the toolbar no
|
|
|
|
|
longer overflows on narrow widths. The two Pro items show
|
|
|
|
|
a small "PRO" pill in the menu so users know they're
|
|
|
|
|
premium BEFORE clicking, instead of being surprised by an
|
|
|
|
|
upgrade prompt. */}
|
|
|
|
|
<div className="tb-overflow-wrap" ref={moreMenuRef}>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setMoreMenuOpen((v) => !v)}
|
|
|
|
|
className={`tb-btn tb-btn-overflow${moreMenuOpen ? ' tb-btn-overflow-active' : ''}`}
|
|
|
|
|
title={t('editor.toolbar.more', 'More')}
|
|
|
|
|
aria-haspopup="true"
|
|
|
|
|
aria-expanded={moreMenuOpen}
|
|
|
|
|
>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
|
|
|
<circle cx="5" cy="12" r="1.8" />
|
|
|
|
|
<circle cx="12" cy="12" r="1.8" />
|
|
|
|
|
<circle cx="19" cy="12" r="1.8" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
{moreMenuOpen && (
|
|
|
|
|
<div className="tb-overflow-menu" role="menu">
|
|
|
|
|
{/* Responsive items — hidden by default, shown via
|
|
|
|
|
container query when the toolbar is too narrow to
|
|
|
|
|
keep their inline twins. Keeps mobile users from
|
|
|
|
|
losing access to Import / Export entirely. */}
|
|
|
|
|
<button
|
|
|
|
|
className="tb-overflow-item tb-overflow-import"
|
|
|
|
|
role="menuitem"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setMoreMenuOpen(false);
|
|
|
|
|
importInputRef.current?.click();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
|
|
|
<polyline points="7 10 12 15 17 10" />
|
|
|
|
|
<line x1="12" y1="15" x2="12" y2="3" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span className="tb-overflow-label">{t('editor.toolbar.importLabel', 'Import project')}</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className="tb-overflow-item tb-overflow-export"
|
|
|
|
|
role="menuitem"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setMoreMenuOpen(false);
|
|
|
|
|
handleExport();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
|
|
|
<polyline points="17 8 12 3 7 8" />
|
|
|
|
|
<line x1="12" y1="3" x2="12" y2="15" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span className="tb-overflow-label">{t('editor.toolbar.exportLabel', 'Export project (.zip)')}</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className="tb-overflow-item"
|
|
|
|
|
role="menuitem"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setMoreMenuOpen(false);
|
|
|
|
|
handleExportBom();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
<rect x="3" y="4" width="18" height="16" rx="2" />
|
|
|
|
|
<line x1="3" y1="10" x2="21" y2="10" />
|
|
|
|
|
<line x1="9" y1="4" x2="9" y2="20" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span className="tb-overflow-label">{t('editor.toolbar.exportBomLabel', 'Bill of Materials (CSV)')}</span>
|
|
|
|
|
<span className="tb-overflow-pro">PRO</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className="tb-overflow-item"
|
|
|
|
|
role="menuitem"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setMoreMenuOpen(false);
|
|
|
|
|
handleExportScreenshot();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
|
|
|
|
|
<circle cx="12" cy="13" r="4" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span className="tb-overflow-label">{t('editor.toolbar.exportScreenshotLabel', 'Schematic image (PNG)')}</span>
|
|
|
|
|
<span className="tb-overflow-pro">PRO</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className="tb-overflow-item"
|
|
|
|
|
role="menuitem"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setMoreMenuOpen(false);
|
|
|
|
|
firmwareInputRef.current?.click();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
|
|
|
|
|
<line x1="12" y1="15" x2="12" y2="22" />
|
|
|
|
|
<polyline points="8 18 12 22 16 18" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span className="tb-overflow-label">{t('editor.toolbar.uploadFirmwareLabel', 'Upload firmware')}</span>
|
|
|
|
|
</button>
|
2026-05-29 13:03:39 +07:00
|
|
|
{/* Sync to GitHub — Pro feature. Fires a window event the
|
|
|
|
|
pro overlay listens for; if no overlay is loaded (OSS
|
|
|
|
|
build) the click is a silent no-op which is fine —
|
|
|
|
|
OSS users can't have linked repos anyway. */}
|
|
|
|
|
<button
|
|
|
|
|
className="tb-overflow-item"
|
|
|
|
|
role="menuitem"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setMoreMenuOpen(false);
|
|
|
|
|
window.dispatchEvent(new CustomEvent('velxio-pro-github-sync-prompt', {
|
|
|
|
|
detail: { projectId: currentProject?.id ?? null },
|
|
|
|
|
}));
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
|
|
|
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.44 9.8 8.21 11.39.6.11.82-.26.82-.58 0-.29-.01-1.05-.02-2.06-3.34.72-4.04-1.61-4.04-1.61-.55-1.38-1.33-1.75-1.33-1.75-1.09-.74.08-.72.08-.72 1.2.08 1.84 1.24 1.84 1.24 1.07 1.84 2.81 1.31 3.5 1 .11-.78.42-1.31.76-1.62-2.66-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.12-.3-.54-1.52.11-3.18 0 0 1.01-.32 3.3 1.23A11.5 11.5 0 0 1 12 5.8c1.02.01 2.05.14 3.01.4 2.29-1.55 3.3-1.23 3.3-1.23.65 1.66.24 2.88.12 3.18.77.84 1.24 1.91 1.24 3.22 0 4.61-2.81 5.62-5.49 5.92.43.37.82 1.1.82 2.22 0 1.6-.02 2.89-.02 3.29 0 .32.22.7.83.58A12 12 0 0 0 24 12c0-6.63-5.37-12-12-12z" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span className="tb-overflow-label">{t('editor.toolbar.githubSyncLabel', 'Sync to GitHub')}</span>
|
|
|
|
|
<span className="tb-overflow-pro">PRO</span>
|
|
|
|
|
</button>
|
2026-05-29 20:14:36 +07:00
|
|
|
{/* Share / Embed — free for all users with a public project.
|
|
|
|
|
Watermark removal on the embed is the Pro perk; the
|
|
|
|
|
Share modal itself is open to everyone so they can
|
|
|
|
|
copy the link / iframe snippet. */}
|
|
|
|
|
<button
|
|
|
|
|
className="tb-overflow-item"
|
|
|
|
|
role="menuitem"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setMoreMenuOpen(false);
|
|
|
|
|
window.dispatchEvent(new CustomEvent('velxio-pro-share-prompt', {
|
|
|
|
|
detail: { projectId: currentProject?.id ?? null },
|
|
|
|
|
}));
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
<circle cx="18" cy="5" r="3" />
|
|
|
|
|
<circle cx="6" cy="12" r="3" />
|
|
|
|
|
<circle cx="18" cy="19" r="3" />
|
|
|
|
|
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
|
|
|
|
|
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span className="tb-overflow-label">{t('editor.toolbar.shareLabel', 'Share / Embed')}</span>
|
|
|
|
|
</button>
|
2026-05-30 02:24:55 +07:00
|
|
|
{/* Record simulation — Pro feature. Dispatches a toggle the
|
|
|
|
|
pro overlay handles (plan check, board-type check,
|
|
|
|
|
start/stop the recorder). OSS build → no listener →
|
|
|
|
|
silent no-op. */}
|
|
|
|
|
<button
|
|
|
|
|
className="tb-overflow-item"
|
|
|
|
|
role="menuitem"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setMoreMenuOpen(false);
|
|
|
|
|
window.dispatchEvent(new CustomEvent('velxio-pro-replay-record-toggle', {
|
|
|
|
|
detail: { projectId: currentProject?.id ?? null },
|
|
|
|
|
}));
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
|
|
|
<circle cx="12" cy="12" r="7" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span className="tb-overflow-label">{t('editor.toolbar.recordLabel', 'Record simulation')}</span>
|
|
|
|
|
<span className="tb-overflow-pro">PRO</span>
|
|
|
|
|
</button>
|
2026-05-29 12:41:05 +07:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
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
|
|
|
);
|
|
|
|
|
};
|