Commit Graph

249 Commits

Author SHA1 Message Date
David Montero 5a23e89eb5 feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.

Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
  program (larson.s) as the active group, editable on the left — previously
  the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
  a sibling tab inside the Arduino sketch group; it sits in its own chip
  section instead. The board group shows only sketch.ino.

Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
  own group (seeded from the example files), sweeps stale chip groups, keeps
  the program OUT of the board group, and for a board-less chip example makes
  the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
  group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
  IC icon; clicking switches the editor to the chip group. Lazy-creates a
  group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
  include them in the dirty-check hash, so chip-program edits persist on
  save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 22:07:56 +02:00
David Montero d5b9a9ceb5 feat(custom-chip): run custom chips with no board (general-purpose sim)
Velxio can now simulate one or more custom-chip CPUs with NO Arduino/ESP32
board on the canvas — a general-purpose electronics simulator, not an
MCU-only one.

- DynamicComponent: board-less parts get the real shared flat PinManager
  (instead of a no-op stub) so a custom chip's digital pin writes/reads reach
  the LEDs/inputs wired to it.
- CustomChipPart: the rAF tick respects board-less Run/Stop (freezes while
  the electrical sim is paused); board behaviour is unchanged.
- EditorToolbar.handleRun: board-less Run compiles each chip's WASM/ROM and
  re-attaches the parts (restartParts) so they pick up the fresh WASM, then
  resumes the solver.
- useSimulatorStore.restartParts(): bump hexEpoch to force part re-attach.
- New example "Z80 Larson Scanner (no board)": a programmable Z80 + 8 LEDs +
  the adjustable power-supply component, no MCU. The chip drives the LEDs
  through the synthetic-pin + ngspice path added earlier.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 21:04:17 +02:00
David Montero 1d6961d03c fix(z80-cpu): map RAM over the whole 0x8000-0xFFFF so vanilla SDCC C runs
SDCC's z80 crt0 sets SP=0x0000 and makes its first stack push at 0xFFFF.
The chip only mapped RAM at 0x8000-0xBFFF (0xC000+ was MMIO/ignored), so the
stack landed on unmapped memory and a plain C program crashed inside crt0 —
before main — which is why z80-led-chaser-c compiled but drove nothing.

Extend RAM to cover 0x8000-0xFFFF (32 KB) with the MMIO window 0xC000-0xC0FF
carved out and checked first, in scripts/make-z80-cpu.py + regenerated
z80-cpu.c. Now SDCC's default stack works and "write C from scratch, click
Run" just works — no manual `LD SP` needed (dropped from chaser.c). Bumped
the chip WASM initial memory to 4 pages to hold the larger RAM buffer. Larson
(asm, SP=0xBFFF, LED at 0xC000) is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 13:31:26 +02:00
David Montero 4cb5748dce feat(custom-chip): make chip pins first-class circuit nodes (digital + SPICE)
A custom-chip output pin wired directly to a component (LED, resistor, ...)
had no Arduino pin on its net, so the chip could drive nothing and the pin
resolved to null. Now:

- Layer A (digital): such chip pins get a stable synthetic pin number
  (syntheticPins.ts). traceDetailed resolves a chip<->component net to that
  shared number, so the chip's PinManager drive reaches the wired components
  through the existing digital event flow. A real board pin still wins.
- Layer B (analog/SPICE): a custom-chip mapper in componentToSpice emits a DC
  voltage source on each driven output pin's net (recorded in chipPinDrives by
  ChipRuntime), exactly like a board GPIO, and the chip requests an electrical
  re-solve when it toggles a pin (electricalResolveHook -> service.tick).
  So LEDs / resistors / analog parts wired to a chip output are driven by
  ngspice too.

This makes the bundled Z80 / i8080 chip examples actually animate their LEDs,
and lets any custom chip drive components, passives and analog circuits from
its own pins. Non-chip circuits are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 05:40:49 +02:00
David Montero 65b2c02f9b feat(custom-chip): one-click Run for programmable CPU chips
Compile/Run now makes every custom-chip on the canvas live in a single
click instead of requiring a manual trip through the chip designer plus a
separate ROM compile:

- Each custom-chip's C source is auto-compiled to WASM when it has none
  yet (via /api/compile-chip), and programmable CPU chips get their
  program file (larson.s, chaser.c, ...) assembled/compiled to ROM bytes
  (via /api/compile-rom) and injected, all before the board starts.
- Chip-program files are excluded from the arduino-cli sketch build, so
  SDCC-only syntax such as __at(0xC000) no longer breaks the Arduino
  compile (this is what made the Z80 LED-chaser-C example error out).

Fixes the Z80 examples that either errored on Run (z80-led-chaser-c) or
compiled but did nothing (z80-larson-scanner, whose chip never had WASM
or ROM). Works for any circuit built from scratch with a programmable
CPU chip, not just the bundled examples.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 04:47:26 +02:00
David Montero 6c35a6ea7f fix(editor): reset compilation console filter on each new compile
The console auto-switched to the 'errors' filter when a compile produced
an error, but never reset it. After one failing compile, every later
SUCCESSFUL compile (info/success lines only) was hidden by the sticky
filter — the console looked empty while the simulation started, 'unless
there was an error'. Now reset the filter to 'all' whenever the log
shrinks (a fresh compile cleared it) so the next batch is always visible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 20:20:54 +02:00
David Montero Crespo 0d5a1d5838 feat(motors): fix stepper rotation + add A4988 driver (real Fritzing SVG)
The stepper-motor and biaxial-stepper parts only decoded a one-hot wave-drive coil sequence, so they never rotated under the common two-phase full-step / Stepper.h / AccelStepper drive that Wokwi's own examples use -- only the servo moved. Rewrote both decoders to track the net magnetic-field vector of the coils (atan2 of the H-bridge currents), so the rotor follows wave, two-phase full-step and half-step drive alike, whether driven directly from GPIO or through a driver's outputs.

