A Visuino-built `Generated.ino.merged.bin` was reported as failing to run in
Velxio. It runs fine. The screenshot shows the ESP32 bootloader completing and
the sketch executing; what it also shows is
[E][STA.cpp:357] connect(): STA connect failed! 0x300a: ESP_ERR_WIFI_SSID
[W][WiFiGeneric.cpp:900] setTxPower(): Neither AP or STA has been started
which is WiFi association, not firmware loading — and reads as "the emulator is
broken" to someone who has no reason to know the difference.
The emulated radio broadcasts four networks and nothing else: Velxio-GUEST,
PICSimLabWifi, Espressif, MasseyWifi, all open. Sketches compiled here never
have to know, because espidf_compiler rewrites their SSID literal on the way to
the emulator. Firmware that arrives already built skips that step by
construction — there is no compile to rewrite anything — so it hunts for the
network it was built for and never finds it.
Confirmed on the real emulator rather than reasoned about: the same sketch
built with SSID "MyHomeNet" reports FAILED, and with "Velxio-GUEST" connects on
192.168.4.x. Same board, same path, only the string differs.
So the upload path now reads the image and, when it carries the Arduino WiFi
stack but names none of the reachable networks, prints one line saying which
networks exist and why a compiled-elsewhere binary cannot be fixed up
automatically. When the image already names one it says nothing: a note nobody
needs is noise, and noise is how people learn to ignore the console. The
markers are deliberately narrow for the same reason — `esp_wifi` alone appears
in images that never touch the radio.
The SSID list moves to EMULATED_WIFI_SSIDS in types/board.ts. It was written by
hand inside the MicroPython prelude string, and a second hand-written copy is
precisely what #268 was made of.
Not closed by this: the reporter's exact error is ESP_ERR_WIFI_SSID from
esp_wifi_connect, meaning the SSID handed to the stack was invalid rather than
merely absent from the air — forcing an empty SSID here produces a different
message from a different line (STA.cpp:387, "SSID too long or missing!"). That
may be his Visuino configuration or his core version; it needs his binary to
tell, which he offered to send.
Export a project to .zip, start a new project, import the zip: the circuit
comes back and the board does not. Two reporters; the second noticed it "only
works with wokwi components", which is the shape of it — the passives are real
Wokwi part types and survive, the board was not one.
The bug was in the EXPORT, not the import. wokwiZip knew four boards and wrote
`wokwi-arduino-uno`/`uno` for anything else, while the connections kept the
board's real canvas id. The attached repro says so in six lines: parts declare
an `uno`, connections name `esp32`.
Of those four, only two round-tripped. 'arduino-mega' was missing from the
export's board-id test, so a Mega's wires named `arduino-mega` while its part
was `mega`. 'raspberry-pi-pico' was tested against 'nano-rp2040', an id nothing
has been issued since boards became instances, and the import mapped it back to
the same dead string. Nobody noticed because the module had no tests.
"Velxio has four boards" was written down five times: two tables in the export,
one in the import, a hand-copied union in the import dispatcher, and BoardType
in the store — which setBoardType already ignored, casting to BoardKind to do
its job. All five are gone, plus BOARD_FQBN and BOARD_LABELS, two more from the
same era that nothing imported.
A board may claim a Wokwi part type only when a Wokwi element draws it. That is
not a preference: connections carry PIN NAMES, and the names a wire carries are
whatever the element on the canvas calls its pins. Those four render through
wokwi-elements, so an Uno wire says `GND.2`, `A4`, `5V` and the file opens over
there. Every other board renders through a velxio-* element with our names —
ours calls a pin `34` where Wokwi's esp32-devkit-v1 calls it `D34` — so
claiming a Wokwi board for one would produce a file that loads in Wokwi and
attaches every wire to a pin that does not exist. They travel as
`board-velxio-<kind>`, which also round-trips the overlay's kinds, runtime
strings this module cannot enumerate.
Nothing is coerced any more. An unrecognised diagram returns a null board and
says so instead of answering 'arduino-uno'; a second board in the diagram is
reported rather than dropped; wires naming a part the diagram lacks are called
out, which is what the reporter's own file now does when you import it.
Two more halves surfaced only by driving the real editor:
The board was still missing after import, because setBoardType re-kinds the
ACTIVE board and a new project has none — nothing to re-kind, so the circuit
arrived without its chip exactly as reported. The importers now add a board
when there is none, activate it (addBoard promotes but does not sync the flat
legacy fields), and retarget the wires to the id the board actually got.
And the export read `boardType`, the flat mirror, which only tracks the active
board through setActiveBoardId/setBoardType and reports every Raspberry Pi as
an 'arduino-uno' on purpose. It now reads the board itself, so the kind and the
canvas id come off one object and cannot disagree.
Adversarial review of the fix caught six more, every one reproduced.
The worst was not a loss but a corruption, and it was mine: a canvas with two
boards wrote BOTH boards' wires against the exported board's part id, because
for every non-Arduino family that id IS the first-of-kind canvas id. The second
board's components came back attached to the first board's pins — a different
circuit, from a file that was internally consistent, so nothing complained. My
first attempt at a dangling-wire warning could not see it either: it allowed
the board KIND as a defined id, which is precisely the id a second board
writes. The check now reads the raw diagram, the export leaves other boards'
wires out rather than mis-addressing them, and it says what did not travel.
The rest: isKnownBoardKind used `in`, so 'toString' and '__proto__' passed a
guard whose input comes from files people send each other; the importer's
extension list admitted only the Arduino four, so a MicroPython project came
back with an empty editor; the exporter never wrote libraries.txt though the
importer has always read it and offered to install what it finds; and the
unknown-board message claimed the circuit landed on the current board without
retargeting its wires there.
41 tests, over every board kind, which is the assertion this module never had:
it was written when Velxio had four boards and nothing failed when it grew to
thirty. Verified in a real browser through the reporter's flow — build the
circuit, File > Export, new project, import — for arduino-uno (unchanged),
arduino-mega and raspberry-pi-pico (both previously broken) and esp32/esp32-s3.
Before and after are identical in all five.
Still lossy, and out of scope here: a .zip carries one board, so a multi-board
project must be saved as .vlx to survive whole. Wire waypoints, one shade of
green, and board-level state (boardOptions, SPIFFS, languageMode) do not travel
either.
"Flash to real board" no longer needs a prior Compile. The menu item is
enabled for every Arduino/ESP-IDF board (MicroPython keeps its own
gating) and the Flash dialog compiles first when the board has no program
or its code changed since the last build — compiler output streams into
the dialog's console, a red build ends in an error banner there with a
"Recompile & flash" retry, and only a green build reaches the flasher.
Web Serial's port picker needs a user gesture, so the web flow asks the
overlay to grant the port at the click (new WebFlashImpl.preparePort)
before the compile that may take minutes.
Stale detection: compileBoardProgram() now records a fingerprint of the
sources + language + build options (BoardInstance.compiledSourceHash,
utils/sourceFingerprint.ts); the dialog warns and rebuilds when the live
sources differ. utils/boardCompile.ts holds the compile request builder
shared with the toolbar (same files, same options).
The whole dialog and the menu hints are translated (editor.flash.* and
editor.canvas.flashHint.* in the 9 locales); before, every string was
hard-coded English.
Entitlement seam for #207 (desktop "upload to board"): lib/proWebFlash.ts
gains installHardwareFlashGate(); OSS default allows, and when a gate
blocks, the dialog shows a paid-license panel with a "See plans" link
instead of the port picker.
deleteFileGroup left editorStore.activeGroupId dangling at the removed
group: Monaco (and agent writes) kept editing a group compile never reads,
so a stale same-named sketch.ino shipped instead of the code on screen
(the 'leftover template Blink built for 35 minutes' failure). The pointer
now re-targets a surviving group, and compile re-points the editor when
the two pointers diverge (with a visible warning log).
compiledProgram is no longer serialised into boards_json: the firmware
blob was 99.99% of a typical ESP32 save (a bare board already ~1.04 MB),
pushing every real project against the request-size ceiling, and a
restored binary is stale the moment the code differs. Loaders still
accept it from old rows.
useProjectStore.currentExampleId — stamped by loadExample, dropped the
moment a real project (or a blank workspace) takes over. The compile
request and the run event carry it as example_id, analytics only, so
'which examples get compiled and run most' becomes answerable.
Several boards share one FQBN (the Pimoroni RP2350 trio all compile as
rpipico2), so FQBN-keyed metrics can't tell them apart. The compile
request and the run event now carry board_kind — analytics only, rides
into the metric hook's extra.
suppressOnlineOnlyAds(): overlay seam so the hosted build can silence
'available in the online editor' ad cards for items it manages — an
embargoed item must vanish, not advertise the site the user is on.
Pure OSS builds never call it; ads behave exactly as before.
tailAfterStream sliced result.stdout at the live-buffer offset, but the
buffer interleaves stdout+stderr in arrival order while result.stdout is
stdout-only plus a joining newline — offsets never line up, so the flush
could start mid-line ('f/esptool_py && ...'). Instead, the poll loop now
emits one final onProgress(state:'done') with the completed buffer and
the existing delta logic flushes the tail perfectly aligned. tailAfterStream
is gone.
With the success-path reprint gone, the lines that land between the
final running-poll and job completion (esptool image creation, the
binary-size summary) silently vanished. tailAfterStream() appends
result.stdout past the streamed offset, noise-filtered, before the
structured analysis — skipped once the live buffer hits its 256 KB
tail-trim cap, where offsets stop lining up.
ESP-IDF builds sprayed the compilation console with configure chatter
(-- cmake status, git-metadata probes that 'fatal' on the synthesized
project tree, IDF component-manager NOTEs, Python env checks) and then
reprinted the entire stdout a second time when parseCompileResult ran
over the final result after the live stream had already shown it.
- isNoiseBuildLine(): pattern list for build-system chatter, applied to
the live stream deltas and to the final-result parsing.
- parseCompileResult(streamedLive): skips the stdout reprint on success;
on failure re-emits only error/warning lines so they still get the
red/yellow markers the plain live stream lacked.
Compiler errors, FAILED blocks, ninja [N/M] progress, esptool output and
binary-size summaries all still show. The full log stays available
server-side.
Three-part fix for the library-contamination class of ESP32 compile
failures (a user's project kept pulling unrelated libraries because 79%
of projects compile with an empty manifest, forcing scan-all resolution
over ~1000 shared libraries):
1. Name-affinity ranking in _find_library_for_header: the provider whose
name matches the header stem wins (exact normalized match > esp32
arch declaration > contains-stem, alphabetical only as tie-break).
Previously first-alphabetical won, so any stray library shipping a
generic header (Servo.h has 6 providers) could hijack the build.
2. Manifest auto-migration: scan-all compiles now report the libraries
the build really used (manifest_incomplete +
manifest_suggested_libraries on the compile result); the editor folds
single-candidate suggestions into the board's declared manifest after
a green build, so the NEXT compile runs scoped. One green build = one
migrated project; empty-manifest scan-all behaviour is untouched.
3. Install-declares: the Wokwi import library modal records successful
installs in the active board's manifest, and .vlx export/import now
round-trips board.libraries.
Tests: test_espidf_library_ranking.py (9) + library-manifest.test.ts (19).
Every control in the unified strip is now 28px tall: tb-btn (was 30),
Libraries (was 26), the C++/MicroPython select (was ~22), the board
selector, Serial/Scope, canvas icon buttons, zoom and Add (all were 32).
The explorer toggle moves inside the Code/Both/Circuit segmented group
as its first segment, and every active-segment blue is the Libraries
blue (var(--color-action-primary)) instead of the hardcoded #0e639c.
The editor zone's overlap floor becomes min-width: max-content — the
hardcoded 280px sat below the real content width, letting the console
button paint over the board selector when the docked chat narrowed the
bar. Now the zone can never shrink under its buttons; flex-wrap moves
the canvas controls to a second line instead. Also closes the gap
between the logo and the File menu (header gap 16->6, menubar
margin-left 14->2).
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.
The toolbar's overflow button held four leftovers (Share/Embed, Sync to
GitHub, Upload firmware, Record simulation). They move into the File
menu, which is where a session-frequency action belongs, and the strip
loses one more button. The PRO pill travels with them — same style the
overflow used (now .emb-pro) — and the two File items that were always
premium (BOM, schematic image) finally show it too: users should know an
item is premium BEFORE clicking, not via a surprise upgrade prompt.
The pro actions keep firing the same window events the overflow items
fired (share/github-sync/record prompts), so the overlay's listeners are
untouched and OSS builds keep their silent no-op. Dead overflow CSS
removed with the button.
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.
Without this we cannot tell whether the in-browser path is actually
displacing guest boots — the whole point of the dual engine is a number
(% of runs that never touched the backend), so the event has to say.
Keeping Play enabled while the guest ran broke the convention every
other board follows (owner report). Run now disables as usual; RESET on
a booted guest re-uploads the edited files and re-runs the script
without the ~45 s reboot, matching Reset's restart-the-program meaning.
QEMU-Linux boards used three competing file surfaces (workspace group
with a meaningless sketch.ino/libraries.json, the VFS panel with its
Upload button, and the Pi workspace's own editor). Now they behave like
every other board:
- the editor file group defaults to script.py for ANY Pi-family kind
(kind-based check via isPiBoardKind, not the old raspberry-pi- string)
- the libraries.json manifest row is hidden for Pi boards
- EditorPage always renders Monaco; the RaspberryPiWorkspace swap is
gone
- the bottom serial panel renders the interactive xterm (PiTerminal,
now seeded with session history) for running Pi boards
- example vfsFiles load into the editor group (single source of truth);
the run path uploads the group into the guest home
- Run on a booted guest re-runs without the 45 s reboot (Ctrl-C +
re-upload + run); starting a Pi board pops the terminal open
- piSyncAndRunScript/piRerunScript exported from the store; auto-run
now applies to the whole family (guestHome/autoRun overridable)
Overlay QEMU-Linux boards are not Raspberry Pis: ProBoardDef.guestSetup
lets a board send one shell line at the boot prompt (hostname/PS1/clear)
to de-brand the generic image, sent before piBooted flips so uploads
cannot interleave; the workspace start button and power-on title carry
the board's own label for non raspberry-pi kinds; the compile console
line uses the board label instead of 'Raspberry Pi 3B'.
The QEMU branch of handleRun required compiledProgram, but Pi-family
boards never have one (handleCompile early-returns 'no compilation
needed' without producing firmware), so the toolbar Run always surfaced
'Compilation produced no firmware' instead of powering the board on.
Start them directly, same as the workspace Start button.
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).
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>
verifyCircuitFromStore() builds the worst-case snapshot (every wired
digital pin driven HIGH) and solves it — extracted verbatim from
EditorToolbar's runVerification so programmatic runners (editor
extensions, agents) can gate their own run paths on the same rules.
No behavior change for the Run button.
The file-tabs strip in the toolbar center duplicated affordances that
already exist elsewhere: the file it showed is selected in the left file
explorer, and its board-owner label duplicated the board selector combo.
It also ate horizontal space and crowded the action row on narrow panes.
Remove the FileTabs component entirely; the left explorer is now the single
place to switch files. The toolbar center slot stays as an empty flex
spacer so the right action group remains pinned to the far right.
In a multi-board project the wired boards are one system, so running just
the active board almost never matches intent (a cross-wired UART pair only
comes alive when both run). The primary Run button now runs ALL boards when
there is more than one, with a split caret-menu to still run only the active
board. Single-board and board-less behaviour is unchanged, and the separate
Run-All double-triangle button is kept only for board+chip / chips-only
projects where the primary Run is not already a run-all.
handleCompile() wiped all logs at the start, including the 'Circuit check'
group the pre-flight verifier had just logged (e.g. an over-voltage warning).
A Run auto-compiles right after verification, so the warning vanished. Preserve
circuit-check entries on compile, same as the boardless path already did.
Clicking Run runs a pre-flight circuit-verification SPICE solve before
compiling. On a cold ngspice worker that solve takes a second or two, but the
Run button kept showing the play icon and stayed enabled, so it looked dead
and got clicked repeatedly -- each click stacking another verification (the
reported 6x [handleRun] click).
- Add a `verifying` state: the Run button now shows the same spinner as the
Compile button and is disabled while the pre-flight solve runs.
- Synchronous re-entrancy guard (runInFlightRef) ignores re-clicks while a
verification is already in flight.
The runtime burnout / pre-flight messages used an inline `setMessage` toast
that rendered as a bar near the Run/Stop buttons and overlapped them. Route
all circuit findings into the compile output console instead — one unified,
red/orange diagnostics log next to the compiler output (Proteus-style):
- New "Circuit check" console group (CIRCUIT_CHECK_TARGET). checkOrBlock logs
every error (red) / warning (orange) there and opens the console; the
blocking modal is kept for the explicit Run-anyway / Cancel decision.
- Runtime `velxio-circuit-fault` events (LED burnout) log to the same group
instead of the toast; no auto-open (the continuous solver can fault on load).
- Warnings-only no longer pop a toast — the console entry is the record.
- The run-path clears preserve circuit-check entries so findings survive a
"Run anyway" auto-compile.
The Run (and Run All) buttons were wired `onClick={handleRun}`, so React
passed the click event as the first argument. handleRun(skipVerify=false)
then treated the truthy event as skipVerify=true and skipped checkOrBlock
entirely -- the pre-flight circuit verifier never ran on a button click.
This is the real reason a 9V battery wired straight to an LED ran with no
warning even though the verifier exists and is correct (project 2840fd12).
- onClick={() => handleRun()} and onClick={() => handleRunAll()} so
skipVerify stays at its false default.
- Give handleRunAll the same checkOrBlock pre-flight gate handleRun has.
Verification failing silently in production is otherwise hard to spot — the
rules read 0 A when branch currents are missing. Log the errors/warnings,
whether a solve landed, and which branch/node vectors came back.
The pre-flight circuit verifier reads branch currents via runNetlist ->
readAllCurrentVectors() (ngSpice_AllVecs enumeration). The production
Web-Worker ngspice WASM build does not surface voltage-source #branch
vectors through that enumeration for an .op plot, so branchCurrents came
back empty and every current rule (short-circuit, LED over-current) read
?? 0 -> no fault. The live solver avoided this by requesting each current
explicitly by name; the Node test build enumerates them, so the gap was
invisible to the suite. Net effect: a 9V battery wired straight to an LED
ran with no warning (reported on project 2840fd12).
- runNetlist: request every V_* source branch current explicitly by name
and merge with the enumeration, so source/LED currents are always present
regardless of the worker WASM's AllVecs behaviour.
- circuitVerifier: non-finite source/LED current -> blocking unstable-solve
fault ("could not solve a stable current - likely a short or a part with
no current limit, e.g. an LED with no series resistor").
- LED runtime (BasicParts): burn out on a non-finite current instead of
falling through to the digital fallback and glowing; raise burnout
threshold 20mA -> 100mA so high-power/RGB channels are not falsely
destroyed; clear the burnt latch on Reset (resetBoard bumps hexEpoch).
Tests: real-data repro, mocked non-finite verifier test, runtime
non-finite / high-power / latch-recovery tests.
Library manifests are now PER-BOARD (each board carries its own velxio.json),
so two boards in one project can use different (even conflicting) libraries
without clashing — the multi-board extension of the no-clash guarantee.
- board.libraries on BoardInstance + serialisableBoard: rides in boards_json,
so it round-trips, dirty-checks, autosaves and restores natively. This also
removes the load-restore hacks (useLibraryManifestStore + applyProjectManifest
deleted): the manifest is plain board state.
- loadProjectState now restores per-board boardOptions/spiffsFiles/libraries
(it previously dropped them).
- EditorToolbar single + compile-all send the COMPILING board's libraries.
- Backend compile.py prefers the client's per-board request.libraries; the
project-level libraries_json (now the union of all boards) is the fallback.
- buildLoadPayload migrates pre-per-board projects: seed each board with the
project union so they keep compiling scoped.
- Library Manager 'In project' tab edits the ACTIVE board's velxio.json (shows
the board name) and the add field is now an autocomplete (installed libs +
index search) so users pick from a list instead of typing names.
Deletes useLibraryManifestStore.ts + applyProjectManifest.ts.
End users can now configure a project's declared libraries (the compile scope):
- Library Manager gains an 'In project' tab = the project's velxio.json:
declared libs as removable rows, quick add-by-name, and a raw velxio.json
editor. Installing a library auto-adds it to the project. Installed-tab rows
get an 'Add to project' toggle.
- FileExplorer shows a velxio.json entry (with declared count) that opens the
Library Manager via a window event the toolbar listens for.
- applyProjectManifest(): restore a saved project's manifest into the store on
load so the editor/toolbar/Library Manager/velxio.json reflect it.
- computeProjectStateHash() includes the manifest so declaring a library marks
the project dirty and autosaves.
Note: the OSS ProjectByIdPage also calls applyProjectManifest for parity, but
velxio.dev routes the pro-overlay ProjectByIdPage (wired separately).
Activates manifest-scoped ESP-IDF resolution for the gallery. loadExample now
records the example's declared libraries in useLibraryManifestStore; EditorToolbar
passes them to compileCode, which sends them as `libraries` in the compile
request. The backend then merges exactly those libraries (P2.0 scope) instead of
picking a stray same-named lib from the shared dir.
Safe: a core-only example sends null (legacy scan-all); a stale/incomplete
manifest degrades to scan-all via the backend graceful fallback, never a wrong
build. Ignored by the backend for non-ESP32 (arduino-cli) boards. Example
manifests were completed (incl. transitive deps) in c671c9b.
Phase 4 of the run-system work. The compile console now groups output into a
section per run target (board or chip) with a status glyph and label, the way
multiple Arduinos already stream — instead of one flat list.
- CompilationLog gains an optional target { id, label, kind: 'board'|'chip' }.
message/type are unchanged so the pro overlay (diagnose-with-AI prompt +
errorCount slot) and the console's length-based clear/auto-error heuristics
are untouched. parseCompileResult stamps the target on every produced line.
- Producers stamp their lines: compileAllBoards (per-board, dropping the old
'<label>: ' string prefix the header now carries), prepareCustomChips
(per-chip, WASM + ROM), handleCompile + handleRun MicroPython (single board) —
including the Pi / MicroPython / FQBN / error paths so a target's lines never
fragment across sections.
- CompilationConsole groups filteredLogs into consecutive-run sections at RENDER
time only (the flat array is unchanged); each target section shows ✓/✕/▸ +
name + kind tag, with no-target lines ('Compiling all targets', 'Done') as
plain narration around them.
Reviewed by an adversarial pass; the flagged un-stamped edge paths (Pi /
MicroPython / single-board errors) are now stamped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 3 of the run-system work. Generalises the boards-only Compile-All/Run-All
to RUN TARGETS = boards + programmable custom-chips, so a board+chip or several
chips compile and run together, the same way multiple Arduinos do.
- targetCount = boards + programmable chips; the Compile-All/Run-All buttons now
appear when targetCount > 1 (was boards.length > 1). Cheap string predicate
(no JSON.parse) since the selector runs on every sim tick.
- compileAllBoards builds chips (WASM+ROM) AND boards; works with zero boards;
prepareCustomChips now returns a failure count folded into the Done summary so
a failed chip no longer shows green / calls markCompiled.
- handleRunAll: compiles all targets, starts every board, then restartParts() so
chips pick up fresh WASM/ROM, and resumes the electrical solver when NO board
actually started (board-less, or a board that compiled to nothing) so chips
aren't left frozen.
Review fixes (2-agent adversarial pass):
- Stop now stops EVERY running board (Run-All can start several); otherwise a
non-active board kept the chip ticking after Stop.
- Run-All / Stop disabled gates use anyBoardRunning (+ digitalRunning) instead of
the flat active-board flag, which misreports multi-target runs.
- shared isQemuBoardKind() helper so handleRun and handleRunAll can't drift.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Phase 1 of the run-system/UX work.
Stop bug: a programmable chip kept running after Stop when a board was present.
The chip rAF tick gated only on board presence (!boardless), so with a board it
ticked forever. Now it gates on the actual run state: board-less -> electrical
paused flag; with board(s) -> board.running. handleStop also clears every chip's
output drives (clearAllChipDrives) and re-solves so chip-driven LEDs go dark on
Stop instead of freezing at their last frame.
Examples to board-less (regulated power supply, no Arduino — the Arduino only
ever supplied 5V):
- z80-larson-scanner -> 'Z80 Comet Scanner': board-less, a faster TWO-LED comet
(scanner.s) so it's visually distinct from z80-larson-no-board's single-bit
walk; green/blue LEDs.
- i8080-killbits -> board-less (psu + resistors), keeps killbits.s as the chip's
editable program; buttons re-powered from the supply.
- i8080-button-counter -> board-less (psu + resistors); behaviour chip, program
baked in, so it shows a note (no editable file) and runs standalone.
banner-streamer stays Arduino-based (its TX/RX go through the AVR USART bridge).
- CustomChipPart: run-state-aware tick gate.
- EditorToolbar: clearAllChipDrives() helper + handleStop clears chip drives.
- examples-retro-intel: 3 conversions; drop now-unused sketch consts; add the
larsonScannerAsm comet program.
- Tests: board+chip routing now uses an inline synthetic example (gallery chip
examples are all board-less).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Velxio can now simulate one or more custom-chip CPUs with NO Arduino/ESP32
board on the canvas — a general-purpose electronics simulator, not an
MCU-only one.
- DynamicComponent: board-less parts get the real shared flat PinManager
(instead of a no-op stub) so a custom chip's digital pin writes/reads reach
the LEDs/inputs wired to it.
- CustomChipPart: the rAF tick respects board-less Run/Stop (freezes while
the electrical sim is paused); board behaviour is unchanged.
- EditorToolbar.handleRun: board-less Run compiles each chip's WASM/ROM and
re-attaches the parts (restartParts) so they pick up the fresh WASM, then
resumes the solver.
- useSimulatorStore.restartParts(): bump hexEpoch to force part re-attach.
- New example "Z80 Larson Scanner (no board)": a programmable Z80 + 8 LEDs +
the adjustable power-supply component, no MCU. The chip drives the LEDs
through the synthetic-pin + ngspice path added earlier.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Compile/Run now makes every custom-chip on the canvas live in a single
click instead of requiring a manual trip through the chip designer plus a
separate ROM compile:
- Each custom-chip's C source is auto-compiled to WASM when it has none
yet (via /api/compile-chip), and programmable CPU chips get their
program file (larson.s, chaser.c, ...) assembled/compiled to ROM bytes
(via /api/compile-rom) and injected, all before the board starts.
- Chip-program files are excluded from the arduino-cli sketch build, so
SDCC-only syntax such as __at(0xC000) no longer breaks the Arduino
compile (this is what made the Z80 LED-chaser-C example error out).
Fixes the Z80 examples that either errored on Run (z80-led-chaser-c) or
compiled but did nothing (z80-larson-scanner, whose chip never had WASM
or ROM). Works for any circuit built from scratch with a programmable
CPU chip, not just the bundled examples.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
Sixth overflow-menu item, Pro-badged. Dispatches
velxio-pro-replay-record-toggle (projectId in detail) which the pro
overlay handles — plan check, board-type check, start/stop the
recorder. OSS build has no listener → silent no-op.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fifth item in the overflow menu next to Sync to GitHub. Free for
all users (no PRO badge); dispatches velxio-pro-share-prompt with the
current project id so the overlay's ShareModal can render the direct
link + iframe snippet copy UI.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Dispatches velxio-pro-upgrade-prompt's sibling event
velxio-pro-github-sync-prompt with the current project id. The pro
overlay's GithubSyncModal listens and runs the four-state link/sync
flow (no-pro / not-connected / not-linked / linked) inline without
leaving the editor.
Pure OSS builds have no listener so the click is a silent no-op —
those users can't have linked repos anyway.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the hard /pricing redirect on 402 with a window-dispatched
'velxio-pro-upgrade-prompt' event so private overlays can surface an
in-editor upgrade modal instead of bouncing the user out of context.
Move BOM, Schematic image and firmware upload buttons into a "..." More
menu next to the existing Export ZIP icon, freeing two button slots in
the inline toolbar. Mark the two premium items with a small "PRO" pill
so free-plan users know they're gated before they click — Notion- /
Linear-style discoverability cue.
Also wire Import + Export ZIP to fall back into that same menu once the
toolbar container drops below 320 / 280 px (container queries on the
editor pane width). Mobile / narrow-split layouts keep full feature
parity through the dropdown instead of overflowing into a horizontally
scrolling row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Front-end half of the schematic image export. New camera-icon button in
the editor toolbar between BOM and Upload-Firmware. Handler:
1. POSTs to /api/pro/projects/{id}/screenshot.png (server renders the
canvas with headless chromium, returns a PNG).
2. 402 → /pricing?from=screenshot_export
3. 401 → /login with redirect-back
4. 422 → friendly "add at least one component" toast
5. 200 → blob download with Content-Disposition filename
6. The "rendering..." toast surfaces during the 5-10 s of headless
chromium time so users know to wait, not click again.
i18n key editor.toolbar.exportScreenshot added in en/es/pt-br/zh-cn.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>