Phase 4 polish: GraceBanner was rendering for state=locked/tampered
even though LockoutOverlay covers the screen for those states. The
banner leaked through the overlay's 96%-opaque background as a
faint red strip - confusing.
- GraceBanner.tsx: bannerFor() returns null for locked/tampered
(LockoutOverlay handles the messaging). Also exported bannerFor
so the new unit tests can exercise the pure decision logic.
- __tests__/GraceBanner.test.ts (new): 13 vitest cases covering
pre-expiry amber/red thresholds (trial_ends_at vs subscription_period_end),
fallback to claims.exp for legacy JWTs, soft/hard grace messaging,
dismissibility rules.
- vitest.config.ts: include also matches src/**/__tests__/ so the
desktop tests are discovered without moving them.
Runtime ~600ms vs 5-25 min for a full installer rebuild - lets
future iterations on the banner state machine skip the build cycle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the frontend half of the v0.3.0 desktop paid model. The Tauri
shell (in velxio-prod) emits velxio://license-required when the
license gate refuses to spawn the sidecar; this commit teaches the
OSS desktop overlay to react.
- LockoutOverlay.tsx (new): full-screen modal with three variants
(no_credential / tampered / expired). Sign-in or paste-key
resolves it via restartApp().
- DesktopWelcomePage.tsx: new grandfather variant - "you have N
days to keep using Velxio Desktop" + "Continue without signing in".
- GraceBanner.tsx: rewrite with pre-expiry tones (5d amber, 24h
red, dismissible), polling every 10 min while document visible,
separates pre/post-expiry messaging.
- Esp32QemuPrompt.tsx: signup gate for grandfather users (ESP32
binaries are not part of the grandfather grace) + inline progress
bar driven by velxio://esp32-qemu-progress events.
- index.ts: rewires on getGateInfo() at first paint to decide
welcome vs lockout vs nothing; installs license-required listener
+ 10-min foreground polling for the locked transition.
- tauriBridge.ts: adds GateInfo type, getGateInfo(), restartApp().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 7.7 follow-up. Previously the WiFi stub returned wlan.isconnected()=False
and ntptime.settime() raised OSError — sketches degraded gracefully but
features like the TIME and WEATHER screens in the smart-ui-eyes example
showed "Sync Failed" / "API Error" instead of real-looking data.
Smart stub now:
- wlan.isconnected() returns True after the first ~2 calls (simulates a
~1 second connection ramp)
- ntptime.settime() pre-loads machine.RTC() with the host's UTC datetime
(captured at code-injection time), so localtime() returns real time
- urequests.get(url) returns a stubbed Response whose .json() decodes a
payload routed by URL substring:
"openweathermap"/"weather" → fake weather dict (temp/humidity/desc)
"ipify"/"myip" → fake public IP
"worldtimeapi" → fake ISO datetime
everything else → {}
- urequests.post/head also stubbed (return {"ok": True} / {})
- Both `urequests` and `requests` aliases registered
End result: smart-ui-eyes example shows real-looking time on TIME
screen and plausible weather data on WEATHER screen, no crashes.
Still no real internet (would need Phase 7 QEMU WiFi emulation), but
visually the example demos correctly.
Drives the simulation backend WebSocket directly, bypassing the
useSimulatorStore WiFi-stub prelude, and injects a minimal MP
program that exercises network.WLAN(STA_IF).active(True) +
.connect() + .isconnected() loop.
Used by Phase 7 of the velxio MP WiFi emulation project to capture
the register trace from a DEBUG=1 build of libqemu-xtensa.so. The
test exercises every API call the smart-ui-eyes example needs from
the network module.
Run:
node --experimental-websocket test/test_micropython_wifi_trace/test.mjs \
--backend=http://localhost:3080 --timeout=60
docker logs velxio-app 2>&1 | grep -E '\[(wifi|phya|ana )\]' | tail -200
Inject a compat shim into the raw-REPL prelude that replaces
sys.modules["network"] and sys.modules["ntptime"] with no-op stubs
BEFORE user main.py runs.
Why: the picsimlab QEMU fork's esp32_wifi NIC emulation handles
Arduino's lightweight WiFi.h but not MicroPython's full esp_wifi_init
path. Calling network.WLAN(STA_IF) (which is what every
network-using MP sketch does) drives the firmware to wait on
peripheral status bits QEMU never sets, eventually tripping the
FreeRTOS task watchdog (TG1WDT_SYS_RESET ~26s after boot, or
TG0WDT ~14s if the NIC is partially attached).
With the stub:
network.WLAN(STA_IF).isconnected() -> False
network.WLAN(STA_IF).connect(...) -> no-op
ntptime.settime() -> raises OSError
Sketches that already have try/except around sync_time (which is
most of the 100-days examples) now degrade gracefully: WELCOME +
EYES screens run, TIME and WEATHER screens show their fallback
behaviour, no panic, no reboot.
Doesn't affect Arduino C++ — sketches that #include <WiFi.h> use
real WiFi.begin() and the existing esp32_wifi NIC handles those fine.
A proper fix is to extend the picsimlab WiFi emulation to support
the full ESP-IDF API, but that's a multi-day project. This stub
unblocks the 31 MicroPython examples shipping with network imports.
Three coordinated changes that fix the "Waiting for browser…" hang
and unblock first-launch UX on the Tauri desktop build:
1. desktop/index.ts — DON'T mountWelcome unconditionally on first
launch. Before, an empty keychain (no key yet) forced the
welcome / sign-in screen on top of the editor, gating 100% of
the app behind an account. Now the editor opens directly:
compile + run + sim + save .vlx all work for free (they're
upstream OSS features), and the license check still runs in
the background just to populate state for the GraceBanner
(which shows for invalid keys — locked, tampered, in
soft/hard grace). Pro-only features (ESP32 QEMU download,
agent IA) prompt for license at use time, where it actually
matters. Matches the "try before you buy" expectation a
desktop install creates.
2. desktop/tauriBridge.ts — rewrite `openExternal` to try every
known IPC path in cascade order and log via the desktop debug
file which one worked. The previous implementation invoked
`plugin:shell|open` with `{ path: url }`, which silently
failed (no ACL match + wrong arg shape) and fell back to
`window.open`, which inside a Tauri webview is a no-op for
external URLs — the browser never opened. New cascade:
plugin:opener|open_url (paired with tauri-plugin-opener which
ships in this revision), then plugin:shell|open with both
`{ path, with: null }` and `{ url }` shapes, then the
window.__TAURI__.shell / opener high-level wrappers that
specific Tauri 2.x flag combos expose. Each attempt logged
via the dlog helper so the next operator can see exactly
which path was used (or that all failed) without devtools.
3. desktop/menu.ts — new `navigate-route` action type. Routes
bundled in the SPA (DocsPage, ExamplesPage, AboutPage) that
used to open velxio.dev in the system browser now navigate
in-window via history.pushState + popstate (mirrors the
locale-switch handler). Respects the current locale prefix
so `/examples` from `/es/editor` lands at `/es/examples`
instead of jumping back to English.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hasWifi auto-detection in useSimulatorStore.startBoard only matched
Arduino C++ patterns (#include <WiFi.h>, WiFi.begin). MicroPython
sketches that call `import network` or `network.WLAN(STA_IF)` were
not detected, so wifi_enabled stayed false and the backend never
attached the esp32_wifi NIC model to QEMU.
Symptom: any MicroPython ESP32 example that touches the network
module hangs in network.WLAN(STA_IF) (the constructor that triggers
esp_wifi_init internally) and the FreeRTOS task watchdog trips with
TG1WDT_SYS_RESET ~26 seconds after boot. The chip then reboot-loops.
Mirror the Pico W detector right below this one — it already handles
both Arduino and MicroPython patterns. Now ESP32 does too.
Affects 31 examples in examples-100-days.ts that use network.WLAN.
Two Node.js tests that hit the simulation backend WebSocket directly
and inject minimal MicroPython programs via raw-paste REPL:
- test_micropython_i2c_minimal: smallest possible repro. I2C(0)+scan+
single-byte writeto. Used to prove the bug is NOT in the basic I2C
layer — this test passes both before and after the fix.
- test_micropython_i2c_ssd1306_repro: walks the full SSD1306 init
sequence (25-cmd init loop + 6x addr writes + writevto 8B + writevto
1024B). Used to prove the bug is NOT in the cmd sequence or the
writevto path — this test also passes both before and after.
These two tests refuted the original "missing TRANS_DONE IRQ"
hypothesis and pointed the investigation toward the file-load vs
raw-REPL difference, which led to identifying the per-byte _emit
bottleneck in esp32_worker.py.
Run with:
node --experimental-websocket test/test_micropython_i2c_minimal/test.mjs \
--backend=http://localhost:3080 --timeout=120
Requires the velxio container (or local backend with QEMU libs) on
the given backend URL.
A 1024-byte oled.show() writevto generates ~1025 calls into
_on_i2c_event, one per byte. The previous code called _log (stderr
write+flush) AND _emit (stdout JSON write+flush) for every event,
saturating the worker subprocess's stdout pipe. The QEMU thread blocks
on the synchronous write, the firmware's ESP-IDF i2c_master ISR
re-enters before the previous one finished, and the Interrupt watchdog
trips on CPU1 with a "Guru Meditation Error: Interrupt wdt timeout"
panic on the second consecutive oled.show() call.
I2CWriteSink already buffers writes internally and emits a single
i2c_transaction event on FINISH, so the per-byte log+emit was pure
overhead with no observability value for display drivers (SSD1306,
PCF8574). Keep them for everything else.
Verified end-to-end via chrome devtools MCP:
- Test minimal (init + 1 explicit show): markers OK, no panic
- Loop test (10x oled.show() + sleep(0.5)): DONE_LOOP, no panic
OSError: [Errno 19] ENODEV at ssd1306.SSD1306_I2C(...) on
100d-esp32-oled-smart-ui-eyes-animation-time-and-weather-micropython.
MicroPython SoftI2C bit-bangs GPIO directly. Velxio's ESP32 QEMU
bridge listens on the emulated I2C peripheral (registers slaves like
0x3C wokwi-ssd1306 against it) and doesn't decode bit-banged GPIO
toggles as I2C frames, so the OLED never sees any writes and
i2c.writeto() returns ENODEV on first use.
machine.I2C(0, ...) routes through the hardware I2C peripheral that
QEMU emulates, the registered slave receives the bytes, the OLED
panel updates. Same code path the other working SSD1306 MicroPython
examples on this repo already use.
API surface is identical to SoftI2C — only the constructor differs —
so the rest of the user sketch needs zero changes.
avr8js's usart.writeByte(value) rejects the call (returns false, drops
the byte) whenever rxBusyValue is set — and rxBusyValue stays true for
one full cyclesPerChar after each accepted call. The old serialWrite()
fed every character in a synchronous for-loop, so only the first byte
made it through and the sketch saw 'h' when the user typed 'hello\n'.
Buffer pending bytes in serialRxQueue and pump them one at a time:
- serialWrite() now just queues + kicks drainSerialRxQueue once
- drainSerialRxQueue calls writeByte on the head of the queue and only
shifts it off if writeByte returned true (avr8js accepted it)
- usart.onRxComplete is wired to drainSerialRxQueue so the next byte
ships as soon as the sketch's RX side actually consumed the previous
one — matches the cyclesPerChar pacing the real chip enforces
Same handler wired in both USART setup paths (the Uno/Nano branch and
the post-loadHex Mega/ATtiny branch). TX path (onByteTransmit +
emitUartTxFrame for the oscilloscope waveform) is unchanged.
ESP-IDF runs component_get_requirements.cmake in script mode (not full
project mode) to scan component dependencies before the build proper.
CMake rejects CONFIGURE_DEPENDS in script mode with:
CONFIGURE_DEPENDS is invalid for script and find package modes.
The persistent build dir is already wiped + restored from the template
on every compile (espidf_compiler.py:220-222 + CMakeLists.txt patches),
so the glob is re-evaluated naturally on the next cmake configure — we
don't need CONFIGURE_DEPENDS to catch new files.
Pairs with 27c9a28 (glob fix) + 21791a8 (template main.cpp decouple).
Now that main/CMakeLists.txt globs every *.cpp/*.c into SRCS (commit
27c9a28 — needed so multi-file Arduino projects link), sketch.ino.cpp
is compiled as its own translation unit. The template main.cpp's
`#include "sketch.ino.cpp"` is left over from when SRCS only named
main.cpp — back then we had to pull the user code into main.cpp's TU
to get it compiled at all.
With the glob in place, keeping the include re-defines setup() and
loop() in main.cpp's TU AND in sketch.ino.cpp.obj, so the linker dies
with "multiple definition of `setup'". Replace the include with a
forward declaration and let the linker resolve setup()/loop() from
sketch.ino.cpp.obj. Matches arduino-cli's per-file compile + auto-link
model, and frees pure-C++ multi-file sketches to put helper TUs in
their own .cpp files without textual-include hacks.
Repro: robot-desktop-eyes after the glob fix (#13 rebuild) — compile
got past resolve+compile and into link, where multi-def of setup()
would have appeared on the next attempt. This commit closes the loop.
Multi-file Arduino sketches (robot-desktop-eyes is the canonical
case) define classes and free functions in their own translation
units — Face::Update(), FaceExpression::GoTo_Surprised(),
AsyncTimer::SetIntervalMillis(...), etc. live in Face.cpp,
FaceExpression.cpp, AsyncTimer.cpp respectively.
The espidf_compiler drops every user-supplied .h/.cpp into
project/main/ but the main/CMakeLists.txt only declared
`SRCS "main.cpp"`, so the helper TUs never got compiled and the
linker died with dozens of "undefined reference to ..." entries.
Switch SRCS to a CONFIGURE_DEPENDS glob over the main/ directory
so every .cpp/.c that lands in main/ becomes part of the IDF main
component automatically. CONFIGURE_DEPENDS makes CMake re-evaluate
the glob on every reconfigure, which matters because the persistent
build dir is reused across compiles and the set of helper files
changes per sketch.
Repro: open robot-desktop-eyes, click Compile. Before this commit:
37 linker errors starting at "undefined reference to `u8g2'" and
"undefined reference to `Face::Update()`". After: the helper TUs
compile and the .elf links.
Two related fixes for the ESP32 Arduino-compat compile path:
(a) backend/app/services/esp-idf-template/main/CMakeLists.txt:
Demote -Werror=comment / =parentheses / =sign-compare / =narrowing
/ =write-strings / =missing-field-initializers / =reorder back to
plain warnings. ESP-IDF's project defaults are stricter than what
Arduino/arduino-cli users expect, so common Arduino idioms (nested
/* */, missing field initializers in struct literals, etc.) were
failing builds that compile fine in the Arduino IDE. -Wall stays
on; we just stop the abort.
(b) examples-robot-desktop.ts (robot-desktop-eyes example):
Replace the nested /* xTaskCreatePinnedToCore( ... /* Task function. */
... */ block with `#if 0 / #endif` so the inner block comments
don't terminate the outer one. Even with -Wno-error=comment the
real-syntax-level issue (the first inner `*/` closes the outer
comment, leaving the rest of the lines as bare code) would still
bite, so this needs an actual code fix.
Drops `/.well-known/assetlinks.json` so the Trusted Web Activity APK
(dev.velxio.twa, generated by bubblewrap from this same manifest.webmanifest)
can prove to Chrome that it's allowed to claim velxio.dev as its own
origin. Without this file the TWA falls back to a Custom Tab with the
URL bar visible — losing the whole "feels native" UX that TWAs
exist for.
The sha256_cert_fingerprints entry pins the production signing key
held locally as android.keystore in the velxio-twa/ build dir (NOT
in any repo). If we ever lose that key + need to re-issue, this
file has to be updated with the new fingerprint and re-deployed
BEFORE the new APK reaches users; otherwise their previously-
installed TWA verifies against an asset link that no longer matches
the APK signature and breaks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a sketch's external library headers were only referenced from
project headers (e.g. esp32-eyes.ino includes Common.h, Common.h
includes <ESP32Servo.h>), the compile failed with
fatal error: ESP32Servo.h: No such file or directory
because _detect_external_includes was only called on main_content
(the processed .ino). Project .h/.cpp files were never scanned, so
ESP32Servo / DHT / Adafruit_Sensor referenced only transitively
through user code never reached _resolve_library_components and
never landed in user_libs_all/.
Fix: collect ext_headers from main_content PLUS every uploaded
.h/.hpp/.ino/.c/.cpp file before resolving libraries. Lib resolver
already walks transitive includes inside the lib bundle once it's
copied; this just makes sure the first-level set covers user
project headers too.
Repro: open https://velxio.dev/example/robot-desktop-eyes, click
Compile. Before this commit: 13 errors starting at ESP32Servo.h.
After: ext_headers includes ESP32Servo.h on the first pass and the
build proceeds.
Sketch fails to compile out of the box with
fatal error: ESP32Servo.h: No such file or directory
because the ESP32Servo / U8g2 / DHT / Adafruit Unified Sensor libs
aren't part of arduino-esp32 and weren't declared on the example.
loadExample.ts already iterates `example.libraries` and runs
arduino-cli lib install for any missing entry before the user
touches Compile. Adding the four real deps the sketch needs gets
the example compiling cleanly on a fresh container without any
manual Library Manager dance.
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.
Three small fixes to frontend/public/manifest.webmanifest so the
Bubblewrap-generated TWA (and Add-to-Home-Screen PWA installs) feel
right on a phone:
- orientation: landscape → any. Landscape-forced on a phone
locks the device in side-grip whenever Velxio is foregrounded;
the editor + simulator work fine in portrait too (the file
explorer collapses gracefully). Tablets and desktops still
default to landscape because they're naturally wider, so this
only changes behaviour where the lock would actively hurt.
- name: "Arduino Emulator" → "Circuit & Arduino Simulator".
Matches the title tag + Open Graph copy that velxio.dev uses
everywhere else and reflects the SPICE / ESP32 / RP2040 work
the project has grown into since the original name was written.
- description: was 'Free local Arduino emulator … No cloud, no
latency.' That was true for the OSS self-host but misleading
for an installed PWA that talks to velxio.dev. Rewritten to
describe the actual product surface (the boards, the SPICE
sim, "free and open source") without making a claim the live
site can't keep.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Snapshot was written when the example had components: [] and wires:
[]. Now that 5bd541e populated the circuit (OLED + 2 buttons) and
2dea3f0 renamed the OLED pins to match wokwi-ssd1306's real
pinInfo, the netlist contains the button pull-down resistors and
floating-net autopulls. Regenerated with `vitest run -u`.
robot-desktop-eyes and the day-30/100-days OLED example wired the
SSD1306 OLED with SDA/SCL/VCC, but the wokwi-ssd1306 element
exposes pinInfo as DATA/CLK/VIN/GND. The mismatched names couldn't
resolve, so all three wire endpoints fell back to (0,0) of the
component and visually attached to the corner instead of the pins.
Same class of bug on the wokwi-big-sound-sensor in
robot-desktop-eyes: the element has AOUT/DOUT (no plain OUT). The
sketch uses digitalRead(SOUND_PIN), so route to DOUT.
The COMPONENT_PIN_ALIASES map in wokwiZip.ts only normalises on
.zip import — static examples have to use the real pinInfo names.
The 100d-esp32-oled-smart-ui-eyes-animation-time-and-weather-micropython
example had components: [] and wires: [] — the MicroPython code wired
an SSD1306 OLED on I2C (GPIO 21/22) plus two buttons (GPIO 14, 27) but
the circuit had nothing on the canvas, so users saw a bare ESP32 board
and the simulation was missing every peripheral the code drives.
Adds:
- wokwi-ssd1306 on I2C (3V3 / GND / SDA=21 / SCL=22)
- two wokwi-pushbuttons wired HIGH-when-pressed (3V3 → 1.l, 2.l → GPIO
14 / 27) to match the `if pin.value(): pressed` check in main.py
Pulls https://github.com/davidmonterocrespo24/robot_desktop into the
examples gallery as a real-world ESP32 + sensors project. Cozmo-style
desktop robot: SSD1306 OLED face that blinks, looks around, and shows
emotions; DHT11 weather mode triggered after 10 min idle; PIR wakeup
from sleep; LDR-driven sleep when the room goes dark; sound-triggered
reactions; and two eyebrow servos.
Ships as 34 separate files (one .ino + 33 headers / source) rather
than the usual single-sketch flatten. The face engine
(Eye / EyeTransition / EyeVariation / FaceBehavior / FaceExpression
/ FaceEmotions / BlinkAssistant / LookAssistant / …) splits
responsibility across enough classes that flattening would obscure
the design. Velxio's multi-file `files: [{ name, content }]`
mechanism handles this cleanly — the editor mounts the .ino as the
active sketch and the rest sit in the same workspace.
Pre-placed components match the original board's pin map verbatim
from Common.h:
- SSD1306 OLED on I²C (SDA=21, SCL=22 — ESP32 default)
- DHT11 on GPIO 15
- PIR motion on GPIO 4
- Big sound sensor on GPIO 2
- Photoresistor on GPIO 34 (ADC1)
- Right eyebrow servo on GPIO 12
- Left eyebrow servo on GPIO 13
Arduino libraries (U8g2lib, DHT, ESP32Servo, Adafruit_Sensor) are
auto-installed by velxio's Library Manager on the first compile.
Category 'displays', difficulty 'advanced', tags cover both the
sensor list and the project's identity (cozmo / robot / animation /
eyes) so the gallery search surfaces it from multiple angles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Two intertwined bugs were leaving every ESP32 ePaper example broken
end-to-end. Only the 5.65" UC8159c panel surfaced the failure
audibly ("Busy Timeout!" repeating in serial), because its inverted
busy polarity caused the firmware to hang inside `_waitBusy()`. The
SSD168x ePaper examples APPEARED to run cleanly but never actually
rendered anything to the panel — the canvas stayed at the idle paper
colour because the same registration path was broken.
Root cause #1 — `setSensors` was a full REPLACE, not a merge.
`Esp32Bridge.setSensors(sensors)` did `this._pendingSensors =
sensors`. At `startBoard()` time the store iterates components,
resolves wires for any entry in `SENSOR_COMPONENT_MAP` (DHT22 /
HC-SR04 / I²C sensors) and calls `setSensors(...)` with that list.
ePaper components live in `PartSimulationRegistry` (not in the
sensor map) and are registered via `sendSensorAttach()` AT
COMPONENT-MOUNT TIME — well before `startBoard()` runs. Full-replace
semantics blew that registration away on every Run click, so the
worker never instantiated an `Ssd168xEpaperSlave` / `Uc8159cEpaperSlave`,
no SPI bytes were decoded, no frames were latched, and BUSY was
never driven.
Fix: upsert by `pin` so pre-existing registrations from
PartSimulationRegistry handlers are preserved alongside the
startBoard-resolved sensors. Confirmed via a WebSocket spy that the
`start_esp32` payload now carries the ePaper sensor entry.
Root cause #2 — BUSY polarity was hard-coded for SSD168x only.
Verified against upstream GxEPD2 source:
* SSD168x family — constructor passes `_busy_level = HIGH`
→ BUSY=HIGH means busy, LOW means ready.
* UC8159c family — constructor passes `_busy_level = LOW`
→ BUSY=LOW means busy, HIGH means ready.
The worker only drove BUSY after a frame flush (and at the wrong
polarity for UC8159c), so the firmware's first `_waitBusy()` inside
`_PowerOn()` / `_InitDisplay()` — which fires BEFORE any frame —
blocked for the full 25 s `_busy_timeout`.
Fix: read `controller_family` from the registration payload, pick the
per-family idle level, and (a) seed the pin to IDLE at registration so
the first `_waitBusy()` sees "ready" immediately, (b) use that
polarity (idle vs. busy) when pulsing on frame flush.
Verified on https://velxio.dev/example/epaper-5in65-7c-esp32-rainbow:
the serial timeline now reads `_InitDisplay reset : 1566` /
`_PowerOn : 148` / `_PowerOff : 183` / `frame done` (all sub-2 ms
busy-waits, no timeouts). Sensor registration confirmed via the
`start_esp32` payload carrying the `epaper-ssd168x` entry.
Two new modules under the existing desktop/ subtree, both no-op
outside a Tauri runtime (tauriBridge.listen / .invoke fail gracefully).
desktop/menu.ts — listens for the `velxio://menu` event the Rust
shell emits from the native menubar (Velxio Desktop / File / Edit
/ View / Help, full menu defined in
pro/desktop/src-tauri/src/menu.rs). Internal actions handled
directly here: Save .vlx and Open .vlx via utils/vlxFile, Toggle
Serial Monitor via useSimulatorStore, Check for Updates via the
tauri-plugin-updater global. The rest (new-project, Find,
Toggle File Explorer) re-emit as window CustomEvent so the owners
of that UI state can subscribe without pulling this module in.
desktop/log.ts — `dlog(message, extra?)` round-trips a line to a
Rust `write_debug_log` command that appends to
`<app_data_dir>/desktop-debug.log`. Packaged Tauri apps have no
devtools or stdout capture, so this is the only way to see what
the webview did when a user reports a bug. Falls back to plain
console.log when the command isn't registered (older shell).
mountDesktop() now installs the menu listener and dlog's its own
start — useful as a "did the desktop module even load" smoke marker.
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.
Closes the same gap as the AVR / RP2040 commits — qemu-lcgamboa's UART
transmits the byte over the WebSocket as a 'serial_output' event with no
GPIO toggle, so an oscilloscope on the ESP32 TX pin saw nothing while
real silicon would render the 8N1 frame at the configured baud rate.
Two changes inside Esp32Bridge:
* New `onPinChangeWithTime: (pin, state, timeMs) => void` callback
that hooks the oscilloscope at parity with AVRSimulator /
RP2040Simulator. The 'gpio_change' event now also flows through it
(timestamped with `performance.now()` — QEMU virtual time isn't
surfaced across the wire, but at 1× sim speed the wall-clock skew
is invisible on any practical sweep). This also fixes the broader
issue that ESP32 boards previously couldn't show ANY digital GPIO
activity on the scope.
* `emitUartTxFrame(byte, uart)` synthesizes start + 8 data LSB-first
+ stop transitions at `this.uartBaudRate` (default 115200) on the
UART0 TX pin, mapped per board variant:
esp32 / esp32-devkit-c-v4 / esp32-cam / wemos-lolin32-lite: GPIO1
esp32-s3 / xiao-esp32-s3 / arduino-nano-esp32: GPIO43
esp32-c3 / xiao-esp32-c3 / aitewinrobot-esp32c3-supermini: GPIO21
Backend doesn't expose the live baud rate so we default to 115200
(the Arduino default). Override path: bridge.uartBaudRate = N
once we surface Serial.begin's argument via a backend event.
Wire-up: `bridge.onPinChangeWithTime = getOscilloscopeCallback(boardId)`
inside the three Esp32Bridge construction sites in useSimulatorStore
(setBoardType, addBoard, changeBoard).
Same gap as the AVR USART: rp2040js's UART fires `onByte(value)` per
transmitted byte but never toggles the corresponding GPIO, so an
oscilloscope on GP0 (UART0 TX, default for Arduino-Pico's Serial1) sees
nothing during `Serial.print`. Real silicon drives the pin with the
full UART frame at the configured baud rate, and Velxio should match.
`emitUartTxFrame(uartIdx, byte)` derives:
* `txPin` via FUNCSEL inspection: walk GP0 / GP12 / GP16 / GP28 (the
four candidates for UART0 TX per RP2040 datasheet) and pick the
first whose `functionSelect == 2` (FUNCTION_UART). Same for UART1.
Fall back to GP0 / GP4 when nothing is mapped (firmware hasn't
called `Serial1.begin()` properly).
* `baudRate` and `bitsPerChar` directly from the UART peripheral
(rp2040js already exposes these as live getters).
* Time from the RP2040 IClock's `nanos` counter, matching the
existing `setupGpioListeners` path — UART waveforms therefore stack
consistently with PIO / SIO traces on the same scope.
Both `uart[0].onByte` and `uart[1].onByte` get hooked. The seed-idle-
HIGH baseline is pushed once per UART per simulation run; `stop()`
clears the flag so a re-run gets a fresh seed (matching how the scope
buffer is cleared on restart).
avr8js intercepts the transmitted byte at the UDR0 register and never
toggles the corresponding GPIO. Real ATmega328P / ATmega2560 hardware
drives PD1 / PE1 with a start bit, 8 data bits LSB-first, and a stop bit
at the configured baud rate the moment TXEN is set. An oscilloscope
probe on D1 therefore showed nothing in Velxio while the same probe in
the real world would resolve the UART frame.
Synthesize the frame from the inside of `onByteTransmit`:
* Read `usart.baudRate`, `usart.bitsPerChar`, `usart.parityEnabled`,
`usart.parityOdd`, `usart.stopBits` so unusual configurations stay
accurate (avr8js already exposes these as public getters).
* Build the bit list start + data(LSB first) + parity? + stopBit(s).
* For each transition vs. previous state (initial = idle HIGH), call
`onPinChangeWithTime(1, state, timeMs)` where
`timeMs = (cpu.cycles + i * cyclesPerBit) / 16_000`. Same
simulator-time clock the existing port-listener path uses, so the
scope draws the UART waveform cycle-accurately alongside other GPIO
activity.
Also hook `onConfigurationChange` to detect TXEN flipping 0→1 and seed
the scope baseline at idle HIGH; without that, the very first byte's
start bit transition would be invisible because the scope's pre-first-
sample default is LOW.
Both USART construction sites (initial setupSimulation around line 423,
re-init after stop around line 749) get the same hook.
Covered by `__tests__/avr-uart-tx-waveform.test.ts` (5 cases): idle seed,
byte with internal transitions, 0xFF edge case, TXEN-disabled no-op,
bit-period timing.
The previous test fed `xValue: 0, yValue: 1023` to the analog-joystick
handler and asserted X=0V, Y=5V. That was wrong for two reasons:
1. wokwi-analog-joystick emits direction (-1/0/+1), never 0..1023, so
the fixture didn't match how the real component behaves.
2. The OLD `(value/1023) * vcc` mapping happened to produce 0V for 0
and 5V for 1023, so the broken test still passed against the
broken handler — and now breaks against the correct one.
Switch the fixture to `xValue: -1, yValue: 1` so the expectations line up
with the corrected handler (-1 → 0V, +1 → Vcc) and exercise the same
voltage rails the real component will produce.
The PartSimulationRegistry handler for 'analog-joystick' was reading
`el.xValue` / `el.yValue` and computing `(value / 1023) * vcc` as if the
component were a potentiometer producing a raw 0..1023 reading. It is
not — `@wokwi/elements/analog-joystick-element` emits xValue / yValue as
a tri-state DIRECTION signal:
* xValue = -1 → "left" (mousedown on left zone)
* xValue = 0 → centered (mouseup snap-back)
* xValue = +1 → "right" (mousedown on right zone)
(same for yValue with up/down)
`(±1) / 1023 ≈ ±0.001`, so the ADC channel sat at ~0 V no matter which
directional zone was clicked. Center-button clicks worked because that
path is digital (`setPinState(SEL, …)`) and bypasses the analog map.
Fix:
* Tri-state → voltage with explicit map: -1 → 0V, 0 → Vcc/2, +1 → Vcc.
* Vcc was hardcoded to 5V for "not RP2040" — wrong for ESP32 / S3 /
Nano-ESP32 / etc., which all run at 3.3V like the Pi Pico. Detect
ESP32 via the BridgeShim's `setAdcVoltage` method and select 3.3V
for everything that isn't pure AVR.
Reported on /example/esp32-joystick where center-button-only worked but
directional zones did nothing. Verification via Chrome MCP after deploy.
The user_libs_all bundler in _resolve_library_components does BFS over the
sketch's external includes, copying each matching Arduino library into
one merged IDF component. Anything not in _BUILTIN_HEADERS is treated as
an external library to resolve, and the lookup just scans
/root/Arduino/libraries/ for a directory whose `src/` (or root) holds a
matching header file.
_BUILTIN_HEADERS only listed C headers (stdint.h, stdio.h, …). The C++
wrappers (cstdint, cstdio, cmath, …) and the STL containers (vector,
complex, string, …) were absent. Result: any library transitively
#including <cstdint> or <vector> caused the bundler to "resolve" the
header against /root/Arduino/libraries/ArduinoSTL/ — an AVR-only
uClibc++ port that ships every C++ stdlib header as plain files. Once
ArduinoSTL was matched the bundler dragged in ALL of it, including
complex.cpp:
template class _UCXXEXPORT complex<float>;
which fails on the ESP-IDF Xtensa toolchain because _UCXXEXPORT isn't
defined in that compile context AND the symbol already exists in the
real libstdc++ pulled in by <complex>. Net effect: every ESP32 sketch
whose deps transitively include a C++ stdlib header (e.g. ESP32Servo
includes <cstdint>) blew up with 66+ errors before the servo example
even reached the link step.
Fix: extend _BUILTIN_HEADERS to cover the full set of C++ stdlib
wrappers and STL headers so the bundler never treats them as installable
libraries. The Xtensa GCC + libstdc++ shipped by ESP-IDF provides them
natively; ArduinoSTL never has any business being part of an ESP32 build.
Verified end-to-end on /example/esp32-servo: compile now succeeds, sketch
boots, moving the potentiometer drives the wokwi-servo angle (Pot=2801 →
Angle=123 deg, servo arm rotates).
arduino-esp32 3.x ledcWrite takes a PIN and looks up the attached channel
internally. arduino-esp32 2.x (the toolchain version we pin) takes a
CHANNEL. The velxio_compat.h shim already aliased the 3.x-only
ledcAttach onto ledcSetup+ledcAttachPin so 3.x sketches would compile,
but ledcWrite still mapped 1:1 — so a call like
#define R_PIN 16
ledcAttach(R_PIN, 5000, 8); // shim → channel 0 attached to pin 16
ledcWrite(R_PIN, 128); // ★ writes to "channel 16" (invalid)
silently wrote to LEDC channel 16, which doesn't exist (valid range
0-15). The hardware duty register never changed, qemu-lcgamboa never
emitted a `ledc_duty` event, and the RGB LED stayed dark even though
the firmware ran cleanly and the wires looked right. Verified end-to-end
with examples/esp32-pwm-led-rgb: gpio_change events fired at boot, no
ledc_duty events fired, ledRed/ledGreen/ledBlue all stayed at 0.
Fix: maintain a 40-entry pin→channel table populated by both ledcAttach
variants. Replace ledcWrite with a macro that calls a helper checking
the table first; if the value isn't a known pin we pass it through as a
channel, preserving 2.x channel-style call sites.
Macro/function name collision is sidestepped with the standard
parenthesizing trick — `(ledcWrite)(channel, duty)` doesn't expand the
function-like macro because the token isn't followed by `(`.
Verified live on velxio.dev/example/esp32-pwm-led-rgb after hot-copying
the new header into the velxio-app container: ledRed/ledGreen/ledBlue
now cycle through the full HSV wheel as expected (samples: (255,41,0),
(41,255,0), (0,41,255), (232,255,0), …).
Single-file sketch only — the table is `static` (internal linkage) and
ledcAttach + ledcWrite live in the header. Multi-file sketches that
attach in file A and write in file B would each see their own table.
Acceptable for now since arduino-esp32 sketches are nearly always
single-file; revisit when we bump the toolchain to 3.x and can drop the
shim entirely.
ATTinyCore >=1.5.0 declares ATTinyCore:micronucleus@2.5-azd1b as a tool
dependency, hosted at https://azduino.com/bin/micronucleus/. That host
has been unreachable (connection refused) for extended periods, causing
every ATtiny85 compile to fail at the core-install step with:
Download failed: performing HEAD request: ... dial tcp ...: connection refused
Failed to install required core: ATTinyCore:avr
micronucleus is only used for USB upload — never for compilation — but
arduino-cli refuses to install a core whose tool deps cannot fetch.
Pin to 1.4.1, the last release whose micronucleus binary is hosted on
github.com (digistump release, reachable). The FQBN clock options we
ship (clock=16pll on attinyx5, etc.) are unchanged across 1.4.x.
- backend/app/services/arduino_cli.py: new CORE_INSTALL_VERSIONS map
consulted by ensure_core_for_board so the runtime auto-install
passes "ATTinyCore:avr@1.4.1" instead of unversioned latest.
- backend/Dockerfile and docker/entrypoint.sh: same pin so a fresh
image bakes 1.4.1 in and never hits the runtime fallback path.
Existing regression tests in test/backend/unit/test_arduino_cli_attinycore.py
still pass (they assert presence, not version).
Two coordinated copy/layout changes on the landing hero:
- Primary CTA label gets "Online" added across the 9 supported
locales — "Try Simulator Free Online →" / "Probar el simulador
online gratis →" / etc. Reason: with the Velxio Desktop
download path now live, users should immediately understand
that the green button is the BROWSER version, and there's a
local install option for people who want it faster offline.
- The pro-overlay slot for the desktop download CTA was
`landing-hero-primary-cta` and sat ABOVE the hero CTAs. Renamed
to `landing-hero-download-cta` and moved BELOW so the funnel
reads "try online (primary) → or install locally (secondary)"
instead of "install locally (primary) → try online (secondary)".
OSS layout is unchanged — the slot is still empty in pure builds.
Also picks up the auto-regenerated sitemap.xml lastmod dates from a
recent deploy (every URL bumped 2026-05-19 → 2026-05-21).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the missing `.vscodeignore` so future `vsce package` runs don't
bundle the webview's `node_modules/` (which made the v0.1.0 release
17 MB instead of <1 MB — webview is a thin React WebView app that
ships only its compiled bundle).
With the ignore in place, 0.2.0 packages down to 100 KB (10 files:
extension.js + webview index.js + manifest + LICENSE + README +
changelog + icon + diagram schema). Matches the precedent of
committing the .vsix alongside the source for offline installation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Velxio Pro subscription gate to the VS Code extension. Every
compile / run validates against `https://velxio.dev/api/pro/license/validate`
before proceeding; a 60-second in-memory cache avoids hammering the
endpoint during a tight compile/run loop. No offline mode by design —
the extension throws OfflineError on network failure rather than
caching a permission grant locally. For offline workflows users get
the desktop app (separate distribution channel).
New surface:
- `LicenseService` (src/LicenseService.ts) — secret-store-backed key
storage, validate, nonce-backed deep-link OAuth handshake,
OfflineError + EntitlementError taxonomy.
- `Velxio: Sign In` — opens velxio.dev/auth/vscode, returns
via vscode://velxio.velxio-simulator/auth.
- `Velxio: Paste License Key` — manual fallback for headless boxes.
- `Velxio: Sign Out` — clears the keychain entry.
- `Velxio: Show License Status` — plan + trial countdown modal.
- Status bar item: Sign in / Trial Nd / Pro / Trial ended with the
appropriate warning/error background colour.
- Setting `velxio.licenseApiBase` for staging overrides.
CHANGELOG.md + README.md added.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New `frontend/src/desktop/` module loaded only when VITE_DESKTOP=true.
Hosts the Tauri-bound UI that pure OSS doesn't need:
- tauriBridge.ts typed invoke / listen / openExternal / beginSignIn
wrappers with no-op fallbacks for `vite dev` outside
the Tauri webview.
- DesktopWelcomePage.tsx sign-in flow with browser handoff +
paste-key fallback; listens for
`velxio://auth-completed` from the shell.
- GraceBanner.tsx renders soft/hard grace banners driven by
`license_status` + `velxio://license-status`
emits from the background checkin loop.
Toggles `body.vlx-desktop-readonly` so the
editor's Save / Compile buttons disable
themselves via CSS in hard-grace.
- Esp32QemuPrompt.tsx one-time download modal when the user picks
an ESP32 board on a fresh install.
- index.ts mounts welcome conditionally and the side
panels unconditionally.
- desktop.css shared styles.
All entry points are no-ops outside Tauri so the existence of the
folder has zero effect on the OSS build.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `lib/apiBase.ts` so the SPA can be repointed at a non-default backend
at runtime (via `window.__VELXIO_API_BASE__`) without losing the existing
`VITE_API_BASE` build-time override or the default `/api` reverse-proxy
behaviour. compilation / libraryService / projectService / metricsService
all flow through it now; axios clients use a request interceptor so the
base resolves per-request rather than at module-load time.
main.tsx grows a `VITE_DESKTOP` flag: when set, the @pro overlay is
skipped (the desktop shell handles license + auth natively) and a
small `./desktop/index` module is dynamic-imported in its place. OSS
builds tree-shake both branches.
LandingPage gets a `data-velxio-slot="landing-hero-primary-cta"` marker
above the existing hero CTAs so velxio.dev can inject an OS-detect
"Download Velxio Desktop" button as the visual primary. The slot is
empty in pure OSS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
ExampleThumbnail.tsx only serves /examples-thumbs/<id>.webp; the .png
copies were sharp's intermediate format committed by mistake on
2026-05-19 (commit 8693f93) when scripts/refresh-example-thumbs.sh
copied both formats to public. Removing ~18 MB of unreferenced PNGs
keeps the public folder lean. WebP is supported by ~97% of browsers
in use; the old Safari fallback path goes through CircuitPreview
(the SVG mock), not the .png.
Generated by velxio-prod's scripts/capture-example-thumbs.mjs after
fixing three bugs (CTA-based navigation against renamed route,
heterogeneous data-file parsing, false-positive prefix match). The
gallery now has a real canvas screenshot for every example
exampleProjects exports — 263 / 263. Previously 44 examples (mostly
digital-* and i8080-* / z80-*) had no preview and rendered the
SVG mock fallback.