Also adds an A4988 STEP/DIR stepper driver (parity with Wokwi's wokwi-a4988): velxio-a4988 element renders the real Pololu A4988 Fritzing breadboard SVG (public/components/a4988.svg); MotorDriverParts.ts finds the wired stepper via the netlist and advances it one (micro)step per STEP rising edge in the DIR direction (MS1-3 microstep + active-low ENABLE). Metadata in component-overrides.json. Three examples (Uno/ESP32/Pico) wire MCU STEP/DIR -> A4988 -> stepper, coil map aligned to Wokwi (1A->B+,1B->B-,2A->A+,2B->A-).

Verified in-browser: motor rotates on Arduino Uno (avr8js) and Raspberry Pi Pico (rp2040js). tsc --noEmit clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 16:28:34 -03:00
David Montero db6537681a fix(stm32): make Stm32BluePillElement import-safe in node (vitest)
useSimulatorStore eagerly imports STM32_LED from this module, so the
top-level `class extends HTMLElement` + customElements.define ran at import
time and threw "HTMLElement is not defined" under vitest's node environment,
breaking 20 test files that load the store. Guard the base class with a
dummy fallback and skip registration when customElements is absent; browser
behavior is unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 08:33:55 +02:00
David Montero 81fdb0b959 feat(examples): order gallery by board (Arduino Uno first), then title
Sort filteredExamples by the board's position in BOARD_TABS — which puts
Arduino Uno first — and alphabetically by title within each board. Applies
to the 'All' view and to each board tab.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 08:26:16 +02:00
David Montero Crespo 6813891f91 feat(boards): add 6 STM32 boards (F4 Discovery, Olimex H405, Netduino 2/+2, Pill variants)
Adds stm32-f4-discovery, stm32-olimex-h405, stm32-netduino-plus2, stm32-netduino2, stm32-blackpill-f401 and stm32-bluepill-f103cb, mapped to existing qemu-lcgamboa machines (netduinoplus2, olimex-stm32-h405, netduino2, stm32vldiscovery). A generic inline board renderer (no SVG) draws the Discovery/Olimex/Netduino boards from a header pin layout; the Pill variants reuse the Blue/Black Pill SVGs. Per-board onboard-LED pin and polarity via STM32_LED. One blink+serial example per board.

tsc --noEmit clean; all new FQBN pnum variants present in STM32 core 2.12.0; worker smoke tests pass for the new machines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 03:19:26 -03:00
David Montero d9f98709d2 fix(examples): show STM32 boards in the gallery filter
Add STM32 Blue Pill / Black Pill tabs to BOARD_TABS, and make getBoardFilter
honor an explicit boardFilter before the boards[] check. The STM32 examples
are authored with the multi-board boards[] format even when single-board, so
they were all bucketed under "Multi-Board" and had no STM32 filter tab.
Now they appear under their dedicated STM32 tabs (attiny85 single-board
examples authored the same way get correctly bucketed too).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 07:54:09 +02:00
David Montero Crespo ca8dcedcc7 feat: STM32 (Blue Pill / Black Pill) QEMU emulation + Pro board gating
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>
2026-05-30 19:06:14 -03:00
David Montero 54f4e23d9f feat(editor-toolbar): "Record simulation" overflow item (replay v2)
Sixth overflow-menu item, Pro-badged. Dispatches
velxio-pro-replay-record-toggle (projectId in detail) which the pro
overlay handles — plan check, board-type check, start/stop the
recorder. OSS build has no listener → silent no-op.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:24:55 +02:00
David Montero 3a2abc48d9 fix(espidf): accurate core-lib warnings + gateway open hook
Two unrelated polish fixes.

espidf_compiler: headers that resolve to an arduino-esp32 CORE lib
(WebServer, WiFi, …) were correctly skipped from the user-lib merge but
then fell through to a scary "Library for <X> not found — build may
fail" warning — even though the build succeeds because the symbols are
compiled into the core. Now logs an accurate "provided by arduino-esp32
core — already compiled in, not merging". Same treatment for core
headers that aren't standalone lib dirs (Udp.h, IPAddress.h,
WiFiUdp.h, …) via a new _CORE_ESP32_HEADERS allowlist.

SimulatorCanvas: the WiFi badge's "open IoT gateway" click now consults
an optional window.__velxio_iot_gateway_open_gate__ hook before opening
the gateway tab. A private overlay can install it to gate the gateway
behind a paid plan and show an in-place upgrade modal instead of dumping
a 402 page in a new tab. OSS builds have no hook → opens normally. The
check is synchronous so it doesn't trip popup blockers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 20:38:03 +02:00
David Montero 44f12f0e53 feat(seo+nav+community): public-project indexing + classroom nav + examples grid
Search-engine indexing of public projects
- Update robots.txt to also list /sitemap-projects.xml so Googlebot /
  Bingbot discover every public project's canonical /:username/:slug URL.
- Add /docs/github-sync + /classroom entries to seoRoutes.ts so the
  build-time sitemap.xml picks them up.

Navigation polish
- AppHeader gains a "For schools" link between Pricing and Download.
- LandingPage's pricing section gets a slim banner under the cards
  pointing institutional visitors to /classroom (visible discovery path,
  not just a footer link).
- Localised header.nav.classroom + landing.pricing.classroomBanner +
  landing.pricing.classroomCta across all 9 maintained locales (en/es/
  pt-br/fr/de/it/ja/ru/zh-cn).

Community examples
- New CommunityProjectsGrid component lives next to ExamplesGallery on
  /examples.  Fetches /api/projects/featured (Pro-overlay-only endpoint)
  and renders the top public projects ranked by run_count.  Quietly
  hides itself when the endpoint returns nothing or fails, so the OSS
  build still ships cleanly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 18:11:28 +02:00
David Montero 5caf40fdeb feat(editor-toolbar): Share / Embed menu item
Fifth item in the overflow menu next to Sync to GitHub.  Free for
all users (no PRO badge); dispatches velxio-pro-share-prompt with the
current project id so the overlay's ShareModal can render the direct
link + iframe snippet copy UI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:36 +02:00
David Montero f4abad4ca2 feat(editor-toolbar): D5 — "Sync to GitHub" item in overflow menu
Dispatches velxio-pro-upgrade-prompt's sibling event
velxio-pro-github-sync-prompt with the current project id.  The pro
overlay's GithubSyncModal listens and runs the four-state link/sync
flow (no-pro / not-connected / not-linked / linked) inline without
leaving the editor.

