velxio/docs/ARCHITECTURE.md

18 KiB
Raw Permalink Blame History

Project Architecture — Velxio

Overview

Velxio is a fully local, multi-board emulator and electronics simulator that runs in the browser and on the desktop. Five CPU backends, one analog (ngspice-WASM) engine, 150+ catalog components, a custom-chip SDK, and a Tauri desktop shell — all tied together by a Zustand state core and a thin FastAPI compile + bridge backend.

+---------------------------------------------------------------------+
|                          USER (Browser or Tauri)                    |
|       Web: http://localhost:3080 (Docker) or velxio.dev             |
|       Desktop: velxio-desktop (Tauri shell)                         |
+--------------------------------+------------------------------------+
                                 |
                                 v
+---------------------------------------------------------------------+
|                FRONTEND (React 19 + Vite 7 + TypeScript)            |
|                                                                     |
|  +-----------------+  +------------------+  +-------------------+   |
|  | Monaco Editor   |  |  Zustand Stores  |  |  SimulatorCanvas  |   |
|  | Multi-file C++  |  |  Editor + Sim    |  |  Components+Wires |   |
|  | / .py / .chip.c |  |  Electrical+VFS  |  |  Pin overlay      |   |
|  +--------+--------+  +--------+---------+  +---------+---------+   |
|           |                    |                      |             |
|           v                    v                      v             |
|  +---------------------------------------------------------------+  |
|  | 5 CPU backends, one PinManager facade                         |  |
|  |  - AVRSimulator (avr8js, browser)                             |  |
|  |  - RP2040Simulator (rp2040js, browser, WFI fast-forward)      |  |
|  |  - Esp32Bridge (WebSocket -> QEMU Xtensa/RISC-V worker)       |  |
|  |  - RaspberryPi3Bridge (WebSocket -> QEMU raspi3b)             |  |
|  |  - RiscVCore (TypeScript ISA, Vitest only)                    |  |
|  +-------------------------------+-------------------------------+  |
|                                  v                                  |
|  +---------------------------------------------------------------+  |
|  | PinManager + PartSimulationRegistry                           |  |
|  | Digital/PWM/Analog listeners, 16+ registered output parts,    |  |
|  | event-driven input parts (buttons, switches, encoders, ...)   |  |
|  +---------------------------------------------------------------+  |
|                                  v                                  |
|  +---------------------------------------------------------------+  |
|  | Electrical engine (lazy, ~39 MB ngspice WASM bundle)          |  |
|  | NetlistBuilder (Union-Find on wires) -> SPICE cards           |  |
|  | useElectricalStore + analog instruments (V/A/scope/funcgen)   |  |
|  +---------------------------------------------------------------+  |
|                                  v                                  |
|  +---------------------------------------------------------------+  |
|  | Component Layer                                                |  |
|  | wokwi-elements (Lit) + Velxio-native Web Components            |  |
|  | DynamicComponent renderer, ComponentRegistry (metadata.json)   |  |
|  | ComponentPickerModal, property dialog, pin selector            |  |
|  +---------------------------------------------------------------+  |
|                                                                     |
|  +---------------------------------------------------------------+  |
|  | Wire System                                                    |  |
|  | Orthogonal routing, segment edit, 8 signal colors,             |  |
|  | overlap offset, pin overlay, 20px grid snap                    |  |
|  +---------------------------------------------------------------+  |
|                                                                     |
|  Custom Chips runtime                                               |
|  WASI shim, SPI/I2C bus bridges, ChipRuntime per instance           |
+--------------------------------+------------------------------------+
                                 | HTTP (Axios) + WebSocket
                                 v
