Board DATA like spiPanelId: the bridge that supports the declared type
instantiates the model at the given address and setBatteryVoltage drives it.
First consumer is the M5Stack Core's IP5306.
Vendor autodetects read the panel's RDDID with the display's own CS and
reject or misdetect the board on the bus's idle 0xFF. The answer is board
DATA (which CS, which id byte), not bridge code, so it moves to the def:
the M5Stack Core is {14, 0xE3}, the Cardputer ADV {37, 0x85}. Bridges that
support the probe consume it generically.
The proWebFlash seam gains an optional flashMicroPython(files) method
plus an 'uploading' progress phase. MicroPython boards enable the
Flash menu when the overlay implements it — no compile step needed,
the modal collects the workspace group's files (same path as the Run
flow) and explains the two-step install+upload. toFirmwareVariant is
exported so the flasher shares the emulator's per-chip offset table.
appendHardwareSerial pushes real-UART bytes through the existing
per-frame serial batcher (no SerialMonitor changes), and
lib/proHardwareSerial lets the overlay intercept a board's serial
input while a physical monitor is attached — TX and RX switch to the
hardware together, detaching hands the tab back to the simulator.
Also adds @types/w3c-web-serial (esptool-js references it but only
dev-depends on it).
The board context menu's Flash item and FlashModal gain a second
backend: a Web Serial flasher the pro overlay can install via
lib/proWebFlash.ts (installWebFlashImpl). Without an overlay the seam
is inert — web builds keep the desktop-only behavior. esptool-js is
declared here because the overlay has no package.json of its own; OSS
code never imports it (verified absent from the vite bundle).
The announcement fetch now fires 250ms after editor mount — early
enough to race the dynamically imported pro overlay chunk that
registers the real news source. Losing the race silently fell back to
the OSS self-host proxy, which on the cloud deployment returns [] (its
hairpin fetch of its own public feed does not survive the round trip),
so cold-cache visitors — every incognito window — got no announcement
at all. getNextNews() in a pro build now waits, bounded at 8s, for
registerNewsSource() before picking a source; OSS builds resolve the
gate immediately and behave exactly as before.
The corner toast went unread. The announcement is a modal again — what
makes it viable this time is sequencing, not stacking: a tiny gate
(lib/newsGate.ts) lets EditorPage hold the pristine-visit starter
dialog until the news flow is done (nothing to show, or modal closed),
so only one startup overlay is ever on screen and news always comes
first. The pending decision is bounded at 2.5s so a dead feed cannot
hold the starter dialog hostage; reading time is unbounded.
The modal is deliberately a touch wider than the starter dialog
(880px vs 840px) and the news fetch delay drops 4s -> 250ms since the
starter now waits on it. Toast markup and styles removed; Help >
What's new keeps reopening the last delivered post.
New lib/newsEvents.ts registry (same pattern as newsSource.ts): OSS
default is a no-op sink; an extension overlay can register one to
forward the signals to its own analytics. NewsAnnouncer reports the
toast impression and modal opens (toast vs Help menu); NewsMarkdown
grows an optional onInteract callback fired on body link clicks and
YouTube preview plays — the admin authoring preview passes nothing, so
authoring never counts as engagement.
Entering the editor could stack two overlays: the starter-template
dialog opened on mount, then four seconds later the "What's new" modal
landed on top of it. Two dimmed backdrops over each other, and the
smaller, *less* important surface in front — the z-indexes said it
outright, news 10000 against the starter's 9000.
The delay made it worse rather than gentler. Arriving after the user has
started reading the templates is the most disruptive moment available;
appearing instantly would at least have let them clear it before
engaging.
The rule this restores: a modal is for what BLOCKS, and an announcement
never blocks. News now slides into the corner as a toast that dims
nothing, steals no focus (role="status", polite), and retires by itself.
The full post is one click behind "Read more" — a modal the user asked
for is a categorically different thing from one thrown at them — and
stays reachable from Help ▸ What's new once the toast is gone.
Worth noting the audience argument too: the starter dialog only fires on
a PRISTINE visit, i.e. for someone brand new, and "what's new" is
meaningless without a baseline to compare against. Returning users, the
ones it is actually written for, arrive with a project loaded and never
saw the collision.
Renamed NewsModal → NewsAnnouncer: it is not a modal by default any more,
and leaving the old name would have mislead the next reader.
Strings are new keys under `news.*`, filled in for all nine locales — the
previous modal had "What's new" and "Got it" hardcoded in English.
Toast sits at z-index 8000, deliberately BELOW the starter dialog, so if
the two ever coincide again the blocking task keeps the foreground.
The editor menubar's Account menu hosted only the shared `user-menu`
slot, so it had no way in or out of a session: the bottom-left dropdown
renders My projects / Sign in from its own markup, which this menubar
never sees. Signed-in users had no route to their saved work from the
editor, and anonymous ones had no way to sign in without leaving it.
Declares two account-scoped command ids and adds an `optional` item flag
that HIDES a row whose command is unregistered instead of the default
"render disabled". Absence here is not "temporarily unavailable" but
"does not apply": OSS has no accounts, and in pro exactly one of the two
is meaningful at a time — a greyed-out My projects would read as broken.
My projects sits third in File, at the head of the project group rather
than tacked onto the two "new …" rows: it opens saved work, which is
what Open/Save beneath it are for.
Labels reuse header.auth.{myProjects,signIn}, already translated in all
nine locales — no new strings.
The emulated BMI270 and battery gauge were faithful and inert: with nothing
driving them the IMU reported the board lying flat for the whole run and the
gauge reported the single voltage it was seeded with, so a sketch that reacts
to tilt or warns on low charge could not be exercised at all.
A Sensors popover on the canvas header supplies the missing half. Tilt is
entered as roll/pitch on a drag pad rather than three raw axes — what a user
means is "lean it left" — and gravity in the board's frame is that rotation
applied to (0, 0, 1) g, which is also why the vector stays at 1 g whatever the
attitude. Turning the board spins the gyro for as long as the drag lasts and
parks it back at zero shortly after, matching a real part: a held tilt reads no
rotation. Battery is a slider over the LiPo working range.
Gated per board by builtInImu / builtInBattery on the pro board def, the same
way the Mic toggle is gated, and each section renders only if its flag is set.
The microphone stream also learns to resample. The engine now tells the source
which rate the guest programmed, so a sketch recording at 8 kHz consumes two
captured samples per read instead of playing back at half speed.
Several boards share one FQBN (the Pimoroni RP2350 trio all compile as
rpipico2), so FQBN-keyed metrics can't tell them apart. The compile
request and the run event now carry board_kind — analytics only, rides
into the metric hook's extra.
suppressOnlineOnlyAds(): overlay seam so the hosted build can silence
'available in the online editor' ad cards for items it manages — an
embargoed item must vanish, not advertise the site the user is on.
Pure OSS builds never call it; ads behave exactly as before.
Boards with a camera surface their webcam state in the header
(CameraToggle); components that ARE cameras — vision sensors whose
element captures the webcam itself — had no header presence, so users
who learned "the header icon says my webcam is in use" read its absence
as a broken feature. A tiny registry seam lets any component register
its camera (label, live/error/off, reason, toggle); the header renders
one CameraToggle-styled button per entry and nothing when none.
Editor shows scheduled product-news posts as a markdown modal: max one
per day, oldest first, each post only ever once (marked seen at
delivery). OSS default source proxies velxio.dev's public feed through
the local backend (cached ~6h, anonymous, VELXIO_NEWS=off to disable)
with seen-state in localStorage; a pro overlay can register a
server-backed per-user source via lib/newsSource.ts.
New canvas-header Mic toggle, shown for boards whose ProBoardDef declares
builtInMicrophone (their bridge implements setMicrophoneSource). Capture
runs at the AudioContext's native rate, is decimated to 16 kHz and ring-
buffered (2 s, drop-oldest on overflow, silence on underflow - the engine
paces pulls in simulated cycles, up to 4x real time during boot boost).
No auto-start: with the toggle off the bridge's 440 Hz test tone keeps
mic sketches alive, so taking the user's microphone stays an explicit
click. The source is re-applied on every run start to survive bridge
rebuilds.
The menubar now matches the desktop app's native menu set: File, Edit,
View, Language, Help.
View collects what the user asked to reach from a menu: Compile (Ctrl+B),
Run, Stop, Reset up top; the panel toggles — File Explorer, Output
Console, Serial Monitor, Oscilloscope/Logic Analyzer — in the middle
(store-backed ones render a live check, like a real desktop menu); and
the canvas view actions (center, zoom) move here from Edit, which goes
back to being undo/redo only, as menus have always worked.
Language lists the nine locales with the current one checked, switching
through the same switchLocale path the header globe uses — so language is
reachable from the menubar regardless of the account state, on top of
living in the signed-in account menu.
Run/Stop/Compile/Reset and the console toggle register through the
editorCommands seam from EditorToolbar (their handlers close over its
state); the explorer toggle from EditorPage; serial and scope call their
stores directly, same as undo/redo.
The toolbar's overflow button held four leftovers (Share/Embed, Sync to
GitHub, Upload firmware, Record simulation). They move into the File
menu, which is where a session-frequency action belongs, and the strip
loses one more button. The PRO pill travels with them — same style the
overflow used (now .emb-pro) — and the two File items that were always
premium (BOM, schematic image) finally show it too: users should know an
item is premium BEFORE clicking, not via a surprise upgrade prompt.
The pro actions keep firing the same window events the overflow items
fired (share/github-sync/record prompts), so the overlay's listeners are
untouched and OSS builds keep their silent no-op. Dead overflow CSS
removed with the button.
The open-source build now ships exactly its product surface: /editor, the
examples gallery (/examples, /examples/:id) and the per-example editor
(/example/:id). Landing, about, pricing, docs, the 14 keyword-targeted
simulator landings and the v2/v2.5/v3 showcases move to the private
overlay, registered through the same registerProRoutes seam that already
carries login/admin/classroom.
Root behaves per build: an overlay that registers an index route claims
'/' (velxio.dev keeps its landing); otherwise '/' redirects to /editor.
The redirect waits for the overlay import to settle — same contract as
markProExamplesSettled — so a velxio.dev visitor is never bounced into
the editor because the landing was 300ms away from registering.
Prerender and sitemap follow the split: entry-server pulls the marketing
page map from '@pro/pages/marketing' behind a VITE_PRO_BUILD-gated dynamic
import (the proven main.tsx pattern, with an OSS stub for tsc), and
generate-sitemap lists only served routes in OSS builds (2 URLs) while
pro builds stay byte-identical — verified against a pre-migration
baseline: sitemap (37 URLs) and prerendered /, /about, /docs,
/arduino-simulator, /esp32-simulator, /v3 all identical modulo hashed
asset names. OSS prerender drops exactly the 32 marketing pages (348→316).
The OSS header slims down to match: Editor, Examples, GitHub, Discord —
the marketing links only render in pro builds, where their routes exist.
The editor Help menu links those pages absolutely (velxio.dev) in OSS,
exactly like the desktop app's Help menu.
La queja: demasiados botones en una sola fila, y en pantallas pequenas se
solapan — medido, no impresion: a 1024px Code/Both/Circuit pisaban a
Compile/Run/Stop por 9-20px y Add pisaba el chat por 29px, con el idioma y
el usuario cortados fuera del viewport.
Tres piezas:
1. En el editor, el nav de marketing (Home/Docs/Pricing/...) sobra: ya
estas dentro, y cuesta exactamente el ancho que le falta a la toolbar.
AppHeader gana la variante editorMenu — mismo mecanismo que ya usa la
build de escritorio (VITE_DESKTOP) — que oculta el nav y pinta un menu
File/Edit junto al logo. El logo sigue llevando a inicio; idioma,
autoguardado, Share y usuario se quedan.
2. File/Edit al estilo escritorio: lo que se usa cada minuto sigue siendo
boton; lo que se usa unas veces por sesion va al menu. File: nuevo
workspace, nuevo fichero, abrir, guardar, importar, exportar, BOM,
imagen del esquema. Edit: undo/redo (con su estado real del historial),
centrar vista, zoom. Las acciones viven en cierres de cuatro
componentes distintos, asi que hay un registro id->handler
(lib/editorCommands, mismo patron de seam que registerProExamples o
registerBoardBuiltins): cada dueno registra al montar y el menu invoca
por id; un item sin dueno montado se pinta deshabilitado, que ademas es
la verdad. El registro sobrevive al remontaje StrictMode (el cleanup
viejo no borra al sucesor) y tiene test de eso.
3. La toolbar adelgaza: fuera Undo/Redo (Ctrl+Z/Y siguen), fuera los
botones inline de Import/Export y sus gemelos responsivos del More
(~70px recuperados); el More queda solo con los extras pro (firmware,
GitHub, share, grabacion). El CSS responsivo de los botones retirados
se va con ellos.
Suite en verde (2300) y build del overlay verificado desde este arbol.
Seam nuevo registerBoardBuiltins/getBoardBuiltins: una placa que el arbol
OSS ya dibuja (la familia Pi) puede recibir perifericos del overlay sin
registrar un ProBoardDef, que es lo que secuestraba el arte de la placa y
dejaba los cables en la esquina. El overlay lo usa para llevar los frames
del guest (cv2.imshow) al panel cableado en el canvas.
Arregla ademas el guard de connect() del bridge: comparaba readyState con
WebSocket.OPEN leido del global, asi que cuando esas constantes no estaban
`undefined === undefined` era cierto con socket a null y connect() volvia
sin abrir nada — el bridge se quedaba muerto. Ahora comprueba que el socket
exista y usa el valor numerico. Recupera los 12 tests de
multi-board-integration que esto habia roto.
El test del UART Pi->Uno pasa a comprobar el contrato vigente: por el cable
va onUartTx (el UART del header), no la consola del guest; y se anade el
caso que fija que la charla de arranque NO se filtra al vecino.
Dos fallos que salieron probando pi-to-arduino-led-control y
pi5-pir-motion-alarm.
1) BoardOnCanvas mira getProBoard() ANTES del switch OSS para decidir si
dibuja un elemento del overlay. El overlay registraba un def minimo
para las seis Pi solo para llevar una linea de setup del guest, y eso
basto para cambiarles el render: en vez de la ilustracion
Raspberry_Pi_3_illustration.svg salia la caja esquematica, con otras
coordenadas de pines, y los cables quedaban colgando en la esquina.
Ahora hay un registro aparte, registerGuestSetup(kind, linea), que
lleva la cadena y nada mas; getGuestSetup() la resuelve dando
prioridad al def del overlay si existe.
2) Las partes de entrada (PIR, botones, sensores) avisan con
simulator.setPinState(pin, nivel). En una placa QEMU-Linux no hay
simulador de MCU -- el CPU es el guest -- asi que la llamada acababa
en la instancia AVR heredada y se perdia: pulsar el sensor no hacia
nada. traceDetailed devuelve ahora tambien la placa a la que llega el
pin, y si es de la familia Pi la parte recibe un simulador que empuja
el nivel al bridge (gpio_in para el guest, el valor pin<N> que leen
los shims del motor de navegador) y al PinManager de esa placa.
Generic seam only — no board, OS or runtime specifics in the OSS tree:
an overlay may register an engine that decides, per board, whether a run
needs the Linux guest or can happen in the browser. The decision is data
(engine + reason + where) so the UI can explain a 90 s boot instead of
just taking it, and BoardInstance carries engineMode / enginePinned so
the user's choice is predictable and the terminal panel knows whether an
interactive shell exists. Nothing registers in OSS: the QEMU path is
untouched.
The generic arm64 image prints another product's banner/motd/login line
during boot, before guestSetup can re-brand the guest. Boards with
quietBoot show a neutral '[Velxio] Booting <label> (Linux guest)...'
progress line (dots every 4 s) while boot detection and the prompt-gated
upload still run underneath; the shell is revealed (already re-branded)
right before the auto-run command, so the user's first visible output is
their own program.
- ProBoardDef.autoRun: after boot (+guestSetup) the VFS uploads itself
and the command runs, so a single Run click boots, uploads and starts
the user's script (same UX as compiled boards)
- ProBoardDef.guestHome: VFS home dir override ('/root' for guests that
log in as root); those boards drop the historic hello.sh sample
- upload sequence extracted to utils/piUpload (shared by the VFS panel
button and autoRun)
- serial monitor strips DEL/C0 control echoes (backspace showed tofu)
Overlay QEMU-Linux boards are not Raspberry Pis: ProBoardDef.guestSetup
lets a board send one shell line at the boot prompt (hostname/PS1/clear)
to de-brand the generic image, sent before piBooted flips so uploads
cannot interleave; the workspace start button and power-on title carry
the board's own label for non raspberry-pi kinds; the compile console
line uses the board label instead of 'Raspberry Pi 3B'.
registerPiFamilyKind + ProBoardDef.piFamily route an overlay board through
the existing Raspberry Pi bridge path (WebSocket qemu, VFS panel, boot
terminal); register_pi_board_profile lets the overlay add the matching
PI_CONFIGS entry (cpu/image_set/...) at registration time.
Round Display, Grove Gesture and ReSpeaker Lite now advertise themselves in
the OSS picker like the M5Stack matrices; they auto-hide in any build whose
overlay merges the real components (registry.getById).
xiao-esp32s3-sense / xiao-esp32c6 / xiao-rp2040 advertise themselves in the
OSS picker like the other hosted-only boards; the ads auto-hide in any build
whose overlay registers the real kinds (same BOARD_KIND_LABELS contract).
The @pro overlay import is dynamic, so board/component registration can land
AFTER the picker mounted and memoized its lists. allBoards had frozen deps —
if the picker rendered first, overlay boards (M5Stack Core, Cardputer,
Pimoroni, C6) vanished for the whole session while their ONLINE ads were
already hidden; the component grid + ONLINE component ads flipped between ad
cards and real entries depending on who won the reload race (different SVG,
ONLINE badge appearing and disappearing, wrong hover thumbnail).
proBoardRegistry and ComponentRegistry.mergeComponents now bump a version and
notify subscribers; the picker's allBoards / filteredComponents /
visibleComponentAds memos key off those via useSyncExternalStore — same
contract as proRoutes and registerProExamples.
Boards with built-in hardware (LCD on the element's own canvas, speaker,
on-board buttons/keyboard) need run-time wiring between the DOM element and
the board's simulator shim / ESP32 bridge. One generic SimulatorCanvas effect
now hands those handles to the overlay's attachBuiltins shortly after run
start and runs its cleanup on stop — no board names in the OSS tree.
registerProBoards() lets a hosted overlay ship boards outside the OSS tree as
data-only definitions: registration patches the exported BoardKind maps
(labels / FQBN / MicroPython) so every existing read site keeps working, and
the sites a map can't cover consult the registry — canvas render (custom
element or overlay render fn), BOARD_SIZE, pin-name mapping, picker list +
descriptions + tag, ESP32 family routing, in-browser simulator construction
and firmware load (structural ProBoardSimulator contract, duck-typed PIO
attach/detach), built-in bridge sensors, and a CS-gated built-in microSD
(sdCsPin -> sd_card.cs_pin worker config). Esp32Bridge additionally gains the
esp32-c6 machine type + TX pin (public chip knowledge — the C6 compile path
already ships) and a generic sendKey() for built-in matrix keyboards.
registerProExamples() appends gallery examples at runtime; the board ONLINE
ads recompute at render so registration hides them. OSS behavior without an
overlay is unchanged — the registry is dead code, same as the other seams.
Components that only exist in the hosted editor now advertise themselves in
the picker exactly like the online-only boards do: ONLINE_ONLY_COMPONENT_ADS
(same module as the board ads) renders an ONLINE-badged card that links to
velxio.com, auto-hidden in any build whose ComponentRegistry has the real
component (the hosted overlay merges it in — no per-build switches). First
entries: the M5Stack Chain RGB/Mono 8x8 matrices. componentDocs gains
registerComponentDoc(id, raw) so an overlay can supply the hover datasheet
for components it injects at runtime.
Generic platform work ported from the internal line:
- Esp32BridgeFactory seam + rebuildEsp32Bridge + sync-I2C seam: a
substitute simulation bridge (e.g. the hosted editor's in-browser JS
emulators) can be installed without touching OSS code
- Component datasheets: hover popover (ComponentInfoPanel) + markdown
docs for common parts
- Per-chip S3/C3 basics examples for the gallery
- .gitignore: never allow pro emulator mask ROMs into the OSS repo
New: online-only board showcase. Boards implemented by the hosted editor
(ESP32-C6, M5Stack Core, Cardputer ADV, Pimoroni RP2350 family) appear
in the picker as advertisement cards with an ONLINE badge linking to
velxio.com, where they are free to use. Ads auto-hide in any build that
registers the real BoardKind.
The proWifiGate seam blocked a free user from running ANY Pico W WiFi sketch.
The product model changed: LOCAL WiFi (the chip associating to the simulator's
virtual AP) is now FREE — only REAL internet (the backend bridge) and the IoT
gateway are paid, gated inside the pro overlay's Cyw43PioPeripheral / backend.
So a free user must be allowed to run WiFi sketches. Remove the gate seam and
its two store call sites. The board-kind firmware variant + the run-time
peripheral re-attach (which fixed the plain-firmware import-network crash) stay.
Pico W WiFi is a paid overlay feature. A free/web user running a Pico W sketch
that uses WiFi had no peripheral attached -> the simulator picked the plain Pico
firmware (no `network` module) -> the run crashed on `import network` with a
raw Python traceback (on a public example page, no less).
Add lib/proWifiGate.ts (mirrors proBoardGate): a stable doorbell the overlay
fills in. useSimulatorStore gates both loadMicroPythonProgram (before loading
firmware) and startBoard (run backstop for example/loaded boards): if the gate
blocks, fire the upgrade prompt and skip the run. Non-WiFi Pico W sketches still
run for free. No-op in OSS (default 'allow' -> a Pico W runs as a plain Pico).
Opening the gateway in a new tab backgrounds the emulation tab and pauses
its rAF, freezing the chip — the page then 502s. The in-tab iframe is the
only way the Pico W device page stays reachable, so the panel no longer
offers an open-in-tab affordance (Reload + Close only).
The first iframe panel was a full-screen modal with a backdrop: it
covered the canvas and blocked the editor, so you couldn't watch the
board react or keep clicking buttons/wiring while it was open, and it
couldn't be moved.
Now it's a small floating panel docked top-right, draggable by its title
bar, resizable, with NO backdrop — the canvas and editor stay fully
interactive underneath.
Also: the relay and async-led device pages now show a big colored ON/OFF
indicator (these are web-server demos; the GP2/onboard LED isn't drawn on
the canvas, so the panel is where you see the state flip).
The Pico W emulation runs in THIS browser tab via requestAnimationFrame.
Opening the gateway with target=_blank / window.open backgrounds the
emulation tab; the browser then pauses its rAF, the simulated chip
freezes, and the gateway can no longer reach the server on it — the
request times out (502) and toggles do nothing.
Render the served page in a same-tab iframe panel (openDeviceGateway) for
the Pico W so the emulation stays in the foreground and keeps answering.
The ESP32 is unchanged (its server runs in QEMU on the backend, immune to
tab visibility), so it keeps opening in a new tab.
Also: the async-led page now shows the LED state (the board's onboard LED
isn't drawn on the canvas) so the toggle has visible feedback.
Add a working microSD card part backed by a FAT16 image, following the
Wokwi storage model: the project's own workspace files are auto-copied
onto the card (free), and an optional "SD Card" panel uploads extra
files (gated as a paid feature by the velxio.dev overlay; OSS default
allows it).
Frontend (in-browser AVR / RP2040):
- ProtocolParts.ts: rewrite the microsd-card part from a handshake stub
into a real SD-over-SPI device (reply-first Ncr timing, SDSC byte
addressing, single/multi-block read+write, CSD/CID, full CMD set).
- utils/fatImage.ts: dependency-free FAT16 super-floppy builder (8.3 + LFN).
- utils/sdCardFiles.ts: assemble the card image from workspace files plus
uploaded files; base64 helpers.
- components/simulator/SdCardPanel.tsx + ComponentPropertyDialog: upload UI.
- DynamicComponent + useSimulatorStore: build and inject the image on run.
- lib/proSdCardGate.ts: overlay-installable gate for the upload action.
- data/examples-storage-microsd.ts: Arduino Uno + ESP32 gallery examples.
Backend (ESP32 via QEMU):
- services/esp32_sd_slave.py: synchronous SD-over-SPI slave (Python port of
the browser part) with a sparse backing store, idle-state R1 tracking and
real CRC16 on data blocks when the host enables CRC (CMD59) -- both
required by ESP-IDF's sdspi driver.
- esp32_worker.py: route SPI bytes to the slave (returns MISO synchronously)
and feed write-only bulk transfers.
- esp32_lib_manager.py + routes/simulation.py: forward the FAT image
(sd_card.image_b64) from the start config into the worker.
Tested:
- frontend: protocol-parts, fat-image, sd-card-gate and microsd-real-firmware
(real Arduino SD.h on avr8js) -- 86 passing.
- backend: test_esp32_sd_slave (10) covering the ESP-IDF init sequence and
CRC16; validated end to end by running a real SD.h sketch in libqemu-xtensa
(mount, directory listing, read and write-readback).
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.
Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>