Pure OSS builds have no listener so the click is a silent no-op —
those users can't have linked repos anyway.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 08:03:39 +02:00
David Montero 8ecbe89609 feat(editor-toolbar): in-place Pro upgrade prompt + progressive overflow menu
Replace the hard /pricing redirect on 402 with a window-dispatched
'velxio-pro-upgrade-prompt' event so private overlays can surface an
in-editor upgrade modal instead of bouncing the user out of context.

Move BOM, Schematic image and firmware upload buttons into a "..." More
menu next to the existing Export ZIP icon, freeing two button slots in
the inline toolbar.  Mark the two premium items with a small "PRO" pill
so free-plan users know they're gated before they click — Notion- /
Linear-style discoverability cue.

Also wire Import + Export ZIP to fall back into that same menu once the
toolbar container drops below 320 / 280 px (container queries on the
editor pane width).  Mobile / narrow-split layouts keep full feature
parity through the dropdown instead of overflowing into a horizontally
scrolling row.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 07:41:05 +02:00
David Montero 9a40de78c0 feat(share-modal): D1.4 — 3-level visibility picker (public/unlisted/private)
Phase 1 D1.4 — replaces the binary public/private toggle in ShareModal
with three radio-button-styled options. Optimistic UI: every option
renders for every user; the backend's 403 (with structured
visibility_not_allowed detail) redirects to /pricing?from=visibility_X
so the pricing page can lead the right pitch.

Why optimistic-then-redirect instead of hiding/locking options:

  1. Discovery — Free / Maker users SEE Pro unlocks Private. That's the
     exact conversion signal the pricing page is trying to surface.
  2. Discovery without surprise — the locked click goes to /pricing
     with a hint, not a dead modal.
  3. Less plan-coupling — this upstream component doesn't need to know
     about the pro overlay's plan store. Backend is the only source of
     truth for what's allowed.

Touched:
  - ShareModal.tsx: full rewrite as a 3-option picker with badges
    (Maker / Pro) on the gated options.
  - projectService.ts: ProjectResponse / ProjectSaveData now declare
    `visibility?: 'public' | 'unlisted' | 'private'`. is_public stays
    declared for backward compat with old callers.
  - useProjectStore.ts: CurrentProject gains `visibility?`; setVisibility
    accepts EITHER the legacy boolean OR the new enum and keeps both
    fields coherent.
  - common.json (4 locales): new editor.share.visibility.{publicLabel,
    publicHint, unlistedLabel, unlistedHint, privateLabel, privateHint}
    + editor.share.updateFailed.

Backend gating + DB migration are in the velxio-prod pro overlay
(commit referencing this submodule pointer).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 05:49:03 +02:00
David Montero c957767e2e feat(editor): add Export-Screenshot button (Phase 3 D3.2)
Front-end half of the schematic image export. New camera-icon button in
the editor toolbar between BOM and Upload-Firmware. Handler:

  1. POSTs to /api/pro/projects/{id}/screenshot.png (server renders the
     canvas with headless chromium, returns a PNG).
  2. 402 → /pricing?from=screenshot_export
  3. 401 → /login with redirect-back
  4. 422 → friendly "add at least one component" toast
  5. 200 → blob download with Content-Disposition filename
  6. The "rendering..." toast surfaces during the 5-10 s of headless
     chromium time so users know to wait, not click again.

i18n key editor.toolbar.exportScreenshot added in en/es/pt-br/zh-cn.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 04:18:43 +02:00
David Montero b5a80a65b9 feat(editor): add BOM-export button + handleExportBom flow
Phase 3 D3.1 — front-end half of the BOM export. The toolbar gains a
new spreadsheet-icon button next to the existing project-export button.
On click:

  1. POST is NOT used — the backend endpoint is GET-based and streams a
     CSV. We just open the URL.
  2. 402 (Pro-required) routes the user to /pricing?from=bom_export
     so the page can show the right upgrade narrative.
  3. 401 routes to /login with redirect-back.
  4. 200 triggers a Blob download with Content-Disposition filename.

i18n key editor.toolbar.exportBom added in en/es/pt-br/zh-cn — the
" — Pro" suffix on the tooltip hints at the gating without forcing the
user to discover it only on click.

The button is shown to everyone, not hidden by plan. Free/Maker users
clicking it gets the 402 route to /pricing, which is intentional — that
is the upgrade-discovery funnel we want, not a silent locked icon.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 03:12:52 +02:00
David Montero bab33e9e55 Merge commit 'e6df4ae8acacce956c1b112123ec7e00438c4bd5' 2026-05-27 08:34:17 +02:00
David Montero 8437f70770 feat(header): add Download nav link → /account/desktop-install
Inserts a Download entry between Pricing and Blog in the main nav.
Routes to the existing DesktopInstallPage in the velxio-prod pro
overlay (auth gate + platform-detect + signed-licence download flow).

i18n: header.nav.download added across all 9 shipped locales
(en/es/ja/it/de/ru/pt-br/zh-cn/fr) with native translations.

Self-hosted OSS image: the route doesn't exist there, so the link
lands on the upstream router's 404 — same fallback behaviour as
/pricing already has for self-hosters. Acceptable until the OSS
side gets its own placeholder.
2026-05-27 08:34:14 +02:00
David Montero Crespo e6df4ae8ac feat(flash): write compiled sketches to real USB boards (phases D1+D3)
Brings hardware flashing into Velxio Desktop. Per-board "Flash to
real board" entry in the canvas context menu opens a modal that
enumerates USB serial ports, lets the user pick one, then
streams arduino-cli upload output live until the board is flashed.

Backend (Phase D1) — backend/app/api/routes/flash.py (new):
  POST /api/flash/upload  (multipart: board_id, port, fqbn,
                           program_format, program)
  → SSE stream of {phase, line?, progress?} events
  → final {phase:'done', success, elapsed_ms, error?}

  - Wraps `arduino-cli upload -p <port> -i <file> --fqbn <fqbn> -v`
    so AVR (avrdude), ESP32 (esptool), RP2040 (picotool), SAMD
    (bossac) all share one code path — arduino-cli internally
    dispatches by FQBN.
  - Per-port asyncio.Lock prevents two simultaneous flashes from
    fighting over the same /dev/ttyACM0.
  - Allow-list of FQBN prefixes (arduino:avr, ATTinyCore:avr,
    rp2040:rp2040, esp32:esp32, arduino:samd) so a typo can't
    cause a confusing arduino-cli error.
  - Format allow-list (hex / bin / uf2 / elf) drives the temp
    file extension - arduino-cli uses the extension to route to
    the right uploader.
  - 8MB hard cap on the uploaded program (real sketches are
    well under that; protects against a runaway frontend).
  - X-Accel-Buffering: no header so nginx doesn't hold the SSE
    chunks until the flash completes.