+---------------------------------------------------------------------+
|                  BACKEND (FastAPI + Python 3.12)                    |
|                     http://localhost:8001                           |
|                                                                     |
|  +-------------------------------------------------------------+    |
|  | /api/compile/      sync + async Arduino / RP2040 compile    |    |
|  | /api/compile/chip/ Custom-chip WASM compile                 |    |
|  | /api/libraries/    arduino-cli library search + install     |    |
|  | /ws/sim/{id}       QEMU bridge (Xtensa, RISC-V, ARM Pi)     |    |
|  | /api/iot/          HTTP proxy to simulated ESP32 servers    |    |
|  | /api/mcp/, /sse/   MCP server (stdio + SSE)                 |    |
|  +-------------------------------+----------------------------+    |
|                                  v                                 |
|  +-------------------------------------------------------------+    |
|  | Services                                                    |    |
|  | arduino_cli  espidf_compiler  qemu_manager  esp32_worker    |    |
|  | wasm_chip_runtime  gpio_shim                                |    |
|  +-------------------------------+----------------------------+    |
+--------------------------------+------------------------------------+
                                 v
       arduino-cli       ESP-IDF       libqemu-xtensa.so
       (subprocess)      (subprocess)  libqemu-riscv32.so
                                       qemu-system-aarch64 (Pi)

The backend is stateless by design — no database, no auth, no per-user files. Compilation results are returned over HTTP and discarded; QEMU instances live only as long as the WebSocket session.

Production overlay (velxio-prod): accounts, public profiles, persistent project URLs, the admin panel, analytics, and Pro features (custom-chip cloud build, premium components, AI assistant) live in a private repo and are layered onto the Docker image at deploy time. The OSS image you build from this repo is the same shell minus those routes. See README.md §License.


Data flow

1. Code editing

User writes code
   v
Monaco Editor (.ino / .cpp / .h / .py / .chip.c)
   v
useEditorStore.files[]  (per-board multi-file workspace)
   v
Active file -> editor; file group switches on board selection

useEditorStore holds:

interface WorkspaceFile { id: string; name: string; content: string; modified: boolean; }
files: WorkspaceFile[]
activeFileId: string
openFileIds: string[]
fileGroups: Record<string, string[]>   // boardInstanceId -> file IDs

Operations: createFile, deleteFile, renameFile, setFileContent, markFileSaved, openFile, closeFile, setActiveFile, loadFiles (for .vlx import).

2. Compilation

Click "Compile"
   v
EditorToolbar -> compileCode({ files, board_fqbn })
   v
Axios POST -> http://localhost:8001/api/compile/
   v
Backend dispatch (by FQBN prefix):
   arduino:avr:*    -> ArduinoCLIService.compile()
   rp2040:rp2040:*  -> same + earlephilhower core
   esp32:esp32:*    -> ESP-IDF compile (full project, ccache + persistent build dir)
   v
First .ino promoted to sketch.ino (RP2040 Serial redirect applied to sketch.ino only)
   v
Returns hex_content / bin_payload + compile log
   v
useSimulatorStore.setCompiledProgram(boardId, payload)  -> auto-loadHex/loadBin

3. Simulation

Browser backends (AVR, RP2040)

Click "Run"
   v
useSimulatorStore.startSimulation(boardId)
   v
Per-board simulator instance.start()
   v
requestAnimationFrame loop @ ~60 FPS
   v
Each frame: Math.floor(cyclesPerSecond / 60 * speed) cycles
   v
For each cycle: instruction() + cpu.tick()
   v
Port writes -> AVRIOPort / RP2040 IOReg listeners
   v
PinManager.updatePort(portName, newValue, oldValue)
   v
Per-pin callbacks fire -> PartSimulationRegistry.onPinStateChange()
   v
Web Component property updates (LED color, LCD frame buffer, NeoPixel grid, ...)

QEMU backends (ESP32, ESP32-C3, Pi family)

Click "Run"
   v
useSimulatorStore.startSimulation(boardId)
   v
WebSocket connect -> /ws/sim/{boardId}
   v
Backend spawns QEMU process (or attaches libqemu lib)
   v
QEMU loads firmware/OS image (qcow2 overlay for Pi)
   v
QEMU emits GPIO/UART/I2C/SPI events -> backend serializes -> WebSocket
   v
Frontend bridge dispatches into PinManager (same as browser backends)

4. Input components

User presses pushbutton on canvas
   v
Web component fires 'button-press' event
   v
DynamicComponent forwards to PartSimulationRegistry.attachEvents handler
   v
Simulator.setPinState(arduinoPin, LOW)
   v
For browser backends: AVRIOPort.setPin(bitIndex) injects external state
For QEMU backends:   pin-state command goes over WebSocket to QEMU

5. Wire creation

Click pin on component A   -> startWireCreation(endpoint)
   v
