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(editor): menus File/Edit en la cabecera y editor sin nav de marketing
La queja: demasiados botones en una sola fila, y en pantallas pequenas se
solapan — medido, no impresion: a 1024px Code/Both/Circuit pisaban a
Compile/Run/Stop por 9-20px y Add pisaba el chat por 29px, con el idioma y
el usuario cortados fuera del viewport.
Tres piezas:
1. En el editor, el nav de marketing (Home/Docs/Pricing/...) sobra: ya
estas dentro, y cuesta exactamente el ancho que le falta a la toolbar.
AppHeader gana la variante editorMenu — mismo mecanismo que ya usa la
build de escritorio (VITE_DESKTOP) — que oculta el nav y pinta un menu
File/Edit junto al logo. El logo sigue llevando a inicio; idioma,
autoguardado, Share y usuario se quedan.
2. File/Edit al estilo escritorio: lo que se usa cada minuto sigue siendo
boton; lo que se usa unas veces por sesion va al menu. File: nuevo
workspace, nuevo fichero, abrir, guardar, importar, exportar, BOM,
imagen del esquema. Edit: undo/redo (con su estado real del historial),
centrar vista, zoom. Las acciones viven en cierres de cuatro
componentes distintos, asi que hay un registro id->handler
(lib/editorCommands, mismo patron de seam que registerProExamples o
registerBoardBuiltins): cada dueno registra al montar y el menu invoca
por id; un item sin dueno montado se pinta deshabilitado, que ademas es
la verdad. El registro sobrevive al remontaje StrictMode (el cleanup
viejo no borra al sucesor) y tiene test de eso.
3. La toolbar adelgaza: fuera Undo/Redo (Ctrl+Z/Y siguen), fuera los
botones inline de Import/Export y sus gemelos responsivos del More
(~70px recuperados); el More queda solo con los extras pro (firmware,
GitHub, share, grabacion). El CSS responsivo de los botones retirados
se va con ellos.
Suite en verde (2300) y build del overlay verificado desde este arbol.
2026-07-30 21:26:36 +07:00
|
|
|
import { registerEditorCommand } from '../../lib/editorCommands';
|
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-07-29 00:43:20 +07:00
|
|
|
import { useSimulatorStore, piRerunScript } from '../../store/useSimulatorStore';
|
2026-07-29 09:41:52 +07:00
|
|
|
import { decideEngine } from '../../lib/instantEngine';
|
2026-05-13 00:26:33 +07:00
|
|
|
import { useElectricalStore } from '../../store/useElectricalStore';
|
2026-07-18 11:44:38 +07:00
|
|
|
import { type VerificationResult } from '../../simulation/verify/circuitVerifier';
|
|
|
|
|
import { verifyCircuitFromStore } from '../../simulation/verify/verifyFromStore';
|
2026-05-13 02:55:15 +07:00
|
|
|
import { CircuitVerificationModal } from '../simulator/CircuitVerificationModal';
|
2026-03-30 07:27:41 +07:00
|
|
|
import type { BoardKind, LanguageMode } from '../../types/board';
|
feat(esp32): pure ESP-IDF language mode for the ESP32 family (#139)
Adds a third entry to the board language selector next to Arduino C++
and MicroPython: ESP-IDF. In this mode the user writes a plain ESP-IDF
project — app_main() entry point, FreeRTOS + driver APIs — and the
backend compiles it through the same ESP-IDF toolchain it already uses
for ESP32 Arduino sketches, just without the arduino-esp32 component.
Backend:
- CompileRequest.language ('espidf') threaded through the sync + async
compile paths and folded into the dedup job key (language='arduino'
and omitted hash identically so old clients keep dedupping).
- espidf_compiler: pure_idf flag. User files are written into main/
as-is (no Arduino.h wrap, no velxio_compat.h, Arduino library
resolution skipped), ARDUINO_ESP32_PATH is dropped from the build env
and VELXIO_PURE_SKETCH raised so the template CMake compiles the
user's own sources via a glob branch. Pure builds get their own
persistent build-dir variant through the eff_hash fold.
- QEMU WiFi compat for IDF-style code: esp_wifi.h/esp_wifi_init
detection sets has_wifi, and literal #define SSID/PASS plus
wifi_config_t designated initializers are normalized to the QEMU AP.
- CONFIG_ARDUINO_* lines are stripped from sdkconfig.defaults in pure
mode (the symbols don't exist without the arduino component).
Frontend:
- LanguageMode gains 'espidf'; BOARD_SUPPORTS_ESPIDF covers the ESP32
family (Xtensa, S3, C3). Toolbar shows the option only for those.
- Switching modes seeds a main.c blink skeleton (app_main + gpio
driver), mirroring the MicroPython main.py flow.
- compileCode sends language='espidf'; run/stop paths are unchanged
(the QEMU worker consumes the same merged flash image).
- New gallery example: esp32-idf-blink (LED + resistor on GPIO 2).
Tests: unit coverage for the build-env switch, IDF wifi normalization,
job-key variance, file-group seeding and the new example; verified
end-to-end in a container from the prod image (pure build produces a
bootable flash image; Arduino-mode build unchanged, same variant hash).
2026-07-24 11:37:01 +07:00
|
|
|
import { BOARD_KIND_FQBN, BOARD_SUPPORTS_ESPIDF, BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind, boardDisplayName } from '../../types/board';
|
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-18 02:46:04 +07:00
|
|
|
/**
|
|
|
|
|
* Output-console group for circuit pre-flight + runtime faults. Routing these
|
|
|
|
|
* into the compile console (instead of an inline toolbar toast that overlapped
|
|
|
|
|
* the Run/Stop buttons) gives one unified, red-coloured diagnostics log —
|
|
|
|
|
* Proteus-style. id is matched when clearing so the findings survive an
|
|
|
|
|
* auto-compile triggered by the same Run.
|
|
|
|
|
*/
|
|
|
|
|
const CIRCUIT_CHECK_TARGET: CompileTarget = {
|
|
|
|
|
id: 'circuit-check',
|
|
|
|
|
label: 'Circuit check',
|
|
|
|
|
kind: 'board',
|
|
|
|
|
};
|
|
|
|
|
|
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
|
2026-07-03 11:50:21 +07:00
|
|
|
* action group. Normally empty (the slot just acts as a flexible spacer
|
|
|
|
|
* that keeps the right action icons pinned); private overlays may inject
|
|
|
|
|
* deployment-specific content here without forking the toolbar.
|
2026-04-29 09:23:20 +07:00
|
|
|
*/
|
|
|
|
|
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);
|
2026-07-03 06:22:17 +07:00
|
|
|
// Multi-board: the primary Run button runs ALL boards (the whole wired
|
|
|
|
|
// project is one system — running a subset is almost never intended), with a
|
|
|
|
|
// split-menu to still run just the active board. Single-board is unchanged.
|
|
|
|
|
const isMultiBoard = boards.length > 1;
|
2026-06-04 12:21:39 +07:00
|
|
|
|
|
|
|
|
// 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(
|
2026-07-29 09:41:52 +07:00
|
|
|
(boardKind: BoardKind | undefined, engine?: 'instant' | 'linux') => {
|
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 fqbn = boardKind ? BOARD_KIND_FQBN[boardKind] : null;
|
|
|
|
|
void reportRunEvent({
|
|
|
|
|
project_id: currentProject?.id ?? null,
|
|
|
|
|
board_fqbn: fqbn ?? null,
|
2026-07-29 09:41:52 +07:00
|
|
|
engine: engine ?? 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
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
[currentProject],
|
|
|
|
|
);
|
2026-03-03 10:20:49 +07:00
|
|
|
const [compiling, setCompiling] = useState(false);
|
2026-06-18 03:06:15 +07:00
|
|
|
// True while the pre-flight circuit verification SPICE solve is running.
|
|
|
|
|
// Drives the Run-button spinner so the user gets feedback during the
|
|
|
|
|
// (sometimes multi-second, cold-worker) solve instead of a dead button.
|
|
|
|
|
const [verifying, setVerifying] = useState(false);
|
|
|
|
|
// Synchronous re-entrancy guard: a click while a run/verify is already in
|
|
|
|
|
// flight is ignored, so rapid clicks can't stack multiple verifications.
|
|
|
|
|
const runInFlightRef = useRef(false);
|
2026-03-03 10:20:49 +07:00
|
|
|
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-07-03 06:22:17 +07:00
|
|
|
// Split-button menu for the multi-board Run control ("Run all" / "Run active only").
|
|
|
|
|
const [runMenuOpen, setRunMenuOpen] = useState(false);
|
|
|
|
|
const runMenuRef = useRef<HTMLDivElement>(null);
|
2026-05-29 12:41:05 +07:00
|
|
|
|
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-06-18 01:37:32 +07:00
|
|
|
// Surface a runtime circuit fault (e.g. an LED that burnt out from
|
2026-06-18 02:46:04 +07:00
|
|
|
// overcurrent during the live SPICE solve) in the output console, in red,
|
|
|
|
|
// under the "Circuit check" group — same place as the pre-flight findings.
|
|
|
|
|
// (Previously an inline toolbar toast that overlapped the Run/Stop buttons.)
|
|
|
|
|
// We do NOT auto-open the console here: the continuous solver can fault on
|
|
|
|
|
// load, and popping the console open then would be intrusive. The pre-flight
|
|
|
|
|
// (on Run) opens it; this entry then lands in the already-open log.
|
2026-06-18 01:37:32 +07:00
|
|
|
useEffect(() => {
|
|
|
|
|
const onFault = (e: Event) => {
|
|
|
|
|
const detail = (e as CustomEvent).detail as { message?: string } | undefined;
|
2026-06-18 02:46:04 +07:00
|
|
|
if (!detail?.message) return;
|
|
|
|
|
const text = detail.message;
|
|
|
|
|
setCompileLogs((prev) => [
|
|
|
|
|
...prev,
|
|
|
|
|
{ timestamp: new Date(), type: 'error', message: text, target: CIRCUIT_CHECK_TARGET },
|
|
|
|
|
]);
|
2026-06-18 01:37:32 +07:00
|
|
|
};
|
|
|
|
|
window.addEventListener('velxio-circuit-fault', onFault);
|
|
|
|
|
return () => window.removeEventListener('velxio-circuit-fault', onFault);
|
2026-06-18 02:46:04 +07:00
|
|
|
}, [setCompileLogs]);
|
2026-06-18 01:37:32 +07:00
|
|
|
|
2026-03-16 10:12:33 +07:00
|
|
|
|
2026-07-03 06:22:17 +07:00
|
|
|
// Close the Run split-menu on outside click / Escape (mirrors the more-menu).
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!runMenuOpen) return;
|
|
|
|
|
const onClickOutside = (e: MouseEvent) => {
|
|
|
|
|
if (runMenuRef.current && !runMenuRef.current.contains(e.target as Node)) {
|
|
|
|
|
setRunMenuOpen(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const onEsc = (e: KeyboardEvent) => {
|
|
|
|
|
if (e.key === 'Escape') setRunMenuOpen(false);
|
|
|
|
|
};
|
|
|
|
|
document.addEventListener('mousedown', onClickOutside);
|
|
|
|
|
document.addEventListener('keydown', onEsc);
|
|
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener('mousedown', onClickOutside);
|
|
|
|
|
document.removeEventListener('keydown', onEsc);
|
|
|
|
|
};
|
|
|
|
|
}, [runMenuOpen]);
|
|
|
|
|
|
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.
|
2026-06-18 03:52:40 +07:00
|
|
|
// Keep the "Circuit check" findings, though: a Run auto-compiles right
|
|
|
|
|
// after the pre-flight verification logs them, and clearing here would
|
|
|
|
|
// wipe a circuit warning the user just triggered.
|
|
|
|
|
setCompileLogs((prev) => prev.filter((l) => l.target?.id === CIRCUIT_CHECK_TARGET.id));
|
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
|
|
|
|
2026-07-28 19:36:13 +07:00
|
|
|
// QEMU-Linux boards don'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-07-28 19:36:13 +07:00
|
|
|
blog('info', `${boardLabel}: 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,
|
feat(esp32): pure ESP-IDF language mode for the ESP32 family (#139)
Adds a third entry to the board language selector next to Arduino C++
and MicroPython: ESP-IDF. In this mode the user writes a plain ESP-IDF
project — app_main() entry point, FreeRTOS + driver APIs — and the
backend compiles it through the same ESP-IDF toolchain it already uses
for ESP32 Arduino sketches, just without the arduino-esp32 component.
Backend:
- CompileRequest.language ('espidf') threaded through the sync + async
compile paths and folded into the dedup job key (language='arduino'
and omitted hash identically so old clients keep dedupping).
- espidf_compiler: pure_idf flag. User files are written into main/
as-is (no Arduino.h wrap, no velxio_compat.h, Arduino library
resolution skipped), ARDUINO_ESP32_PATH is dropped from the build env
and VELXIO_PURE_SKETCH raised so the template CMake compiles the
user's own sources via a glob branch. Pure builds get their own
persistent build-dir variant through the eff_hash fold.
- QEMU WiFi compat for IDF-style code: esp_wifi.h/esp_wifi_init
detection sets has_wifi, and literal #define SSID/PASS plus
wifi_config_t designated initializers are normalized to the QEMU AP.
- CONFIG_ARDUINO_* lines are stripped from sdkconfig.defaults in pure
mode (the symbols don't exist without the arduino component).
Frontend:
- LanguageMode gains 'espidf'; BOARD_SUPPORTS_ESPIDF covers the ESP32
family (Xtensa, S3, C3). Toolbar shows the option only for those.
- Switching modes seeds a main.c blink skeleton (app_main + gpio
driver), mirroring the MicroPython main.py flow.
- compileCode sends language='espidf'; run/stop paths are unchanged
(the QEMU worker consumes the same merged flash image).
- New gallery example: esp32-idf-blink (LED + resistor on GPIO 2).
Tests: unit coverage for the build-env switch, IDF wifi normalization,
job-key variance, file-group seeding and the new example; verified
end-to-end in a container from the prod image (pure build produces a
bootable flash image; Arduino-mode build unchanged, same variant hash).
2026-07-24 11:37:01 +07:00
|
|
|
// Pure ESP-IDF mode (issue #139): tell the backend to compile the
|
|
|
|
|
// user's app_main() sources without the arduino-esp32 component.
|
|
|
|
|
language: activeBoard?.languageMode === 'espidf' ? 'espidf' : undefined,
|
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.
|
|
|
|
|
*/
|
2026-07-18 11:44:38 +07:00
|
|
|
const runVerification = useCallback(
|
|
|
|
|
(): Promise<VerificationResult | null> => verifyCircuitFromStore(),
|
|
|
|
|
[],
|
|
|
|
|
);
|
2026-05-13 02:55:15 +07:00
|
|
|
|
|
|
|
|
/**
|
2026-06-18 02:46:04 +07:00
|
|
|
* Returns true if the caller should proceed inline. All findings are written
|
|
|
|
|
* to the output console (red errors / orange warnings, "Circuit check"
|
|
|
|
|
* group). If the verifier finds errors we also stash a resume callback in
|
|
|
|
|
* `pendingRunRef` and pop the verification modal; the resume callback
|
|
|
|
|
* re-enters `handleRun` with `skipVerify = true` so we don't loop.
|
|
|
|
|
* Warnings-only results don't block — the console entry is enough.
|
2026-05-13 02:55:15 +07:00
|
|
|
*/
|
|
|
|
|
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;
|
2026-06-18 02:46:04 +07:00
|
|
|
|
|
|
|
|
// Write every finding to the output console under "Circuit check" — red
|
|
|
|
|
// for errors, orange for warnings — so there's one persistent, unified
|
|
|
|
|
// diagnostics log next to the compiler output (Proteus-style). Replace
|
|
|
|
|
// any prior circuit-check entries so repeated runs stay clean, and open
|
|
|
|
|
// the console so the findings are visible.
|
|
|
|
|
const now = new Date();
|
|
|
|
|
setCompileLogs((prev) => [
|
|
|
|
|
...prev.filter((l) => l.target?.id !== CIRCUIT_CHECK_TARGET.id),
|
|
|
|
|
...result.errors.map((e) => ({
|
|
|
|
|
timestamp: now,
|
|
|
|
|
type: 'error' as const,
|
|
|
|
|
message: e.message,
|
|
|
|
|
target: CIRCUIT_CHECK_TARGET,
|
|
|
|
|
})),
|
|
|
|
|
...result.warnings.map((w) => ({
|
|
|
|
|
timestamp: now,
|
|
|
|
|
type: 'warning' as const,
|
|
|
|
|
message: w.message,
|
|
|
|
|
target: CIRCUIT_CHECK_TARGET,
|
|
|
|
|
})),
|
|
|
|
|
]);
|
|
|
|
|
setConsoleOpen(true);
|
|
|
|
|
|
|
|
|
|
// Warnings only — non-blocking; the console entry is enough, run continues.
|
|
|
|
|
if (result.errors.length === 0) return true;
|
|
|
|
|
|
|
|
|
|
// Errors → also pop the modal so the user makes an explicit Run-anyway /
|
|
|
|
|
// Cancel decision; the console keeps the persistent red record.
|
2026-05-13 02:55:15 +07:00
|
|
|
pendingRunRef.current = resume;
|
|
|
|
|
setVerification(result);
|
|
|
|
|
return false;
|
|
|
|
|
},
|
2026-06-18 02:46:04 +07:00
|
|
|
[runVerification, setCompileLogs, setConsoleOpen],
|
2026-05-13 02:55:15 +07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-06-18 03:06:15 +07:00
|
|
|
// The verification solve can take a second or two (cold ngspice worker).
|
|
|
|
|
// Show the Run-button spinner and ignore re-clicks while it runs — the
|
|
|
|
|
// button otherwise looks idle and gets clicked repeatedly, stacking
|
|
|
|
|
// multiple verifications.
|
|
|
|
|
if (runInFlightRef.current) return;
|
|
|
|
|
runInFlightRef.current = true;
|
|
|
|
|
setVerifying(true);
|
|
|
|
|
let ok = false;
|
|
|
|
|
try {
|
|
|
|
|
ok = await checkOrBlock(() => handleRun(true));
|
|
|
|
|
} finally {
|
|
|
|
|
setVerifying(false);
|
|
|
|
|
runInFlightRef.current = false;
|
|
|
|
|
}
|
2026-05-13 02:55:15 +07:00
|
|
|
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);
|
2026-06-18 02:46:04 +07:00
|
|
|
// Fresh chip output, but keep the circuit pre-flight findings just
|
|
|
|
|
// logged by checkOrBlock so they survive a "Run anyway".
|
|
|
|
|
setCompileLogs((prev) => prev.filter((l) => l.target?.id === CIRCUIT_CHECK_TARGET.id));
|
2026-06-04 02:04:17 +07:00
|
|
|
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-07-29 00:43:20 +07:00
|
|
|
// QEMU-Linux boards (Raspberry Pi family + overlay piFamily kinds)
|
|
|
|
|
// boot straight from the rootfs — there is no firmware to compile,
|
|
|
|
|
// and handleCompile's Pi early-return never sets compiledProgram, so
|
|
|
|
|
// the gate below would surface a bogus "Compilation produced no
|
2026-07-29 03:05:36 +07:00
|
|
|
// firmware" error. Power the board on directly (Run follows the
|
|
|
|
|
// standard disabled-while-running convention; RESET is the fast
|
|
|
|
|
// re-run-without-reboot on a booted guest). Must stay ABOVE the
|
|
|
|
|
// generic stop-then-boot restart below.
|
2026-07-29 00:43:20 +07:00
|
|
|
if (isPiBoardKind(board?.boardKind ?? '')) {
|
|
|
|
|
trackRunSimulation(board?.boardKind);
|
2026-07-29 09:41:52 +07:00
|
|
|
reportRun(
|
|
|
|
|
board?.boardKind,
|
|
|
|
|
decideEngine(activeBoardId, board?.enginePinned).engine,
|
|
|
|
|
);
|
2026-07-29 03:05:36 +07:00
|
|
|
if (board?.running) {
|
|
|
|
|
// Zombie/edge case (Run is normally disabled while running):
|
|
|
|
|
// power-cycle for a clean boot.
|
|
|
|
|
stopBoard(activeBoardId);
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
2026-07-29 00:43:20 +07:00
|
|
|
}
|
2026-07-29 03:05:36 +07:00
|
|
|
console.log('[handleRun] → startBoard (QEMU-Linux, no firmware)', activeBoardId);
|
|
|
|
|
startBoard(activeBoardId);
|
2026-07-29 00:43:20 +07:00
|
|
|
setMessage(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
fix(sim): clean restart on Run after agent + display body occludes crossing wires
Two issues from a real ESP32 7-segment clock the agent built.
Run after the agent didn't work until a page reload
---------------------------------------------------
The agent's run_simulation leaves the ESP32 board RUNNING (live QEMU
WebSocket). Esp32Bridge.connect() is a no-op while the socket is non-CLOSED,
so the user's subsequent Run click called startBoard() → connect() → did
NOTHING. And if the backend QEMU session had since died while the frontend
socket lingered (CONNECTING/OPEN/CLOSING), the user saw a dead sim that only
a reload cleared — exactly the "di Run y no funcionó; recargué y sí" report.
The Arduino/C++ QEMU path now stops a running board first (closing the WS),
waits for it to settle, then boots fresh — the MicroPython path already did
this for the same reason.
Wires painted over the 7-segment digits
----------------------------------------
The agent bridges each segment strip to its resistor from a breadboard hole
that is physically UNDER the seated display; on the flat canvas those wires
(wire layer z 35) painted over the digits (component z 1) — "casi ni se ven
los dígitos". A large-bodied display seated on a breadboard now renders
ABOVE the wire layer, so its face occludes the wires crossing it exactly as
the real part's body would (the wire passes behind it to reach the hole).
Scoped to display bodies (7segment, matrix, oled, lcd, ili9341, led-ring…)
and only when actually seated; thin parts and free-floating displays are
untouched. The pin overlay + seated-pin markers share the display's stacking
group, so they rise with it and wiring still works.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 11:46:57 +07:00
|
|
|
// Clean restart when the board is already running. Esp32Bridge.connect()
|
|
|
|
|
// is a no-op while the socket is non-CLOSED, so startBoard() on a live
|
|
|
|
|
// session does NOTHING — and if the backend QEMU session has since died
|
|
|
|
|
// but the frontend socket is still zombie (CONNECTING/OPEN/CLOSING), the
|
|
|
|
|
// user sees a dead sim that only a page reload fixes. This is the exact
|
|
|
|
|
// "el agente terminó, di Run y no funcionó; recargué y sí" report: the
|
|
|
|
|
// agent's run_simulation left the board running, so the user's Run
|
|
|
|
|
// no-op'd. Stop first (closes the WS), let it settle, then boot fresh —
|
|
|
|
|
// mirrors what the MicroPython branch above already does.
|
|
|
|
|
if (board?.running) {
|
|
|
|
|
stopBoard(activeBoardId);
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
|
|
|
}
|
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-07-29 03:05:36 +07:00
|
|
|
// QEMU-Linux boards: Reset = re-upload the edited files and re-run the
|
|
|
|
|
// script on the live guest (no ~45 s reboot). Mirrors what Reset means
|
|
|
|
|
// elsewhere — restart the program — while Run keeps the standard
|
|
|
|
|
// disabled-while-running behaviour.
|
|
|
|
|
if (activeBoard && isPiBoardKind(activeBoard.boardKind) && activeBoard.piBooted) {
|
|
|
|
|
void piRerunScript(activeBoard.id, activeBoard.boardKind);
|
|
|
|
|
setMessage(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
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
|
|
|
})),
|
|
|
|
|
]);
|
|
|
|
|
},
|
feat(esp32): pure ESP-IDF language mode for the ESP32 family (#139)
Adds a third entry to the board language selector next to Arduino C++
and MicroPython: ESP-IDF. In this mode the user writes a plain ESP-IDF
project — app_main() entry point, FreeRTOS + driver APIs — and the
backend compiles it through the same ESP-IDF toolchain it already uses
for ESP32 Arduino sketches, just without the arduino-esp32 component.
Backend:
- CompileRequest.language ('espidf') threaded through the sync + async
compile paths and folded into the dedup job key (language='arduino'
and omitted hash identically so old clients keep dedupping).
- espidf_compiler: pure_idf flag. User files are written into main/
as-is (no Arduino.h wrap, no velxio_compat.h, Arduino library
resolution skipped), ARDUINO_ESP32_PATH is dropped from the build env
and VELXIO_PURE_SKETCH raised so the template CMake compiles the
user's own sources via a glob branch. Pure builds get their own
persistent build-dir variant through the eff_hash fold.
- QEMU WiFi compat for IDF-style code: esp_wifi.h/esp_wifi_init
detection sets has_wifi, and literal #define SSID/PASS plus
wifi_config_t designated initializers are normalized to the QEMU AP.
- CONFIG_ARDUINO_* lines are stripped from sdkconfig.defaults in pure
mode (the symbols don't exist without the arduino component).
Frontend:
- LanguageMode gains 'espidf'; BOARD_SUPPORTS_ESPIDF covers the ESP32
family (Xtensa, S3, C3). Toolbar shows the option only for those.
- Switching modes seeds a main.c blink skeleton (app_main + gpio
driver), mirroring the MicroPython main.py flow.
- compileCode sends language='espidf'; run/stop paths are unchanged
(the QEMU worker consumes the same merged flash image).
- New gallery example: esp32-idf-blink (LED + resistor on GPIO 2).
Tests: unit coverage for the build-env switch, IDF wifi normalization,
job-key variance, file-group seeding and the new example; verified
end-to-end in a container from the prod image (pure build produces a
bootable flash image; Arduino-mode build unchanged, same variant hash).
2026-07-24 11:37:01 +07:00
|
|
|
{ boardOptions: board.boardOptions, spiffsFiles: board.spiffsFiles, libraries: board.libraries?.length ? board.libraries : null, language: board.languageMode === 'espidf' ? 'espidf' : undefined },
|
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-06-18 02:02:57 +07:00
|
|
|
const handleRunAll = async (skipVerify = false) => {
|
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;
|
|
|
|
|
|
2026-06-18 02:02:57 +07:00
|
|
|
// Same pre-flight safety check as handleRun — block on shorts / overcurrent
|
|
|
|
|
// before starting every board, with a "Run anyway" escape.
|
|
|
|
|
if (!skipVerify) {
|
|
|
|
|
const ok = await checkOrBlock(() => handleRunAll(true));
|
|
|
|
|
if (!ok) return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 12:21:39 +07:00
|
|
|
// 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.' });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
feat(editor): menus File/Edit en la cabecera y editor sin nav de marketing
La queja: demasiados botones en una sola fila, y en pantallas pequenas se
solapan — medido, no impresion: a 1024px Code/Both/Circuit pisaban a
Compile/Run/Stop por 9-20px y Add pisaba el chat por 29px, con el idioma y
el usuario cortados fuera del viewport.
Tres piezas:
1. En el editor, el nav de marketing (Home/Docs/Pricing/...) sobra: ya
estas dentro, y cuesta exactamente el ancho que le falta a la toolbar.
AppHeader gana la variante editorMenu — mismo mecanismo que ya usa la
build de escritorio (VITE_DESKTOP) — que oculta el nav y pinta un menu
File/Edit junto al logo. El logo sigue llevando a inicio; idioma,
autoguardado, Share y usuario se quedan.
2. File/Edit al estilo escritorio: lo que se usa cada minuto sigue siendo
boton; lo que se usa unas veces por sesion va al menu. File: nuevo
workspace, nuevo fichero, abrir, guardar, importar, exportar, BOM,
imagen del esquema. Edit: undo/redo (con su estado real del historial),
centrar vista, zoom. Las acciones viven en cierres de cuatro
componentes distintos, asi que hay un registro id->handler
(lib/editorCommands, mismo patron de seam que registerProExamples o
registerBoardBuiltins): cada dueno registra al montar y el menu invoca
por id; un item sin dueno montado se pinta deshabilitado, que ademas es
la verdad. El registro sobrevive al remontaje StrictMode (el cleanup
viejo no borra al sucesor) y tiene test de eso.
3. La toolbar adelgaza: fuera Undo/Redo (Ctrl+Z/Y siguen), fuera los
botones inline de Import/Export y sus gemelos responsivos del More
(~70px recuperados); el More queda solo con los extras pro (firmware,
GitHub, share, grabacion). El CSS responsivo de los botones retirados
se va con ellos.
Suite en verde (2300) y build del overlay verificado desde este arbol.
2026-07-30 21:26:36 +07:00
|
|
|
// File-menu commands owned by this toolbar (the handlers close over its
|
|
|
|
|
// state). Registered through a latest-ref so the one-time registration
|
|
|
|
|
// always invokes the current render's closure, never a stale one.
|
2026-07-31 00:18:37 +07:00
|
|
|
const makeMenuCommands = () => ({
|
feat(editor): menus File/Edit en la cabecera y editor sin nav de marketing
La queja: demasiados botones en una sola fila, y en pantallas pequenas se
solapan — medido, no impresion: a 1024px Code/Both/Circuit pisaban a
Compile/Run/Stop por 9-20px y Add pisaba el chat por 29px, con el idioma y
el usuario cortados fuera del viewport.
Tres piezas:
1. En el editor, el nav de marketing (Home/Docs/Pricing/...) sobra: ya
estas dentro, y cuesta exactamente el ancho que le falta a la toolbar.
AppHeader gana la variante editorMenu — mismo mecanismo que ya usa la
build de escritorio (VITE_DESKTOP) — que oculta el nav y pinta un menu
File/Edit junto al logo. El logo sigue llevando a inicio; idioma,
autoguardado, Share y usuario se quedan.
2. File/Edit al estilo escritorio: lo que se usa cada minuto sigue siendo
boton; lo que se usa unas veces por sesion va al menu. File: nuevo
workspace, nuevo fichero, abrir, guardar, importar, exportar, BOM,
imagen del esquema. Edit: undo/redo (con su estado real del historial),
centrar vista, zoom. Las acciones viven en cierres de cuatro
componentes distintos, asi que hay un registro id->handler
(lib/editorCommands, mismo patron de seam que registerProExamples o
registerBoardBuiltins): cada dueno registra al montar y el menu invoca
por id; un item sin dueno montado se pinta deshabilitado, que ademas es
la verdad. El registro sobrevive al remontaje StrictMode (el cleanup
viejo no borra al sucesor) y tiene test de eso.
3. La toolbar adelgaza: fuera Undo/Redo (Ctrl+Z/Y siguen), fuera los
botones inline de Import/Export y sus gemelos responsivos del More
(~70px recuperados); el More queda solo con los extras pro (firmware,
GitHub, share, grabacion). El CSS responsivo de los botones retirados
se va con ellos.
Suite en verde (2300) y build del overlay verificado desde este arbol.
2026-07-30 21:26:36 +07:00
|
|
|
import: () => importInputRef.current?.click(),
|
|
|
|
|
export: () => void handleExport(),
|
|
|
|
|
bom: () => void handleExportBom(),
|
|
|
|
|
screenshot: () => void handleExportScreenshot(),
|
2026-07-31 00:18:37 +07:00
|
|
|
firmware: () => firmwareInputRef.current?.click(),
|
|
|
|
|
// Pro actions fire the same window events the old "..." menu items
|
|
|
|
|
// fired; without the overlay they are silent no-ops, which is fine —
|
|
|
|
|
// OSS builds cannot have linked repos or shared projects anyway.
|
|
|
|
|
share: () =>
|
|
|
|
|
window.dispatchEvent(new CustomEvent('velxio-pro-share-prompt', {
|
|
|
|
|
detail: { projectId: currentProject?.id ?? null },
|
|
|
|
|
})),
|
|
|
|
|
githubSync: () =>
|
|
|
|
|
window.dispatchEvent(new CustomEvent('velxio-pro-github-sync-prompt', {
|
|
|
|
|
detail: { projectId: currentProject?.id ?? null },
|
|
|
|
|
})),
|
|
|
|
|
record: () =>
|
|
|
|
|
window.dispatchEvent(new CustomEvent('velxio-pro-replay-record-toggle', {
|
|
|
|
|
detail: { projectId: currentProject?.id ?? null },
|
|
|
|
|
})),
|
feat(editor): View and Language menus — full desktop-menu parity
The menubar now matches the desktop app's native menu set: File, Edit,
View, Language, Help.
View collects what the user asked to reach from a menu: Compile (Ctrl+B),
Run, Stop, Reset up top; the panel toggles — File Explorer, Output
Console, Serial Monitor, Oscilloscope/Logic Analyzer — in the middle
(store-backed ones render a live check, like a real desktop menu); and
the canvas view actions (center, zoom) move here from Edit, which goes
back to being undo/redo only, as menus have always worked.
Language lists the nine locales with the current one checked, switching
through the same switchLocale path the header globe uses — so language is
reachable from the menubar regardless of the account state, on top of
living in the signed-in account menu.
Run/Stop/Compile/Reset and the console toggle register through the
editorCommands seam from EditorToolbar (their handlers close over its
state); the explorer toggle from EditorPage; serial and scope call their
stores directly, same as undo/redo.
2026-07-31 02:05:55 +07:00
|
|
|
compile: () => void handleCompile(),
|
|
|
|
|
run: () => void handleRun(),
|
|
|
|
|
stop: () => handleStop(),
|
|
|
|
|
resetBoard: () => handleReset(),
|
|
|
|
|
toggleConsole: () => setConsoleOpen((v) => !v),
|
feat(editor): menus File/Edit en la cabecera y editor sin nav de marketing
La queja: demasiados botones en una sola fila, y en pantallas pequenas se
solapan — medido, no impresion: a 1024px Code/Both/Circuit pisaban a
Compile/Run/Stop por 9-20px y Add pisaba el chat por 29px, con el idioma y
el usuario cortados fuera del viewport.
Tres piezas:
1. En el editor, el nav de marketing (Home/Docs/Pricing/...) sobra: ya
estas dentro, y cuesta exactamente el ancho que le falta a la toolbar.
AppHeader gana la variante editorMenu — mismo mecanismo que ya usa la
build de escritorio (VITE_DESKTOP) — que oculta el nav y pinta un menu
File/Edit junto al logo. El logo sigue llevando a inicio; idioma,
autoguardado, Share y usuario se quedan.
2. File/Edit al estilo escritorio: lo que se usa cada minuto sigue siendo
boton; lo que se usa unas veces por sesion va al menu. File: nuevo
workspace, nuevo fichero, abrir, guardar, importar, exportar, BOM,
imagen del esquema. Edit: undo/redo (con su estado real del historial),
centrar vista, zoom. Las acciones viven en cierres de cuatro
componentes distintos, asi que hay un registro id->handler
(lib/editorCommands, mismo patron de seam que registerProExamples o
registerBoardBuiltins): cada dueno registra al montar y el menu invoca
por id; un item sin dueno montado se pinta deshabilitado, que ademas es
la verdad. El registro sobrevive al remontaje StrictMode (el cleanup
viejo no borra al sucesor) y tiene test de eso.
3. La toolbar adelgaza: fuera Undo/Redo (Ctrl+Z/Y siguen), fuera los
botones inline de Import/Export y sus gemelos responsivos del More
(~70px recuperados); el More queda solo con los extras pro (firmware,
GitHub, share, grabacion). El CSS responsivo de los botones retirados
se va con ellos.
Suite en verde (2300) y build del overlay verificado desde este arbol.
2026-07-30 21:26:36 +07:00
|
|
|
});
|
2026-07-31 00:18:37 +07:00
|
|
|
const menuCommandsRef = useRef(makeMenuCommands());
|
|
|
|
|
menuCommandsRef.current = makeMenuCommands();
|
feat(editor): menus File/Edit en la cabecera y editor sin nav de marketing
La queja: demasiados botones en una sola fila, y en pantallas pequenas se
solapan — medido, no impresion: a 1024px Code/Both/Circuit pisaban a
Compile/Run/Stop por 9-20px y Add pisaba el chat por 29px, con el idioma y
el usuario cortados fuera del viewport.
Tres piezas:
1. En el editor, el nav de marketing (Home/Docs/Pricing/...) sobra: ya
estas dentro, y cuesta exactamente el ancho que le falta a la toolbar.
AppHeader gana la variante editorMenu — mismo mecanismo que ya usa la
build de escritorio (VITE_DESKTOP) — que oculta el nav y pinta un menu
File/Edit junto al logo. El logo sigue llevando a inicio; idioma,
autoguardado, Share y usuario se quedan.
2. File/Edit al estilo escritorio: lo que se usa cada minuto sigue siendo
boton; lo que se usa unas veces por sesion va al menu. File: nuevo
workspace, nuevo fichero, abrir, guardar, importar, exportar, BOM,
imagen del esquema. Edit: undo/redo (con su estado real del historial),
centrar vista, zoom. Las acciones viven en cierres de cuatro
componentes distintos, asi que hay un registro id->handler
(lib/editorCommands, mismo patron de seam que registerProExamples o
registerBoardBuiltins): cada dueno registra al montar y el menu invoca
por id; un item sin dueno montado se pinta deshabilitado, que ademas es
la verdad. El registro sobrevive al remontaje StrictMode (el cleanup
viejo no borra al sucesor) y tiene test de eso.
3. La toolbar adelgaza: fuera Undo/Redo (Ctrl+Z/Y siguen), fuera los
botones inline de Import/Export y sus gemelos responsivos del More
(~70px recuperados); el More queda solo con los extras pro (firmware,
GitHub, share, grabacion). El CSS responsivo de los botones retirados
se va con ellos.
Suite en verde (2300) y build del overlay verificado desde este arbol.
2026-07-30 21:26:36 +07:00
|
|
|
useEffect(() => {
|
|
|
|
|
const offs = [
|
|
|
|
|
registerEditorCommand('project.import', () => menuCommandsRef.current.import()),
|
|
|
|
|
registerEditorCommand('project.export', () => menuCommandsRef.current.export()),
|
|
|
|
|
registerEditorCommand('project.exportBom', () => menuCommandsRef.current.bom()),
|
|
|
|
|
registerEditorCommand('project.exportScreenshot', () => menuCommandsRef.current.screenshot()),
|
2026-07-31 00:18:37 +07:00
|
|
|
registerEditorCommand('firmware.upload', () => menuCommandsRef.current.firmware()),
|
|
|
|
|
registerEditorCommand('project.share', () => menuCommandsRef.current.share()),
|
|
|
|
|
registerEditorCommand('project.githubSync', () => menuCommandsRef.current.githubSync()),
|
|
|
|
|
registerEditorCommand('sim.record', () => menuCommandsRef.current.record()),
|
feat(editor): View and Language menus — full desktop-menu parity
The menubar now matches the desktop app's native menu set: File, Edit,
View, Language, Help.
View collects what the user asked to reach from a menu: Compile (Ctrl+B),
Run, Stop, Reset up top; the panel toggles — File Explorer, Output
Console, Serial Monitor, Oscilloscope/Logic Analyzer — in the middle
(store-backed ones render a live check, like a real desktop menu); and
the canvas view actions (center, zoom) move here from Edit, which goes
back to being undo/redo only, as menus have always worked.
Language lists the nine locales with the current one checked, switching
through the same switchLocale path the header globe uses — so language is
reachable from the menubar regardless of the account state, on top of
living in the signed-in account menu.
Run/Stop/Compile/Reset and the console toggle register through the
editorCommands seam from EditorToolbar (their handlers close over its
state); the explorer toggle from EditorPage; serial and scope call their
stores directly, same as undo/redo.
2026-07-31 02:05:55 +07:00
|
|
|
registerEditorCommand('sim.compile', () => menuCommandsRef.current.compile()),
|
|
|
|
|
registerEditorCommand('sim.run', () => menuCommandsRef.current.run()),
|
|
|
|
|
registerEditorCommand('sim.stop', () => menuCommandsRef.current.stop()),
|
|
|
|
|
registerEditorCommand('sim.resetBoard', () => menuCommandsRef.current.resetBoard()),
|
|
|
|
|
registerEditorCommand('view.toggleConsole', () => menuCommandsRef.current.toggleConsole()),
|
feat(editor): menus File/Edit en la cabecera y editor sin nav de marketing
La queja: demasiados botones en una sola fila, y en pantallas pequenas se
solapan — medido, no impresion: a 1024px Code/Both/Circuit pisaban a
Compile/Run/Stop por 9-20px y Add pisaba el chat por 29px, con el idioma y
el usuario cortados fuera del viewport.
Tres piezas:
1. En el editor, el nav de marketing (Home/Docs/Pricing/...) sobra: ya
estas dentro, y cuesta exactamente el ancho que le falta a la toolbar.
AppHeader gana la variante editorMenu — mismo mecanismo que ya usa la
build de escritorio (VITE_DESKTOP) — que oculta el nav y pinta un menu
File/Edit junto al logo. El logo sigue llevando a inicio; idioma,
autoguardado, Share y usuario se quedan.
2. File/Edit al estilo escritorio: lo que se usa cada minuto sigue siendo
boton; lo que se usa unas veces por sesion va al menu. File: nuevo
workspace, nuevo fichero, abrir, guardar, importar, exportar, BOM,
imagen del esquema. Edit: undo/redo (con su estado real del historial),
centrar vista, zoom. Las acciones viven en cierres de cuatro
componentes distintos, asi que hay un registro id->handler
(lib/editorCommands, mismo patron de seam que registerProExamples o
registerBoardBuiltins): cada dueno registra al montar y el menu invoca
por id; un item sin dueno montado se pinta deshabilitado, que ademas es
la verdad. El registro sobrevive al remontaje StrictMode (el cleanup
viejo no borra al sucesor) y tiene test de eso.
3. La toolbar adelgaza: fuera Undo/Redo (Ctrl+Z/Y siguen), fuera los
botones inline de Import/Export y sus gemelos responsivos del More
(~70px recuperados); el More queda solo con los extras pro (firmware,
GitHub, share, grabacion). El CSS responsivo de los botones retirados
se va con ellos.
Suite en verde (2300) y build del overlay verificado desde este arbol.
2026-07-30 21:26:36 +07:00
|
|
|
];
|
|
|
|
|
return () => offs.forEach((off) => off());
|
|
|
|
|
}, []);
|
|
|
|
|
|
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}>
|
feat(esp32): pure ESP-IDF language mode for the ESP32 family (#139)
Adds a third entry to the board language selector next to Arduino C++
and MicroPython: ESP-IDF. In this mode the user writes a plain ESP-IDF
project — app_main() entry point, FreeRTOS + driver APIs — and the
backend compiles it through the same ESP-IDF toolchain it already uses
for ESP32 Arduino sketches, just without the arduino-esp32 component.
Backend:
- CompileRequest.language ('espidf') threaded through the sync + async
compile paths and folded into the dedup job key (language='arduino'
and omitted hash identically so old clients keep dedupping).
- espidf_compiler: pure_idf flag. User files are written into main/
as-is (no Arduino.h wrap, no velxio_compat.h, Arduino library
resolution skipped), ARDUINO_ESP32_PATH is dropped from the build env
and VELXIO_PURE_SKETCH raised so the template CMake compiles the
user's own sources via a glob branch. Pure builds get their own
persistent build-dir variant through the eff_hash fold.
- QEMU WiFi compat for IDF-style code: esp_wifi.h/esp_wifi_init
detection sets has_wifi, and literal #define SSID/PASS plus
wifi_config_t designated initializers are normalized to the QEMU AP.
- CONFIG_ARDUINO_* lines are stripped from sdkconfig.defaults in pure
mode (the symbols don't exist without the arduino component).
Frontend:
- LanguageMode gains 'espidf'; BOARD_SUPPORTS_ESPIDF covers the ESP32
family (Xtensa, S3, C3). Toolbar shows the option only for those.
- Switching modes seeds a main.c blink skeleton (app_main + gpio
driver), mirroring the MicroPython main.py flow.
- compileCode sends language='espidf'; run/stop paths are unchanged
(the QEMU worker consumes the same merged flash image).
- New gallery example: esp32-idf-blink (LED + resistor on GPIO 2).
Tests: unit coverage for the build-env switch, IDF wifi normalization,
job-key variance, file-group seeding and the new example; verified
end-to-end in a container from the prod image (pure build produces a
bootable flash image; Arduino-mode build unchanged, same variant hash).
2026-07-24 11:37:01 +07:00
|
|
|
{/* Language selector — only when active board supports an
|
|
|
|
|
alternative to Arduino C++ (MicroPython on Pico/ESP32 boards,
|
|
|
|
|
pure ESP-IDF on the ESP32 family — issue #139). The board
|
|
|
|
|
context pill that used to live here was removed: it duplicated
|
|
|
|
|
the BoardSelector dropdown elsewhere in the toolbar. */}
|
2026-05-08 10:53:27 +07:00
|
|
|
{activeBoard && BOARD_SUPPORTS_MICROPYTHON.has(activeBoard.boardKind) && (
|
|
|
|
|
<select
|
|
|
|
|
className="tb-lang-select"
|
|
|
|
|
value={activeBoard.languageMode ?? 'arduino'}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
if (activeBoardId)
|
|
|
|
|
setBoardLanguageMode(activeBoardId, e.target.value as LanguageMode);
|
|
|
|
|
}}
|
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>
|
feat(esp32): pure ESP-IDF language mode for the ESP32 family (#139)
Adds a third entry to the board language selector next to Arduino C++
and MicroPython: ESP-IDF. In this mode the user writes a plain ESP-IDF
project — app_main() entry point, FreeRTOS + driver APIs — and the
backend compiles it through the same ESP-IDF toolchain it already uses
for ESP32 Arduino sketches, just without the arduino-esp32 component.
Backend:
- CompileRequest.language ('espidf') threaded through the sync + async
compile paths and folded into the dedup job key (language='arduino'
and omitted hash identically so old clients keep dedupping).
- espidf_compiler: pure_idf flag. User files are written into main/
as-is (no Arduino.h wrap, no velxio_compat.h, Arduino library
resolution skipped), ARDUINO_ESP32_PATH is dropped from the build env
and VELXIO_PURE_SKETCH raised so the template CMake compiles the
user's own sources via a glob branch. Pure builds get their own
persistent build-dir variant through the eff_hash fold.
- QEMU WiFi compat for IDF-style code: esp_wifi.h/esp_wifi_init
detection sets has_wifi, and literal #define SSID/PASS plus
wifi_config_t designated initializers are normalized to the QEMU AP.
- CONFIG_ARDUINO_* lines are stripped from sdkconfig.defaults in pure
mode (the symbols don't exist without the arduino component).
Frontend:
- LanguageMode gains 'espidf'; BOARD_SUPPORTS_ESPIDF covers the ESP32
family (Xtensa, S3, C3). Toolbar shows the option only for those.
- Switching modes seeds a main.c blink skeleton (app_main + gpio
driver), mirroring the MicroPython main.py flow.
- compileCode sends language='espidf'; run/stop paths are unchanged
(the QEMU worker consumes the same merged flash image).
- New gallery example: esp32-idf-blink (LED + resistor on GPIO 2).
Tests: unit coverage for the build-env switch, IDF wifi normalization,
job-key variance, file-group seeding and the new example; verified
end-to-end in a container from the prod image (pure build produces a
bootable flash image; Arduino-mode build unchanged, same variant hash).
2026-07-24 11:37:01 +07:00
|
|
|
{BOARD_SUPPORTS_ESPIDF.has(activeBoard.boardKind) && (
|
|
|
|
|
<option value="espidf">ESP-IDF</option>
|
|
|
|
|
)}
|
2026-05-08 10:53:27 +07:00
|
|
|
</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-07-03 06:22:17 +07:00
|
|
|
{/* Run — in a multi-board project this runs ALL boards (the wired
|
|
|
|
|
boards are one system; running a subset is almost never
|
|
|
|
|
intended), with a split-menu to still run only the active board.
|
|
|
|
|
Single-board / board-less behaviour is unchanged. */}
|
|
|
|
|
<div className="tb-run-split" ref={runMenuRef}>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => (isMultiBoard ? handleRunAll() : handleRun())}
|
|
|
|
|
disabled={
|
|
|
|
|
isBoardless
|
|
|
|
|
? digitalRunning || verifying
|
|
|
|
|
: isMultiBoard
|
|
|
|
|
? compileAllRunning || anyBoardRunning || verifying
|
2026-07-29 03:05:36 +07:00
|
|
|
: running || compiling || verifying || !activeBoard
|
2026-07-03 06:22:17 +07:00
|
|
|
}
|
|
|
|
|
className="tb-btn tb-btn-run"
|
|
|
|
|
title={
|
|
|
|
|
verifying
|
|
|
|
|
? t('editor.toolbar.run.verifying', 'Checking circuit...')
|
|
|
|
|
: isBoardless
|
|
|
|
|
? digitalRunning
|
|
|
|
|
? 'Digital simulation running'
|
|
|
|
|
: 'Resume digital simulation'
|
|
|
|
|
: isMultiBoard
|
|
|
|
|
? t('editor.toolbar.runAll')
|
|
|
|
|
: !activeBoard
|
|
|
|
|
? t('editor.toolbar.run.addBoard')
|
|
|
|
|
: activeBoard?.languageMode === 'micropython'
|
|
|
|
|
? t('editor.toolbar.run.runMicropython')
|
|
|
|
|
: t('editor.toolbar.run.run')
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{verifying || compiling ? (
|
|
|
|
|
<svg
|
|
|
|
|
width="18"
|
|
|
|
|
height="18"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
className="spin"
|
|
|
|
|
>
|
|
|
|
|
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
|
|
|
</svg>
|
|
|
|
|
) : (
|
|
|
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
|
|
|
|
<polygon points="5,3 19,12 5,21" />
|
|
|
|
|
</svg>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
{isMultiBoard && (
|
|
|
|
|
<button
|
|
|
|
|
className="tb-btn tb-btn-run-caret"
|
|
|
|
|
onClick={() => setRunMenuOpen((o) => !o)}
|
|
|
|
|
disabled={compileAllRunning || anyBoardRunning || verifying}
|
|
|
|
|
title={t('editor.toolbar.run.options', 'Run options')}
|
|
|
|
|
aria-haspopup="true"
|
|
|
|
|
aria-expanded={runMenuOpen}
|
2026-06-18 03:06:15 +07:00
|
|
|
>
|
2026-07-03 06:22:17 +07:00
|
|
|
<svg
|
|
|
|
|
width="12"
|
|
|
|
|
height="12"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2.5"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
|
|
|
|
<polyline points="6 9 12 15 18 9" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
2026-06-18 03:06:15 +07:00
|
|
|
)}
|
2026-07-03 06:22:17 +07:00
|
|
|
{isMultiBoard && runMenuOpen && (
|
|
|
|
|
<div className="tb-run-menu" role="menu">
|
|
|
|
|
<button
|
|
|
|
|
role="menuitem"
|
|
|
|
|
className="tb-run-menu-item"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setRunMenuOpen(false);
|
|
|
|
|
handleRunAll();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{t('editor.toolbar.runAll')}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
role="menuitem"
|
|
|
|
|
className="tb-run-menu-item"
|
|
|
|
|
disabled={!activeBoard}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setRunMenuOpen(false);
|
|
|
|
|
handleRun();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{t('editor.toolbar.run.runActiveOnly', {
|
|
|
|
|
name: activeBoard ? boardDisplayName(activeBoard) : '',
|
|
|
|
|
defaultValue: `Run only ${activeBoard ? boardDisplayName(activeBoard) : ''}`,
|
|
|
|
|
})}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
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-07-29 03:05:36 +07:00
|
|
|
{/* Reset — for a booted QEMU-Linux guest this re-uploads the
|
|
|
|
|
edited files and re-runs the script without rebooting. */}
|
2026-03-25 02:52:01 +07:00
|
|
|
<button
|
2026-04-22 02:45:45 +07:00
|
|
|
onClick={handleReset}
|
2026-07-29 03:05:36 +07:00
|
|
|
disabled={
|
|
|
|
|
isPiBoardKind(activeBoard?.boardKind ?? '')
|
|
|
|
|
? !activeBoard?.piBooted
|
|
|
|
|
: !compiledHex && !activeBoard?.compiledProgram
|
|
|
|
|
}
|
2026-04-22 02:45:45 +07:00
|
|
|
className="tb-btn tb-btn-reset"
|
2026-07-29 03:05:36 +07:00
|
|
|
title={
|
|
|
|
|
isPiBoardKind(activeBoard?.boardKind ?? '')
|
|
|
|
|
? t(
|
|
|
|
|
'editor.toolbar.rerunScript',
|
|
|
|
|
'Re-run script with your latest edits (no reboot)',
|
|
|
|
|
)
|
|
|
|
|
: 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
|
|
|
|
2026-07-03 06:22:17 +07:00
|
|
|
{/* Run All — only when the primary Run isn't already the
|
|
|
|
|
"run all boards" action (i.e. board + chip or chips-only
|
|
|
|
|
projects). For 2+ boards the split Run button covers it. */}
|
|
|
|
|
{!isMultiBoard && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleRunAll()}
|
|
|
|
|
disabled={compileAllRunning || anyBoardRunning || digitalRunning}
|
|
|
|
|
className="tb-btn tb-btn-run-all"
|
|
|
|
|
title={t('editor.toolbar.runAll')}
|
|
|
|
|
>
|
|
|
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
|
|
|
|
<polygon points="3,3 11,12 3,21" />
|
|
|
|
|
<polygon points="13,3 21,12 13,21" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
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-07-03 11:50:21 +07:00
|
|
|
{/* Center slot — a flexible spacer that keeps the right action group
|
|
|
|
|
pinned to the far right. Rendered unconditionally so the layout
|
|
|
|
|
holds even when no overlay supplies content here. */}
|
|
|
|
|
<div className="toolbar-center-slot">{centerSlot}</div>
|
2026-04-29 09:23:20 +07:00
|
|
|
|
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
|
|
|
|
feat(editor): menus File/Edit en la cabecera y editor sin nav de marketing
La queja: demasiados botones en una sola fila, y en pantallas pequenas se
solapan — medido, no impresion: a 1024px Code/Both/Circuit pisaban a
Compile/Run/Stop por 9-20px y Add pisaba el chat por 29px, con el idioma y
el usuario cortados fuera del viewport.
Tres piezas:
1. En el editor, el nav de marketing (Home/Docs/Pricing/...) sobra: ya
estas dentro, y cuesta exactamente el ancho que le falta a la toolbar.
AppHeader gana la variante editorMenu — mismo mecanismo que ya usa la
build de escritorio (VITE_DESKTOP) — que oculta el nav y pinta un menu
File/Edit junto al logo. El logo sigue llevando a inicio; idioma,
autoguardado, Share y usuario se quedan.
2. File/Edit al estilo escritorio: lo que se usa cada minuto sigue siendo
boton; lo que se usa unas veces por sesion va al menu. File: nuevo
workspace, nuevo fichero, abrir, guardar, importar, exportar, BOM,
imagen del esquema. Edit: undo/redo (con su estado real del historial),
centrar vista, zoom. Las acciones viven en cierres de cuatro
componentes distintos, asi que hay un registro id->handler
(lib/editorCommands, mismo patron de seam que registerProExamples o
registerBoardBuiltins): cada dueno registra al montar y el menu invoca
por id; un item sin dueno montado se pinta deshabilitado, que ademas es
la verdad. El registro sobrevive al remontaje StrictMode (el cleanup
viejo no borra al sucesor) y tiene test de eso.
3. La toolbar adelgaza: fuera Undo/Redo (Ctrl+Z/Y siguen), fuera los
botones inline de Import/Export y sus gemelos responsivos del More
(~70px recuperados); el More queda solo con los extras pro (firmware,
GitHub, share, grabacion). El CSS responsivo de los botones retirados
se va con ellos.
Suite en verde (2300) y build del overlay verificado desde este arbol.
2026-07-30 21:26:36 +07:00
|
|
|
{/* Import / Export moved to the File menu in the header — the
|
|
|
|
|
hidden input above stays because the File-menu command
|
|
|
|
|
clicks it through the editorCommands registry. */}
|
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. */}
|
2026-07-31 00:18:37 +07:00
|
|
|
{/* The "..." menu is gone: every item it held now lives in the
|
|
|
|
|
File menu (with PRO pills where they apply). */}
|
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
|
|
|
);
|
|
|
|
|
};
|