Frontend (Phase D3):
  - frontend/src/services/flashService.ts (new):
      async generator streamFlash() yields parsed SSE events.
      Handles the base64-vs-text gotcha (compile returns hex_content
      as text but binary_content as base64; for binary formats we
      atob() into a Uint8Array before posting so the form upload
      sends actual bytes, not the base64 ASCII).
  - frontend/src/components/simulator/FlashModal.tsx (new):
      Three-state UI: picking (port dropdown), flashing (progress
      bar + live log), success/error (verdict + retry).
      Empty-ports state shows a Linux dialout-group hint.
  - SimulatorCanvas.tsx: board context menu gains "Flash to real
    board" entry, gated on isTauri() + presence of compiledProgram.
    Hidden in web (WebSerial is a separate sprint).
  - tauriBridge.ts: SerialPortInfo type + listSerialPorts() helper
    that invokes the Rust shell command added in Phase D2.

The sidecar already has arduino-cli on PATH (per
`pro/desktop/sidecar/main.py::_expose_bundled_arduino_cli`), so
no installer changes are needed — flash works the moment the
0.4.x desktop bundle ships with these commits.

Plan + remaining phase tracked in project/hardware-flashing/.
D2 (Rust serial enum) committed separately as a Tauri-shell-only
concern; D4 (manual smoke matrix with real boards) requires
physical hardware so it stays a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:20:20 -03:00
David Montero Crespo 145710561a fix: 5 user-reported issues (#208 #209 #210 #211 #212)
#208 — stale binary executes after compile error
EditorToolbar.handleCompile: on failed compile, clear the active
board's compiledProgram so a subsequent Run can't silently execute
the previous successful build (which doesn't match the editor any
more). The Run gate already short-circuits on !compiledProgram and
forces a fresh compile.

#209 — compile terminal kept stale messages across runs
EditorToolbar.handleCompile: setCompileLogs([]) at the top of the
handler. Previously logs from the prior compile lingered, making it
hard to tell new errors / warnings apart from old ones.

#210 — desktop File > New Project did nothing
desktop/menu.ts: the menu action used to dispatch a CustomEvent
nobody listened to. Replaced with a real `newProject()` function
that stops the running simulation, removes every board (also drops
the bridges + wires touching them), clears components / wires,
loads the default Blink sketch into the editor, clears project
metadata, and wipes the compile output. Confirms first if there's
unsaved work on the canvas.

#211 — deleting the only board made every other component
unresponsive (wires still worked)
SimulatorCanvas.tsx::interactionRunning: the old expression
treated boards.length === 0 as "boardless electrical mode is
running" — which suppressed the property dialog on click and made
non-sensor components look frozen. Fixed by also requiring
useElectricalStore.submittedNetlist !== '' before flipping to the
boardless-running branch. SPICE has to have actually solved at
least once for the mode to engage.

#212 — ESP32 Support 404 with no actionable message
desktop/Esp32QemuPrompt.tsx: catch the raw "download HTTP 404" /
"not found" upstream error and reword it to "ESP32 support is not
yet available for your platform. The Velxio team is preparing
this build - try again in a few days, or use Arduino/RP2040
boards in the meantime." The real fix is server-side (the velxio
team needs to publish a qemu-xtensa.tar.gz for the user's
platform into the asset bucket and update esp32-qemu/latest.json).
Tracked in project/desktop-agent-v040/ follow-ups.