Mouse move                 -> updateWireInProgress(x, y)
   v
WireInProgressRenderer shows dashed green L-shape preview
   v
Click pin on component B   -> finishWireCreation(endpoint)
   v
Wire object stored in useSimulatorStore.wires[]
   v
WireLayer renders SVG orthogonal path (with overlap offset)
   v
Components subscribe to Arduino pin via wire lookup (DynamicComponent)

6. Electrical co-simulation (when toggled on)

Each frame (debounced):
   v
NetlistBuilder runs Union-Find on wires[] -> electrical nodes
   v
For each component, componentToSpice() emits SPICE cards
   v
MCU digital pins -> Thevenin sources (V = port driver, R_out = driver model)
   v
Lazy-loaded ngspice WASM (.op transient) computes node voltages
   v
Voltmeter / ammeter / oscilloscope read probes
ADCs read their node voltage on the next analogRead()

State stores

Store File Purpose
useEditorStore frontend/src/store/useEditorStore.ts Multi-file workspace, file groups per board
useSimulatorStore frontend/src/store/useSimulatorStore.ts Boards, components, wires, simulators, running state
useProjectStore frontend/src/store/useProjectStore.ts Current loaded project metadata (id, slug, name) for .vlx export
useElectricalStore frontend/src/store/useElectricalStore.ts SPICE engine state, node voltages, probe readings
useVfsStore frontend/src/store/useVfsStore.ts Virtual File System for Pi family (Python files mounted at boot)
useAuthStore (overlay) pro/frontend/src/pro/store/useAuthStore.ts Lives only in velxio-prod overlay — OSS has no auth

Component plugin system

PartSimulationRegistry decouples simulation logic from rendering. A part registers two optional hooks:

interface PartSimulation {
  onPinStateChange?(pinName: string, state: PinState, element: HTMLElement): void;
  attachEvents?(element: HTMLElement, simulator: Simulator, pinHelper: PinHelper): () => void;
}

Registered parts (16+):

Part Type Key behavior
led, wokwi-led Output Pin -> element.value
rgb-led Output Digital + PWM on R/G/B -> ledRed/Green/Blue
led-bar-graph Output 10 LEDs (A1A10) -> .values array
7segment Output 8 segments (AG + DP) -> .values array
pushbutton, pushbutton-6mm Input Press/release -> setPinState(pin, LOW/HIGH)
slide-switch Input Change event -> pin state
dip-switch-8 Input 8 independent switches
potentiometer, slide-potentiometer Input Value (01023) -> ADC voltage injection; also a SPICE resistor in analog mode
analog-joystick Input VRX/VRY (ADC) + SW (digital)
servo Output Polls OCR1A/ICR1 -> angle 0180°
buzzer Output Web Audio API, reads Timer2 registers
lcd1602, lcd2004 Output Full HD44780 4-bit protocol
neopixel-* Output RMT-decoded 24-bit GRB frame -> per-LED color
epaper-* Output Waveshare driver, full-buffer refresh
oscilloscope, voltmeter, ammeter, function-gen Velxio-native Read/write the ngspice node graph

Adding a new behavior: write the handler, register it in PartSimulationRegistry, ship. No edits to SimulatorCanvas needed.


Custom chips runtime

User-defined chips compile from C to WebAssembly via backend/app/api/routes/compile_chip.py and run inside frontend/src/simulation/customChips/ChipRuntime.ts. The runtime exposes:

  • Digital GPIO — pin watch, edge detection
  • Analog out (DAC) — sets a node voltage that ngspice picks up
  • I2C slave, SPI slave, UART — bus bridges that look like real peripherals to the MCU side
  • Timers, framebuffer, log — utility hooks
  • WASI shim — minimal subset for printf/clock

30+ example chips ship in frontend/src/components/customChips/examples/ (Intel 4004, 8080, 8086, Z80, 74HC595, 24LC256, MCP3008, PCF8574, DS3231, …). See CUSTOM_CHIPS.md and Custom Chips ESP32 Runtime.


Persistence

OSS Velxio has no server-side state. Projects are saved as .vlx files — a single JSON snapshot of:

  • All boards (kind, position, board options, file groups)
  • All components (type, position, properties, rotation)
  • All wires (endpoints, color, signal type)
  • Code for every file group
  • VFS contents (Pi Python files)
  • Electrical-sim toggle state, instrument configurations

