When the auto-compile path in handleRun() finishes without producing a
compiledProgram, the previous code dropped the failure on the floor with
only a `console.warn` — the user clicked Run, nothing happened, and they
had no idea why. The accompanying comment also promised "always start
even if compiledProgram is empty" but the code did the opposite.
This commit replaces the dead comment + silent warn with a top-level
error toast + addLog entry, with a different copy for MicroPython mode
(suggests "click Load MicroPython to retry") vs Arduino C++ mode
(directs the user to the output console for the underlying error).
handleCompile already writes the actual cause to the compile-output
console via addLog — this fix just makes sure the user knows their
click failed and where to look.
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.
Three QoL fixes for the Tauri shell:
1. Hide the entire AppHeader strip in VITE_DESKTOP, not just the
marketing nav. The previous gate left the black bar painting
over the editor with the brand + auto-save + share + auth
slot, all of which are irrelevant in desktop (cloud Pro
features, license is handled by DesktopWelcomePage, the title
bar already says "Velxio Desktop"). Return null at the top so
the editor takes the full window height.
2. Splash screen during sidecar boot + Monaco hydration. Cold
launch was a 3-8 s black window — now there's an inline SVG
logo, "Velxio" wordmark, slogan, animated spinner, and a
"Starting local backend…" caption. Lives in index.html as a
fixed-position overlay with display:none by default; the inline
script reveals it only when `window.__TAURI__` is present, so
web users never see it. main.tsx fades it out (250 ms ease-out)
after two animation frames — guarantees React's first paint has
committed before the handoff, no black flash. Self-contained:
inline styles, inline SVG, inline CSS keyframes, zero external
requests.
3. Native locale switcher under View → Language. Emits
`velxio://menu` with action='set-locale' + the locale code; the
desktop/menu.ts handler navigates via history.pushState +
popstate so React Router picks it up without a hard reload
(Monaco + simulator state preserved). Locale list mirrors
i18n/config.ts::LOCALES.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The marketing nav (Home/Docs/Examples/Pricing/Blog/GitHub/Discord) and
the LandingPage hero are great for velxio.dev visitors but become
clutter once the SPA ships inside a Tauri shell — the user installed
the desktop app to land in the editor, not to read about the project.
Two small VITE_DESKTOP gates handle this:
- AppHeader.tsx hides the <nav> + the mobile hamburger that toggles
it. The brand, language switcher, auto-save indicator, share
button, and the pro overlay's auth slot all stay visible — they
carry real per-session info, not navigation.
- App.tsx swaps the `/` route's element for a <Navigate to=/editor>
so first-launch (and any future `velxio://` deep-link that lands
on `/`) goes straight to the editor.
Equivalent actions for the items being hidden live on the native
menubar that the velxio-prod overlay builds via
pro/desktop/src-tauri/src/menu.rs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Real digital storage scopes have a trigger that pins the visible window
around a detected edge — without it, sparse activity (UART bytes once
per loop, an interrupt firing every few seconds) scrolls off the screen
faster than the eye can catch. Velxio's scope was free-running only,
which made the recent UART TX waveform work effectively invisible at
fine time/div settings: the byte burst was 87 µs but the window only
showed the most recent 1 ms.
Three trigger modes, matching what you'd find on a Rigol / Tektronix:
* Auto — current free-running behaviour, window's right edge
tracks the most recent sample. Default.
* Normal — window pins around each triggering edge so the event
lands at `triggerPosition * windowMs` from the left
(default centred at 0.5). Keeps re-pinning on every
new triggering edge.
* Single — arms once, freezes the trace on the first triggering
edge by flipping `running = false`. User clicks
"Re-arm" to capture again.
Three knobs configurable per mode:
- source: which channel produces the trigger event
- edge: rising (↑) / falling (↓) / either (⇅)
- position: trigger lands at this fraction of the window
(UI hard-codes centre 0.5 for now; the store field
accepts any value if we want a draggable handle later)
UI additions in the scope header (only shown when mode != auto):
- source / edge dropdowns
- status badge (Armed / Triggered / Captured) with pulse animation
on Armed so the user knows the scope is waiting for an event
- Re-arm button in Single mode after capture
Canvas changes:
- Dashed orange "T" marker drawn at the trigger position when an
edge is latched and within the visible window.
Store changes:
- pushSample peeks at the trigger channel's previous state, detects
a matching edge, sets triggeredAtMs (and stops `running` for
Single mode). matchesTriggerEdge() exported for unit testing.
- clearSamples / setTriggerMode / setTriggerChannel / setTriggerEdge
all re-arm the trigger; rearmTrigger() explicitly resets and resumes
capture (used by the Re-arm button after a single-shot).
Covered by 11 new vitest cases (oscilloscope-trigger.test.ts) plus the
existing 1892 tests still pass.
Closes the "I set 0.1 ms/div on a Serial.print sketch and see a flat
line" UX trap reported on the Discord follow-up — at 0.1 ms/div the
window is 1 ms but bytes fire every 2 s, so without a trigger the
chance of catching the burst is < 0.05 %. With Normal trigger on
rising D1 the burst pins in the middle of the window and the user can
zoom down to bit level (8.68 µs each) without losing it.
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.
On the ESP32 DevKit V1 the silkscreen labels GPIO 16 / 17 as RX2 / TX2,
and Esp32Element.PINS_ESP32 only exposed the silkscreen names. Examples
that wire to numeric pin "16" or "17" (e.g. ledcAttach(16, 5000, 8) on
esp32-pwm-led-rgb) couldn't resolve those names — pinPositionCalculator
failed lookups, the wire endpoint fell back to (0,0)/(50,50) and the
LED component visually floated off the board, breaking the SPICE
netlist for the example.
Add "16" and "17" as aliases pointing to the same (134,143) / (134,131)
coordinates as RX2 / TX2 so both naming conventions resolve to the same
physical pin tip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ATtiny85 (AVRSimulator + collectPinStates + connectAnalogInputsToMcu + SimulatorCanvas + Attiny85Element + examples):
- Add attiny85AdcConfig with correct register addresses (ADMUX=0x27,
ADCSRA=0x26, ADCSRB=0x23, ADCL=0x24, ADCH=0x25, DIDR0=0x34, adcInterrupt=0x08).
Without this, analogRead() polled the wrong address forever and the
firmware hung on first ADC read.
- Add attiny85Timer0Config + instantiate AVRTimer so OVF fires at the
ATTinyCore-expected ~1.024 ms cadence. delay() advance is still blocked
on avr8js TIFR auto-clear semantics (separate upstream issue, see
ATTINY85_TIMER0_UPSTREAM_ISSUE.md in velxio-prod test plan).
- Map ATtiny85 ADC channels to PB-style pin names (PB5/PB2/PB4/PB3 -> 0..3)
in connectAnalogInputsToMcu so SPICE node voltages reach the right ADC
channel.
- Recognise /^PB\d+$/ in collectPinStates.pinNameToArduinoPin so wires
named "PB1" emit v_attiny85_pb1 V-source and the LED responds to MCU
writes. Previously every PB-wire returned -1 and SPICE saw no source.
- SimulatorCanvas: subscribe pin 1 (PB1) for the built-in LED on the
attiny85 board kind (Digispark convention), instead of falling through
to the pin-13 default.
- Attiny85Element: remove the hand-drawn "yellow LED" circle that was
floating above the chip. The bare DIP-8 has no on-board LED; examples
wire a real wokwi-led + resistor instead.
- examples.ts: add a real wokwi-led + 220 Ohm wokwi-resistor + wires to
attiny85-blink, and add missing series resistors to attiny85-button-led
and attiny85-ntc-sensor. attiny85-pwm-fade was already correct.
Custom-chip pipeline (CustomChipPart + simulatorBridges):
- Add a requestAnimationFrame loop that calls instance.tickTimers() every
frame in CustomChipPart. Chips that register vx_timer_create (e.g. an
i8080 stepping its core, or a sensor publishing samples) had timers
added to the queue but nothing fired them; tickTimers was dead code.
- Gate the ESP32 backend path with detectSimulatorKind(sim)==='esp32'.
The previous `typeof sim.registerSensor === 'function'` check matched
AVR and RP2040 simulators too (they expose registerSensor for I2C
sensor proxies), routing client-side chips to a non-existent ESP32
worker on those boards.
- Replace direct simulator.usart.writeByte calls in avrUartTx with a
JS-level FIFO + setTimeout(1ms) drainer. avr8js writeByte drops bytes
under burst load (a chip emitting print_string lost ~99% of bytes via
non-immediate, or kept only the last byte via immediate). The drainer
attempts one non-immediate write per tick and retries on RXC busy /
RXEN off. Added a guard for ATtiny85 (no USART -> would queue forever).
End-to-end verified: i8080-banner-streamer now prints the boot banner
followed by "uptime ticks: 0xNN" lines stepping every ~50 ms, executing
real Intel 8080 instructions inside the WASM chip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous fix (dd22bcf) used `isInteractive` to decide whether to let
the wokwi component own the pointerdown. That heuristic was too broad —
DHT22, HC-SR04, NTC, photoresistor, LED all register `attachEvents` for
the SPICE/sensor-update bridge but have NO internal pointer handlers, so
clicks on them got silently swallowed by the wokwi shadow DOM and the
property dialog never opened.
Replace with an explicit whitelist of wokwi tags that ACTUALLY own
pointerdown (rotary knobs, pushbuttons, slide switches, joysticks,
keypads, encoders, rotary dialer). Every other component, including
sensors/displays/LEDs with attachEvents, falls through to the canvas
which decides between drag-to-rearrange and click-to-open-dialog.
Documented the model in docs/wiki/component-interaction.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three independent fixes uncovered during a systematic example-by-example
audit (plan/full_test_plan/):
1. DynamicComponent.handleMouseDown was calling e.stopPropagation()
unconditionally in the capture phase. That swallowed pointerdown
BEFORE wokwi-potentiometer / pushbutton / slide-switch / joystick
could see it, so the rotary knob would not rotate and buttons
wouldn't press even with a real OS mouse. Now we skip the swallow
when the click target is an inner wokwi-* element during a live
simulation, letting the wokwi component own its own pointerdown
while still allowing the canvas drag-to-rearrange flow on the
wrapper / non-interactive surface.
2. examples.ts uno-ntc (and pico-ntc) sketch had the NTC divider
formula inverted relative to both the SPICE mapper topology
(VCC -> R_NTC -> A1 -> R_pull -> GND, the standard module wiring)
and real wokwi-ntc-temperature-sensor modules. Moving the slider
to 60 C made the firmware print -3.42 C. Flipped the formula to
r = SERIES_R * (VCC - v) / v. Now slider 60 C -> Serial reports
60.12 C and A1 voltmeter shows 4.00 V.
3. componentToSpice.ts photoresistor mapper was only registered under
the bare key `photoresistor`, but example components use the
metadataId `photoresistor-sensor`. Added an alias so the LDR +
pull-down divider gets emitted for the real component instance.
All three reproduce visually in seconds; documented per-example in
plan/full_test_plan/examples/.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When the editor opens a .s or .asm file (the chip-program files routed
to /api/compile-rom), Monaco now colorizes 8080/Z80 mnemonics, registers,
hex/binary literals, comments, and directives. Same highlighter covers
both ISAs since most mnemonics overlap.
- frontend/src/components/editor/retroAsmLanguage.ts: a Monarch tokenizer
+ LanguageConfiguration + idempotent registration helper. Recognises
the full 8080 ISA, all the Z80 additions (LD/JR/DJNZ/EXX/EX/IM/LDIR/
bit ops/index ops), the directives ORG/DB/DW/EQU/END, and registers
including condition codes (NZ/Z/NC/etc.) and IX/IY.
- CodeEditor.tsx: maps `.s` and `.asm` to the new `retro-asm` language
and calls `registerRetroAsm(monaco)` in beforeMount so the language
exists by the time the editor first paints. Other extensions
(.ino/.cpp/.c/.py/.json/.md) behave exactly as before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a third format to /api/compile-rom: `c` (C source compiled by SDCC
to Z80 bytes). Same chip-program flow as 8080/Z80 asm — write C in a
project file, click Compile, click Run.
Backend:
- backend/app/services/c_compile.py — async SDCC wrapper. Locates the
sdcc binary on PATH (or via SDCC env var, or common Windows install
paths) and shells out with target=mz80 + --code-loc 0x100 --data-loc
0x8000. Parses the resulting Intel HEX into raw ROM bytes. Pure 8080
is rejected with a clear error (SDCC has no 8080 backend; Z80 ROMs
also run on the i8080-cpu chip if you avoid Z80-only ops).
- rom_compile.py: compile_rom is now async; the new c branch delegates
to c_compile. compile_rom_endpoint awaits it.
Frontend:
- romCompileService: RomFormat gains 'c'; formatForFile maps .c/.cpp to
'c'. isChipProgramFile intentionally still excludes .c — disambiguation
happens at the EditorToolbar level.
- EditorToolbar: the chip-program path also fires when a custom-chip
has programFile === activeFile.name (regardless of extension). That
lets .c files route to /api/compile-rom (SDCC) when bound to a CPU
chip, while .c files NOT bound to any chip continue to route to
arduino-cli as before.
Docker:
- Dockerfile.standalone adds `sdcc` to the apt-get install list, so the
prod image ships with SDCC out of the box.
Example:
- /examples/z80-led-chaser-c — z80-cpu chip + chaser.c (a Larson
scanner written in C with __at() MMIO definitions). Compiles cleanly
with SDCC's --code-loc 0x100 default crt0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Zilog Z80 to the programmable-retro-CPU lineup. Same compile-rom
flow that landed for the 8080 in PR #189: write Z80 asm in a project
file, click Compile (backend assembles via in-tree two-pass asm-z80),
click Run, the chip emulator boots from the resulting ROM bytes.
Backend:
- backend/app/services/asmz80.py — two-pass Z80 assembler covering the
practical demo subset: LD r,n / r,r' / rp,nn / (nn),A / A,(nn) +
ALU r/n + INC/DEC + JP/JR/DJNZ/CALL/RET + PUSH/POP + IN/OUT +
EX/EXX + LDIR/LDDR/IM/NEG + RLCA/RRCA/RLA/RRA + the simple
ED-prefix variants. Not yet: CB-prefix bit ops, DD/FD index ops.
- rom_compile.py routes target=z80 through the new assembler.
Chip:
- frontend/src/components/customChips/examples/intel/z80-cpu.{c,chip.json}
Generated by scripts/make-z80-cpu.py from the existing z80.c emulator
(same clean-room implementation that passes ZEXDOC end-to-end). The
external pin/bus protocol is replaced with internal RAM + ROM + MMIO
for LED/BTN/UART. 35 KB WASM.
Example:
- /examples/z80-larson-scanner — Knight-Rider-style walking LED.
Demonstrates JR/DJNZ/RLCA which the 8080 can't run.
Plus a small Z80 smoke-test asm under scripts/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
`PinTracer` signature is `(componentId, componentPinName) => number | null`
but the local `getArduinoPin` lambda only accepted one arg and used the
closure-captured `id`. When `createDefaultPinResolver` passed both args
(per the typed signature), JS bound the FIRST arg (the componentId) into
the lambda's single `componentPinName` parameter. `traceDetailed` then
looked up a pin literally named "rgb-led-1" on component "rgb-led-1",
returned null, and the resolver locked itself into 'FLOATING' state —
its onChange path never subscribed and the wokwi-rgb-led element's
ledRed/ledGreen/ledBlue stayed at 0 forever even as the SPICE side
correctly cycled through R, G, B, Y, C, M, W via analogWrite().
Same bug latent for any multi-pin component that goes through the
PinResolver path (multi-pin LEDs, RGB strips, 7-seg drivers, anything
that calls `getPinResolver(<pinName>)` for several pin names).
Fix: lambda now accepts both shapes — `getArduinoPin(pinName)` (legacy
single-arg used by every PartSimulationRegistry handler) AND
`getArduinoPin(componentId, pinName)` (PinTracer 2-arg form used by
createDefaultPinResolver / createSpiceResolvedPinResolver). Picks the
right componentId in either case.
Verified via the rgb-led example: ledRed/ledGreen/ledBlue now cycle
0→255→0 in sync with the SPICE node voltages on pins 9/10/11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a new BoardOptionsModal accessible from the EditorToolbar that exposes
per-board options (currently used for board-specific compile flags). Wires
the modal through:
- types/boardOptions.ts new BoardOptions shape
- types/board.ts BoardInstance gains `boardOptions` + `spiffsFiles`
- store/useSimulatorStore.ts boardOptions persisted in loadProjectState
- components/editor/EditorToolbar.tsx button to open the modal
- components/simulator/BoardOptionsModal.{tsx,css} the modal itself
- components/simulator/SimulatorCanvas.tsx passes the options through
- utils/projectPayload.ts board options serialised in saved projects
- pages/ProjectByIdPage.tsx re-includes the by-id loader needed for
project URLs that reference boards with
their persisted options.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the public extension points the velxio-prod overlay uses to bind
real canvas-side I2C/SPI/UART models (BME280, future MCP23017, etc.)
to a running Pi guest's protocol shims:
- qemu_manager: set_pi_slave_handler(fn) / get_pi_slave_handler() for
pi_attach_slave + pi_detach_slave WebSocket messages. OSS image
leaves the hook unset so the messages are silently dropped.
- simulation route: parses the two new WS message types and forwards
them to the registered handler when present.
- RaspberryPi3Bridge: attachSlave(spec) / detachSlave(spec) frontend
side of the protocol.
- piSlaveScanner: at simulation start walks components + wires,
identifies I2C/SPI/UART peers wired to Pi protocol pins (40-pin
header physical-pin numbering), and emits one attach per
bus/address pair (deduped across SDA+SCL wires).
- RaspberryPiWorkspace: invokes the scanner once the bridge is open,
with retries to ride out the WS-still-connecting race.
- integration test: pi3_bme280_attach.py boots the Pi, pre-attaches a
BME280 via the slave handler, runs a host-side proto loop, runs
guest python smbus2.read_byte_data(0x76, 0xD0) and asserts the
console reads back CHIP=0x60.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The user reported the default editor canvas — Arduino Uno + LED +
220Ω resistor — was correctly powered (1.84 V at the LED anode,
14 mA through the diode) but the LED visual stayed dark. Only the
built-in pin-13 LED on the wokwi-arduino-uno element lit up.
Root cause: ngspice's WASM build truncates branch-current vector
keys at the first hyphen. A sense source named V_led-builtin_sense
ends up exposed under a key like v_led#branch rather than the
expected v_led-builtin_sense#branch. CircuitSimulationService and
BasicParts.ts both look up the FULL key, miss, and the LED's
brightness update treats raw as undefined → digital-fallback path
runs but the SPICE memo timestamp is fresh so HOLD keeps zero
brightness. Visible symptom: a perfectly conducting LED that never
lights.
Fix in two places:
- Default canvas (useSimulatorStore.ts): rename 'led-builtin' /
'r-builtin' to 'led_builtin' / 'r_builtin' (and the matching
wire ids).
- DynamicComponent.tsx makeNewComponent: the id template was
'metadata.id-timestamp-rand' producing hyphens for every
user-added component too. Switched to underscores, AND replace
any hyphens already in metadata.id (e.g. 'led-bar-graph') so
the prefix doesn't reintroduce the bug.
Existing saved projects whose ids contain hyphens are not migrated
here — those will keep the visual bug until either the operator
edits the components or we add a sanitisation step inside
componentToSpice + BasicParts. The next follow-up commit can add
that if you confirm this default-canvas fix works.
Adds 17 chips from the test/test_intel clean-room research to the Custom
Chip gallery, all sourced from manufacturer datasheets and validated by
the existing 129-test vitest harness (CPUDIAG end-to-end for the 8080,
ZEXDOC for the Z80).
CPUs: 4004, 4040, 8080, 8086, Z80 (categoria retro-cpu)
Bus chips: rom-32k, ram-64k, rom-1m, latch-8282, 4001-rom, 4002-ram,
8255-ppi, 8251-usart, 8259-pic, 8253-pit (retro-bus)
Two bundled "mini-computer" demos under retro-bundle that drop on the
canvas as a single chip and run real 8080 code out of an embedded ROM:
* i8080-repl 8080 + RAM + ROM + UART, prints a banner and an
"uptime ticks: 0xNN" counter every ~50 ms via a real
DCR/JNZ busy-wait. Visible in Serial Monitor.
* i8080-counter 8080 + RAM + ROM + 8 LED pins + 2 button pins.
Counts up in binary on BTN_INC, clears on BTN_RST.
Two example projects under /examples reuse these chips end-to-end:
* /examples/i8080-banner-streamer
* /examples/i8080-button-counter
The bundled chips inline a 328 / 34-byte 8080 ROM produced by a new
two-pass 8080 assembler in Python (scripts/asm8080.py) from the .s
sources in scripts/. Both ROMs are pre-assembled and committed under
scripts/*.txt so contributors can rebuild deterministically.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related correctness fixes that make the simulator's realism
match what users actually see.
1. circuitVerifier was running pre-flight against the IDLE circuit
(every pin LOW). A Blink sketch is going to write pin 13 HIGH
eventually — at which point a missing series resistor produces a
~500 mA spike through the diode. But because pre-flight ran with
pin 13 LOW the led-overcurrent rule never fired, and the user
sailed through Run only to see the LED stay mysteriously dark on
the canvas.
The verifier now forces every digital pin connected to a load to
HIGH = vcc, the worst case any well-defined sketch will eventually
impose. The existing rules (led-overcurrent, resistor-overpower,
short-circuit) now fire correctly and the existing
CircuitVerificationModal blocks Run until the user adds a proper
current limiter or chooses Run Anyway.
Pins that are inputs-only (a pull-up + button) get over-driven
here too, but the rules tolerate that — a pull-up at 5 V draws
~0.5 mA, well below all thresholds. A circuit that would actually
fault under HIGH is flagged.
2. LED simulator was crashing visually on non-finite ngspice branch
currents. A degenerate diode (no series R) makes ngspice return
NaN, which fell through 'raw !== undefined && current > 1e-6' as
false and never triggered the digital fallback. Now we check
Number.isFinite(raw) before trusting it — non-finite returns
route to the digital fallback so the LED at least lights visually
when its driver pin is HIGH (the user still sees the verifier
warning that the real-world circuit is wrong, but Run Anyway is
not a black screen).
PiTerminal didn't call term.focus() on mount, so xterm.js stayed
passive — onData only fires when the DOM element has focus. Users
saw the boot prompt but their keystrokes went to whatever element
held focus when they clicked Run (canvas, code editor), never
reaching the bridge. Calling focus() right after fit() makes the
prompt receive input the moment it's visible.
The qemu_manager change adds INFO-level logging when serial_input
WebSocket messages reach send_serial_bytes — useful diagnostic for
future Pi3 input problems (proves whether bytes reached the backend
before we look at TTY / kernel / PL011 wiring).
Clicking a second photoresistor (or any second sensor of the same
metadataId) showed the previously-clicked sensor's slider value because
the panel was reused across clicks and its useState only ran once. The
mount useEffect also unconditionally dispatched config defaults, which
would have wiped any prior customisation if we naively remounted.
Three changes:
- SensorUpdateRegistry caches the last-dispatched values per componentId
(and clears them on unregister) so the panel has a place to read from.
- SensorControlPanel hydrates from that cache on mount, falling back to
config defaults only when the sensor has never been touched. The
default-dispatch useEffect skips when cached values already exist.
- SimulatorCanvas keys the panel on sensorControlComponentId, forcing a
fresh mount when the user switches sensors — without that, hydration
wouldn't run on subsequent opens.
The previous fix opened the SensorControlPanel on a desktop sensor
click during simulation, but the slider thumb still couldn't be
dragged — the canvas pan handler claims any left mousedown that isn't
explicitly stopped, so grabbing the slider was panning the canvas.
The panel only stopped click events. We now stop mousedown and
pointerdown on the panel wrapper as well, so input[type=range] gets
its native drag and the pan handler stays out.
Commit 77a63ca made handleComponentMouseDown return early while the
simulator was running so clicks on pushbuttons / switches / pots would
reach the wokwi-element shadow DOM. That was correct for components
whose interaction lives inside the Web Component, but wrong for sensors
(photoresistor, DHT22, MPU6050, NTC, gas, flame, sound, joystick, tilt,
PIR, ultrasonic, BMP280) whose only interaction is the React-side
SensorControlPanel we open ourselves. Their mousedowns were bubbling to
the canvas pan handler — the user saw the grab cursor and no panel.
Touch already handled this correctly: tap-up checks SENSOR_CONTROLS and
opens the panel even while running. The mouse path now mirrors that —
if interactionRunning is true we only short-circuit for non-sensors.
Mobile was working fine; desktop had a string of issues that surfaced
together on the Pico Doom example after the simulator/wiring fixes.
1. Selection action bar appeared during simulation, intercepting button
presses. handleComponentMouseDown unconditionally called
e.stopPropagation() + setSelectedComponentId, so clicking a wokwi-
pushbutton on a running canvas ate the mousedown — the
button-press event never fired and the floating Rotate/Delete bar
popped up on top of the button. Now: while running, the handler
returns early so the event propagates to the underlying component
and the canvas stays read-only.
2. The selection action bar was always visible on desktop. It was
introduced as the primary delete UI for touch devices (no Delete
key, no right-click), but it kept showing on mouse-and-keyboard
too — covering pins and intercepting clicks. Now gated on
isTouchDevice (already wired via useIsCoarsePointer) AND !running.
Desktop users keep Delete key + right-click context menu for the
same operations.
3. Left-click drag on the canvas background didn't pan. Pan was
limited to middle/right click. Now left-click on empty canvas
panning works too (component mousedowns stopPropagation so they
still drag the component, not the camera). Wiring mode keeps left
click for waypoint drops, so the pan only kicks in when not in
wire mode and not in a property dialog. Matches Figma / Miro /
draw.io convention.
Build verified.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Production crash on the simulator page after init:
Uncaught ReferenceError: traceDetailed is not defined
at Z (index.js)
at Object.attachEvents (index.js)
Root cause (introduced in 27c5966 Phase 1b skeleton): `traceDetailed`
was declared as a `const` inside `getArduinoPin` but called from the
sibling `getPinResolver`, which is a separate inner function. Vite dev
sometimes inlined the call differently so the bug only surfaced in the
minified Rollup bundle. Reproduces with any part that has an Arduino
pin reachable through wires (i.e. almost every canvas component).
Fix: hoist `traceDetailed` (and its `PASSIVE_PIN_PAIRS` /
`PRESET_TO_BASE` data) to module scope. Pure function takes the
simulator state as an argument. Both `getArduinoPin` (now a thin
wrapper) and `getPinResolver` call it correctly.
No behavioural change. 1853 tests still pass, build:docker green.
Replaces the Phase 1b vcc/2-flat threshold with per-logic-family
Vil/Vih thresholds + Schmitt-trigger hysteresis where applicable.
SPICE-resolved digital reads now match what real ICs actually do —
TTL noise margins, CMOS rail-to-rail, 74HC14 Schmitt hysteresis,
LVCMOS33 vs CMOS-5V interop.
New module: simulation/LogicFamilies.ts
- LogicFamily interface (vcc, vil, vih, vil_schmitt?, vih_schmitt?,
cin_pF, vol_max?, voh_min?, output_impedance_ohm?)
- FAMILIES catalog: TTL, CMOS-5V, CMOS-5V-SCHMITT, CMOS-5V-TTL-INPUTS,
LVCMOS33, AVR_HC, CMOS-3.3V — all sourced from TI / ATmega328P /
JEDEC datasheets.
- BOARD_FAMILY: per-board lookup. Uno/Mega/Nano/ATtiny → AVR_HC,
ESP32 family + Pi Pico → LVCMOS33, fall back to AVR_HC for
unknown boards.
- getBoardLogicFamily() and getLogicFamilyById() helpers.
PinResolver:
- SpiceResolvedConfig docstring rewritten with Phase 3 wording.
- New `configFromLogicFamily()` builder — picks Schmitt thresholds
when the family declares them, falls back to vih/vil otherwise.
DynamicComponent:
- When the trace crosses an active device, the SPICE-resolved
resolver is now built with the OWNER BOARD's logic family
instead of vcc/2. Hysteresis comes through automatically for
boards whose native family is Schmitt-capable.
- Phase 3 continued: per-component logicFamily override from
components-metadata.json (so e.g. a 74HC14 placed on an Arduino
Uno gets Schmitt thresholds even though the BOARD is AVR_HC).
Tests:
- logic-families.test.ts (new) — 19/19 passing.
Covers catalog sanity (vil < vih, vol_max ≤ vil, voh_min ≥ vih),
per-board lookup, Schmitt vs non-Schmitt config, noise rejection
behavior of 74HC14 Schmitt resolver, last-state-wins behavior
of CMOS-5V dead band.
- Phase 0 + Phase 1b regression: 16/16 still passing.
- tsc --noEmit on new files: clean.
No deploy in this commit — staged for end-of-session rebuild.
Adds the architecture pieces for mixed-mode coupling without yet
driving the SPICE engine. Components on a path that crosses an active
device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route
through a new SPICE-resolved PinResolver variant; everything else
keeps the digital fast-path from Phase 0.
What ships:
- simulation/PinResolver.ts
* `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES`
list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay).
* `DetailedPinTrace` / `DetailedPinTracer` types — the trace
function now reports whether it crossed an active device, on
top of the Arduino pin number.
* `createSpiceResolvedPinResolver()` — new factory; reads voltages
from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW
with hysteresis (thresholdHigh != thresholdLow → Schmitt-like).
- simulation/spice/MixedModeScheduler.ts (new)
* Singleton orchestrator that holds the NgSpiceInteractive engine
and the SpiceVoltageSource subscription registry.
* `start()` / `stop()` / `dispose()` lifecycle.
* `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource.
* `onMcuPinChange()` placeholder for the alter+tran event loop.
* Skeleton: subscribers register but never receive events yet.
Phase 1b continued will wire NgSpiceInteractive into the loop.
- components/DynamicComponent.tsx
* Trace function extended with `traceDetailed()` that tracks
whether the BFS crossed an active component.
* PinResolver factory branches: active-path → SPICE-resolved (uses
the scheduler), digital-only → existing default impl. Default
threshold = vcc/2 with no hysteresis; Phase 3 will replace with
per-logic-family Vil/Vih.
Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved
path falls back to FLOATING until Phase 1b continued wires the engine.
Tests:
- pin-resolver-phase1b.test.ts (new) — 8/8 passing.
Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator
metadata id; SPICE-resolved resolver state reporting, threshold
conversion, hysteresis dead-band, unsubscribe.
- pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression).
- tsc --noEmit on the new files: clean.
No deploy in this commit — staged for end-of-session rebuild + push
per user preference.
Decouple per-component handlers from direct pinManager.onPinChange +
getArduinoPinHelper subscriptions by introducing a small PinResolver
interface. The Phase 0 default impl is functionally identical to the
legacy path — it just routes through PinResolver instead of being
inlined in every handler. Zero behavior change.
The point is to make Phase 1 possible: swap the default impl for a
SPICE-resolved version that watches node voltages and threshold-
converts to digital events, without rewriting every handler.
Files:
- simulation/PinResolver.ts (new) — interface + default factory
- parts/PartSimulationRegistry.ts — additive 5th arg to
attachEvents (getPinResolver?), legacy 4-arg signatures keep
working unchanged
- components/DynamicComponent.tsx — assembles the PinResolver from
the wire-trace logic + PinManager subscriptions + board Vcc
lookup, passes it as the 5th arg to attachEvents
- parts/BasicParts.ts — LED handler migrated as proof of concept
(resolver-first path, legacy 4-arg path kept as fallback for
tests / unmigrated harnesses)
- __tests__/pin-resolver.test.ts (new) — 8 unit tests covering
FLOATING / GND / HIGH / LOW / GPIO subscriptions / unsubscribe
Vitest: 8/8 pin-resolver tests pass. 1300+ existing tests still pass;
the one pre-existing flake (spice-rectifier-live-repro timing out >60s)
is unrelated to this commit — verified by running the test on plain
HEAD without these changes (same timeout).
See project/sim-mixedmode/phase-00-pin-resolver.md (in the velxio-prod
repo) for full phase context.
The canonical "Arduino pin → resistor → BJT base, BJT collector →
load" pattern for multiplexed 7-segment clocks was breaking in the
simulator: getArduinoPinHelper('COM.1') couldn't resolve through
the transistor, so the multiplex-aware 7-segment driver thought no
digit-select pin was wired and fell back to "all digits enabled".
Result: every display in the multiplex array rendered the same
rapidly-changing pattern → user-visible flicker.
Fix: add the NPN/PNP BJTs to the PASSIVE_PIN_PAIRS map with
[collector, base] — the trace function continues from B when it
arrives at C (and vice versa). That makes the Arduino pin driving
the base reported as the controller of the collector — exactly the
relationship the user's multiplex code expects.
Conventions covered:
- NPN (2n2222, bc547, 2n3055): Arduino HIGH → transistor on →
COM pulled LOW → common-cathode digit enabled. Our 7-segment
driver treats "digit pin HIGH = enabled" which matches.
- PNP (2n3906, bc557): inverse logic. We expose the same pin
mapping; users writing PNP-driver code will see the polarity
behave inverted, which is what real hardware does too.
This is a one-line shortcut, not a true active-device model. We're
not simulating BJT saturation, β, base current, or PNP polarity —
just reporting "this Arduino pin is the boss of this collector".
That's enough for the multiplexing use case and the only place
getArduinoPinHelper is consulted today.
The 'raspberry-pi-pico' boardKind used to render <NanoRP2040> — a
<wokwi-nano-rp2040-connect> Web Component. That's a completely
different board: it has pin labels D2..D13 / A0..A7 / 5V / VIN,
and a horizontal 168×68 layout. The actual Raspberry Pi Pico has
GP0..GP28 / 3V3 / VBUS / VSYS and is vertical-narrow (105×264).
Symptom: every wire in a Pi-Pico example that referenced a real Pico
pin (GP10, GP18, 3V3, GND.5, etc.) silently fell back to (0, 0) in
pinPositionCalculator — the calculator looks up `element.pinInfo`
by name, doesn't find GP* on the Nano RP2040 Connect component, and
returns the board's top-left corner. The Pico Doom example was the
loudest casualty (cables to the corner instead of the TFT), but
seven other GP-style examples (pico-7segment, pico-button-led,
pico-rgb, pico-dht22, pico-doom-raycaster, plus pico-ntc/pico-joystick
which use A0/A1 aliases that map to GP26/GP27) all silently routed
to nowhere.
Fix is a two-liner: 'raspberry-pi-pico' shares the same case as
'pi-pico-w' (both use the same Web Component because the Pico and
Pico W are pin-compatible). BOARD_SIZE updated to 105×264 to match
the real Pico footprint. Dropped the now-unused NanoRP2040 import.
Known regression — eleven older examples (pico-blink, pico-serial-led-
control, pico-i2c-scanner, pico-i2c-rtc-read, pico-i2c-eeprom-rw,
pico-spi-loopback, pico-adc-read, pico-multi-protocol, pico-hcsr04,
pico-pir, pico-servo) were wired against D2..D12 of the wrong board.
Their wires will now land at (0,0). Those examples' sketches were
written for the Pi Pico (use LED_BUILTIN = GP25, A0..A3 = GP26..GP29)
so the wires were ALREADY electrically nonsense — they connected
external components to pins the sketch never touched. Visible bug
trades silent bug; both need a follow-up commit to rewire each one
to the Pico pin its sketch actually expects.
Combined with the earlier MADCTL fix (6edc715) and the SPI adapter
fix (6a7b721), Pico Doom should now finally render end-to-end on
velxio.dev.
Build verified (vite OSS+pro, 285 SEO pages).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two unrelated minimap issues from user feedback:
1. Click on the red viewport rect was sometimes teleporting the
canvas instead of starting a drag. Cause: insideRect compared
click coords against the UNCLAMPED rectX/rectY/rectW/rectH, but
the rendered rect uses clampedX/clampedY (which differ when the
user pans past a world edge). The user clicked on the visible
red rect, but the logical rect was off-minimap → insideRect
returned false → fell through to the teleport branch.
Fix: compute clamped values once at the top, render and hit-test
against the same values. Drag now only fires when the click
really lands inside the visible rect.
2. The 140x105 default still ate too much canvas at typical zoom.
Drop to 100x75 (12% of world width by 2.5%, same proportions as
the world). Mobile breakpoint dropped to 90x68 to stay
proportionally smaller on phones.
User feedback: the default 200x150 minimap eats too much of the
canvas-content area on a typical 13"/14" laptop, and the white
viewport rectangle against a dark canvas blends with the boards
once enough components are placed.
Drop the desktop default down to the size we already use on phones
(140x105 — the mobile media query still wins on screens ≤720px so
that block continues to apply identically). At this size the rect
becomes the focal indicator of where you are in the world; switch
its outline to brand red (#ef4444 — Tailwind red-500) with a faint
red fill so it pops without overpowering the boards (which stay
brand blue).
Body of the work is two number changes + two color tokens; the
rest of the component logic (pointer routing, world rendering,
clamping) is untouched.
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3 of the OSS / pro split — frontend side. Phase 2 already moved
the auth/DB stack out of the OSS backend; this commit does the same
for the React app. After this, the OSS image is editor + simulator
+ landing + docs only.
What moved to the private overlay (pro/frontend/src/pro/):
pages/{Login,Register,ForgotPassword,ResetPassword}Page.tsx
pages/{Admin,UserProfile,Project,ProjectById}Page.tsx
components/admin/{AdminBoardsTab,AdminDashboardTab,UserActivityModal}.tsx
components/layout/{SaveProjectModal,LoginPromptModal}.tsx
services/{authService,adminService}.ts
store/useAuthStore.ts
hooks/autoSaveImpl.ts
New seams added so OSS components stay decoupled:
* lib/proRoutes.ts — registerProRoutes()/useProRoutes() via
useSyncExternalStore. mountPro() injects the moved pages at runtime;
App.tsx subscribes to the registry, so registration after the
initial render re-renders without a Not-Found flash.
* lib/proSession.ts — registerSessionCheck()/triggerSessionCheck().
App.tsx fires this on mount instead of useAuthStore.checkSession();
pure OSS no-ops.
* lib/proSaveAction.ts — installSaveActionImpl()/triggerSaveAction().
EditorPage's Save button dispatches through this; the overlay
decides whether to show SaveProjectModal or LoginPromptModal based
on auth state. In OSS without an overlay it's a no-op today; in
Phase 4 of the split it becomes the .vlx Export entry point.
OSS-side rewrites:
* App.tsx drops the 8 page imports + 8 route entries; uses
triggerSessionCheck() instead of useAuthStore directly.
* AppHeader.tsx drops the user/login/register block entirely. The
header-auth slot (introduced in Phase 1) now stays empty in OSS
and gets filled by the overlay's portal mount.
* EditorPage.tsx drops useAuthStore + SaveProjectModal +
LoginPromptModal imports. The Save handler is now triggerSaveAction().
* LandingPage.tsx drops the dead UserMenu component (defined but
never rendered) + its useAuthStore imports.
* main.tsx drops the side-effect import of hooks/autoSaveImpl — the
impl lives in pro now and self-registers via mountPro().
Build config:
* vite.config.ts adds @velxio alias → src/. Lets the overlay import
upstream modules (lib/proRoutes etc.) by stable name regardless of
whether it's symlinked (local dev) or COPYed (Docker).
* preserveSymlinks now gated on VITE_PRO_BUILD only (not on serve
mode). Needed so Rollup keeps the overlay logically inside src/pro/
during local junction-based builds.
Build verification:
* OSS-only: 20-ish routes, no /login, /admin, /:username — 285 SEO
pages prerendered. Bundle drops ~80-120 KB.
* OSS + overlay: full 38 routes (30 upstream + 8 from registerProRoutes),
HeaderAuth dropdown injected via slot, save action wired to the
overlay's modal flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First phase of the OSS / pro split. Goal: open the seams so the auth/DB/admin
stack can move into the private overlay (Phase 2-3) without the routes that
stay in OSS (compile, libraries, simulation, iot_gateway) having to know.
Backend
-------
* New app/core/hooks.py — registry for record_compile, get_current_user_id,
and lifespan startup tasks. Each hook is a no-op by default; overlays
call register_* in register_pro(app) to plug in a real implementation.
* compile.py now imports only from app.core.hooks. Drops the direct deps on
app.core.dependencies, app.database.session, app.models.user, and
app.services.metrics. Route signatures use `Depends(get_current_user_id)`
instead of `Depends(get_current_user)`; the metric helper passes user_id
through rather than a User instance.
* compile_chip.py drops the unused _current_user Depends entirely.
* main.py wraps the auth/DB stack import in try/except. When it succeeds
(today's behavior on velxio.dev), an adapter bridges record_compile and
get_current_user_id to the existing app.services.metrics + dependencies,
and the create_all + ALTER TABLE migration block runs via a registered
lifespan_startup hook. When it fails (the post-Phase-2 OSS image), main
logs "running stateless" and skips registering anything — the routes
still load and behave as no-ops for metrics + always-anonymous for auth.
Frontend
--------
* useAutoSaveProject becomes a skeleton: one useState + one useEffect that
delegates to an installed AutoSaveImpl. installAutoSaveImpl() replaces
the impl without changing hook count, so React's rules-of-hooks stay
satisfied even after the impl moves out of OSS.
* New hooks/autoSaveImpl.ts holds the original logic (debouncing, dirty
detection, owner eligibility, fetch keepalive on unload), refactored to
emit() instead of useState. It self-registers at module load; main.tsx
imports it for the side effect.
* AppHeader wraps the entire user-vs-login UI in a data-velxio-slot
="header-auth" boundary. Today the OSS UI still renders inside the slot
— the overlay can portal-inject additional items now, and in Phase 3
the slot becomes the sole owner of header auth UX.
Behavior is identical on velxio.dev (pro overlay imports everything
successfully, every adapter wires up). The change is purely structural:
deleting the auth/DB modules tomorrow no longer crashes OSS at import.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two minimal hooks so the velxio-pro agent overlay can offer a 'Diagnose
this compile failure with AI' affordance without touching upstream
component internals:
- New store/useCompileLogsStore: holds the editor's compile output as
Zustand state instead of local React useState in EditorPage. The
setter accepts both a value and an updater fn so the EditorToolbar
callers that used setCompileLogs(prev => [...prev, log]) keep
working without changes.
- CompilationConsole header now renders a
<div data-velxio-slot='compile-console-actions' /> when errorCount
> 0. The pro overlay mounts a 'Diagnose with AI' button into this
slot via slotMounter. Empty in the OSS image — no behaviour change.
EditorPage replaces its local useState<CompilationLog[]> with the store
selector. The downstream prop-drilled setCompileLogs callers (toolbar,
sub-toolbars) keep their signature.
Companion commit lands the button + diagnostic prompt builder in the
velxio-prod overlay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /pricing page exists (PricingPlaceholder upstream, real PricingPage
portal-mounted by the private overlay) but had no entry in the top nav.
Adds 'pricing' to header.nav in all 9 locales (de, en, es, fr, it, ja,
pt-br, ru, zh-cn), wires the Link in AppHeader between About and Blog,
and mirrors the link in the landing-page footer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renders a 200×150 px overview of the whole 4000×3000 world in the
bottom-right corner of .canvas-content. Boards show as filled blue
rectangles, components as small white dots, and the current viewport
appears as an outlined rectangle the user can drag to pan.
Geometry mirrors the canvas's existing pan+zoom model:
SCALE_X = MINIMAP_W / WORLD_W = 0.05
rect.x = -pan.x / zoom * SCALE_X
rect.w = viewport.width / zoom * SCALE_X
Two interaction modes, decided at pointerdown by hit-testing the
rectangle:
- Inside the rect → drag-pan: keep updating pan as the pointer
moves, with delta in minimap-px converted back to world units
by (delta / SCALE) * zoom.
- Outside the rect → teleport: re-center the viewport on the
clicked world point.
Pan is clamped so the viewport rectangle never escapes the minimap
bounds (matches the canvas's implicit world boundaries at 4000×3000).
ResizeObserver on the canvas-content keeps the rect accurate when
the user toggles side panels or resizes the window.
Mobile: at ≤720 px width the minimap shrinks to 140×105 px so it
doesn't eat too much of the canvas. Touch events go through the same
pointerdown / pointermove path — no separate touch code path needed
thanks to Pointer Events.
Bundles with: matching CSS file, import + JSX hookup inside
.canvas-content's render tree.
Closes the long-standing "components are frozen during simulation"
complaint. Once the user clicked Run, interactive wokwi parts
(pushbuttons, slide-switches, potentiometers …) called
stopPropagation in their bubble-phase mousedown handlers and the
canvas's React onMouseDown never fired — so dragging them to
rearrange the layout was impossible without first stopping the sim.
Two surgical changes:
1. DynamicComponent.tsx switches the wrapper from `onMouseDown` to
`onMouseDownCapture`. Capture phase runs before the inner
wokwi-element, so the canvas sees the mousedown regardless of
stopPropagation downstream. The existing posDiff < 5 check in
mouseup keeps disambiguating click vs drag: a click still falls
through to the wokwi-element's own mousedown/up for button-press
semantics, only sustained movement promotes to a drag.
2. SimulatorCanvas.tsx's touch path used to early-return on touchstart
when interactionRunning + .web-component-container, killing any
chance of a touch-drag. Now we remember the touch's start position
in pendingTouchDragRef and let the browser keep synthesizing mouse
events for the wokwi-element. If the finger drifts past
DRAG_PROMOTE_THRESHOLD_PX (8 px) onTouchMove cancels the
passthrough and starts a real component drag — dispatching a
synthesized mouseup on the original target so the wokwi-element
doesn't stay visually pressed mid-drag.
In digital / analog board-less examples the user clicks a slide-switch
or pushbutton expecting it to flip its state. Until this commit the
component property dialog opened instead and the click never reached
the wokwi-element underneath, so:
- The user couldn't change switch state through the canvas at all.
- With no state change the SPICE solver kept the old netlist, and
every downstream LED stayed dark — the symptom that read as
"voltages change but no LED lights".
Root cause was the gating: SimulatorCanvas only suppressed the
property dialog when `useSimulatorStore.running` was true, but that
flag is bound to an MCU's start/stop. Board-less circuits have no MCU
to start so `running` is permanently false, even when the SPICE engine
has been live since the example loaded.
New derived flag `interactionRunning = running || (boards.length === 0
&& !electricalPaused)` — true whenever the user is in an "interactive"
session, MCU or SPICE-only. Used in three click-handling paths:
- SimulatorCanvas mouse-up handler: dialog is suppressed and the
click falls through to the wokwi-element (line 1395).
- SimulatorCanvas touch-start passthrough: same for touch (line 474).
- SimulatorCanvas touch-end short-tap: same for tap (line 774).
Also propagated to DynamicComponent so the cursor becomes pointer (not
move) for interactive parts in board-less mode — visual cue that the
user can click instead of just drag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Implemented `i2c-esp32-real-firmware.test.ts` to test ESP32 I2C communication via backend and WebSocket.
- Created `load-example-transitions.test.ts` to ensure proper loading of examples between board-less and board-based contexts.
- Added `CircuitVerificationModal.tsx` to display circuit verification results before running simulations.
- Developed `circuitVerifier.ts` to perform pre-flight checks for circuit safety, identifying potential issues like short circuits and component overloads.
- Introduced minimal ESP32 I2C master sketch `esp32_i2c_writer.ino` for testing I2C transactions.
- Implement HD44780Decoder for decoding I2C commands to HD44780-compatible LCDs.
- Add bmp280_bridge_reader.ino to read BMP280 chip_id and status registers via I2C.
- Create i2c_scanner_multi.ino to scan I2C addresses and report responding devices.
- Introduce lcd_i2c_hello.ino to demonstrate basic LCD functionality with I2C.
- Implement pcf8574_bidirectional.ino to test bidirectional communication with PCF8574.
- Add pico_i2c_master_reader.ino for reading BMP280 from a Raspberry Pi Pico.
- Create rtc_lcd_clock.ino to display time from a DS1307 RTC on an I2C LCD.
- Add color picker button to SelectionActionBar for wire selections
- Toggle palette using WIRE_KEY_COLORS swatches
- Pass currentColor and onColorChange from SimulatorCanvas
- Reset showPalette on kind/onColorChange change (Copilot suggestion)
- Use t('editor.selectionBar.changeColor') for title/aria-label (Copilot suggestion)
- Add changeColor i18n key to all 9 locale files
Co-authored-by: naweiss <naweiss@users.noreply.github.com>
Block 9 added `const { t } = useTranslation()` at line 50 but forgot the
matching `import { useTranslation } from 'react-i18next'`. The component
then crashes the moment a user clicks a sensor on the canvas with
`Uncaught ReferenceError: useTranslation is not defined`, taking the
whole simulator render tree down.
A user reported on Discord: "the Velxio Console doesn't update anything,
it just waits until the very end and displays everything in one go".
True for the async compile path — /compile/status only carried `state`
and the final `result`, so the editor's CompilationConsole stayed empty
during the 5-7 minute cold ESP-IDF builds and dumped 1500 lines at once
when the build finished.
This wires live build output through the whole stack.
Backend (espidf_compiler.py)
- New _run_with_streaming() helper. When a progress_callback is provided
it spawns the subprocess via Popen + stdout/stderr drain threads and
invokes the callback line-by-line. When None it falls back to the
existing subprocess.run(capture_output=True) one-shot path so the
unit-test code that doesn't care about live output is unaffected.
- compile() and _compile_in_dir() take an optional ProgressCallback.
- _run_cmake / _run_ninja closures now go through _run_with_streaming
with that callback. cmake configure (~2-5 s) + ninja (~5-300+ s) both
stream now; the ninja output is the one users actually want to watch.
Backend (compile.py)
- _compile_job seeds COMPILE_JOBS[id]['stdout_buffer'] = '' and defines
on_progress_line(line) which appends to it. Buffer capped at 256 KB
(tail kept) so a runaway build can't OOM the FastAPI process.
- The buffer is preserved on both the success and the error path so
late polls still see the log even after state transitions to
done/error.
- /compile/status now returns the buffer as a `stdout` field.
CompileStatusResponse gains the field with default '' so old clients
that don't read it still work.
Frontend (compilation.ts)
- compileCode() takes a 4th argument: optional CompileProgress
callback fired every poll while state ∈ {pending, running}. Carries
the cumulative stdout (caller computes deltas) plus elapsed seconds.
- Surfaces the new `stdout` field of /compile/status and forwards it
to the callback. Errors thrown from the callback are swallowed —
a faulty UI hook must never break the polling loop.
Frontend (EditorToolbar.tsx)
- Both compileCode() call sites (Run and Compile-All) now pass an
onProgress callback. It tracks `lastStreamedLen` per-compile, splits
each new delta on newlines, and appends them as `info`-typed
CompilationLog entries via setCompileLogs. The Compile-All flow
prefixes each line with the board label so multi-board builds stay
readable.
- After the build settles, the existing parseCompileResult call still
runs and appends the structured analysis on top of the live stream
— that's where FAILED-block detection + the `error`-typed entries
that drive the auto-switch-to-errors filter live.
Net effect on the user complaint: cold ESP-IDF builds now show the
ninja [N/1483] progress lines streaming into the console as they
happen, instead of staring at an empty panel for 5-7 minutes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>