All five fixes verified with `tsc --noEmit` clean and the existing
25-test vitest suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:06:58 -03:00
David Montero 5ca7e293b7 fix(canvas): use wrapper top-left for rotation pivot (#205 follow-up)
The previous fix rotated overlay hotspots but the pivot was off by
+6px in each axis, which manifested as a 12px X-offset for a 90°
rotation (because (I - R) maps (6,6) to (12, 0) for R = 90° CW).

The wrapper top-left in container-local coords is -wrapperOffsetX,
not -(6 - wrapperOffsetX). The container origin already sits INSIDE
the wrapper's padding+border by exactly wrapperOffsetX/Y; we have
to back out by that same amount, not by 6 - that amount.

Visual verification on https://velxio.dev/example/esp32-pwm-led-rgb:
overlay centers of rotated resistor now match the rotated pin tips
exactly (was 12px off in X).
2026-05-26 20:42:27 +02:00
David Montero 0a69c9facf fix(canvas): rotate pin overlay hotspots with the component (#205)
Reporter on GitHub: after rotating a component the WIRES followed
the pin tips (already fixed in the (6,6) offset commit) but the
clickable connection boxes stayed in the unrotated layout —
visible misalignment between the rotated component and its
hotspots, no way to start a fresh wire from a rotated pin.

Root cause: PinOverlay renders as a SIBLING of the DynamicComponent
wrapper, not as a child. CSS rotation on the wrapper doesn't reach
the overlay div, so its child pin boxes stay at the unrotated
(pin.x, pin.y) coordinates.

Fix:

- Plumb component.properties.rotation from SimulatorCanvas into
  PinOverlay as a new `rotation` prop.
- In PinOverlay, capture wrapper.offsetWidth/Height when reading
  pinInfo and apply the same rotation matrix the wire calculator
  uses (pivot at wrapper center, transform-origin: center center).
- Use the rotated (pinX, pinY) for both the visual `left/top` AND
  the canvas-coord passed to onPinClick, so wires that get started
  from the hotspot anchor at the rotated tip too.

Also align the default wrapperOffsetX from 4 to 6 (padding:4 +
border:2 on each side of the DynamicComponent wrapper). The
previous asymmetric (4, 6) was the same 2px X bias we fixed in
pinPositionCalculator a few commits back; the overlay was reading
its own copy of the bad number and putting hotspots 2 px left of
the pin tip on unrotated components too. Board paths that pass
wrapperOffsetX/Y = 0 explicitly are unaffected.

All 29 vitest tests in the rotation + simulator suites pass.
2026-05-26 20:02:10 +02:00
David Montero 2f60e3816a fix(editor): surface QEMU compile failure to the user instead of silent warning
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.
2026-05-23 08:39:26 +02:00
David Montero f619a2cc7e feat(boards): expose Raspberry Pi 4 and Pi 5 in the picker (UI + pin wiring)
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.
2026-05-23 04:46:59 +02:00
davidmonterocrespo24 2146b09c29 feat(desktop): hide header strip, splash screen, native locale switcher
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>
2026-05-22 19:39:22 -03:00
davidmonterocrespo24 54adac3ae3 feat(desktop): hide web nav + redirect / → /editor in Tauri builds
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>
2026-05-22 18:08:33 -03:00
David Montero aab380e80b feat(scope/trigger): add Auto / Normal / Single-shot trigger modes
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.
2026-05-22 21:11:03 +02:00
David Montero 737ec5c6eb feat(import): unify project import — accept .vlx and .zip in both entry points
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.
2026-05-22 20:55:06 +02:00
David Montero be24243e4b fix(esp32): add GPIO 16/17 pin aliases on DevKit V1 (RX2/TX2)
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>
2026-05-20 21:16:33 +02:00
David Montero Crespo 603b791daa feat(attiny85+customchip): full ATtiny85 ADC/Timer0 + custom-chip pipeline fixes
ATtiny85 (AVRSimulator + collectPinStates + connectAnalogInputsToMcu + SimulatorCanvas + Attiny85Element + examples):
- Add attiny85AdcConfig with correct register addresses (ADMUX=0x27,
  ADCSRA=0x26, ADCSRB=0x23, ADCL=0x24, ADCH=0x25, DIDR0=0x34, adcInterrupt=0x08).
  Without this, analogRead() polled the wrong address forever and the
  firmware hung on first ADC read.
- Add attiny85Timer0Config + instantiate AVRTimer so OVF fires at the
  ATTinyCore-expected ~1.024 ms cadence. delay() advance is still blocked
  on avr8js TIFR auto-clear semantics (separate upstream issue, see
  ATTINY85_TIMER0_UPSTREAM_ISSUE.md in velxio-prod test plan).
- Map ATtiny85 ADC channels to PB-style pin names (PB5/PB2/PB4/PB3 -> 0..3)
  in connectAnalogInputsToMcu so SPICE node voltages reach the right ADC
  channel.
- Recognise /^PB\d+$/ in collectPinStates.pinNameToArduinoPin so wires
  named "PB1" emit v_attiny85_pb1 V-source and the LED responds to MCU
  writes. Previously every PB-wire returned -1 and SPICE saw no source.
- SimulatorCanvas: subscribe pin 1 (PB1) for the built-in LED on the
  attiny85 board kind (Digispark convention), instead of falling through
  to the pin-13 default.
- Attiny85Element: remove the hand-drawn "yellow LED" circle that was
  floating above the chip. The bare DIP-8 has no on-board LED; examples
  wire a real wokwi-led + resistor instead.
- examples.ts: add a real wokwi-led + 220 Ohm wokwi-resistor + wires to
  attiny85-blink, and add missing series resistors to attiny85-button-led
  and attiny85-ntc-sensor. attiny85-pwm-fade was already correct.

Custom-chip pipeline (CustomChipPart + simulatorBridges):
- Add a requestAnimationFrame loop that calls instance.tickTimers() every
  frame in CustomChipPart. Chips that register vx_timer_create (e.g. an
  i8080 stepping its core, or a sensor publishing samples) had timers
  added to the queue but nothing fired them; tickTimers was dead code.
- Gate the ESP32 backend path with detectSimulatorKind(sim)==='esp32'.
  The previous `typeof sim.registerSensor === 'function'` check matched
  AVR and RP2040 simulators too (they expose registerSensor for I2C
  sensor proxies), routing client-side chips to a non-existent ESP32
  worker on those boards.
- Replace direct simulator.usart.writeByte calls in avrUartTx with a
  JS-level FIFO + setTimeout(1ms) drainer. avr8js writeByte drops bytes
  under burst load (a chip emitting print_string lost ~99% of bytes via
  non-immediate, or kept only the last byte via immediate). The drainer
  attempts one non-immediate write per tick and retries on RXC busy /
  RXEN off. Added a guard for ATtiny85 (no USART -> would queue forever).

End-to-end verified: i8080-banner-streamer now prints the boot banner
followed by "uptime ticks: 0xNN" lines stepping every ~50 ms, executing
real Intel 8080 instructions inside the WASM chip.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:10:08 -03:00
David Montero Crespo a9285c6698 fix(ui): narrow pointer-passthrough whitelist; restore DHT22/HC-SR04 dialog
The previous fix (dd22bcf) used `isInteractive` to decide whether to let
the wokwi component own the pointerdown. That heuristic was too broad —
DHT22, HC-SR04, NTC, photoresistor, LED all register `attachEvents` for
the SPICE/sensor-update bridge but have NO internal pointer handlers, so
clicks on them got silently swallowed by the wokwi shadow DOM and the
property dialog never opened.

Replace with an explicit whitelist of wokwi tags that ACTUALLY own
pointerdown (rotary knobs, pushbuttons, slide switches, joysticks,
keypads, encoders, rotary dialer). Every other component, including
sensors/displays/LEDs with attachEvents, falls through to the canvas
which decides between drag-to-rearrange and click-to-open-dialog.

Documented the model in docs/wiki/component-interaction.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 15:16:47 -03:00
David Montero Crespo dd22bcfe50 fix(ui+spice+example): interactive wokwi components, NTC formula, photoresistor alias
Three independent fixes uncovered during a systematic example-by-example
audit (plan/full_test_plan/):

1. DynamicComponent.handleMouseDown was calling e.stopPropagation()
   unconditionally in the capture phase. That swallowed pointerdown
   BEFORE wokwi-potentiometer / pushbutton / slide-switch / joystick
   could see it, so the rotary knob would not rotate and buttons
   wouldn't press even with a real OS mouse. Now we skip the swallow
   when the click target is an inner wokwi-* element during a live
   simulation, letting the wokwi component own its own pointerdown
   while still allowing the canvas drag-to-rearrange flow on the
   wrapper / non-interactive surface.

2. examples.ts uno-ntc (and pico-ntc) sketch had the NTC divider
   formula inverted relative to both the SPICE mapper topology
   (VCC -> R_NTC -> A1 -> R_pull -> GND, the standard module wiring)
   and real wokwi-ntc-temperature-sensor modules. Moving the slider
   to 60 C made the firmware print -3.42 C. Flipped the formula to
   r = SERIES_R * (VCC - v) / v. Now slider 60 C -> Serial reports
   60.12 C and A1 voltmeter shows 4.00 V.

3. componentToSpice.ts photoresistor mapper was only registered under
   the bare key `photoresistor`, but example components use the
   metadataId `photoresistor-sensor`. Added an alias so the LDR +
   pull-down divider gets emitted for the real component instance.

All three reproduce visually in seconds; documented per-example in
plan/full_test_plan/examples/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 15:10:42 -03:00
David Montero Crespo 321997715d feat(editor): Monaco syntax highlight for 8080/Z80 assembly
When the editor opens a .s or .asm file (the chip-program files routed
to /api/compile-rom), Monaco now colorizes 8080/Z80 mnemonics, registers,
hex/binary literals, comments, and directives. Same highlighter covers
both ISAs since most mnemonics overlap.

- frontend/src/components/editor/retroAsmLanguage.ts: a Monarch tokenizer
  + LanguageConfiguration + idempotent registration helper. Recognises
  the full 8080 ISA, all the Z80 additions (LD/JR/DJNZ/EXX/EX/IM/LDIR/
  bit ops/index ops), the directives ORG/DB/DW/EQU/END, and registers
  including condition codes (NZ/Z/NC/etc.) and IX/IY.

- CodeEditor.tsx: maps `.s` and `.asm` to the new `retro-asm` language
  and calls `registerRetroAsm(monaco)` in beforeMount so the language
  exists by the time the editor first paints. Other extensions
  (.ino/.cpp/.c/.py/.json/.md) behave exactly as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:49:51 -03:00
David Montero Crespo 0e2f0790db feat(chips): C-to-Z80 compile via SDCC + LED chaser example
Adds a third format to /api/compile-rom: `c` (C source compiled by SDCC
to Z80 bytes). Same chip-program flow as 8080/Z80 asm — write C in a
project file, click Compile, click Run.

Backend:
- backend/app/services/c_compile.py — async SDCC wrapper. Locates the
  sdcc binary on PATH (or via SDCC env var, or common Windows install
  paths) and shells out with target=mz80 + --code-loc 0x100 --data-loc
  0x8000. Parses the resulting Intel HEX into raw ROM bytes. Pure 8080
  is rejected with a clear error (SDCC has no 8080 backend; Z80 ROMs
  also run on the i8080-cpu chip if you avoid Z80-only ops).
- rom_compile.py: compile_rom is now async; the new c branch delegates
  to c_compile. compile_rom_endpoint awaits it.

Frontend:
- romCompileService: RomFormat gains 'c'; formatForFile maps .c/.cpp to
  'c'. isChipProgramFile intentionally still excludes .c — disambiguation
  happens at the EditorToolbar level.
- EditorToolbar: the chip-program path also fires when a custom-chip
  has programFile === activeFile.name (regardless of extension). That
  lets .c files route to /api/compile-rom (SDCC) when bound to a CPU
  chip, while .c files NOT bound to any chip continue to route to
  arduino-cli as before.

Docker:
- Dockerfile.standalone adds `sdcc` to the apt-get install list, so the
  prod image ships with SDCC out of the box.

Example:
- /examples/z80-led-chaser-c — z80-cpu chip + chaser.c (a Larson
  scanner written in C with __at() MMIO definitions). Compiles cleanly
  with SDCC's --code-loc 0x100 default crt0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:31:10 -03:00
David Montero Crespo 96ef12b585 feat(chips): programmable Z80 chip + Larson scanner example
Adds the Zilog Z80 to the programmable-retro-CPU lineup. Same compile-rom
flow that landed for the 8080 in PR #189: write Z80 asm in a project
file, click Compile (backend assembles via in-tree two-pass asm-z80),
click Run, the chip emulator boots from the resulting ROM bytes.

Backend:
- backend/app/services/asmz80.py — two-pass Z80 assembler covering the
  practical demo subset: LD r,n / r,r' / rp,nn / (nn),A / A,(nn) +
  ALU r/n + INC/DEC + JP/JR/DJNZ/CALL/RET + PUSH/POP + IN/OUT +
  EX/EXX + LDIR/LDDR/IM/NEG + RLCA/RRCA/RLA/RRA + the simple
  ED-prefix variants. Not yet: CB-prefix bit ops, DD/FD index ops.
- rom_compile.py routes target=z80 through the new assembler.

Chip:
- frontend/src/components/customChips/examples/intel/z80-cpu.{c,chip.json}
  Generated by scripts/make-z80-cpu.py from the existing z80.c emulator
  (same clean-room implementation that passes ZEXDOC end-to-end). The
  external pin/bus protocol is replaced with internal RAM + ROM + MMIO
  for LED/BTN/UART. 35 KB WASM.

Example:
- /examples/z80-larson-scanner — Knight-Rider-style walking LED.
  Demonstrates JR/DJNZ/RLCA which the 8080 can't run.

Plus a small Z80 smoke-test asm under scripts/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:21:08 -03:00
David Montero Crespo bbf8cd0303 feat(chips): programmable retro CPU chips with external ROM
Adds a new way to use the retro CPU chips: write your program in a
project file (.s / .asm / .hex / .bin), click Compile, click Run, and
the same chip emulates whatever you wrote. Same chip + different ROMs =
mini PC, calculator, LED demo, Kill-the-Bit game, etc.

SDK:
- velxio-chip.h gets two new host imports:
    uint32_t vx_rom_size(void);
    void     vx_rom_read(uint32_t off, uint8_t* dst, uint32_t len);
  CPU-emulator chips call these in chip_setup to pull their program out
  of the host's romBytes property.

Frontend runtime:
- ChipRuntime accepts opts.romBytes (Uint8Array) and exposes the new
  imports, copying bytes into chip memory on vx_rom_read.
- CustomChipPart pulls component.properties.romBytes (base64) and passes
  it through.
- Component registry declares three new custom-chip properties:
  romBytes (base64), programFile (matching project filename), and
  programTarget (cpu name).

New programmable bundled chip:
- frontend/src/components/customChips/examples/intel/i8080-cpu.{c,chip.json}
  Same clean-room 8080 emulator as i8080-repl/i8080-counter, but ROM is
  loaded externally via vx_rom_*. Has 8 LEDs, 8 buttons, UART, 16 KB RAM,
  32 KB of external ROM.

Backend:
- New /api/compile-rom endpoint and rom_compile service that turns
  chip-program source into ROM bytes. 8080 ASM is assembled by the
  in-tree two-pass assembler (moved to backend/app/services/asm8080.py).
  Intel HEX records are parsed; raw .bin is passed through. Future targets
  (z80, 8086, 4004) are scaffolded but not wired yet.

EditorToolbar:
- Compile button detects when the active file is .s/.asm/.hex/.bin and
  routes to compile-rom instead of arduino-cli. The compiled bytes are
  injected into every custom-chip on the canvas whose programFile property
  matches the active filename (or is empty).

Example:
- /examples/i8080-killbits loads Dean McDaniel's 1975 Kill-the-Bit on
  the programmable i8080-cpu chip. killbits.s is shipped as a project
  file alongside sketch.ino; the user clicks Compile then Run and the
  LED walks across 8 outputs, buttons kill it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:38:18 -03:00
David Montero Crespo 55b3dd29e5 fix(DynamicComponent): pass componentId through to PinTracer
`PinTracer` signature is `(componentId, componentPinName) => number | null`
but the local `getArduinoPin` lambda only accepted one arg and used the
closure-captured `id`. When `createDefaultPinResolver` passed both args
(per the typed signature), JS bound the FIRST arg (the componentId) into
the lambda's single `componentPinName` parameter. `traceDetailed` then
looked up a pin literally named "rgb-led-1" on component "rgb-led-1",
returned null, and the resolver locked itself into 'FLOATING' state —
its onChange path never subscribed and the wokwi-rgb-led element's
ledRed/ledGreen/ledBlue stayed at 0 forever even as the SPICE side
correctly cycled through R, G, B, Y, C, M, W via analogWrite().

Same bug latent for any multi-pin component that goes through the
PinResolver path (multi-pin LEDs, RGB strips, 7-seg drivers, anything
that calls `getPinResolver(<pinName>)` for several pin names).

Fix: lambda now accepts both shapes — `getArduinoPin(pinName)` (legacy
single-arg used by every PartSimulationRegistry handler) AND
`getArduinoPin(componentId, pinName)` (PinTracer 2-arg form used by
createDefaultPinResolver / createSpiceResolvedPinResolver). Picks the
right componentId in either case.

Verified via the rgb-led example: ledRed/ledGreen/ledBlue now cycle
0→255→0 in sync with the SPICE node voltages on pins 9/10/11.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:08:06 -03:00
David Montero Crespo 16f168ba7f feat(board-options): per-board options modal + persistence
Adds a new BoardOptionsModal accessible from the EditorToolbar that exposes
per-board options (currently used for board-specific compile flags). Wires
the modal through:

- types/boardOptions.ts            new BoardOptions shape
- types/board.ts                   BoardInstance gains `boardOptions` + `spiffsFiles`
- store/useSimulatorStore.ts       boardOptions persisted in loadProjectState
- components/editor/EditorToolbar.tsx     button to open the modal
- components/simulator/BoardOptionsModal.{tsx,css}  the modal itself
- components/simulator/SimulatorCanvas.tsx  passes the options through
- utils/projectPayload.ts          board options serialised in saved projects
- pages/ProjectByIdPage.tsx        re-includes the by-id loader needed for
                                   project URLs that reference boards with
                                   their persisted options.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:50:45 -03:00
davidmonterocrespo24 2072011fa4 feat(pi): pluggable slave handler + canvas wire detection for I2C/SPI/UART
Adds the public extension points the velxio-prod overlay uses to bind
real canvas-side I2C/SPI/UART models (BME280, future MCP23017, etc.)
to a running Pi guest's protocol shims:

- qemu_manager: set_pi_slave_handler(fn) / get_pi_slave_handler() for
  pi_attach_slave + pi_detach_slave WebSocket messages. OSS image
  leaves the hook unset so the messages are silently dropped.
- simulation route: parses the two new WS message types and forwards
  them to the registered handler when present.
- RaspberryPi3Bridge: attachSlave(spec) / detachSlave(spec) frontend
  side of the protocol.
- piSlaveScanner: at simulation start walks components + wires,
  identifies I2C/SPI/UART peers wired to Pi protocol pins (40-pin
  header physical-pin numbering), and emits one attach per
  bus/address pair (deduped across SDA+SCL wires).
- RaspberryPiWorkspace: invokes the scanner once the bridge is open,
  with retries to ride out the WS-still-connecting race.
- integration test: pi3_bme280_attach.py boots the Pi, pre-attaches a
  BME280 via the slave handler, runs a host-side proto loop, runs
  guest python smbus2.read_byte_data(0x76, 0xD0) and asserts the
  console reads back CHIP=0x60.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 16:26:37 +02:00
David Montero Crespo 32f5b407af fix(simulator): underscore-separated component ids for SPICE safety
The user reported the default editor canvas — Arduino Uno + LED +
220Ω resistor — was correctly powered (1.84 V at the LED anode,
14 mA through the diode) but the LED visual stayed dark. Only the
built-in pin-13 LED on the wokwi-arduino-uno element lit up.

Root cause: ngspice's WASM build truncates branch-current vector
keys at the first hyphen. A sense source named V_led-builtin_sense
ends up exposed under a key like v_led#branch rather than the
expected v_led-builtin_sense#branch. CircuitSimulationService and
BasicParts.ts both look up the FULL key, miss, and the LED's
brightness update treats raw as undefined → digital-fallback path
runs but the SPICE memo timestamp is fresh so HOLD keeps zero
brightness. Visible symptom: a perfectly conducting LED that never
lights.

Fix in two places:
  - Default canvas (useSimulatorStore.ts): rename 'led-builtin' /
    'r-builtin' to 'led_builtin' / 'r_builtin' (and the matching
    wire ids).
  - DynamicComponent.tsx makeNewComponent: the id template was
    'metadata.id-timestamp-rand' producing hyphens for every
    user-added component too. Switched to underscores, AND replace
    any hyphens already in metadata.id (e.g. 'led-bar-graph') so
    the prefix doesn't reintroduce the bug.

Existing saved projects whose ids contain hyphens are not migrated
here — those will keep the visual bug until either the operator
edits the components or we add a sanitisation step inside
componentToSpice + BasicParts. The next follow-up commit can add
that if you confirm this default-canvas fix works.
2026-05-18 10:01:48 -03:00
David Montero Crespo 5c5336acc0
Merge pull request #187 from davidmonterocrespo24/feat/retro-intel-cpus
feat(chips): port retro Intel/Zilog CPUs as Velxio custom chips + 2 d…
2026-05-18 02:30:23 -03:00
David Montero Crespo b714c79e3d feat(chips): port retro Intel/Zilog CPUs as Velxio custom chips + 2 demos
Adds 17 chips from the test/test_intel clean-room research to the Custom
Chip gallery, all sourced from manufacturer datasheets and validated by
the existing 129-test vitest harness (CPUDIAG end-to-end for the 8080,
ZEXDOC for the Z80).

CPUs: 4004, 4040, 8080, 8086, Z80 (categoria retro-cpu)
Bus chips: rom-32k, ram-64k, rom-1m, latch-8282, 4001-rom, 4002-ram,
           8255-ppi, 8251-usart, 8259-pic, 8253-pit (retro-bus)

Two bundled "mini-computer" demos under retro-bundle that drop on the
canvas as a single chip and run real 8080 code out of an embedded ROM:

  * i8080-repl     8080 + RAM + ROM + UART, prints a banner and an
                   "uptime ticks: 0xNN" counter every ~50 ms via a real
                   DCR/JNZ busy-wait. Visible in Serial Monitor.

  * i8080-counter  8080 + RAM + ROM + 8 LED pins + 2 button pins.
                   Counts up in binary on BTN_INC, clears on BTN_RST.

Two example projects under /examples reuse these chips end-to-end:

  * /examples/i8080-banner-streamer
  * /examples/i8080-button-counter

The bundled chips inline a 328 / 34-byte 8080 ROM produced by a new
two-pass 8080 assembler in Python (scripts/asm8080.py) from the .s
sources in scripts/. Both ROMs are pre-assembled and committed under
scripts/*.txt so contributors can rebuild deterministically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 02:27:53 -03:00
David Montero Crespo 06d6e0afbb fix(simulator): circuitVerifier worst-case GPIO + LED NaN guard
Two related correctness fixes that make the simulator's realism
match what users actually see.

1. circuitVerifier was running pre-flight against the IDLE circuit
   (every pin LOW). A Blink sketch is going to write pin 13 HIGH
   eventually — at which point a missing series resistor produces a
   ~500 mA spike through the diode. But because pre-flight ran with
   pin 13 LOW the led-overcurrent rule never fired, and the user
   sailed through Run only to see the LED stay mysteriously dark on
   the canvas.

   The verifier now forces every digital pin connected to a load to
   HIGH = vcc, the worst case any well-defined sketch will eventually
   impose. The existing rules (led-overcurrent, resistor-overpower,
   short-circuit) now fire correctly and the existing
   CircuitVerificationModal blocks Run until the user adds a proper
   current limiter or chooses Run Anyway.

   Pins that are inputs-only (a pull-up + button) get over-driven
   here too, but the rules tolerate that — a pull-up at 5 V draws
   ~0.5 mA, well below all thresholds. A circuit that would actually
   fault under HIGH is flagged.

2. LED simulator was crashing visually on non-finite ngspice branch
   currents. A degenerate diode (no series R) makes ngspice return
   NaN, which fell through 'raw !== undefined && current > 1e-6' as
   false and never triggered the digital fallback. Now we check
   Number.isFinite(raw) before trusting it — non-finite returns
   route to the digital fallback so the LED at least lights visually
   when its driver pin is HIGH (the user still sees the verifier
   warning that the real-world circuit is wrong, but Run Anyway is
   not a black screen).
2026-05-18 00:00:45 -03:00
davidmonterocrespo24 09a227466a fix(pi3): auto-focus terminal + log serial input bytes
PiTerminal didn't call term.focus() on mount, so xterm.js stayed
passive — onData only fires when the DOM element has focus.  Users
saw the boot prompt but their keystrokes went to whatever element
held focus when they clicked Run (canvas, code editor), never
reaching the bridge.  Calling focus() right after fit() makes the
prompt receive input the moment it's visible.

The qemu_manager change adds INFO-level logging when serial_input
WebSocket messages reach send_serial_bytes — useful diagnostic for
future Pi3 input problems (proves whether bytes reached the backend
before we look at TTY / kernel / PL011 wiring).
2026-05-17 16:37:56 +02:00
David Montero Crespo a177471ed0 fix(sensor-panel): per-sensor state when switching between same-type sensors
Clicking a second photoresistor (or any second sensor of the same
metadataId) showed the previously-clicked sensor's slider value because
the panel was reused across clicks and its useState only ran once. The
mount useEffect also unconditionally dispatched config defaults, which
would have wiped any prior customisation if we naively remounted.

Three changes:

- SensorUpdateRegistry caches the last-dispatched values per componentId
  (and clears them on unregister) so the panel has a place to read from.
- SensorControlPanel hydrates from that cache on mount, falling back to
  config defaults only when the sensor has never been touched. The
  default-dispatch useEffect skips when cached values already exist.
- SimulatorCanvas keys the panel on sensorControlComponentId, forcing a
  fresh mount when the user switches sensors — without that, hydration
  wouldn't run on subsequent opens.
2026-05-16 22:28:52 -03:00