frontend/src/utils/vlxFile.ts handles export and import. The format is versioned for forward compatibility.

The Save button is registered through frontend/src/lib/proSaveAction.ts — OSS default is "download .vlx"; the velxio-prod overlay replaces it with a "Save to your account" modal.


Routes

Route Page Purpose
/ LandingPage Marketing landing page
/editor EditorPage Main multi-board editor + canvas
/circuit CircuitSimulatorPage Pure analog mode (no MCU)
/electronics ElectronicsSimulatorPage Alt analog entry point
/arduino, /arduino-mega, /attiny85 Board-specific simulator pages Direct deep-link entry
/esp32, /esp32-c3, /esp32-s3 Board-specific ESP32 pages Same
/raspberry-pi-3 Pi 3 simulator page Same
/custom-chip CustomChipSimulatorPage Custom-chip editor + sandbox
/examples ExamplesPage Filterable gallery (380+ projects)
/examples/:id ExampleDetailPage Single example with run-now
/docs DocsPage In-app docs reader

The pro overlay injects /account, /login, /:username, /project/:slug, and /admin routes at runtime via frontend/src/lib/proRoutes.ts.


Apps and integrations

Surface Status Doc
Web (OSS + Pro) Stable, deployed at velxio.dev This repo
Desktop (Tauri) Pro — 30-day trial, license-gated desktop-app.md
MCP server OSS — stdio + SSE entry points MCP.md
IoT gateway OSS — HTTP proxy real browser <-> simulated ESP32 web server backend/app/api/routes/iot_gateway.py

Technology Stack

Frontend

Layer Tech
Framework React 19, Vite 7, TypeScript 5.9
Editor Monaco Editor
State Zustand 5
Routing React Router 7
Networking Axios + native WebSocket
UI components wokwi-elements (Lit) + Velxio-native Web Components
AVR sim avr8js (npm)
RP2040 sim rp2040js (npm)
ESP32 sim WebSocket bridge to backend QEMU worker
Pi sim WebSocket bridge to backend QEMU process
Electrical sim eecircuit-engine (ngspice-WASM), lazy-loaded
Custom-chip runtime WASI shim + ChipRuntime in-browser

Backend

Layer Tech
Framework FastAPI
Runtime Python 3.12, uvicorn
Compile arduino-cli (subprocess), ESP-IDF (subprocess), Emscripten (custom chips)
QEMU lcgamboa fork (libqemu-xtensa.so, libqemu-riscv32.so) + upstream QEMU 8.1.3 (qemu-system-aarch64 for Pi)
Bridge WebSockets (per-board)
MCP backend/mcp_server.py (stdio), backend/mcp_sse_server.py (SSE)

Deploy

  • DockerDockerfile.standalone builds a multi-stage image. Published to GHCR + Docker Hub by GitHub Actions on push to master.
  • Manual — frontend + backend independently. ESP32 / Pi need QEMU .so libs.
  • velxio.dev — Pro overlay layered on the base image. Lives in velxio/velxio-prod (private).

Architecture Advantages

  • Real emulation everywhere. No board has a hand-rolled state machine — every backend executes the real ISA.
  • Co-simulated digital + analog. ngspice runs in the same loop as the MCU. ADC reads return real solved voltages, not lookup-table approximations.
  • Plugin-based component behaviors. New parts register a handler; no SimulatorCanvas edits.
  • Build-time component discovery. TypeScript AST parser extracts metadata from wokwi-elements source. Override file for Velxio-native parts.
  • Stateless backend. Compile is HTTP; QEMU is per-WebSocket. No DB, no migrations, no user state in OSS.
  • Open-core split. OSS is fully self-hostable, anonymous, single-user. Pro features layer in via overlay without touching the OSS code path.
  • Portable projects. .vlx files round-trip every detail of a canvas. No server needed to share work.

Planned

  • More boards (STM32, RP2350, AVR-DA family)
  • Wire validation (short detection, missing-GND warnings)
  • Undo/redo for canvas operations
  • Streaming compile (incremental ESP-IDF builds)
  • WebGPU acceleration for the canvas at >200 components

See roadmap.md for the full list.


References