Commit Graph

868 Commits

Author SHA1 Message Date
David Montero 6f3603d88b feat(sim): P2 wiring ERC slice 2 — VCC-to-GND short + shorted-out parts
- Power short (blocking error): a wire joining a VCC-type pin directly to a
  GND-type pin shorts the supply to ground. The current-based short-circuit
  rule only inspects battery/signal-generator/power-supply sources, so it
  misses a board-rail-to-GND short with no such source -> name it structurally.
- Shorted-out part (warning): a 2-terminal part with both terminals on the same
  node has no effect on the circuit.

Both graph-based, run before the solve. Zero false positives across the 69
gallery examples; gallery pre-flight tests still pass (no spurious blocking).
2026-06-18 03:32:17 +02:00
David Montero 8683c1ecf0 feat(sim): P2 wiring ERC — missing power + dangling 2-terminal parts
First slice of the connection ("malas conexiones") checks, graph-based and run
before the solve so they report even on circuits too incomplete to solve:

- Missing power: a rated peripheral (sensor/display) wired into the circuit but
  missing its VCC or GND connection -> warning. Boards are excluded (they live
  in input.boards and self-power).
- Dangling 2-terminal part: a resistor / LED / capacitor / diode / inductor
  connected on only one side (the other terminal floating) -> warning.

Both non-blocking. Verified zero false positives across all 69 gallery
examples. Tests: dangling resistor warns, fully-wired doesn't, module missing
GND warns.
2026-06-18 02:05:42 +02:00
David Montero 4e20d03f4c feat(sim): P1 over-voltage for boards + electrolytic capacitors
Extends the over-voltage rule to the two cases the previous slice deferred:

- Boards (ESP32 / Pico / Arduino / ...): a board's supply pins all collapse to
  the self-driven vcc_rail net, so an external source on them makes the .op
  singular rather than readable. Added a graph-based check (runs before the
  solve): if a power source is wired to a board supply pin and its nominal
  voltage exceeds that pin's rating, warn. Threaded boardKind into
  BoardForSpice so the verifier can look up the board rating.
- Electrolytic capacitors: new `voltage` rating property (select, default 25V,
  on capacitor-electrolytic + cap-elec-* presets, via component-overrides +
  regenerated metadata). The verifier reads the DC voltage across the +/- pins
  and warns on over-voltage (vent/burst) and on reverse polarity (a polarized
  cap wired backwards). Defaults to 25V when the property is unset.

Tests: 9V battery -> ESP32 VIN warns, 1.5V doesn't; 24V across a 16V cap warns,
5V across a 25V cap doesn't; reverse-biased cap warns. All real-ngspice.
2026-06-18 01:19:13 +02:00
David Montero 12ed306efb fix(editor): keep Circuit check findings when a Run auto-compiles
handleCompile() wiped all logs at the start, including the 'Circuit check'
group the pre-flight verifier had just logged (e.g. an over-voltage warning).
A Run auto-compiles right after verification, so the warning vanished. Preserve
circuit-check entries on compile, same as the boardless path already did.
2026-06-17 22:52:40 +02:00
David Montero 5a9bb3a70e feat(sim): P1 over-voltage warnings for parts with a rated input voltage
Adds a non-blocking circuit-verifier rule: a component whose supply pin sees
more than its datasheet absolute-maximum voltage warns ("X V on the VIN pin --
above its Y V maximum; not emulated accurately"). This is the "fed too much
voltage" mistake the operator asked for (a 3.3-5V module wired to a 9V battery).

- New componentRatings.ts: per-PIN abs-max table (SSD1306/ILI9341 displays,
  DHT/BMP280/HC-SR04/MPU6050 sensors, NeoPixel, servo). Per-pin thresholds so a
  3V3 pin (3.6V) and a VIN pin (6V) are judged separately. Unknown parts are
  simply not checked; an unwired or floating supply pin is skipped.
- circuitVerifier reads each rated part's supply-vs-ground voltage from the
  solved nets (via pinNetMap) and warns when it exceeds the rating.
- VCC/VDD/3V3/5V pins ride the shared vcc_rail net (NetlistBuilder convention);
  VIN is a normal net. Both handled.
- Tests: 9V on a module VIN warns; 5V on VIN does not; a 3.3V pin on a 5V rail
  warns.

Boards (esp32/pico/arduino) carry ratings in the table but aren't checked yet
-- BoardForSpice doesn't thread its boardKind; follow-up.
2026-06-17 22:40:03 +02:00
David Montero b7d1e6469d fix(editor): show spinner + block re-clicks during circuit verification
Clicking Run runs a pre-flight circuit-verification SPICE solve before
compiling. On a cold ngspice worker that solve takes a second or two, but the
Run button kept showing the play icon and stayed enabled, so it looked dead
and got clicked repeatedly -- each click stacking another verification (the
reported 6x [handleRun] click).

- Add a `verifying` state: the Run button now shows the same spinner as the
  Compile button and is disabled while the pre-flight solve runs.
- Synchronous re-entrancy guard (runInFlightRef) ignores re-clicks while a
  verification is already in flight.
2026-06-17 22:06:15 +02:00
David Montero 9ec48d5021 polish(sim): route circuit faults to the output console instead of a toolbar toast
The runtime burnout / pre-flight messages used an inline `setMessage` toast
that rendered as a bar near the Run/Stop buttons and overlapped them. Route
all circuit findings into the compile output console instead — one unified,
red/orange diagnostics log next to the compiler output (Proteus-style):

- New "Circuit check" console group (CIRCUIT_CHECK_TARGET). checkOrBlock logs
  every error (red) / warning (orange) there and opens the console; the
  blocking modal is kept for the explicit Run-anyway / Cancel decision.
- Runtime `velxio-circuit-fault` events (LED burnout) log to the same group
  instead of the toast; no auto-open (the continuous solver can fault on load).
- Warnings-only no longer pop a toast — the console entry is the record.
- The run-path clears preserve circuit-check entries so findings survive a
  "Run anyway" auto-compile.
2026-06-17 21:46:04 +02:00
David Montero f7f0eb4ba8 fix(editor): Run button bypassed circuit pre-flight verification
The Run (and Run All) buttons were wired `onClick={handleRun}`, so React
passed the click event as the first argument. handleRun(skipVerify=false)
then treated the truthy event as skipVerify=true and skipped checkOrBlock
entirely -- the pre-flight circuit verifier never ran on a button click.
This is the real reason a 9V battery wired straight to an LED ran with no
warning even though the verifier exists and is correct (project 2840fd12).

- onClick={() => handleRun()} and onClick={() => handleRunAll()} so
  skipVerify stays at its false default.
- Give handleRunAll the same checkOrBlock pre-flight gate handleRun has.
2026-06-17 21:02:57 +02:00
David Montero f523dfb554 chore(sim): log circuit pre-flight verification outcome (observability)
Verification failing silently in production is otherwise hard to spot — the
rules read 0 A when branch currents are missing. Log the errors/warnings,
whether a solve landed, and which branch/node vectors came back.
2026-06-17 20:52:10 +02:00
David Montero 3372151405 fix(sim): circuit verifier was silently blind to current faults in prod
The pre-flight circuit verifier reads branch currents via runNetlist ->
readAllCurrentVectors() (ngSpice_AllVecs enumeration). The production
Web-Worker ngspice WASM build does not surface voltage-source #branch
vectors through that enumeration for an .op plot, so branchCurrents came
back empty and every current rule (short-circuit, LED over-current) read
?? 0 -> no fault. The live solver avoided this by requesting each current
explicitly by name; the Node test build enumerates them, so the gap was
invisible to the suite. Net effect: a 9V battery wired straight to an LED
ran with no warning (reported on project 2840fd12).

- runNetlist: request every V_* source branch current explicitly by name
  and merge with the enumeration, so source/LED currents are always present
  regardless of the worker WASM's AllVecs behaviour.
- circuitVerifier: non-finite source/LED current -> blocking unstable-solve
  fault ("could not solve a stable current - likely a short or a part with
  no current limit, e.g. an LED with no series resistor").
- LED runtime (BasicParts): burn out on a non-finite current instead of
  falling through to the digital fallback and glowing; raise burnout
  threshold 20mA -> 100mA so high-power/RGB channels are not falsely
  destroyed; clear the burnt latch on Reset (resetBoard bumps hexEpoch).

Tests: real-data repro, mocked non-finite verifier test, runtime
non-finite / high-power / latch-recovery tests.
2026-06-17 20:37:32 +02:00
David Montero 2d23b878e7 fix(canvas): rotated-component pin positions in 3 paths (fixes #230, #231, #232)
All three bugs are rotated components whose pin geometry is computed in a
path that ignores the rotation, so pins/wire-starts land tens of pixels off
the visual pin tips. The live rotate action already recalculates correctly;
these are the paths that didn't.

#231 (context-menu 'Tap a pin to wire'): both onPinSelect handlers in
SimulatorCanvas computed the wire start as getBoundingClientRect().left +
pin.x — adding the UNROTATED pin offset to the ROTATED bounding-box corner.
On a 90-deg HC-SR04 that put the start ~70-100px off (measured). Replaced
with calculatePinPosition(id, x+6, y+6, rotation), the same rotation-aware
helper wires and the pin overlay use.

#232 (rotate -> delete -> undo): recordRemoveComponent's undo restored the
component + wires but never recalculated wire endpoints, so a rotated part's
wires kept the unrotated coords captured at delete time. Added a
requestAnimationFrame updateWirePositions(id) after restore.

#230 + #232 (pin boxes wrong after import / undo / load, 'fixes if rotated
again'): PinOverlay captured the wrapper's layout box (the rotation pivot)
once at mount. On import/undo/load the component mounts already-rotated and
its wokwi-element may not be sized on the mount tick, baking a wrong pivot
that only refreshed when rotation changed. PinOverlay now re-measures after
layout (rAF) and whenever it is about to become visible (showPins dep).
2026-06-16 05:25:31 +02:00
David Montero 3f23a7950c fix(sim): emulate AVR EEPROM (fixes #203)
AVRSimulator never instantiated avr8js's AVREEPROM peripheral, so any
EEPROM.read/write/update hung the sketch: the Arduino EEPROM library spins
on `while (EECR & (1<<EEPE))` waiting for the write-complete bit to clear,
and with no peripheral driving EECR that bit never cleared (issue #203 —
EEPROM.update(0,123) + EEPROM.read(0) hangs instead of printing 123).

Wire AVREEPROM to the CPU in both loadHex() and reset() via a new
attachEeprom() helper. The EEPROMMemoryBackend is created once per
simulator instance and reused across firmware reloads and resets, so a
value written in one run is still readable on the next boot — matching
real hardware, where re-flashing leaves EEPROM intact. Sizes per variant
(Uno 1024 B, Mega2560 4096 B, ATtiny85 512 B); ATtiny85 gets its own
register map (EECR 0x3C / EEDR 0x3D / EEARL 0x3E / EEARH 0x3F) since
avr8js's default eepromConfig targets the ATmega328P.

Adds eeprom.test.ts: drives the EEPROM register protocol against the
production AVRSimulator (loadHex + step), asserting a byte round-trips,
the EEPE poll terminates (no hang), and contents survive a reset.
2026-06-16 04:18:51 +02:00
David Montero 608c2538c9 fix(sim): correct NTC temperature sensor divider + reset to default on restart
The NTC breakout's SPICE topology was inverted relative to the example
sketch's decode formula (rNtc = R_PULL * v / (5 - v)), which assumes a 10k
pull-up from VCC to OUT and the NTC from OUT to GND. The mapper had the NTC
on top (VCC->OUT) and the pull-down on the bottom, so the recovered
temperature ran backwards: dragging the slider to 100C made the sketch
print -25C. Swap the two resistors so V_OUT = 5 * Rntc / (Rntc + Rpull),
matching the sketch and the hand-built reference netlist in
spice-avr-mixed.test.ts (T=0 -> ADC 789, T=25 -> 511, T=50 -> 270).

Also replace the SensorParts linear approximation (2.5 - (t-25)*0.02) with
the same beta-model divider so the non-SPICE ADC injection decodes back to
the slider value, and drop the dead onInput path that treated the element's
value as a raw ADC count.

Reset now restores interactive sensors (temperature/lux/gas sliders) to
their configured defaults: resetBoard re-dispatches each sensor's default
into the running sim and bumps sensorResetNonce so the open
SensorControlPanel remounts and the slider snaps back. Previously a restart
left the NTC frozen at the last dragged temperature.

Updated the examples netlist snapshot for the swapped NTC cards.
2026-06-15 23:21:36 +02:00
David Montero e51d26ce33 feat(editor): follow-up GitHub star prompt for past dismissers
The star banner used a single localStorage flag (velxio_star_prompted)
set identically whether the user clicked through to the repo or just
closed it, so once dismissed it never showed again and clickers and
closers were indistinguishable.

Now track three flags:
  velxio_star_prompted     - dismissed the first ask
  velxio_star_prompted_v2  - dismissed the follow-up (stop forever)
  velxio_star_clicked      - clicked through to the repo (stop forever)

Anyone who dismissed the first ask without clicking through gets ONE
follow-up (round 2) with a stronger message; clicking the repo link at
any time opts them out permanently. Capped at two asks total.

Adds starBanner.title2/body2 copy in all 9 locales.
2026-06-15 22:15:14 +02:00
David Montero c81e5b839d fix(sim): finish dropping the proWifiGate wiring (9360f95 was incomplete)
Commit 9360f95 deleted lib/proWifiGate.ts but left useSimulatorStore importing
it (the store edits weren't staged), so a clean checkout of master failed to
build (import of a deleted module). velxio.dev was unaffected — deploy.sh builds
from the working tree, which had the removal applied. Commit the removal so HEAD
is consistent.
2026-06-15 22:09:17 +02:00
David Montero 9360f9521b revert(sim): drop the WiFi run-gate seam (WiFi becomes freemium)
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.
2026-06-15 20:04:42 +02:00
David Montero d1088aefc2 fix(rp2040): Pico W always boots the W firmware (no `import network` crash)
Two robustness fixes for the paid-WiFi open-core split:

1. A pi-pico-w board now boots the RPI_PICO_W firmware variant (which has the
   `network` module) based on its BOARD KIND, not on whether the WiFi
   peripheral happens to be attached. Previously the variant was
   `pioPeripheral ? 'pico-w' : 'pico'`, so any moment the peripheral was
   absent (see #2) booted the plain Pico firmware and a Pico W sketch crashed
   with "ImportError: no module named 'network'". Store boardKind in
   attachPioPeripheral and pick the variant from it. (OSS: 'pico-w' isn't
   registered, so firmwareConfig falls back to 'pico' — a self-hosted Pico W
   has no WiFi engine anyway.)

2. Re-attach the PIO peripheral in loadMicroPythonProgram before loading
   firmware. An example deep-link adds the board during render, which races the
   pro overlay's async mountPro that installs the CYW43 factory — so the
   board-add attach returned null and a PAID user's Pico W booted plain
   firmware too. attachPioPeripheral is idempotent; by run time the factory is
   installed, so a paid user gets the W peripheral and real WiFi.
2026-06-15 18:55:13 +02:00
David Montero 90c2d1da51 feat(sim): WiFi run-gate seam so free users get an upgrade prompt, not a crash
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).
2026-06-15 18:16:50 +02:00
David Montero Crespo fae9208fae Merge branch 'master' of https://github.com/davidmonterocrespo24/velxio 2026-06-15 11:57:59 -03:00
David Montero Crespo cf6a609671 Raspberri pi 4 y 5 2026-06-15 11:57:10 -03:00
David Montero 375b952d47 refactor(examples): move Pico W WiFi examples to the pro overlay seam
The Pico W WiFi showcase examples are a paid-overlay feature now (the WiFi
engine moved to the overlay). Read them through a build-time `@pro` seam so the
SSR prerender + gallery + sitemap include them when built with the overlay, and
OSS gets an empty stub.

- data/examples.ts: import { proExamples } from '@pro/data/proExamples' (static,
  build-time) instead of the local examples-picow-wifi.ts; delete that file.
- src/__pro_stub__/data/proExamples.ts: OSS no-op (empty list) for the @pro alias.
- vitest.config.ts: mirror the @pro alias (stub by default / overlay when
  VITE_PRO_BUILD) so tests loading examples.ts resolve it.
- scripts/generate-sitemap.mjs: also parse <PRO_OVERLAY_PATH>/data/proExamples.ts
  when building with the overlay (the script reads example IDs from source text,
  so it can't follow the alias).
- Tests: drop the picow-wifi import/usage from the 5 OSS example tests (they
  validate the OSS set now); prune the 4 obsolete picow netlist snapshots. The
  overlay's proExamples get their own coverage in pro/.../__tests__/.
2026-06-15 15:54:44 +02:00
David Montero b820c372cc feat(seo): add /v3 release landing page + refresh About for 3.0
- New Velxio3Page (/v3): retro CPUs (Z80/8080/4004/4040/8086), MicroSD,
  ePaper, multi-board interconnect, ngspice WASM migration, undo/redo,
  -88% bundle, 100+ examples. JSON-LD (SoftwareApplication 3.0.0, FAQ,
  breadcrumb). Mirrors the v2.5 page; reuses SEOPage.css/Velxio2Page.css.
- Wire-up: App.tsx route, entry-server.tsx prerender map, seoRoutes.ts
  entry (priority 0.95, prerendered to dist/v3/index.html). v2-5 demoted
  to 0.9.
- AboutPage: Velxio 3.0 is now the 'latest' release card (v2.5 + v2 kept);
  stats refreshed (boards 17 -> 19+, CPU architectures 6 -> 10+ for the
  new retro ISAs).
- i18n: new v3 block in releases.json + about.releases.v3* in common2.json,
  auto-translated to all 9 locales via translate-i18n.mjs.
2026-06-15 08:52:09 +02:00
David Montero b166dfd3e2 test: refresh stale picow-wifi-relay-web-server netlist snapshot
The stored snapshot predated the relay-LED netlist emission (current-sense
V-source + LED diode model), so examples-netlist-snapshot failed on a clean
checkout regardless of any source change. Regenerate it to match the current
NetlistBuilder output. Unblocks the deploy test gate.
2026-06-15 08:45:27 +02:00
David Montero fb813ffde0 feat(opencore): extract Pico W WiFi to a pluggable PIO peripheral seam
Move the CYW43439 (Pico W) WiFi emulation out of the open-source tree so it
can ship as a paid feature in a private overlay. OSS keeps a plain Pico W
(no WiFi); the overlay registers the cyw43 protocol + backend network stack
at runtime via generic seams.

Frontend:
- Add simulation/PioPeripheral.ts: a generic "PIO bus peripheral" seam
  (feedWord / inDiscardableWriteData / resetFraming / hostWakeLevel /
  onHostWake / onSimulationStart). No factory is installed in OSS, so
  createPioPeripheral() returns null and a Pico W simulates as a plain Pico.
- RP2040Simulator: keep the fragile PIO-FIFO plumbing (it must re-run after
  loadMicroPython swaps the chip) but drive it through PioPeripheral instead
  of an inlined cyw43 import (attachCyw43 -> attachPioPeripheral, etc.).
- useSimulatorStore: generic attach/detach + setBoardWifiStatus; drop the
  cyw43 bridge map.
- MicroPythonLoader: add registerFirmwareVariant() so an overlay can add the
  RPI_PICO_W build; remove the OSS pico-w config + bundled .uf2.
- Delete simulation/cyw43/ (moved to the overlay).

Backend:
- core/hooks.py: add generic register_ws_sim_handler / dispatch_ws_sim_message
  and register_gateway_proxy / dispatch_gateway_proxy seams.
- simulation.py: route start_picow / stop_picow / picow_packet_out through the
  ws_sim_handler hook (the overlay handles + gates them).
- iot_gateway.py: resolve the Pico W gateway through the gateway_proxy hook.
- Delete services/picow_net/ + picow_net_bridge.py (moved to the overlay).

Tests: move the cyw43/picow suites to the overlay; update RP2040Simulator
mock stubs to attachPioPeripheral.
2026-06-15 08:33:28 +02:00
David Montero 07a0bfef6e seo: emit trailing-slash canonical + sitemap URLs
Static/docs/example routes are served as <route>/index.html and nginx
301-redirects the slash-less form to add the trailing slash. The sitemap
generator, the prerender canonical/og:url, and the client useSEO canonical
all emitted the slash-LESS form, so every sitemap URL was fetched as a
redirect (filed under 'Page with redirect' in Search Console) and each
canonical pointed at a redirecting URL.

Align all three to the trailing-slash form so sitemap URL == canonical ==
served URL == 200, with no redirect hop.

- generate-sitemap.mjs: append '/' to every <loc> (root stays '/')
- prerender-seo.mjs: withSlash() on canonical + og:url (routes + examples)
- useSEO.ts: withTrailingSlash() on canonical + og:url (covers dynamic
  project pages too)
2026-06-15 06:10:32 +02:00
David Montero Crespo 09eaf00c55 fix(canvas): don't treat the active board as a delete target on Delete/Backspace
The canvas had two independent window keydown listeners. The wire handler
removed the selected wire and returned, but that return cannot stop the
separate component/board handler, which fell into 'else if (activeBoardId)'
and popped the board-removal confirmation. activeBoardId is not a visual
selection -- it is just the board whose code is open in the editor, so it is
effectively always set. Pressing Delete to remove a wire (or after deleting a
component) therefore always asked to remove the board.

Remove the keyboard board-delete branch entirely: Delete/Backspace now only
removes the selected component. Board removal stays on its deliberate paths
(right-click Remove board, touch pin-picker delete). Also add the text-field
guard (input/textarea/select/contenteditable) to the wire handler so Backspace
while typing in the AI chat no longer deletes a selected wire.
2026-06-14 23:28:49 -03:00
David Montero Crespo f9ebdc240e fix(i18n): load + resolve non-English locales on direct navigation
Every non-English page fell back to English when loaded directly (deep link,
refresh, SEO-indexed URL). Two causes:

1. index.ts seeded lng with the URL locale at init, but only the English
   bundle is inlined. LocaleSync only fetches a locale bundle when
   i18n.language !== target, which was already false on a direct non-default
   load, so the bundle was never loaded. Init at DEFAULT_LOCALE and let
   LocaleSync drive the locale from the URL (a real changeLanguage that
   re-renders).

2. The region-coded locales (zh-cn, pt-br) never resolved their own bundle:
   i18next's default code formatting rewrote them to zh-CN / pt-BR, which
   failed the lowercase supportedLngs check and were dropped from the resolve
   hierarchy. Add lowerCaseLng: true.

Verified in a production build: /zh-cn/* and /pt-br/* render translated;
toResolveHierarchy now returns ['zh-cn','en'] / ['pt-br','en'].
2026-06-14 08:17:05 +02:00
David Montero 54e265b108 fix(picow): drop the broken 'Open in tab' link from the device panel
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).
2026-06-14 07:42:44 +02:00
David Montero ec40204f92 feat(picow): wire a visible LED to GP2 in the relay example
Confirmed the RP2040 GPIO -> PinManager -> wokwi-led path is fully wired
(identical to the AVR path): a wokwi-led wired anode->GP2, cathode->GND
lights when MicroPython drives GP2 HIGH. The board's componentId is its
boardType ('pi-pico-w') since loadExample's addBoard gives the first
board of a kind id == kind.

Add a red LED on GP2 to picow-wifi-relay-web-server and flip the relay
logic to active-high (ON => GP2 HIGH => LED lit) so the toggle is visible
on the canvas, not just in the device panel.
2026-06-14 07:20:52 +02:00
David Montero 1478f7f543 fix(picow): make the device panel a draggable, non-modal floating window
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).
2026-06-14 06:28:29 +02:00
David Montero 80d2086907 fix(picow): open the IoT gateway in an in-app iframe, not a new tab
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.
2026-06-14 04:19:48 +02:00
David Montero 2dcdabe72f fix(picow): web examples use relative URLs + a resilient server loop
The served page loads under /api/gateway/<id>/, so absolute fetches like
fetch('/on') hit velxio.dev/on instead of the chip — the LED/relay/servo
controls did nothing. Use relative paths (fetch('on')) so they resolve
under the gateway. The relay example now has real ON/OFF buttons and
wraps its blocking accept loop in try/except so a dropped browser
connection can't kill it.

e2e now fires two sequential requests (first with a browser-sized header)
against a non-resilient blocking server and asserts both are served.
2026-06-14 03:11:54 +02:00
David Montero 1aba6e2e48 feat(picow): surface the IoT gateway in the UI (parity with ESP32)
- SerialMonitor linkifies http://10.13.37.x (the Pico W subnet) the same
  way it already does http://192.168.4.x for the ESP32, turning the
  sketch's printed URL into an 'Open IoT Gateway' link.
- SimulatorCanvas shows the clickable WiFi badge for the Pico W too
  (normalizing its 'started' status, which carries the fixed IP, to
  got_ip so it reuses the ESP32 badge styling + launcher).
- async-led and servo-web examples print a clickable http://<ip>/ line
  so the gateway link appears (relay-web-server already did).

Gated e2e (CYW43_GATEWAY_E2E=1) drives the real emulator + a running
backend and asserts the served page comes back through /api/gateway.
2026-06-14 02:15:53 +02:00
David Montero Crespo 9fc0655612 i18n: fill 19 missing keys in de/fr/it/ja/ru
quotaModal.* (10), editor.share.updateFailed + visibility labels/hints (7),
editor.toolbar.exportBom + exportScreenshot (2) were present in en but absent
in de/fr/it/ja/ru. es/pt-br/zh-cn were already complete. Translated via
DeepSeek; interpolation tokens and JSON shape preserved, existing values
untouched.
2026-06-13 08:01:21 +02:00
David Montero fd0edd8c6d feat(cyw43): bridge Pico W DNS/TCP/UDP to the backend for real internet
Wi-Fi sketches on the emulated Pico W associate via the chip's built-in
virtual net (DHCP/ARP answered locally), but outbound traffic had no
route, so DNS/MQTT/HTTP failed with OSError -2.

Wire the emulator's outbound DATA path to the backend picow_net bridge:

- Cyw43Emulator forwards every outbound Ethernet frame EXCEPT DHCP/ARP
  (still answered locally) to firePacketOut -> the WS bridge, which NATs
  DNS/TCP/UDP to the real internet and injects replies back.
- The virtual net stays ON unconditionally and shares the backend's
  subnet, gateway and gateway MAC (10.13.37.0/24, gw 10.13.37.1). Nothing
  is mutually exclusive, so an absent or flaky bridge can never break the
  Wi-Fi association -- it just falls back to no-internet, as before.
- useSimulatorStore opens the bridge (cyw43.connect()) for Wi-Fi sketches.

Validated end to end against a running backend: WiFi connect + DHCP, DNS
resolves example.com, TCP connect + HTTP GET returns 200 OK. Gated e2e in
picow-bridge-e2e.investigate.test.ts (CYW43_BRIDGE_E2E=1).
2026-06-13 05:27:49 +02:00
David Montero 2639f80a22 fix(cyw43): word-align SDPCM frames so the F2 byte-swap preserves the tail
The CYW43439 F2 (radio frame) channel is word-oriented: the real chip
always drives frames padded up to a 4-byte boundary and the host reads
that word-aligned length, byte-swapping every 32-bit word on the way in.

encodeSdpcm built buffers of exactly 12 + payload bytes, so any frame
whose total length was not a multiple of 4 ended with a partial word.
The emulator's F2 read path (encodeFrameWords) byte-swaps whole words and
copies the leftover tail raw; the host's symmetric per-word swap then
mangles that final word, corrupting the last 1-3 bytes of the frame.

This was invisible for DHCP/ARP (UDP checksum 0 -> lwIP skips the check,
and the damage lands in trailing option padding) but silently dropped
every DNS answer and TCP segment (real checksum -> lwIP discards the
frame), so getaddrinfo()/connect() retried forever.

Pad the backing buffer to a 4-byte boundary while keeping the size header
at the true length, so the driver still parses exactly the real frame and
ignores the pad. Matches real hardware framing.
2026-06-13 05:27:38 +02:00
David Montero 13e0841681 fix(micropython): write LittleFS files with UTF-8 byte length
loadUserFiles passed content.length (UTF-16 code units) as the byte count
to lfs_write_file, but cwrap marshals the content to the heap as UTF-8. A
file with multi-byte chars (e.g. an em-dash in a comment) is then written
short by the multi-byte overhead, truncating the tail. The async-LED Wi-Fi
example (2 em-dashes) lost its last 4 bytes, turning the final
'asyncio.run(main())' into 'asyncio.run(main' -> SyntaxError at EOF. Use
the UTF-8 byte length so the whole file lands; ASCII files are unaffected.
2026-06-13 04:08:02 +02:00
David Montero 1061685e84 feat(cyw43): WiFi-now — virtual net handles association, bridge deferred
For the first deploy, keep the chip emulator's built-in virtual DHCP/ARP
net ON and leave the backend internet bridge dormant (not validated end
to end yet). A Pico W board now associates and gets a link-local IP
locally (isconnected True); outbound internet (MQTT/HTTP) has no route
until the picow_net bridge is wired. Revert is a one-liner in the store
(cyw43.wifiEnabled = hasWifi; cyw43.connect()) + setVirtualNet(null).
2026-06-13 03:33:15 +02:00
David Montero e68746ed57 test(cyw43): validate WiFi on the production RP2040Simulator path
Headless test that drives the REAL RP2040Simulator (attachCyw43 +
installCyw43PioHooks + lockstep PIO stepping in runFrameForTime), boots
the Pico W firmware, injects a WiFi-connect snippet over the raw REPL, and
asserts isconnected(). Result:

  PYBOOT
  ACTIVE False            (this fw's active() getter reports link status)
  CONN_OK 192.168.4.2     (DHCP-leased IP, isconnected() == True)
  MAINPY_DONE

Reaches link-up in ~31s wall — the production lockstep PIO stepping is
faster than the harness's setTimeout-cranked PIO.

Also fixes a real production bug: the RP2040 logger was
ConsoleLogger(LogLevel.Error) which THROWS on rp2040js unaligned-read
warnings — lwIP reads the IPv4 header at ethernet offset 14 on every
received packet, so WiFi would have crashed on the first DHCP reply.
Now constructed with throwOnError=false.

Gated behind CYW43_PROD_HARNESS=1 (boots real firmware, ~30s).
2026-06-13 00:43:48 +02:00
David Montero c10e63764c feat(cyw43): wire WiFi bring-up into production RP2040Simulator
Port the gSPI wiring proven in the boot harness into the real simulator
so WiFi works in the browser, not just the test:

- Non-dropping TX FIFO (head-pointer queue) in installCyw43PioHooks, so
  the 260-word F2 IOCTL writes aren't truncated, with the firmware/
  backplane bulk-write fast-path (inDiscardableWriteData) keeping the
  ~224 KB download cheap. Fully restorable on detach.
- Drive WL_HOST_WAKE (GPIO24) from emu.onHostWake, and re-sync the pin
  level after installCyw43PioHooks (loadMicroPython resets GPIO while the
  chip's queue persists).
- With a backend bridge attached, disable the built-in DHCP/ARP net so
  the bridge owns the network.

Production steps the PIO in lockstep with the CPU (pioStepAccum), so no
PIO-rate crank is needed (that was harness-only). The emulator-side fixes
(host-wake, F2 byte order, join events, BDC header, virtual DHCP/ARP) are
already shared. Not yet exercised in a browser e2e — the headless harness
is the verification today.
2026-06-12 23:48:04 +02:00
David Montero 4d631b3a98 feat(cyw43): virtual DHCP/ARP net — WiFi reaches LINK_UP (isconnected)
The Pico W now connects end to end with NO backend: status reaches
CYW43_LINK_UP (3) and network.WLAN().isconnected() returns True.

After association the STA's lwIP broadcasts DHCP DISCOVER and ARPs the
gateway over the cyw43 DATA channel. A self-contained virtual network
(new virtualNet.ts) answers them:
  - DHCP DISCOVER -> OFFER, REQUEST -> ACK (Ethernet+IPv4+UDP+BOOTP, valid
    IPv4 header checksum, UDP checksum 0), leasing 192.168.4.2 with gateway
    192.168.4.1.
  - ARP who-has the gateway -> is-at the AP MAC.
On by default (Cyw43EmulatorOptions.virtualNet); pass null when an
external packet bridge owns the network.

Also fixes injectPacket to prepend the 4-byte BDC header that chip->host
DATA frames need (same as the event-frame fix), so injected packets parse.

Boot harness now reports: STEP_CONNECT_CALLED status=3 / POLL 0 status 3
conn True / HARNESS_DONE.

Known: receiving packets triggers ~30 rp2040js unaligned-read warnings
(lwIP reads the IPv4 header at ethernet offset 14); non-fatal here, but
the production RP2040Simulator must use a non-throwing logger.
2026-06-12 23:43:14 +02:00
David Montero 74365d4ae9 feat(cyw43): WiFi associates — join events drive link up (status NOIP)
The Pico W now joins the virtual AP end to end: active(True) returns,
connect() runs the full WPA/SET_SSID sequence, and the link comes up.

Root causes fixed (each blocked the join):
- mcast_list GET returned empty, so the driver read its own request bytes
  as the address count (ASCII 'mcas' ~1.9e9) and looped ~2e9 times,
  hanging wifi_on. GETs now return a zero-filled buffer of the asked-for
  length (count 0 / status 0), never empty.
- Async event frames lacked the 4-byte BDC header the driver expects at
  SDPCM header_length, so it read the broadcast-MAC byte as data_offset
  and the payload pointed out of bounds (WRONG_PAYLOAD_TYPE). Prepend BDC.
- WLC_E_LINK signalled link-up via the reason field, but the driver
  checks ev->flags & 1. encodeEventFrame now takes a flags arg; LINK uses
  flags=1.
- Join needs WIFI_JOIN_STATE_KEYED, which only a WLC_E_PSK_SUP(status=6)
  event sets (connect(ssid, "") still configures the WPA supplicant).
  Emit it on a successful join.
- Join events were raised synchronously during the SET_SSID ioctl, so the
  driver processed them before cyw43_wifi_join set wifi_join_state=ACTIVE,
  wiping the bits. Defer events until just after the ioctl reply.
- Event-mask stored 4 bytes misaligned vs queueEvent's read offset.
- SET/GET kind bit is 0x2 (SDPCM_SET), not 0x1.

Remaining for status UP / isconnected: DHCP (needs the packet-transport
bridge or an emulator-side DHCP responder).
2026-06-12 23:33:24 +02:00
David Montero 6bdb590b0a perf(cyw43): fast-path firmware download in boot harness
Add PioBusSniffer.inDiscardableWriteData(): true while framing a large
non-F2 write (firmware/backplane bulk write the chip discards). The boot
harness drops those data words (keeping ~4 so the PIO raises TXSTALL,
which is all the driver's write path waits for) instead of bit-banging
the full ~224 KB through the PIO. F2/SDPCM IOCTL writes and every
count/command word are retained in full, so the bring-up still completes
the 23-IOCTL wifi_on sequence (F1 framing 3613 -> 97, F2 unchanged).

Also adds IPSR + PC-histogram sampling: confirmed the post-mcast_list
stall is thread-mode (no GPIO IRQ storm) inside MicroPython's host-side
cyw43_cb_tcpip_init (lwIP), above the chip emulation.
2026-06-12 23:01:00 +02:00
David Montero c4cbb17591 feat(cyw43): host-wake IRQ + F2 frame byte-order + SET/GET fix
Unblocks the full wifi_on IOCTL sequence in the boot harness (clm_load
through the 23-IOCTL bring-up, no crash):

- Drive WL_HOST_WAKE (GPIO24, active-high): the driver gates poll_device
  on this pin until its first packet (had_successful_packet), so without
  it the first IOCTL response is never read. Emulator now exposes
  onHostWake(level) and toggles it with the inbound-frame queue.
- Encode F2/SDPCM frame reads per 32-bit word (encodeFrameWords), same
  as register reads: the DMA-in sets channel bswap=true, so an un-encoded
  frame landed byte-reversed -> header_length read back as garbage and
  the driver dereferenced ioctl_header at an unaligned address (crash).
  Guarded to boot mode pass-through (no F2 traffic there; keeps unit tests).
- Fix SET/GET detection: SDPCM_SET is bit 1 (0x2), not 0x1; echo the
  kind bit in IOCTL responses.
- Add IOCTL/SDPCM debug counters + sequence log for the harness.

Harness (investigation, CYW43_HARNESS=1 only): non-dropping TX FIFO so
large F2 writes are not truncated, crank PIO steps/tick so the firmware
drains in wall-clock, GPIO24 host-wake wiring, CPU-fault + PC-histogram
+ PIO-state instrumentation.

Remaining: stall after mcast_list (#22) inside cyw43_cb_tcpip_init.
2026-06-12 22:42:49 +02:00
David Montero b172cadbc5 wip(picow): deterministic gSPI framing via per-transfer restart hook
cyw43_spi_transfer calls pio_sm_restart before each transfer's count words, so
hooking restart() to reset the sniffer makes framing deterministic across the
firmware-stream fast-path (no phantom-transfer carryover). Verified: restarts
fire 3625x (once per transfer), F1 phantom count drops, and the CLM IOCTL write
now frames correctly (cmd decodes to F2, 'clmload' payload). Wired into
RP2040Simulator + the harness.

Remaining (next session): the CLM/IOCTL write doesn't complete its payload and
wifi_on still fails (active()=False) — bus_init stalls at/around clm_load with
only 2 STATUS reads and goes idle. Next: trace the CLM write's DMA/PIO drain and
the SDPCM IOCTL response path. See findings.md F-13.
2026-06-12 21:21:28 +02:00
David Montero f183f8add2 wip(picow): Phase 3 instrumentation — confirm credit frame is queued+visible
debugInboundCount + STATUS-read tracking show initInbound=1, statusReads=2,
statusReadsWithPkt=2, finalInbound=1: the credit frame IS visible at both STATUS
reads (not a credit tight-loop). The driver reaches clm_load's F2-ready check
(passes) but the F2 IOCTL write never appears on the bus and bus_init returns.
Next: instrument the F2-write path. See findings.md F-13.
2026-06-12 20:18:50 +02:00
David Montero 847c2b894b wip(picow): Phase 3 diagnosis — harness function histogram + active() probes
Pins the connect blocker: zero F2 transfers (F0=11 F1=97 F2=0), so the host
never sends an IOCTL — it stalls on SDPCM bus credits in clm_load (STATUS shows
no F2_PACKET_AVAILABLE) and times out, so wifi_on fails and active() stays
False. Next: make the credit-granting frame visible in SPI_STATUS during the
stall. See project/picow-wifi-emulation/findings.md F-13.
2026-06-12 20:15:12 +02:00
David Montero 9197fcaaf6 wip(picow): CYW43 emulation — chip bring-up works, active(True) returns
Brings the Pico W CYW43439 gSPI emulation from "fails at the first register
read" to "the chip boots fully and MicroPython's network.WLAN().active(True)
returns" — validated end-to-end against the real RPI_PICO_W firmware via a
headless boot harness.

What now works (Phases 1-2):
- PioBusSniffer rewritten to the real cyw43_bus_pio_spi framing
  [out_bits][in_bits][cmd][write_data], skipping the two PIO loop-counter
  words. Self-healing: validates count1 (= tx_length*8-1, 4-aligned, <=2052)
  and skips non-conforming words — re-syncs after the extra word rp2040js
  pushes on large writes AND fast-paths the ~224 KB firmware stream.
- Dual word-order regime: boot 16-bit-LE (swap16x2 / swap16) flips to 32-bit
  big-endian (bswap32) at the SPI_BUS_CONTROL write. Calibrated empirically
  against the firmware. Sniffer reads the mode via setModeProvider().
- Cyw43Emulator: encodeReadWord (per-regime), readBytes-sized backplane reads
  with the value in the last word (response-delay pad), ALP+HT clocks and F2
  always ready, AI core registers (IOCTRL/RESETCTRL), interrupt register
  reports no errors, f1Mem echo store, SDPCM bus-credit granting + initial
  frame.
- RP2040Simulator: serves chip responses on rxFIFO.pull (on-demand) instead of
  racing the async DMA/PIO; passes readBytes through.

Not done yet (Phase 3+): connect() runs but stalls in the power-management /
save-restore phase before any F2/IOCTL traffic; packet transport (Tier 2) and
firmware-clocking perf are open. See project/picow-wifi-emulation/ for the full
research, phases, and findings.

The boot harness (picow-cyw43-boot-harness.investigate.test.ts) is gated behind
CYW43_HARNESS=1 so it stays out of the normal test run.
2026-06-12 20:04:06 +02:00
David Montero 4d80a9d1c3 fix(sim): Pico W MicroPython loads the RPI_PICO_W firmware (network + CYW43)
The RP2040 MicroPython loader always fetched the plain RPI_PICO build, which
ships no `network` module and no CYW43 WiFi driver. Every Pico W WiFi/MQTT
example therefore failed at `import network` ("no module named 'network'"),
which surfaced as a compile/run error in the editor.

- getFirmware()/loadUserFiles() are now variant-aware. pi-pico-w boards load
  RPI_PICO_W-20230426-v1.20.0 (network/socket/ssl + the CYW43439 driver) and
  write the LittleFS at the W board's flash offset (0x12c000, 212 blocks)
  instead of the plain Pico's 0xa0000/352. The W firmware spans flash to
  ~0xab000 and would otherwise be clobbered by the filesystem. Each variant
  gets its own IndexedDB cache key.
- The variant is selected by the presence of the already-wired CYW43 emulator
  (attachCyw43 runs for pi-pico-w boards only).
- loadMicroPython swaps in a fresh RP2040 each run, so the CYW43 PIO-FIFO hooks
  are re-installed on the new instance; otherwise the driver's gSPI traffic
  never reaches the emulator and WiFi never comes up.
- Bundle micropython-rp2040w.uf2 as the offline fallback.
- Point the ThingsBoard example at the simulator's Velxio-GUEST network.
2026-06-12 16:46:41 +02:00
velxio-deploy a19f5940ee chore(examples): refresh 1 thumb file(s) [auto] 2026-06-12 16:08:06 +02:00
David Montero Crespo 61c549de47
Merge pull request #237 from davidmonterocrespo24/fix/picow-wifi-examples-boardtype
fix(examples): move WiFi/MQTT 100-days examples to Pico W (network module)
2026-06-12 10:58:19 -03:00
David Montero a44a3e700f fix(examples): WiFi/MQTT 100-days examples must run on Pico W, not plain Pico
8 MicroPython examples that use `import network` (Blynk IoT relay, ThingsBoard
IoT, OTA update, DHT11 HTTP CSV logger, async LED control, web servo, websocket
LED, IoT relay web server) had boardType "raspberry-pi-pico". A plain Pico
(RP2040) has no WiFi and no `network` module, so they failed at runtime with
`ImportError: no module named 'network'` (the banner even shows "Raspberry Pi
Pico with RP2040"). Move them all to "pi-pico-w", which has WiFi + network.
2026-06-12 15:57:23 +02:00
David Montero Crespo 3c580cac36
Merge pull request #236 from davidmonterocrespo24/feat/esp32-wifi-mqtt-example
feat(examples): ESP32 WiFi + MQTT (PubSubClient) gallery example
2026-06-12 10:54:21 -03:00
David Montero 0fc76611b2 feat(examples): ESP32 WiFi + MQTT (PubSubClient) gallery example
Adds a self-contained ESP32 networking example for the /examples gallery
(addresses feature request #115). The sketch joins the emulator AP
"Velxio-GUEST", connects to a public MQTT broker (broker.hivemq.com:1883),
then publishes to its own topic and subscribes to it so each message
round-trips through the broker and toggles GPIO2 -- no external client or
local broker needed; just open the Serial Monitor.

Verified end to end in QEMU: WiFi associates (IP 192.168.4.15), DNS resolves
and outbound TCP to :1883 succeeds via slirp NAT. PubSubClient is auto-
installed via the example's `libraries` field.
2026-06-12 15:52:11 +02:00
David Montero fddc03aa60 fix(interconnect): classify Arduino Mega UART/I2C function-label pins
Follow-up audit after the ESP32 fix: classifyPin() was run for every board
against the protocol pin labels its element actually exposes. One real gap
remained -- Arduino Mega. Its dedicated SDA/SCL pins are only labelled (not
numbered), so I2C links drawn on them came back 'digital' and never bridged.
Map every Mega function label (TX/RX, TX0-3/RX0-3, SDA/SCL) to its pin number.

Audit result for the rest (added as board-protocols-audit.test.ts):
- Arduino Uno/Nano, Pico/Pico-W, STM32 Blue Pill: already OK.
- ESP32 / ESP32-C3: fixed earlier (esp32-uart-pin-classify).
- Raspberry Pi 3/4/5: OK -- the element labels pins by physical number (1..40)
  which normalize to BCM, so no function-label gap exists there.
2026-06-12 08:29:03 +02:00
David Montero 8ef730b9f7 fix(interconnect): classify ESP32 UART pin names so multi-board Serial works
Wiring two ESP32s TX2->RX2 (Serial2) or TX->RX for board-to-board serial
produced no data on the receiver: classifyPin() returned 'digital' for the
UART pins, so the Interconnect never installed the byte-level UART bridge.

Two causes in boardProtocols.ts normalizePinName:
- TX/RX aliases only matched boardKind === 'esp32' exactly, missing every
  variant (esp32-devkit-c-v4, esp32-cam, esp32-s3), and TX2/RX2 were not
  handled at all. Resolve them via startsWith('esp32') (esp32-c3 kept
  separate) and map TX2/RX2 -> GPIO17/16.
- 'GPIO17'-style labels fell into the 'GP' (RP2040) branch first, where
  parseInt('IO17') = NaN swallowed them to null. Exclude 'GPIO' from the
  'GP' branch so the ESP32 GPIO-prefix handling runs.

Adds esp32-uart-classify.test.ts (6 cases, green).
2026-06-12 07:21:47 +02:00
David Montero Crespo c01f9d8d75
Merge pull request #220 from ciegovolador/fix/buzzer-sample-accurate-audio
fix(sim): sample-accurate, glitch-free buzzer audio (+ metronome quality tests)
2026-06-12 00:40:46 -03:00
David Montero Crespo 6ac425c2fb
Merge pull request #227 from davidmonterocrespo24/fix/release-version-autobump
fix(ci): auto-increment release version on each Discord announce (baseline 3.0.0)
2026-06-11 09:56:08 -03:00
David Montero f27303264f fix(ci): auto-increment release version on each Discord announce; baseline 3.0.0
The Discord release-notify workflow read the version from
frontend/package.json but never wrote it back, so every merge to release
announced the SAME version (the CHANGELOG ended up with two "[2.0.1]"
entries). Now, after generating the CHANGELOG and before announcing, the
workflow bumps the PATCH in frontend/package.json and commits it alongside
the CHANGELOG to release. Each merge advances the counter:
3.0.0 -> 3.0.1 -> 3.0.2 ...

Also sets the baseline to 3.0.0 so the next release is announced as v3.0.0.
To jump the major/minor, edit frontend/package.json on the release branch
(e.g. "version": "3.1.0") and the next merge continues from there.
2026-06-11 14:53:03 +02:00
ciegovolador 9b86c816c1 fix(sim): keep PwmCallback 2-arg compatible via arity dispatch
Revert the earlier approach of widening the existing PWM-callback assertions to
accept the new timeMs arg — that masked a contract change rather than fixing it.
Instead, updatePwm now hands the optional timeMs only to listeners that declare
a 3rd parameter (cb.length >= 3) — i.e. the buzzer, which needs the precise
onset time. Plain (pin, dutyCycle) listeners, and the existing
toHaveBeenCalledWith(pin, dutyCycle) tests, see an unchanged 2-arg call, so the
original PwmCallback contract is preserved.

Add a PinManager test locking the dispatch: a 2-param listener stays 2-arg; a
3-param listener receives timeMs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 03:29:44 -03:00
ciegovolador e6ba5ed9c7 test(sim): update PWM-callback assertions for the new timeMs arg
The sample-accurate scheduling (06526c7) added an optional 3rd `timeMs`
argument to PwmCallback / updatePwm, which broke 9 existing strict
toHaveBeenCalledWith(pin, duty) assertions (PinManager, AVRSimulator,
mega-emulation, attiny85). Match the real signature: PinManager drives
updatePwm directly with no timeMs (assert `undefined`); the AVR OCR-poll path
computes timeMs = cpu.cycles / 16000 (assert `expect.anything()`).

Leaves one pre-existing red — component-to-spice "custom-chip missing fixture"
— which fails on master too and is unrelated to this PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 03:14:32 -03:00
ciegovolador 6a1f79e331 fix(sim): monophonic buzzer guard — replace note on pitch change (no stacking)
A melody / continuous tone (consecutive tone() with no noTone() between) is
back-to-back nonzero-OCR PWM writes with no note-off, so startTone() overwrote
activeOsc without stopping the previous node — oscillators stacked and were
never stopped (reported: created 6, started 6, never stopped 6).

Add a monophonic guard at the top of startTone(): release the live note
(gain ramp + stop) before starting the new one, so a pitch change REPLACES
rather than STACKS. Extract a shared releaseActive(off) helper (also used by
stopTone). Add two melody tests: one asserts starts === stops (no orphans),
monotonic onsets and per-note pitch; one asserts a melody ending without a
trailing noTone() leaves only the final note ringing (stops === starts - 1).

The metronome path is unaffected (each click is an onset→note-off pair, so the
guard never fires there); the three existing metronome tests stay green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 02:35:33 -03:00
David Montero Crespo 871f89caf0
Merge pull request #226 from davidmonterocrespo24/fix/sd-card-add-button-style
fix(microsd): style the SD Card upload panel for the dark property dialog
2026-06-11 01:38:00 -03:00
David Montero 878e84e98a fix(microsd): match the SD Card upload panel to the dark property dialog
The panel was styled with light-theme CSS-var fallbacks that render wrong on
the editor's dark (#2d2d2d) property dialog:
- "Add files" button used `var(--surface, #f6f6f6)` + light border, so it
  rendered a washed-out light-gray box that looked broken. Restyle it as a
  primary action like `.rotate-button` (solid #007acc, white text, hover lift).
- Section divider and secondary text used light fallbacks (#e2e2e2 / #777);
  switch to the dialog's dark values (#444 border, #aaa text).

Cosmetic only.
2026-06-11 05:20:48 +02:00
David Montero 190bb204a0 test(spice): exclude custom-chip from the static-fixture catalog check
The `custom-chip` SPICE mapper emits its sources from getChipDrivenPins()
(the chip's live driven output pins), so a static pin/property fixture can
never exercise it -- it always returns null. The "every mapped metadataId
has a test fixture" check flagged it as missing a fixture, failing the
suite. Exclude it via a RUNTIME_STATE_MAPPERS set; custom-chip SPICE
behaviour is covered by the chip-bus integration tests.

Pre-existing since 4cb5748 (custom-chip first-class circuit nodes).
2026-06-11 04:19:35 +02:00
velxio-deploy 2408ccde54 chore(examples): refresh 1 thumb file(s) [auto] 2026-06-11 04:18:32 +02:00
David Montero 22de488de2 feat(microsd): SD-over-SPI card storage for AVR, RP2040 and ESP32
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).
2026-06-11 03:59:53 +02:00
David Montero 50823e9d49 fix(rp2040): keep delay()-based sketches real-time on slower hosts
The RP2040 core (125 MHz Cortex-M0) is ~8x heavier to emulate than the
AVR. The run loop used a FIXED per-frame cycle budget, and arduino-pico
delay() busy-waits the timer (no WFI), so a host that cannot sustain
125M instr/s rendered a 1s blink every 4-5s (sim ran in slow motion).

- Derive the frame budget from the MEASURED wall-clock delta (mirrors
  AVRSimulator) instead of assuming a perfect 60fps.
- Add IdleSpinDetector: recognise a side-effect-free busy-wait spin and
  advance the clock over it (capped at the next timer alarm / scheduled
  pin change) instead of executing every idle cycle - the same idea the
  WFI fast-path already uses for sleep(). Conservative: a bit-bang loop,
  an input-poll that just saw its pin move, or a loop that calls out are
  never elided; a false positive only ever advances time up to the
  wall-clock budget, never past the next event.
- Bound WFI sleeps to the wall-clock budget so they advance in real
  time across frames rather than leaping ahead.

Cuts emulation work for a delay-bound sketch ~1900x (125M -> ~65k
instructions per simulated second) so it tracks wall-time even on hosts
that cannot emulate 125 MHz in real time. Public API unchanged;
step()/stepCycles() untouched.

Adds rp2040-realtime.test.ts: IdleSpinDetector unit tests plus
end-to-end scheduler tests driving a real rp2040js core through a
hand-assembled busy-wait loop (no firmware fixture needed).
2026-06-10 18:21:29 +02:00
David Montero c580a7e418 feat(library-manager): read-only libraries.json file + drop Uninstall for shared index libs
(1) The explorer's per-board manifest entry is renamed velxio.json -> libraries.json
and clicking it now opens a READ-ONLY JSON view of that board's declared libraries
(board.libraries) in the editor, instead of the modal. New editor state
manifestViewBoardId: when set, CodeEditor renders a read-only Monaco showing
{libraries:[...]} live; opening/activating any real file clears it. No file is
added to the workspace, so nothing touches compile or save. Library actions are
done in the Library Manager modal (toolbar button).

(2) Drop the 'Uninstall' button for shared index/cache libraries — you can't
uninstall a copy everyone shares (content-addressed cache). Only your own custom
.zip uploads keep a 'Remove' (per-user store). Index libs: just Add to / In project.
2026-06-09 15:49:14 +02:00
ciegovolador b06f500ad6 fix(sim): mature the buzzer — per-note oscillators, sim-time scheduling, ramps + metronome tests
Builds on the previous commit; reworks the buzzer audio for glitch-free,
cross-browser playback and adds a metronome quality suite.

- Per-note oscillators with short attack/release ramps, instead of one
  long-lived oscillator gated by gain: a fresh fixed frequency per note and no
  gain/frequency automation on a persistent node — Firefox in particular clicks
  and glitches the pitch otherwise.
- Schedule onsets by their SIMULATED inter-onset spacing (exact, even) with a
  light latency hold, instead of a wall-clock average. Turning a control (BPM,
  K…) re-locks immediately and the rhythm stays even — no bursts, no overlaps,
  no audio drifting away from the display.
- Place each note-off relative to its own onset, preserving the exact click
  length from the simulation (the onset scheduler now tracks onsets only).
- Poll PWM every 256 cycles (was 64): finer than any audible pulse, lighter on
  the frame loop.
- New src/__tests__/buzzer-metronome.test.ts: drives the buzzer as a metronome
  against a controllable audio clock and asserts even spacing, one oscillator
  per click with no overlap, correct pitch per metric level, burst absorption,
  and a clean re-lock on tempo change.

All simulation-parts + metronome tests pass (57).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 09:22:57 -03:00
ciegovolador 06526c7922 fix(sim): sample-accurate buzzer audio — precise PWM detection + display-aligned scheduling
A PWM-driven buzzer (analogWrite / Timer tones) was chaotic and unusable as a
metronome. Causes, all on the PWM path:

1. PWM was polled once per animation frame AFTER the cycle loop, so short clicks
   that started and ended within one frame were merged or lost, and onsets were
   quantised to the frame.
2. The buzzer started the oscillator with `oscillator.start()` (no scheduled
   time) — frame-delivery jitter and per-onset oscillator churn.
3. The digital HIGH/LOW path also fired on the ~490Hz PWM carrier edges,
   injecting spurious onsets (OCR read as 0 → 20kHz squeaks).

Fix:
- AVRSimulator: poll PWM sub-frame (every 256 cycles) so no pulse is merged or
  lost; pass the precise simulated time through updatePwm.
- PinManager: PwmCallback / updatePwm carry an optional timeMs (backward compat).
- Buzzer: one continuous oscillator gated by the gain node, each on/off scheduled
  on the AudioContext clock. The schedule predicts the next onset at a smoothed
  interval (de-jittering the simulator's bursty per-frame delivery) and holds a
  small bounded latency so the click stays aligned with the on-screen playhead
  (driven from the same clock) instead of drifting behind it. A `pwmActive` flag
  mutes the digital path once hardware PWM drives the pin.

Result: onset jitter for a firmware metronome drops from chaotic (σ ≈ 250ms,
dropped/extra beats, unbounded audio latency) to σ ≈ 15ms at ~30ms latency —
steady and aligned with the display. All 54 simulation-parts tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 22:46:21 -03:00
David Montero 02c8ad756d feat(library-manager): collapse to a single unified tab with state-aware row actions
Remove the 3 tabs (In project / Search / Installed). One list now: browse your
installed + custom libraries by default, search the index when you type. Each
row is state-aware:
  + Add to project   — installs if needed, then declares it on the active board
  In project (toggle) — click to remove from this board's manifest
  Uninstall / Remove  — free the cache / remove your custom upload
'Install' is folded into 'Add to project' (install-on-add) for simplicity. The
per-board manifest (board.libraries) stays the compile scope. The pro custom-zip
upload button still injects into .lib-modal-header. The in-modal velxio.json
editor tab is gone (the manifest is shown by the explorer's libraries.json file).
2026-06-08 15:51:34 +02:00
David Montero 7674b7b15e feat(examples): declare library manifests for the two lib-using AVR examples (P2.4-arduino examples)
lcd-hello -> ['LiquidCrystal'], uno-servo -> ['Servo']. These were the only
non-ESP32 gallery examples using a USER library without a manifest; loading +
compiling them now sends the library scope (resolved from the content-addressed
cache) instead of falling back to the global scan-all. Every other non-ESP32
example is core-only (Wire/SPI are core-bundled; the RP2040 core bundles Servo,
so pico-servo needs no manifest) or already declared its libraries.
2026-06-08 04:24:02 +02:00
David Montero 97f390719f feat(P2.2c): show per-user custom libs in the Library Manager + autocomplete
The Library Manager Installed tab + the velxio.json add-autocomplete now merge
the user's per-user custom uploads (getCustomLibraries -> GET /api/pro/libraries/
custom) with the shared global index list, so users can see and reuse their own
uploads (which live in the per-user store, not the global list). A custom lib's
button removes it via the per-user delete endpoint (not arduino-cli uninstall,
which would not find it). Degrades to [] for OSS/anon.
2026-06-07 19:58:13 +02:00
David Montero cc40bda3eb feat(P2.2): owner falls back to requester + auto-declare uploaded custom lib
- compile.py: owner_id = project owner ELSE the requester (so an unsaved
  compile resolves the libs the user just uploaded, which are their own);
  threaded requester_id into _run_compile from both call sites.
- LibraryManagerModal: on a custom .zip upload, auto-add the lib to the active
  board's velxio.json + show the Project tab, so the compile resolves it via the
  owner per-user path (the upload now lands in the per-user store, not the
  shared dir, so it must be declared to be found).
2026-06-07 17:20:12 +02:00
David Montero 96b8ca309e feat(library-manifest): one velxio.json per board, grouped with its code
Moved the velxio.json entry out of a single top-level row (ambiguous about
which board it applied to) into EACH board's file group, next to that board's
sketch. Each board now shows its own velxio.json with its own declared-library
count; clicking it switches to that board and opens the Library Manager on its
list. Makes the per-board manifest model unambiguous.
2026-06-07 07:17:00 +02:00
David Montero 8617d3b224 feat(library-manifest): per-board manifests + autocomplete
Library manifests are now PER-BOARD (each board carries its own velxio.json),
so two boards in one project can use different (even conflicting) libraries
without clashing — the multi-board extension of the no-clash guarantee.

- board.libraries on BoardInstance + serialisableBoard: rides in boards_json,
  so it round-trips, dirty-checks, autosaves and restores natively. This also
  removes the load-restore hacks (useLibraryManifestStore + applyProjectManifest
  deleted): the manifest is plain board state.
- loadProjectState now restores per-board boardOptions/spiffsFiles/libraries
  (it previously dropped them).
- EditorToolbar single + compile-all send the COMPILING board's libraries.
- Backend compile.py prefers the client's per-board request.libraries; the
  project-level libraries_json (now the union of all boards) is the fallback.
- buildLoadPayload migrates pre-per-board projects: seed each board with the
  project union so they keep compiling scoped.
- Library Manager 'In project' tab edits the ACTIVE board's velxio.json (shows
  the board name) and the add field is now an autocomplete (installed libs +
  index search) so users pick from a list instead of typing names.

Deletes useLibraryManifestStore.ts + applyProjectManifest.ts.
2026-06-07 06:07:48 +02:00
David Montero c93924541c feat(library-manifest): user-facing velxio.json config + load-restore
End users can now configure a project's declared libraries (the compile scope):
- Library Manager gains an 'In project' tab = the project's velxio.json:
  declared libs as removable rows, quick add-by-name, and a raw velxio.json
  editor. Installing a library auto-adds it to the project. Installed-tab rows
  get an 'Add to project' toggle.
- FileExplorer shows a velxio.json entry (with declared count) that opens the
  Library Manager via a window event the toolbar listens for.
- applyProjectManifest(): restore a saved project's manifest into the store on
  load so the editor/toolbar/Library Manager/velxio.json reflect it.
- computeProjectStateHash() includes the manifest so declaring a library marks
  the project dirty and autosaves.

Note: the OSS ProjectByIdPage also calls applyProjectManifest for parity, but
velxio.dev routes the pro-overlay ProjectByIdPage (wired separately).
2026-06-07 05:08:34 +02:00
David Montero ee41f361b6 fix(frontend): don't clobber a project's saved library manifest on save
buildSavePayload omitted libraries_json=[] whenever the manifest store was empty
— so an autosave right after loading a project (whose manifest the store hadn't
restored) wiped the saved manifest. Now omit libraries_json entirely when the
store value is null (unknown), so the backend preserves the saved manifest. The
compiler reads it server-side regardless (get_project_libraries hook).
2026-06-07 02:13:58 +02:00
David Montero 4a21c4f938 fix(frontend): restore project library manifest inside buildLoadPayload (P2.4)
The inline manifest-restore in the load .then was being tree-shaken out of the
lazy ProjectByIdPage chunk (the deployed bundle had the save wiring but not the
load). Move it into buildLoadPayload, which is an exported helper (used by tests)
so its body is never dropped. Reloaded projects now re-send their manifest.
2026-06-07 01:13:55 +02:00
David Montero 288ab46521 feat(frontend): persist + restore project library manifest (P2.4 projects)
Saved projects now round-trip their declared library manifest (compile scope):
buildSavePayload includes libraries_json from useLibraryManifestStore; loading a
project restores it (and clears any stale example manifest). Existing projects
load with an empty manifest -> legacy scan-all (unchanged); new saves capture
whatever manifest is active. Pairs with the backend libraries_json column.
2026-06-07 00:47:39 +02:00
David Montero e947f1e600 feat(frontend): send the example library manifest as the compile scope (P2.3)
Activates manifest-scoped ESP-IDF resolution for the gallery. loadExample now
records the example's declared libraries in useLibraryManifestStore; EditorToolbar
passes them to compileCode, which sends them as `libraries` in the compile
request. The backend then merges exactly those libraries (P2.0 scope) instead of
picking a stray same-named lib from the shared dir.

Safe: a core-only example sends null (legacy scan-all); a stale/incomplete
manifest degrades to scan-all via the backend graceful fallback, never a wrong
build. Ignored by the backend for non-ESP32 (arduino-cli) boards. Example
manifests were completed (incl. transitive deps) in c671c9b.
2026-06-07 00:10:15 +02:00
David Montero c671c9b21c data(examples): complete ESP32 example library manifests with transitive deps (P2.4)
Auto-completed the library manifests for the 9 ESP32-family examples that use
external libraries, so each declares its full dependency set (direct +
transitive). Found genuinely-missing deps that the previous fields omitted:

- esp32-dht22, c3-dht22:  + Adafruit Unified Sensor
- esp32-mpu6050, esp32-bmp280, esp32-oled, esp32-doom:  + Adafruit BusIO
- esp32cam-lcd-preview:  add manifest [Adafruit GFX Library, Adafruit BusIO, Adafruit ILI9341]

Each completed manifest was validated by compiling the example against ONLY
its manifest (manifest-scoped resolution, no fallback). esp32-servo / c3-servo
were already complete. This unblocks turning on scoped resolution for the
gallery (P2.3): with complete manifests, scope picks the declared libs and
excludes strays, and the P2.3-safety fallback covers any residual gap.
2026-06-06 20:14:17 +02:00
velxio-deploy 11fabfc67c chore(examples): refresh 3 thumb file(s) [auto] 2026-06-06 08:56:44 +02:00
David Montero ee88479356 chore(examples): add 13 missing gallery thumbnails 2026-06-06 08:46:48 +02:00
David Montero Crespo 6b281c9d6d
Merge pull request #217 from davidmonterocrespo24/feat/chipbus-phase0
Feat/chipbus phase0
2026-06-06 03:18:45 -03:00
David Montero Crespo 70558eb31b feat(examples): mixed digital+analog coexistence demo (MCU + AND gate + transistor)
A board example proving digital and analog coexist in ONE circuit: the Arduino
drives two logic levels, a physical AND gate combines them, and the AND output
switches an NPN 2N2222 transistor that drives the "motor" LED. Verified live: it
compiles, the MCU drives the AND gate (5 V), the transistor conducts and the LED
lights — MCU -> logic gate -> transistor -> load works across the digital and
ngspice motors together.

Known limitation (sim-mixedmode step 2, pending): the ngspice side does not
re-solve on every MCU pin edge, so a fast (1 Hz) blink does not track in real
time — the analog output changes on a slower cadence. User-driven / slow changes
track fine. Snapshot updated for the new example.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 23:09:41 -03:00
David Montero Crespo a5edeb74c7 fix(examples): AND Gate Alarm uses latching slide switches (was momentary buttons)
The AND Gate Alarm needed BOTH inputs HIGH at once, but it used momentary
pushbuttons buffered through an Arduino — with one mouse you can only hold one
button at a time, so the AND never fired and the alarm could never be
demonstrated.

Rebuilt it as a board-less digital circuit: two SLIDE switches (they latch) feed
a real AND gate that drives the alarm LED. Slide both switches ON and they stay,
so the alarm arms. No MCU / compilation — it runs on the digital gate engine.
Verified live: the LED lights only on 11 (00/01/10 -> off, 11 -> on).

Snapshot updated: the new board-less and-gate-alarm netlist, plus the digital
bucket count label (38 -> 39) from the earlier ripple-counter example.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 22:42:15 -03:00
David Montero Crespo b93b1b42c5 feat(digital-gate-engine): 4-bit ripple counter gallery example + sequential controller fix
Adds the first board-less SEQUENTIAL gallery example (digital-ripple-counter-4bit):
four T flip-flops chained into a ripple counter, LEDs showing the binary count,
clocked by a slide switch. Impossible on the SPICE engine (no edge detection at
DC) - it runs on the digital gate engine.

Controller fix (found by testing the counter live): the controller rebuilt the
network on every change, which reset flip-flop state so a counter never counted.
Now the network is built once and KEPT ALIVE; a switch toggle applies
incrementally via setSwitch (preserving sequential state), and a rebuild happens
only on a structural change (components/wires). Correct for combinational AND
sequential circuits.

examples-digital.test.ts: flip-flop examples are digital-engine-only, so they are
exempt from the SPICE-mapping / has-a-gate / netlist checks (the "logic" check
now accepts a gate OR a flip-flop). digitalgate-engine-examples: a correctness
test clocks the real counter example and asserts it counts 1..15,0 in binary.

Verified live (?digitalgates default ON): the counter counts 0..6 on the canvas;
and the complex examples all work - comparator-4bit (A=B correct), decoder-3to8
(perfect one-hot x8), alu-slice-1bit (32 combos deterministic), multiplier-2x2
(3*3=9, 7 distinct products), adder-subtractor-4bit (5+3=8).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:58:42 -03:00
David Montero Crespo 8aa6e93460 feat(digital-gate-engine): Phase 5 - sequential logic (D/T/JK flip-flops)
Flip-flops are edge-triggered and hold state, which the combinational settle
kernel cannot model alone. buildDigitalNetwork now gives each flip-flop explicit
state + rising-CLK-edge detection (reusing the LogicGateParts sample semantics):
sample the data nets on the edge, drive Q + Qbar. Because a flip-flop only
updates on the clock edge, a Q->D / Q->CLK feedback (counter / shift register)
does not oscillate the settle loop. isAllDigital now accepts a gate OR a
flip-flop, so pure sequential circuits qualify.

Test digitalgate-sequential (4): D (capture + hold), T (toggle), JK
(hold/set/reset/toggle), and a 2-bit ripple counter (FF0.Qbar clocks FF1)
counting 1,2,3,0,1 - impossible on the SPICE path (no edge detection at DC, no
SPICE mapper). The controller already routes all-digital circuits through
buildDigitalNetwork, so a board-less counter/shift-register example would run
live; authoring those gallery examples is the only follow-up. Full digitalgate
+ examples-digital + circuit-simulation-service suites green (127 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:16:43 -03:00
David Montero Crespo 77c85eefac feat(digital-gate-engine): Phase 3 core - digital/analog boundary handoff
buildMixedNetwork evaluates the gate (digital) side of a MIXED circuit on the
settle kernel and exposes the boundary with the analog (ngspice) domain. Unlike
buildDigitalNetwork it does not bail on non-primitive components - those are the
analog side; their pins mark the nets they touch as boundary. Exposes
boundaryNets, readBoundary(net) (digital->analog: the gate-driven level to seed
an ngspice voltage source) and setBoundaryInput(net, level) (analog->digital:
ngspice's solved+thresholded level, which re-evaluates downstream gates).

Test digitalgate-mixed-boundary (4): the boundary nets are exactly the
digital/analog bridges; both directions track; a digital->analog->digital
coupler loop converges. No ngspice needed - the analog side is supplied by the
test. Wiring the handoff to the live ngspice netlist (0/Vcc sources + threshold
+ settle<->solve iteration) is the remaining step; it needs the running solver
(the node loader is broken by a pre-existing path bug) and a mixed example.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:01:05 -03:00
David Montero Crespo 4f4ee0bf60 feat(digital-gate-engine): sweep all 38 examples + default the flag on
Phase 4 (brought forward before the mixed-mode boundary). digitalgate-sweep
proves the engine handles 38/38 gallery digital examples: every one builds,
resolves every LED, and never oscillates. Tightened isAllDigital to also require
at least one logic gate, so a degenerate analog {source, resistor, LED} circuit
stays on ngspice rather than being claimed by the digital path. Flipped
digitalGatesEnabled() default to ON (override with ?digitalgates=off).

Full frontend suite 2120 pass / 5 fail — the 5 are the same pre-existing
unrelated failures (ngspice node-path, attiny85 arduino-cli, component-to-spice
catalog); the default flip adds no new breakage and examples-digital +
circuit-simulation-service stay green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 20:29:32 -03:00
David Montero Crespo b08df89c9b feat(digital-gate-engine): evaluate logic gates on the event-driven kernel
Board-less digital circuits (logic gates + switches + LEDs) run today as ngspice
analog B-sources, which is fragile for deep logic: a 4-bit ripple adder re-solves
but never lights its result LEDs live. This adds an event-driven digital motor
that reuses the multichip-bus settle kernel, so the same engine that boots a Z80
over a chip bus evaluates a gate network exactly and instantly.

Phases 0-2 (project/digital-gate-engine/), all behind ?digitalgates=on (default
OFF — flag off is byte-for-byte the old behaviour):

- digitalGateEngine.ts: buildDigitalNetwork(components, wires) does union-find
  over the wires (merging pass-through resistors), identifies the rail/gnd from
  the signal-generator, registers drivers (rail STRONG-1, gnd 0, pull resistors
  PULL, slide-switch as a pass-gate) and event-driven gates (reusing the
  LogicGateParts boolean semantics), settles on busKernel, and exposes
  setSwitch / readLed / netOf. Tolerant of both the raw example `type` and the
  store `metadataId`. Returns {ok:false} for any non-primitive, so mixed/analog
  circuits stay entirely on ngspice.

- digitalGateController.ts + a SimulatorCanvas useEffect: when the flag is on and
  the circuit is all-digital, rebuild from the store on switch-toggle / load
  (rAF-coalesced) and paint the wokwi-led DOM. CircuitSimulationService.tick()
  skips the SPICE solve for all-digital circuits when the flag is on, so the two
  motors never fight over the LEDs.

Tests: digitalgate-kernel (22 — single gates -> half/full adder -> 4-bit
adder/subtractor -> exhaustive ADD 256 -> mux/decoder/comparator/parity/
multiplier) and digitalgate-engine-examples (6 — the real gallery data for
and/or/xor/not + the full adder/subtractor). Verified live: ?digitalgates=on
lights the adder's result LEDs that the SPICE path leaves dark. Full suite
2117 pass / 5 pre-existing unrelated fails.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 20:21:33 -03:00
David Montero Crespo 47adb0b1c8 fix(chipbus): Galaksija boots + displays + types live in the browser
The gallery example loaded but the Z80 never visibly ran: the screen stayed
frozen on garbage. Two multi-chip async-load races, neither caught by the
existing headless tests (which drive RESET manually and attach the display
before boot):

1. RESET edge-vs-level race. The Z80 only left reset on the RISING edge of
   RESET (a pin watch). In the browser the 7 chips instantiate asynchronously,
   so the small power-on-reset chip releases RESET before the larger Z80 has
   registered its watch -> the edge is lost and the CPU stays in reset forever.
   Fix: on_clock samples the RESET level (hardware-accurate; RESET is
   level-sensitive) so a missed edge self-corrects. An undriven RESET reads low,
   so the CPU safely stays in reset until something drives it high.
   Repro/guard: chipbus-galaksija-reset-race (race ordering must still boot).

2. Display-snoop load-order race. galaksija-display was a passive write-snoop;
   the ROM paints the screen ONCE at boot then idles, so a display that comes up
   late misses every write and shows stale content forever. A snoop cannot
   recover writes it never saw. Fix: fold the screen into the RAM chip
   (galaksija-ram-display) and render from the ACTUAL video RAM (0x2800-0x2BFF,
   internal 0x0800 with A0-A12 wiring) on a ~30 fps timer - correct regardless
   of load order, exactly how the real machine scans video RAM.
   Repro/guard: chipbus-galaksija-display-snoop-race (late snoop shows nothing)
   + chipbus-galaksija-ram-display (renders even when first paint is post-boot).

The example now has 6 chips (RAM+display merged, gdisp dropped), 76 wires.
Verified live in the browser: boots to "@'READY", shows the ">" prompt, and
pressing A echoes ">A_" through keyboard -> Z80 -> video RAM -> display. The
full chipbus suite is 45/45.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:47:30 -03:00
David Montero Crespo 94627b99d2 feat(chipbus): Galaksija keyboard - type BASIC over the bus
Adds a memory-mapped keyboard so you can type into the Galaksija. Based on
the libretro Galaksija core's scheme (not guessed): reading 0x2000+offset
returns 0xFE when the key at that matrix offset is held, 0xFF otherwise;
the keyMap gives the offset per key ('A'=1 ... Enter=48, Space=31, etc.).

- galaksija-keyboard.c: drives reads of 0x2000-0x203F from a keys[] table and
  exports set_key(offset, down) for the host to push key events. Never drives
  outside the keyboard range.
- galaksija-ram.c: ram-64k variant that yields reads of 0x2000-0x203F to the
  keyboard (writes still go to RAM), so the two never fight for the bus.
- ChipRuntime: ChipInstance.hasKeyboard + setKey() expose the chip's set_key.
- CustomChipPart: bridges browser keydown/keyup (by KeyboardEvent.code, via
  GALAKSIJA_KEY_OFFSET) into the chip, ignoring keystrokes while the code
  editor or an input is focused so typing code is never hijacked.
- The gallery example gains the keyboard chip (now 7 chips, 99 wires) and uses
  galaksija-ram.

Test chipbus-galaksija-keyboard: pressing 'A' (offset 1) makes the BASIC
monitor echo "A" after its ">" prompt and advances the cursor. 41 chipbus
tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:37:42 -03:00
David Montero Crespo a393e3e91d feat(chipbus): Galaksija home computer gallery example + browser perf throttle
Ships the full Galaksija (1983 Z80 home computer) as a runnable Retro
gallery example, plus the pieces needed to run a multi-chip bus live in the
browser.

Gallery example (examples-retro-intel.ts, id 'galaksija-z80-computer'):
Z80 + galaksija-rom (public-domain ROM A+B) + ram-64k + inverter (A13
decode) + galaksija-display + a power-on reset chip, wired chip-to-chip
over the bus (76 wires), no board. Click Resume and it boots the real ROM
to the "READY" prompt on the green display. Chip wasm is embedded
(wasmBase64) so it runs without a backend compile.

- ChipRuntime.tickTimers gains a wall-clock budget (CustomChipPart passes
  6 ms): a faithful-but-slow event-driven bus can't run a real-time CPU in
  one animation frame, so without a cap a Z80 fetching over the settle
  kernel froze the tab. With the budget the sim advances slower than real
  time (boots over a few seconds) and the UI stays responsive; fast
  single-chip examples finish under budget and are unaffected.
- galaksija-display: blits its framebuffer on a ~30 fps timer instead of on
  every character write, so a clear-screen burst doesn't flood the canvas.
- reset-gen: power-on reset (pulses RESET high, ties WAIT/BUSREQ/INT/NMI
  high) so the machine boots on Resume without a manual reset.
- chipbus flag now defaults ON (override with ?chipbus=off): chip-to-chip
  buses are a core capability; single-chip and board nets never take this
  path, so the only thing enabled is multi-chip buses, previously broken.

Verified live in the browser: the example boots and renders "@'READY" with
the ">_" prompt, responsive. Full suite 2084 pass (5 pre-existing,
unrelated env failures).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:49:33 -03:00
David Montero Crespo a3562ba93f fix(chipbus): Galaksija display renders legible text (ASCII font8x8)
Galaksija stores ASCII codes in its 0x2800 video RAM (verified by snooping
the boot: it writes "@'READY" + ">_" prompt). The original CHRGEN ROM uses
a hardware-specific addressing that does not map char-code*8 to a glyph, so
rendering through it produced garbled output. Render the ASCII codes with
the public-domain IBM/VGA 8x8 font (font8x8 by Daniel Hepper / Marcel
Sondaar) instead -- legible green-on-black phosphor text. The boot screen
now reads "@'READY" with the ">_" input prompt, exactly like a real
Galaksija. Tests updated to check the bright-green channel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:21:17 -03:00
David Montero Crespo e3d21cd6fc feat(chipbus): Galaksija video display chip - full computer renders READY
galaksija-display.c: a 32x16 text video chip that renders the Galaksija
video RAM. It is a passive bus snoop -- watches WR + address + data, and on
a write into the 0x2800 video region stores the character and renders that
cell into a 256x128 framebuffer using the public-domain CHRGEN font (code*8,
bit 0 = lit). It never drives the bus. The host blits the framebuffer to the
chip canvas (vx_framebuffer_init / vx_buffer_write).

Two tests:
- chipbus-galaksija-display: snoop+render smoke test (a write of 'R' to
  0x2802 lights its cell; unwritten cells stay blank).
- chipbus-galaksija-computer: the COMPLETE machine over the chip-to-chip bus
  (Z80 + galaksija-rom + ram-64k + inverter decode + galaksija-display) boots
  the public-domain ROM and renders the monitor's "READY" prompt on screen.

40 chipbus tests across 10 files pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:14:40 -03:00
David Montero Crespo 8441d370d3 test(chipbus): a real Galaksija (1983 Z80 home computer) boots over the bus
The public-domain Galaksija ROM (Voja Antonic; ROM A monitor + integer
BASIC, ROM B float BASIC, 8 KB) runs on a standalone Z80 + external ROM +
RAM + an inverter for address decode, all chip-to-chip over the shared bus,
no board:

  ROM 0x0000-0x1FFF   rom.CE = A13
  RAM 0x2000-0x3FFF   ram.CE = NOT A13   (the inverter chip)
  RD -> both OE ; WR -> RAM WE

Pin-level boot proof (mirrors test_intel/test_z80/galaksija.test.js): watch
M1, read the address bus on each opcode fetch, and confirm the Z80 leaves
the reset vector (DI; SUB A; JP 0x03DA), reaches the init routine at 0x03DA,
and runs 1000+ fetches across 50+ distinct ROM addresses -- the real
firmware executing end-to-end through the settle-kernel bus. The on-screen
"READY" prompt is the next milestone (needs the video display chip
rendering the 0x2800 video RAM).

galaksija-rom.c embeds the public-domain ROM A+B image. 38 chipbus tests
across 8 files pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 11:55:28 -03:00
David Montero Crespo 0cd2dc2062 test(chipbus): Phase 3 core - Z80 + ROM + RAM + address decode over the bus
The architectural heart of the retro computer, proven on real chips. A Z80,
a 32K ROM, a 64K RAM and an inverter (address-decode glue) are wired
chip-to-chip over a shared address + data bus, no board:

  ROM at 0x0000-0x7FFF   rom.CE = A15
  RAM at 0x8000-0xFFFF   ram.CE = NOT A15  (the inverter chip)
  RD -> both OE ; WR -> RAM WE

The ROM program writes 0x5A to RAM at 0x8000, clears A, reads it back, and
HALTs only if the byte survived. HALT going low proves the full core works:
the Z80 runs from ROM, the inverter decodes A15 to select RAM (the settle
kernel drives the combinational glue across hops), and the RAM latches a
write and returns it on a read over the shared tri-state bus, all within
synchronous bus cycles. Adds z80-ram-rom.c (boot image) + ram-64k/inverter
fixtures. 37 chipbus tests across 7 files pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 10:23:57 -03:00
David Montero Crespo 89a5298f47 test(chipbus): live proof - a real Z80 boots from a ROM over the bus
End-to-end validation of Phases 0-2 on an actual CPU. The real Z80
(examples/intel/z80.c) and a 32K EPROM (z80-boot-rom.c, a rom-32k variant
holding JP 0x0006 / HALT) are wired chip-to-chip over a shared address +
data bus with no board. RD drives the ROM's OE; CE is left enabled.

Booting exercises all three phases at once: the Z80 drives the address ->
the ROM reacts on the shared net key (Phase 0); asserts RD -> the ROM
tri-state-drives the data bus while the Z80 released it (Phase 1); and reads
the data bus in the SAME tickTimers step, getting the settled byte
(Phase 2 settle-before-read). The Z80 fetches C3,06,00, jumps to 0x0006,
fetches 76, and HALTs -> drives HALT low, which the test observes.

z80.wasm is compiled from the committed examples/intel/z80.c; the boot ROM
source + chip.json live in test_custom_chips/sdk/examples. All 36 chipbus
tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 10:07:26 -03:00
David Montero Crespo 3137c40a90 feat(chipbus): Phase 2 - synchronous settle kernel (settle-before-read)
Fixes root cause B: a CPU bus cycle drives address+strobe then reads the
data bus in the same tickTimers call, so the memory chip must react before
the read. Phase 0/1 applied each net change by firing PinManager listeners
immediately, which recurses one JS frame per hop - deep glue chains are
deep recursion and a combinational loop overflows the stack.

- busKernel.ts: a delta-cycle settle loop. A net change is recorded in a
  pending set, not applied recursively; settle() drains it in batches
  (deltas), applying each and letting the driven chips re-dirty the next,
  until a fixed point or DELTA_CAP trips (oscillation -> warn, not hang).
  Two-phase: a drive lands in pending and is applied on the next delta, so
  a chip evaluating mid-settle reads last-stable nets. The first drive of a
  cycle settles synchronously before returning to the chip's C code, so the
  in-cycle vx_pin_read sees settled data.
- busNets: publishes resolved levels through the kernel instead of calling
  triggerPinChange directly.

Tests (chipbus-buskernel): multi-hop chain settles; settle-before-read; a
5000-hop chain settles without stack overflow; a ring oscillator trips the
cap and warns instead of hanging. The two-real-chip integration still
exchanges 0xA5 through the kernel. Full suite 2079 pass flag-off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 02:36:54 -03:00
David Montero Crespo 23516247a6 feat(chipbus): Phase 1 - 4-valued logic + drive-strength tri-state buses
A chip-to-chip net is now resolved by (value, strength), not last-writer-
wins, so a real multi-driver bus works: many chips on one data line, only
the enabled one drives, the rest release to Hi-Z.

- busLogic.ts: 4-valued (0/1/Z/X) + drive-strength resolution. Strongest
  driver wins; equal strength + opposite = X (contention); no driver = Z;
  pull resistor = pull strength. modeToDrive maps VX_OUTPUT -> strong,
  VX_INPUT -> Hi-Z (the rom/ram/8255 "release by input" idiom becomes real
  tri-state), VX_INPUT_PULLUP/DOWN -> pull.
- busNets.ts: per-net driver registry; resolves and pushes the resolved
  level into PinManager; warns once on contention.
- syntheticPins.ts: isSyntheticNetPin distinguishes bus net keys.
- ChipRuntime.ts: pin register/write/set_mode route bus-net pins through
  busNets (gated by chipBusEnabled + isSyntheticNetPin); non-bus pins keep
  the legacy path; dispose releases the chip's bus drivers. SPICE source
  emission is skipped for bus pins (digital fast path beside SPICE).

Tests: busLogic (14), busNets (6, incl. tri-state hand-off + contention),
and the two-real-chip integration now exchanges 0xA5 through the registry.
Full suite 2074 pass with the flag off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 02:18:47 -03:00
David Montero Crespo 474d132368 test(chipbus): Phase 0 live proof - two real WASM chips exchange a byte
End-to-end proof of the chip-to-chip net-key fix through the real
ChipRuntime + PinManager (not a unit stub). Two chips compiled from C
with wasi-sdk:
- bus-driver.c: drives 0xA5 onto D0..D7 at setup.
- bus-reader.c: polls D0..D7 on a 1ms timer, mirrors onto OUT0..OUT7.
Wired chip-to-chip with no board; with the chipbus flag both chips' Dn
pins resolve to one shared net key, so the reader reproduces 0xA5.

- sdk/examples/bus-{driver,reader}.{c,chip.json}: the proof chips.
- __tests__/fixtures/chipbus/*.wasm: committed fixtures (regenerate with
  the test_intel/scripts/compile-chip.sh flags).
- __tests__/chipbus-twochip-integration.test.ts: loads the fixtures via a
  relative path; skipIf they are absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 02:10:08 -03:00
David Montero Crespo c050ae6e49 feat(chipbus): Phase 0 - net-identity shared key for chip-to-chip buses
Fixes root cause A of the multi-chip digital bus track
(project/multichip-bus/): chip-to-chip nets were keyed per-endpoint by
syntheticChipPin(chipId, pinName), so two chips on one wire resolved to
two different PinManager keys and never shared a net.

- chipNets.ts: union-find over the wire graph mints one canonical
  syntheticNetPin per net; resolveChipNetKey returns it only for pure
  chip-to-chip nets (>=2 chip endpoints, no board pin). Reuses the
  existing spice/unionFind.ts.
- syntheticPins.ts: add syntheticNetPin(netId), same allocator/space.
- DynamicComponent.tsx: traceDetailed consults resolveChipNetKey at
  depth 0 before the chipNeighbour fallback. Board priority (rule 1) and
  chip-to-component (rules 2/3) are unchanged.
- Gated behind ?chipbus=on / localStorage.velxio.chipbus (off by default).

Proof (D-008 go/no-go): __tests__/chipbus-netkey.test.ts - a byte written
on one chip's keys is visible synchronously to another via PinManager.
9 new tests; 85 resolver/PinManager/parts regression tests green flag-off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 01:18:08 -03:00
David Montero Crespo bfe94a19f5 feat(epaper): decode the UC8179/GD7965 7.5" panel + fix its BUSY polarity
The 7.5" 800x480 dashboard (GxEPD2_750_T7) rendered blank: it is a UC8179 /
GD7965 controller, but the panel config claimed controllerFamily 'ssd168x',
so the SSD168x decoder (which only reads 0x24/0x26/0x44/0x45) ignored its
0x10/0x13 DTM stream.

- Add a Uc8179 decoder (worker Uc8179EpaperSlave + browser Uc8179Decoder).
  UC8179 is the same UltraChip command family as the UC8159c (0x10/0x13 DTM,
  0x12 refresh) but mono (1 bit/px). GxEPD2 writes the visible image to 0x13
  (DTM2 "current"; 0x10 is the ignored "previous"), framed by 0x91/0x90
  (partial window, pixel coords MSB-first)/0x13 data/0x92. Data lands at
  absolute pixel coords inside the window, so compose is just the RAM. The
  Frame reuses the SSD168x palette (0=black, 1=white) so paintFrame renders it.
- EPaperPanels.ts: add the 'uc8179' family and point epaper-7in5-bw at it.
  EPaperPart.ts + esp32_worker.py dispatch 'uc8179' to the new decoder.
- Fix the BUSY polarity: UC8179 (like the UC8159c) idles BUSY HIGH, not LOW.
  The worker seeded BUSY LOW for every non-uc8159c panel, so GxEPD2_750_T7's
  _PowerOn()/_InitDisplay() busy-wait timed out (~10 s, "Busy Timeout!") on
  every refresh. Now _PowerOn returns in ~129 us.
- esp32_worker.py: the runtime sensor_attach epaper path still emitted the
  epaper_update payload nested under 'data' (the old double-wrap bug); emit
  it flat like the init path.

The 5.65" ACeP UC8159c example already rendered (it has its own decoder and
got the WS-plumbing fix); verified the 7 colour bars are correct.
2026-06-04 23:45:37 -03:00
David Montero Crespo 3bb6f95a67 fix(epaper): wrap RAM Y counter at window end (tri-colour red plane)
The 2.9" tri-colour ESP32 alert badge rendered the red ALERT pill as white:
the red plane (0x26) was received but landed out of bounds and was dropped.

GxEPD2_3C writes the 0x24 (black) plane then the 0x26 (red) plane WITHOUT
re-seeking the RAM address counter between them — it relies on the SSD168x
counter wrapping back to the window start after the last byte of the window.
Our decoder advanced Y past the window end instead of wrapping, so every
0x26 byte hit y >= rows and was discarded (red_ram stayed all-init).

Mirror the hardware: when the X cursor wraps at the end of a row, advance Y
with a wrap at the active window boundary (yrange), honouring the data-entry
Y direction. Applied identically to the worker slave, the browser decoder,
and the Python golden reference so the three stay in lockstep. No regression
on the mono panels (their counter is re-seeked per plane, so the wrap is a
no-op for them); verified the tri-colour pill now renders red and the 2.9"
weather / 2.13" clock / 1.54" hello panels are unchanged.
2026-06-04 23:15:37 -03:00
David Montero Crespo 9ba8687743 fix(epaper): correct orientation across all boards + Pico VCC wire
ePaper panels rendered rotated/misaligned on AVR and RP2040 (e.g. the 2.13"
Pico clock came out sideways and clipped). The ESP32 worker decoder was just
taught to compose in the controller's native RAM geometry and rotate to the
display orientation, but the browser-side SSD168xDecoder (used by AVR/RP2040)
still composed at display dims with no rotation, so the two diverged.

- SSD168xDecoder.ts: port the worker's native-window compose + rotation.
  * Size RAM to the longer side both ways so a rotated native layout
    (128x296 behind a 296x128 panel) isn't truncated.
  * Compose in the active RAM window, then rotate via the inverse of
    Adafruit_GFX setRotation(1). Detect orientation by BYTE width so a
    non-multiple-of-8 native width (the 2.13" panel is 122 px) is handled.
  * Track the UNION of windows per frame: paged drivers (GxEPD2 page height
    < panel) set one partial window per page, so compose must use the full
    native area, not just the last page's strip. Fixes the all-white render
    on paged panels (1.54" Uno, 4.2" Pico, 7.5" ESP32).
  * Add an isBwr option: B/W panels treat 0x26 as a 2nd mono plane (white
    only if both planes white), tri-colour panels keep red-wins.
  * Default the active window to display geometry; the firmware overrides it.
- EPaperPart.ts: pass isBwr = cfg.palette === 'bwr' to the decoder.
- esp32_spi_slaves.py / esp32_worker.py: mirror the byte-aware rotation +
  window-union in the worker, and derive is_bwr from panel_kind on the
  runtime sensor_attach path too (fixes the tri-colour ESP32 alert badge).
- test_epaper/ssd168x_decoder.py: re-port the golden reference to match
  (keeps the 3-way TS/Python/worker identity invariant). Tests updated to
  construct tri-colour cases with is_bwr/palette='bwr'.
- examples-displays-epaper.ts: the Pico VCC wire referenced '3V3(OUT)',
  which the velxio-pi-pico-w element doesn't expose (it has '3V3'), so the
  wire snapped to the board corner. Use '3V3'.
2026-06-04 23:15:37 -03:00
David Montero 7b483f6109 feat(examples): add a Retro category to the gallery sidebar
A tag-based 'Retro' tab (next to All) collects the Z80 / Intel / vintage-CPU
examples via their 'retro' tag, regardless of board filter (they still also
appear under Digital). One-file change: BOARD_TABS + an isRetro predicate
special-cased in the filter and the tab count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 22:08:43 +02:00
David Montero 5f04b42bdd fix(canvas): while running, the canvas is interact-only (no wire/pin/edit)
Reported: on a running circuit, clicking a pushbutton SELECTED the wire under it
instead of pressing the button — and you could still move wires / pick pins to
make connections during a run.

Root cause: component dragging was already locked during a run, but the
canvas-level onClick (wire selection via findWireNearPoint) wasn't — so a click
on a button bubbled to the canvas and selected the wire. The button press itself
fired (shadow DOM), but the wire-select made it feel broken.

Gate every EDIT interaction on the existing interactionRunning predicate while
keeping part interaction (buttons/switches/pots) and pan/zoom:
- canvas onClick wire-selection + onDoubleClick waypoint-insert
- wire segment / waypoint drag handles (mouse + touch)
- pin-click wire creation
- touch tap wire-selection
- hide the PinOverlay (was gated on !running, so board-less runs still showed
  clickable pins) and skip wire-hover highlighting while running
- clear any wire/component selection when a run starts so leftover handles don't
  linger over the live circuit

Component drag + property dialog were already gated on interactionRunning; this
extends the same 'freeze to edit, run to interact' model to wires and pins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:14:51 +02:00
David Montero dd322b27e3 feat(editor): per-target compilation console — a section per board/chip
Phase 4 of the run-system work. The compile console now groups output into a
section per run target (board or chip) with a status glyph and label, the way
multiple Arduinos already stream — instead of one flat list.

- CompilationLog gains an optional target { id, label, kind: 'board'|'chip' }.
  message/type are unchanged so the pro overlay (diagnose-with-AI prompt +
  errorCount slot) and the console's length-based clear/auto-error heuristics
  are untouched. parseCompileResult stamps the target on every produced line.
- Producers stamp their lines: compileAllBoards (per-board, dropping the old
  '<label>: ' string prefix the header now carries), prepareCustomChips
  (per-chip, WASM + ROM), handleCompile + handleRun MicroPython (single board) —
  including the Pi / MicroPython / FQBN / error paths so a target's lines never
  fragment across sections.
- CompilationConsole groups filteredLogs into consecutive-run sections at RENDER
  time only (the flat array is unchanged); each target section shows ✓/✕/▸ +
  name + kind tag, with no-target lines ('Compiling all targets', 'Done') as
  plain narration around them.

Reviewed by an adversarial pass; the flagged un-stamped edge paths (Pi /
MicroPython / single-board errors) are now stamped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 07:56:54 +02:00
David Montero 6ae1ed560d feat(editor): unified Compile-All / Run-All across boards + programmable chips
Phase 3 of the run-system work. Generalises the boards-only Compile-All/Run-All
to RUN TARGETS = boards + programmable custom-chips, so a board+chip or several
chips compile and run together, the same way multiple Arduinos do.

- targetCount = boards + programmable chips; the Compile-All/Run-All buttons now
  appear when targetCount > 1 (was boards.length > 1). Cheap string predicate
  (no JSON.parse) since the selector runs on every sim tick.
- compileAllBoards builds chips (WASM+ROM) AND boards; works with zero boards;
  prepareCustomChips now returns a failure count folded into the Done summary so
  a failed chip no longer shows green / calls markCompiled.
- handleRunAll: compiles all targets, starts every board, then restartParts() so
  chips pick up fresh WASM/ROM, and resumes the electrical solver when NO board
  actually started (board-less, or a board that compiled to nothing) so chips
  aren't left frozen.

Review fixes (2-agent adversarial pass):
- Stop now stops EVERY running board (Run-All can start several); otherwise a
  non-active board kept the chip ticking after Stop.
- Run-All / Stop disabled gates use anyBoardRunning (+ digitalRunning) instead of
  the flat active-board  flag, which misreports multi-target runs.
- shared isQemuBoardKind() helper so handleRun and handleRunAll can't drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 07:21:39 +02:00
David Montero a68e7f8e94 feat(editor): rename boards & custom chips; show which target owns each file
Phase 2 of the run-system/UX work.

- BoardInstance gains an optional user ; boardDisplayName(board) resolver
  (name || kind label) routes every INSTANCE-label surface: file-explorer
  section header, compile console (EditorToolbar), canvas selector/tooltip/
  context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options
  subtitle. Board/component pickers keep the KIND label (they pick new boards).
- Inline rename on board AND chip section headers (double-click the name, or a
  hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in
  properties. Enter commits, Escape cancels (cancel-flag ref guards the
  unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'.
- FileTabs shows an owner badge naming the board/chip whose files are shown
  (resolved as a selector so it doesn't re-render on every sim pin toggle).
- CustomChipDialog no longer clobbers a user-given chipName: chip.json's name
  only seeds the blank defaults (My Chip / Custom Chip); loading an example
  relabels explicitly.
- Persistence: board name round-trips via projectPayload (+ dirty hash),
  vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json.
- Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal
  (early return before a useCallback).

Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness);
all major findings folded in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 06:22:25 +02:00
David Montero aa4c8123f2 fix(examples): wire board-less button power via 2.l so the pushbutton enters SPICE
The pushbutton SPICE mapper reads pins '1.l' and '2.l', but killbits/counter
wired the power side to '2.r' (an un-unioned sub-pin), so netLookup('2.l')
returned null and the button was omitted from the netlist entirely — pressing
did nothing electrically board-less. Wire the power side via '2.l' so the
button becomes a real (pressed -> 0.01 ohm) bridge to VCC, which the pull-down
+ connectChipInputsToSolve then turn into a HIGH the chip reads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 05:23:51 +02:00
David Montero dd62b94c1d feat(sim): custom chips read inputs (buttons/switches/sensors) with no board
The chip-output board-less path existed (chipPinDrives -> SPICE voltage sources
-> LEDs). The INPUT direction was missing: a chip pin wired to a pushbutton had
its net solved by ngspice, but nothing fed that net's state back to the
PinManager key the chip reads via vx_pin_read. So a board-less chip could light
LEDs but never read a button (verified: i8080 counter stayed at 0 on press).

connectChipInputsToSolve subscribes to the electrical store and, after each
solve, thresholds every wired chip input pin's net voltage to HIGH/LOW and
triggerPinChange()s the chip's synthetic pin — updating getPinState (polling)
and firing onPinChange edges. Pins the chip is actively driving are skipped so
it never fights its own outputs. Hooked alongside connectAnalogInputsToMcu in
start.ts. Solver-agnostic; reads only the electrical store shape.

Also gives the board-less button examples a pull-down on each chip BTN pin so
they read a clean LOW when open (a button-to-VCC floats HIGH otherwise):
i8080-button-counter (2) and i8080-killbits (8).

- new connectChipInputsToSolve.ts; start.ts wiring.
- examples-retro-intel: pull-down resistors + wires for the button examples.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 05:09:51 +02:00
David Montero 694f038988 feat(sim): Stop halts custom chips + LEDs go dark; retro chip examples go board-less
Phase 1 of the run-system/UX work.

Stop bug: a programmable chip kept running after Stop when a board was present.
The chip rAF tick gated only on board presence (!boardless), so with a board it
ticked forever. Now it gates on the actual run state: board-less -> electrical
paused flag; with board(s) -> board.running. handleStop also clears every chip's
output drives (clearAllChipDrives) and re-solves so chip-driven LEDs go dark on
Stop instead of freezing at their last frame.

Examples to board-less (regulated power supply, no Arduino — the Arduino only
ever supplied 5V):
- z80-larson-scanner -> 'Z80 Comet Scanner': board-less, a faster TWO-LED comet
  (scanner.s) so it's visually distinct from z80-larson-no-board's single-bit
  walk; green/blue LEDs.
- i8080-killbits -> board-less (psu + resistors), keeps killbits.s as the chip's
  editable program; buttons re-powered from the supply.
- i8080-button-counter -> board-less (psu + resistors); behaviour chip, program
  baked in, so it shows a note (no editable file) and runs standalone.
banner-streamer stays Arduino-based (its TX/RX go through the AVR USART bridge).

- CustomChipPart: run-state-aware tick gate.
- EditorToolbar: clearAllChipDrives() helper + handleStop clears chip drives.
- examples-retro-intel: 3 conversions; drop now-unused sketch consts; add the
  larsonScannerAsm comet program.
- Tests: board+chip routing now uses an inline synthetic example (gallery chip
  examples are all board-less).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 04:32:44 +02:00
David Montero 780b80778c feat(custom-chip): newly-added programmable chip auto-gets an editable program; chaser-c goes board-less
Two fixes from live testing feedback:

1. Adding a programmable chip (Z80/8080) from the gallery created NO program
   group — only the chip(s) from the example had one. Root cause: 'programmable'
   was detected by a non-empty programFile, but a fresh chip's programFile is
   empty until the user writes one. Now detection uses the canonical signal —
   chip.json's programTargets — via isProgrammableChip(). When such a chip
   lands with no program yet, the file explorer seeds an editable program.c
   (DEFAULT_CHIP_PROGRAM_C, a working walking-LED skeleton) into its own group
   and stamps programFile/programTarget onto the component so Compile/Run can
   build it. Behaviour/driver and predefined chips (no programTargets) still
   get no group — edited in the chip designer.

2. z80-led-chaser-c now runs board-less on a regulated power supply (no Arduino,
   mirroring z80-larson-no-board) — the Arduino only ever supplied 5V and added
   confusion. chaser.c stays the chip's editable program in its own section.

- romCompileService: isProgrammableChip(), DEFAULT_CHIP_PROGRAM_FILE/_C.
- FileExplorer: detect by programTargets; auto-seed program.c + persist
  programFile/programTarget for fresh chips.
- examples-retro-intel: chaser-c -> board-less (psu + 8 resistors + 8 LEDs),
  drop the now-unused Arduino sketch const; fix a stale sdcc --code-loc comment.
- Tests: board+chip case moved to z80-larson-scanner (still board-based);
  isProgrammableChip unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 22:52:11 +02:00
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 fe001d728c fix(examples): board-less chip example — Run enabled on load + editable program
Two UX bugs in the board-less "Z80 Larson Scanner (no board)" example:

- It loaded "running" (electrical sim defaults to paused=false), so Run was
  disabled and Stop enabled even though the chip hadn't started — the user had
  to Stop then Run. loadExample now starts a board-less example that contains a
  custom chip in the STOPPED state (paused=true) so Run is enabled; pure
  analog/digital circuits stay live.
- The chip's program wasn't editable: it shipped a pre-baked ROM and the
  board-less loader only setCode'd into an orphan file group (no-op → blank
  editor). The example now ships larson.s as a real file (programFile), and
  the board-less loader points the editor at the default group and loadFiles()
  the example's files, so the program shows on the left and is editable, like
  the board-backed examples. Run compiles it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 21:30:42 +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 a57951d854 fix(examples): z80-led-chaser-c dir must be signed char (SDCC z80)
SDCC treats plain `char` as unsigned on Z80, so `dir = -1` read back as 255,
`if (dir > 0)` was always true, the "walk right" branch never ran, and the
bit just shifted left until it fell off the end and the LEDs went dark after
one pass. Use `signed char dir`. Verified in a chip-WASM harness: with plain
char the chaser does 8 LED writes then stops; with signed char it walks the
bit back and forth continuously (14894 writes). Completes the C example fix
together with dropping --code-loc 0x100 in c_compile.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:23: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 bf36487642 fix(examples): z80-led-chaser-c set the Z80 stack pointer in main
SDCC's z80 crt0 defaults SP to 0x0000; on the z80-cpu chip's memory map
(RAM 0x8000-0xBFFF, MMIO at 0xC000+) the stack would grow into unmapped
high memory and the program crashed on the first CALL (delay), so the LEDs
never moved. Set SP to the top of RAM (0xBFFF) at the start of main, the
same thing the asm Larson example does with "LD SP, 0xBFFF". Verified the
ROM runs and walks the LEDs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 06:09:28 +02:00
David Montero a77dbcafad fix(examples): z80-led-chaser-c used invalid SDCC __at() cast syntax
`(*(volatile unsigned char __at(0xC000)))` uses __at as a cast operator,
which neither avr-gcc nor sdcc accept (sdcc: "syntax error: token -> ')'").
__at is a storage specifier, not an operator. Use the portable absolute-
address pointer form `(*(volatile unsigned char *)0xC000)`, which sdcc -mz80
compiles cleanly. Verified: produces a 462-byte ROM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 05:53:53 +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 083823d945 chore(pricing): free tier shows 20 daily AI credits (up to 600/month)
Landing pricing card copy updated across all 9 locales: free was advertised
as '100 daily AI credits (up to 1,500/month)'; lowered to 20/day, 600/month
to match the backend quota (see velxio-prod quota.py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 05:37:57 +02:00
David Montero 18f2e4598a feat(examples): add ESP32 Doom raycaster (ILI9341) gallery example
A full-screen Wolfenstein/Doom-style raycaster for ESP32 + ILI9341 over
hardware SPI (Adafruit_ILI9341, block writes), with auto-demo and 4 control
buttons. Doubles as an emulation-speed benchmark. Category: games.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 05:29:17 +02:00
David Montero 10d88a44b7 perf(esp32): drop per-edge gpio_change console.log that throttled the sim
Esp32Bridge logged every GPIO transition (one per SPI clock edge on a
display-heavy sketch), which floods the console and measurably throttles
the main thread and simulation throughput. A full-screen 320x240 ILI9341
raycaster went from ~0.3-0.6 FPS to ~6-8 FPS once this log was removed.
Keep the functional onPinChange / oscilloscope callbacks intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 05:10:24 +02:00
David Montero 909c94c5fb feat(examples): add single-board Raspberry Pi 3/4/5 GPIO examples
Six gpiozero (Python) examples to exercise the Pi 3/4/5 QEMU Linux boards
with different sensors/actuators. All strictly digital — the Pi has no ADC
and PWM is not simulated, so this covers the GPIO in/out paths that work:
  - [Pi 3] Blink an LED
  - [Pi 3] Running Lights (5 LEDs)
  - [Pi 4] Button Toggles LED
  - [Pi 4] RGB LED Color Cycle (digital, 7 colors, pwm=False)
  - [Pi 5] PIR Motion Alarm
  - [Pi 5] Traffic Light

Structure mirrors the existing Pi example (boards[] + vfsFiles['script.py'],
run via 'python3 /home/pi/script.py'); LEDs wired directly like
nano-button-led. gpiozero is used because it works across Pi 3/4/5 (RPi.GPIO
doesn't on Pi 5). Adds a smoke test loading all six (board kind, components,
wiring consistency, gpiozero script present in the VFS).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 04:36:58 +02:00
David Montero daa46c6724 fix(sim): desktop QEMU sims use injected sidecar URL; fix nano-button-led wiring
- ESP32 / Raspberry Pi / STM32 / Pico-W bridges built their WebSocket URL
  from a bespoke API_BASE() that read only VITE_API_BASE (fallback
  localhost:8001) and ignored the desktop shell's runtime-injected
  window.__VELXIO_API_BASE__. On the desktop the sidecar runs on a random
  127.0.0.1 port, so the sim WebSocket dialed localhost:8001 and never
  connected: compile succeeded but the simulation never started. Honor
  __VELXIO_API_BASE__ first; web (/api) and dev (localhost:8001) unchanged.
- nano-button-led example: button was wired D2->1a and 1b->GND (same
  terminal), tying D2 to GND permanently. Rewire D2->1.l and GND->2.l
  (opposite terminals), matching the other examples.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 03:24:19 +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 5bbf2427ec feat(pricing): surface STM32 & Raspberry Pi 3/4/5; refresh board count
The board roster grew to 30+ (8 STM32 variants + Raspberry Pi 3/4/5), so
the home pricing cards and SEO FAQ were stale at '19 boards'.

- Home pricing (9 locales): free bullet '19 boards' -> '30+ boards';
  the Maker bullet that just repeated the board count now states the real
  paid differentiator — unlimited ESP32 / STM32 / Raspberry Pi simulation
  time (free is time-capped on these server-side QEMU boards).
- SEO FAQ: roster updated to 30+ boards across 6 CPU architectures,
  adding ARM Cortex-M (STM32) and Raspberry Pi 3/4/5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 07:29:54 +02:00
David Montero af39225688 refactor(sim): single-board example load rebuilds from scratch
Cleaner follow-up to the multi-board residue fix. Instead of removing the
extra boards and retyping the surviving one (which left a stale id such as
"stm32-bluepill" on what was now an Arduino Uno), the single-board path now
tears every board down and adds exactly one fresh board of the target kind.
This mirrors the multi-board and board-less paths and guarantees the
surviving board's id matches its kind.

Drops the now-unused setBoardType/activeBoardId destructures and tightens
the boardFilter cast off `any`. Strengthens the regression test to assert
the surviving board's id and kind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 23:23:41 +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 c66a5b0514 fix(sim): reconcile running flag on board removal + clear multi-board residue
Three reported circuit bugs:

- Deleting the active/running board left the global `running` flag stale
  at true. That flag mirrors the active board, but removeBoard reassigned
  activeBoardId without re-deriving running, so the circuit looked
  "running" (toolbar stuck on Stop, canvas locked) and SimulatorCanvas's
  master-switch effect auto-started sibling remote boards. New Project
  hits the same path (it removes every board in a loop). removeBoard now
  re-derives running from the new active board (false if none remain).
- loadExample's single-board path called setBoardType when boards already
  existed but never dropped the extra boards a previous multi-board
  example had added, so they lingered as residue. It now removes every
  board past the first before retyping, matching the multi-board and
  board-less paths.

Adds board-removal-running-reconcile.test.ts (6 regression tests; full
suite 1917 passing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 21:19:46 +02:00
David Montero Crespo 310271bdb2 feat(components): make KY-040 rotary encoder discoverable + add example (#104)
The KY-040 rotary encoder was already fully simulated (wokwi-ky-040 element + PartSimulationRegistry 'ky-040' driving CLK/DT quadrature and the SW button) and present in the catalog, but unfindable: named 'KY040', in the 'other' category, with no rotary/encoder search tags and a placeholder thumbnail. A user searching 'rotary encoder' got nothing (issue #104).

- generate-component-metadata.ts: let component-overrides.json patch category, description and tags on scanned wokwi parts (previously only name/thumbnail) -- the fields the picker category tab and ComponentRegistry.search() actually use. - component-overrides.json: ky-040 override -> name 'KY-040 Rotary Encoder', category 'input', rotary/encoder/knob tags, description, real encoder thumbnail SVG. - examples.ts: KY-040 + Arduino Uno example (quadrature read + SW reset). Regenerated components-metadata.json; searching rotary/encoder/knob now returns the KY-040. tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 14:30:59 -03:00
velxio-deploy 6f0aa232db chore(examples): refresh 6 thumb file(s) [auto] 2026-05-31 08:48:21 +02:00
David Montero e1656d063e test(board-coverage): accept the 6 new STM32 variant boards as uncovered
The boards added in 6813891 (F103CB / F401 pill variants, F4 Discovery,
Olimex H405, Netduino 2/+2) run on the libqemu-arm backend with no
in-browser canvas example, like the existing Blue/Black Pill. Add them to
ACCEPTED_UNCOVERED so the coverage matrix passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 08:37:58 +02: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
velxio-deploy 17507d308d chore(examples): refresh 19 thumb file(s) [auto] 2026-05-31 02:59:25 +02:00
David Montero 62e6f76147 test(board-coverage): accept STM32 Blue/Black Pill as uncovered
stm32-bluepill and stm32-blackpill are Pro features emulated on the backend
via the licensed libqemu-arm QEMU lib (no in-browser canvas engine, same as
the Raspberry Pi boards), and their gallery examples are intentionally not
shipped to the free tier. Add them to ACCEPTED_UNCOVERED so the board-kind
coverage matrix passes — this was missed when the boards were introduced.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 02:46:30 +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 0bcfbde617 feat(seo+landing): /classroom in footer + seoRoutes for sitemap inclusion
LandingPage footer gains a "For schools" link sitting between Pricing
and About — gives institutional visitors a discoverable path to the
Classroom landing without burying it inside the FAQ.

seoRoutes.ts adds the /classroom entry (priority 0.85, monthly
changefreq) so the auto-generated sitemap picks it up on every build.
Bonus: getSeoMeta('/classroom') now returns the institutional title +
description if any other code wants to read it programmatically.

The static public/sitemap.xml is not committed — `npm run generate:sitemap`
overwrites it during the Docker build, so any hand-edit would be wiped.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:21:41 +02:00
David Montero 1d847d19cc fix(landing): pricing card prices, names + features for the shipped tiers
The home page's pricing section was still showing the dropped Phase 0
shape (Free / Pro $15 / Pro Max $35) instead of what /pricing and the
billing backend actually serve (Free / Maker $7 / Pro $19).

  - Replace the middle card from "Pro $15" → "Maker $7" (CTA "Start
    Maker") + the right card from "Pro Max $35" → "Pro $19" (CTA
    "Subscribe to Pro").  "Most popular" badge moves to the now-Pro
    card (still the upsell sweet spot).
  - i18n keys renamed in lockstep: tiers.pro → tiers.maker, tiers.pro_max
    → tiers.pro.  Updated in all 9 locales (en/es/pt-br/fr/de/it/ja/ru/
    zh-cn) with translated copy that mentions the actually-shipped Pro
    perks (private projects, GitHub Sync, BOM CSV, schematic PNG,
    watermark-free embed).  The Spanish line about "Maker" is left as
    the loanword so it stays consistent with /pricing.

No backend changes — quota.py PLANS was already correct.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:00:02 +02:00
David Montero d34fc2059d feat(seo): llms.txt for AI search engine indexing (D5.6)
Adds the proposed llmstxt.org file under frontend/public/ so the SPA
nginx serves it at https://velxio.dev/llms.txt.  ChatGPT-driven traffic
has had the highest engagement of any channel (82% session quality per
GA), so feeding the AI crawlers a curated, machine-readable summary
is high-leverage: the model gets accurate tier prices, supported
boards, comparison framing vs Wokwi / Tinkercad / Proteus, plus FAQ
answers — instead of stitching together a fuzzy view from blog posts.

Notable departures from the original phase-5 draft:
  - Tier shape is the shipped one (Free / Maker $7 / Pro $19), not the
    proposed Pro/Hobbyist + LemonSqueezy variants that never landed.
  - Geo-pricing section dropped (Phase 2 deferred — same reason).
  - Supported boards list reflects the actual MCU emulator coverage in
    the latest velxio image, not the aspirational roadmap.
  - GitHub Sync and embed iframe (D3.5) are now first-class features.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:54:29 +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 6df53a0d32 i18n: refresh landing.hero subtitle + trustLine with AI/offline/AGPLv3 USPs
Phase 1 D1.8 — the home-page hero leads with three defensible
differentiators Velxio has that the dominant alternative doesn't:

  - AI agent integrated (Wokwi has none)
  - Works offline as a desktop app (Wokwi is cloud-only)
  - AGPLv3 open source (Wokwi is proprietary)

Subtitle and trustLine rewritten across all four shipped locales
(en, es, pt-br, zh-cn). No layout change — the LandingPage.tsx
component renders both strings already.

The competitor name isn't mentioned anywhere — the user comparing
side-by-side does the math themselves. Anchoring on the USPs makes
the eventual /pricing visit ("Maker $7 = AI included") land in
context instead of cold.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 22:57:23 +02:00
David Montero 0beee4af3d i18n: add quotaModal block in en/es/pt-br/zh-cn (10 keys × 4 langs)
The quota-exhausted modal (rendered by the velxio.dev pro overlay when
a free user hits the daily AI cap) was hardcoded English. The modal is
seen by users from CN/BR/MX/CO/AR/PE/IN — the audiences most likely to
bounce on English-only UX. This adds the four key languages.

Keys:
  titleFree    — "You've hit today's free limit"
  titlePaid    — "You've reached your daily limit"
  bodyFree     — explainer + Pro upgrade pitch (interpolates cap/proCap/multiplier)
  bodyPaid     — explainer for paid users who hit their own tier's cap
  today / thisMonth / resets — stats labels
  ctaUpgrade   — primary CTA ("Upgrade to Pro — $15/mo")
  ctaSeePlans  — fallback CTA for non-free users
  ctaWait      — secondary "Wait until reset"

Upstream-only change — the velxio-prod overlay's AgentChatPanel.tsx is
wired to consume these via useTranslation in a separate commit.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 20:57:29 +02:00
David Montero 0d37472aa3 test(esp32): add hardResetPinStates to PinManager mocks in 6 test files
Same root cause as the previous test fix in 3e38397 — upstream commit
d64eebc (fix(stop): reset CPU to PC=0) added a hardResetPinStates() call
to useSimulatorStore.stopBoard. The vi.mock factories in these 6 ESP32-
adjacent test files only exposed updatePort/onPinChange/getListenersCount,
so any test path that hits stopBoard crashed with "is not a function"
once the real prod code called the new method.

Each gets a single-line addition: this.hardResetPinStates = vi.fn();

Verified with full vitest run: 127 files pass, 2,005 tests pass, 0 failures.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 19:31:43 +02:00
David Montero 3e38397366 test(frontend): fix 2 test-mock bugs that blocked the deploy gate
1. multi-board-integration.test.ts — PinManager mock was missing
   hardResetPinStates(). Upstream commit d64eebc (fix(stop): reset CPU
   to PC=0) added that method to PinManager and useSimulatorStore.stopBoard
   calls it, but this test's vi.mock factory never exposed it. Result:
   "TypeError: getBoardPinManager(...)?.hardResetPinStates is not a function"
   even though the optional chain looks safe — the chain only short-circuits
   on null/undefined, not on a non-function property.

2. vitest.config.ts — was missing the @velxio alias that vite.config.ts
   defines. defineConfig from vitest/config does NOT auto-inherit from
   vite.config.ts; the alias has to be re-declared. Without it, overlay
   tests importing @velxio/store/useEditorStore failed with "Cannot find
   package '@velxio/...'" even though the build (which DOES inherit the
   alias) resolves them fine.

Verified: full set of 3 previously-failing tests now pass cleanly
(multi-board-integration: 43 passed, snapshot: 0, pinIntrospection: 10).

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 19:26:21 +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 d64eebc200 fix(stop): reset CPU to PC=0 on Stop (real-life power-cycle semantics)
The previous fix preserved display state on Stop so Resume could pick
up the multiplexed frame seamlessly — but that's Pause semantics, not
Stop. On a real Arduino, hitting the physical Stop is cutting power:
the next Run must boot from setup(), not continue at the saved PC.

User report on https://velxio.dev/example/uno-7segment :
  > empieza a contar, le doy stop en el 6, le doy run y sigue desde 6

stopBoard now:
  - calls sim.reset() (was sim.stop()) — CPU back to PC=0
  - calls hardResetPinStates() (was the soft resetPinStates) — clears
    cached states AND notifies listeners so 7-seg / NeoPixel / LCD
    blank out instead of freezing on whatever was lit.

Reset and Stop are now the same cold-boot semantics; Reset still
additionally clears serial output + baud rate. The soft
resetPinStates() helper stays for internal SPICE-classification-only
paths that don't want listener fan-out.
2026-05-26 21:28:41 +02:00
David Montero 858e160e6f chore(sitemap): bump lastmod dates to 2026-05-26 2026-05-26 20:53:24 +02:00
David Montero Crespo 91b0829562 feat(desktop): in-app update toast with auto-check at startup (v0.4.0)
Replaces the native OS-modal update dialog (which blocked the editor
and looked dated) with a non-intrusive bottom-right toast that
appears 30 s after app mount when the Tauri updater finds a newer
release.

State machine:
  idle → no update detected, render nothing
  available → "Update available - Velxio Desktop X.Y.Z" + Install/Later
  downloading → progress bar with "X.X / Y.Y MB (NN%)"
  installing → "Installing X.Y.Z... will restart automatically"
  error → error message + Retry/Dismiss

Click "Install and restart":
  1. downloadAndInstall() streams the full signed installer (~70 MB)
  2. Tauri verifies the minisign sig against the embedded pubkey
  3. Replaces the install in-place
  4. Auto-relaunch (the app exits and reopens on the new version)

"Later" dismisses for the rest of the session (sessionStorage flag).
A close+reopen re-checks. Manual re-check via the menu still works.

Companion change in velxio-prod flips tauri.conf.json
updater.dialog from true to false so our custom toast is the only
update UI - no double-prompting.

Files:
- frontend/src/desktop/UpdateAvailableToast.tsx (new): the component
- frontend/src/desktop/desktop.css: toast styles + slide-in animation
- frontend/src/desktop/index.ts: mount alongside GraceBanner +
  Esp32QemuPrompt in the existing sidePanelRoot

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:46: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 Crespo d347cac51e test(vitest): allow tests outside frontend/ + discover velxio-prod pro overlay tests
Two related changes for the v0.4.0 desktop-agent rollout:

- include glob now also matches `../../pro/frontend/src/pro/**/__tests__/`
  so the agent-overlay tests in velxio-prod are discovered when this
  config is used from a velxio-prod checkout. On pure-OSS clones the
  glob has nothing to match - harmless.

- server.fs.allow extended to `..` and `../..` so Vite's filesystem
  sandbox doesn't reject the cross-project test paths with
  "Cannot find module '/@fs/...'".

No behavior change for OSS-only contributors. velxio-prod gets the
agent's `desktopAuth` unit tests picked up automatically by
`npx vitest run` in this directory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:53:04 -03:00
David Montero Crespo 3bd6f144f2 feat(desktop): allow slim pro overlay when VITE_PRO_BUILD + VITE_DESKTOP both set
v0.3.x main.tsx explicitly REFUSED to load @pro when VITE_DESKTOP was
true ("desktop owns its own license + auth UI"). v0.4.0 brings the
AI agent into the desktop bundle, so the refusal needs to relax for
that one use case.

New routing inside the if(VITE_PRO_BUILD) branch:
  - VITE_DESKTOP also set → load @pro/desktop_index (slim entry that
    only mounts AgentChatPanel + DiagnoseCompileButton, no analytics
    / sessions / billing / admin / save overrides - those expect
    velxio.dev cookies the desktop has no way to send)
  - VITE_DESKTOP not set → load @pro/index (existing web behavior)

VITE_DESKTOP alone (no pro) still loads zero overlay - that's the
pure-OSS desktop build path for self-hosters who don't have the
pro source tree at $PRO_OVERLAY_PATH.

Companion commit in velxio-prod creates @pro/desktop_index, adapts
the agent client for license-key Bearer auth, and updates
build-frontend.sh to pass both flags.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:09:22 -03:00
David Montero b2474bf5d1 fix(stop): preserve display state on Stop, only blank on Reset
Reporter feedback after 7aca3db: pressing Stop on the uno-7segment
example turned the 7-segment off, and pressing Start again left
random segments lit / no number at all. The previous fix made
resetPinStates() notify every listener with (pin, false) on both
Stop and Reset, which was right for Reset (full reboot) but wrong
for Stop:

  - On Stop the AVR CPU is just paused. Internally it still has
    PORTD=0xFF (or whatever the last drive was).
  - resetPinStates blanked the pinStates cache + fan-out LOW
    notifications. Display turns off, fine.
  - On Start the CPU resumes from where it paused. avr8js's port
    listener fires only for bits that CHANGED relative to its OWN
    oldValue (which still holds the pre-stop value). If oldValue
    matches the live register, no pinChange event fires for that
    bit, and the display has no signal telling it to come back on.

Split the API into two methods:

  resetPinStates()    — soft cleanup, drops outputPins only. Used by
                        stopBoard. Cached pinStates and visual state
                        stay so the resume picks up where it left off.

  hardResetPinStates() — full cleanup, drops outputPins + pinStates
                        and fan-outs (pin, false) to listeners.
                        Used by resetBoard (CPU starts at PC=0,
                        firmware re-drives every pin from setup()).

Updated the test helper clearAllPinManagerState to call
hardResetPinStates between tests so the same-state short-circuit in
triggerPinChange doesn't suppress fresh events.

All 32 vitest tests pass (AVRSimulator, interconnect-routing,
dual-arduino-software-serial, pin-position-rotation).
2026-05-26 18:54:17 +02:00
David Montero 7aca3db51c fix(reset): clear display state + don't clobber Interconnect on Reset
Two paired bugs that surfaced on the Reset button.

(1) 7-segment / NeoPixel / LCD freeze on last pattern after Reset.
    resetPinStates() was wiping the pinStates cache + outputPins set
    silently — no listener notifications fired, so visual components
    that update on pinChange kept rendering whatever segments were
    lit at the instant the user pressed Reset. Now we snapshot every
    pin that was HIGH before clearing and fan out a synthetic
    (pin, false) to each registered listener. Stateful displays
    redraw cleanly to all-off; passive listeners (analog sensors,
    debounce-only buttons) ignore the synthetic LOW and recover on
    their next real write.

(2) Cross-board serial silently dies after pressing Reset. resetBoard
    was unconditionally reassigning:
        sim.onSerialData = (ch) => appendSerial(boardId, ch);
    immediately after sim.reset(). The comment said "re-wire after
    reset" but reset() does NOT clear that property — the new USART's
    onByteTransmit chains through `this.onSerialData` which IS the
    Interconnect wrapper. The reassignment destroyed that wrapper and
    sibling-board UART forwarding (Uno TX → Nano RX) stopped working
    until a full page reload. Same root pattern as the initSimulator
    bug fixed in 5480052 — Interconnect's __icSerialHookInstalled
    flag is on the live sim, so once the wrapper is blown away
    nothing reinstalls it. Removed the reassignment and left a NOTE
    so the next person doesn't reintroduce it.

Verified the AVRSimulator + dual-arduino-software-serial +
interconnect-routing test suites still pass (26 tests).
2026-05-26 17:30:44 +02:00
David Montero 5480052379 fix(multi-board): initSimulator wiped Interconnect's UART wrapper
Cross-board UART forwarding silently broke for any project loaded
with > 1 board. User report: Arduino Uno → Arduino Nano serial echo
test where the Uno transmits fine but the Nano's Serial.available()
is never true.

Root cause traced live with chrome-devtools-mcp + temporary debug
logs in AVRSimulator.onSerialData setter and Interconnect:

  1. loadProjectState → addBoard(uno) → createSimulator → sim.onSerialData = appendSerial
  2. addBoard(nano) → same
  3. setWires → Interconnect.updateWires → ensureSerialHook(uno)
     wraps sim.onSerialData with a fan-out callback that ALSO pushes
     to the Nano's RX queue. __icSerialHookInstalled flag set.
  4. SimulatorCanvas mounts → useEffect calls store.initSimulator()
  5. initSimulator unconditionally did:
        simulatorMap.delete(boardId);
        const sim = createSimulator(...);   // ← brand-new sim
        simulatorMap.set(boardId, sim);     // ← Interconnect's wrapper is gone
     The new sim's onSerialData is just appendSerial. The old sim
     (where the wrapper lived) has been orphaned; Interconnect never
     re-installs because its flag was on the discarded sim.
  6. Run all boards → Uno.usart.onByteTransmit → this.onSerialData →
     appendSerial (Uno's monitor shows TX) but no fan-out call →
     Nano never receives anything.

initSimulator is a legacy single-board helper from the days when the
store only knew about one MCU. Multi-board flows already create
their sims in addBoard. Bail out early if a sim for the active
boardId already exists, so the legacy helper becomes a no-op when
the multi-board path has already done the work.

Verified the 3 related test suites still pass (AVRSimulator,
dual-arduino-software-serial, interconnect-routing).
2026-05-26 17:08:04 +02:00
David Montero 1663236184 debug: trace onSerialData setter 2026-05-26 16:57:37 +02:00
David Montero c8d06c9b90 debug: temp console.log in cross-board UART path 2026-05-26 16:47:13 +02:00
David Montero 56d3bc4f12 fix(avr/serial): drain RX queue every frame and clear it on stop
User report: Arduino Nano connected to an Uno-TX wire received bytes
but displayed them poorly, and pressing Stop then Run "killed" the
serial link until the page reloaded.

Two paired bugs in the cross-board serial path:

(1) drainSerialRxQueue was only ever re-fired from usart.onRxComplete,
    which itself only fires AFTER a successful delivery. If the very
    first delivery attempt fails (rxEnable=false because the sketch
    hasn't reached Serial.begin yet — extremely common when one board
    starts emitting bytes before the receiving board's setup() runs)
    nothing re-kicks the queue and every subsequent byte from the
    sibling board sits in serialRxQueue indefinitely. Adding a
    per-frame drain attempt (no-op when queue is empty or rxBusyValue
    is set, so cost is negligible) makes the link self-heal across
    cold-start races and Serial.end()/begin() toggles.

(2) stop() never cleared serialRxQueue. On Run after Stop the new
    USART would re-drain the previous run's leftovers into the fresh
    sketch before its setup() ran, corrupting the first bytes the
    user saw on the receiving side. Clearing the queue in stop() —
    same place we already clear scheduledPinChanges — keeps each Run
    a clean slate.

Verified 52 existing tests still pass (dual-pico-serial-passthrough,
dual-arduino-software-serial, interconnect-routing, avr-uart-tx
-waveform, serial-batching, AVRSimulator, pin-position-rotation).
2026-05-26 15:47:55 +02:00
David Montero 467ca4455f fix(canvas): wires off pins after rotation — wrapper offset was (4,6) instead of (6,6)
User report: "rotating components messes up their connections" — pressing R
on a placed component visibly slid every wire endpoint off its pin tip.

Root cause: the DynamicComponent wrapper has padding:4px + border:2px on
EVERY side, so the inner web-component element sits 6 px in from the
wrapper top-left on BOTH axes. The wire layer assumed an asymmetric
(4, 6) offset, baked into:

  * useSimulatorStore.updateWirePositions       — store.x + 4, store.y + 6
  * useSimulatorStore.recalculateAllWirePositions
      — start (startComp.x + 4, startComp.y + 6)
      — end   (endComp.x   + 4, endComp.y   + 6)
  * pinPositionCalculator.calculatePinPosition  — inverse: (componentX - 4, componentY - 6)

Unrotated the 2 px X bias was visible only as a very-slightly-off wire,
which nobody filed. When the user rotated the component, the bias
rotated WITH it — at 90° it became a 2 px Y offset (wires hanging below
the pin), at 180° a 2 px X offset on the other side, at 270° upward. UX
read as "wires disconnected".

Verified the real CSS box via chrome-devtools-mcp against several live
components on velxio.dev (RGB LED + 3 resistors + analog joystick): all
report padding-left/top = 4 px, border-left/top = 2 px, inner offset = 6
on both axes.

Fix: use (+6, +6) at every site, single source of truth in a comment
explaining padding+border arithmetic. Updated the rotation regression
test to match the corrected math (numbers shift by 2 px on every
expectation that referenced the old offset).

Pin position math, pivot derivation and the rotate-N×90° round trip
unchanged — only the offset constant moved.
2026-05-26 15:16:04 +02:00
David Montero Crespo 161335a5cf test(desktop): unit tests for bannerFor + suppress redundant banner in lockout
Phase 4 polish: GraceBanner was rendering for state=locked/tampered
even though LockoutOverlay covers the screen for those states. The
banner leaked through the overlay's 96%-opaque background as a
faint red strip - confusing.

- GraceBanner.tsx: bannerFor() returns null for locked/tampered
  (LockoutOverlay handles the messaging). Also exported bannerFor
  so the new unit tests can exercise the pure decision logic.
- __tests__/GraceBanner.test.ts (new): 13 vitest cases covering
  pre-expiry amber/red thresholds (trial_ends_at vs subscription_period_end),
  fallback to claims.exp for legacy JWTs, soft/hard grace messaging,
  dismissibility rules.
- vitest.config.ts: include also matches src/**/__tests__/ so the
  desktop tests are discovered without moving them.

Runtime ~600ms vs 5-25 min for a full installer rebuild - lets
future iterations on the banner state machine skip the build cycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:05:39 -03:00
David Montero Crespo 6f63fceb20 feat(desktop): paywall v0.3.0 - LockoutOverlay + staged expiry banners
Adds the frontend half of the v0.3.0 desktop paid model. The Tauri
shell (in velxio-prod) emits velxio://license-required when the
license gate refuses to spawn the sidecar; this commit teaches the
OSS desktop overlay to react.

- LockoutOverlay.tsx (new): full-screen modal with three variants
  (no_credential / tampered / expired). Sign-in or paste-key
  resolves it via restartApp().
- DesktopWelcomePage.tsx: new grandfather variant - "you have N
  days to keep using Velxio Desktop" + "Continue without signing in".
- GraceBanner.tsx: rewrite with pre-expiry tones (5d amber, 24h
  red, dismissible), polling every 10 min while document visible,
  separates pre/post-expiry messaging.
- Esp32QemuPrompt.tsx: signup gate for grandfather users (ESP32
  binaries are not part of the grandfather grace) + inline progress
  bar driven by velxio://esp32-qemu-progress events.
- index.ts: rewires on getGateInfo() at first paint to decide
  welcome vs lockout vs nothing; installs license-required listener
  + 10-min foreground polling for the locked transition.
- tauriBridge.ts: adds GateInfo type, getGateInfo(), restartApp().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:16:48 -03:00
David Montero 9e9a3e7800 fix(esp32): use JS template literals (backticks) for stub interpolation 2026-05-25 00:17:47 +02:00
David Montero 8a89322808 feat(esp32): smart WiFi/HTTP stubs so MicroPython examples actually work
Phase 7.7 follow-up. Previously the WiFi stub returned wlan.isconnected()=False
and ntptime.settime() raised OSError — sketches degraded gracefully but
features like the TIME and WEATHER screens in the smart-ui-eyes example
showed "Sync Failed" / "API Error" instead of real-looking data.

Smart stub now:
- wlan.isconnected() returns True after the first ~2 calls (simulates a
  ~1 second connection ramp)
- ntptime.settime() pre-loads machine.RTC() with the host's UTC datetime
  (captured at code-injection time), so localtime() returns real time
- urequests.get(url) returns a stubbed Response whose .json() decodes a
  payload routed by URL substring:
    "openweathermap"/"weather" → fake weather dict (temp/humidity/desc)
    "ipify"/"myip"             → fake public IP
    "worldtimeapi"             → fake ISO datetime
    everything else            → {}
- urequests.post/head also stubbed (return {"ok": True} / {})
- Both `urequests` and `requests` aliases registered

End result: smart-ui-eyes example shows real-looking time on TIME
screen and plausible weather data on WEATHER screen, no crashes.
Still no real internet (would need Phase 7 QEMU WiFi emulation), but
visually the example demos correctly.
2026-05-25 00:15:47 +02:00
David Montero 835ca6d7a8 fix(esp32): stub network + ntptime modules for MicroPython in QEMU
Inject a compat shim into the raw-REPL prelude that replaces
sys.modules["network"] and sys.modules["ntptime"] with no-op stubs
BEFORE user main.py runs.

Why: the picsimlab QEMU fork's esp32_wifi NIC emulation handles
Arduino's lightweight WiFi.h but not MicroPython's full esp_wifi_init
path. Calling network.WLAN(STA_IF) (which is what every
network-using MP sketch does) drives the firmware to wait on
peripheral status bits QEMU never sets, eventually tripping the
FreeRTOS task watchdog (TG1WDT_SYS_RESET ~26s after boot, or
TG0WDT ~14s if the NIC is partially attached).

With the stub:
  network.WLAN(STA_IF).isconnected() -> False
  network.WLAN(STA_IF).connect(...)  -> no-op
  ntptime.settime()                   -> raises OSError

Sketches that already have try/except around sync_time (which is
most of the 100-days examples) now degrade gracefully: WELCOME +
EYES screens run, TIME and WEATHER screens show their fallback
behaviour, no panic, no reboot.

Doesn't affect Arduino C++ — sketches that #include <WiFi.h> use
real WiFi.begin() and the existing esp32_wifi NIC handles those fine.

A proper fix is to extend the picsimlab WiFi emulation to support
the full ESP-IDF API, but that's a multi-day project. This stub
unblocks the 31 MicroPython examples shipping with network imports.
2026-05-23 23:13:20 +02:00
davidmonterocrespo24 e4ecefe46a feat(desktop): skip welcome screen, robust openExternal, in-app nav
Three coordinated changes that fix the "Waiting for browser…" hang
and unblock first-launch UX on the Tauri desktop build:

  1. desktop/index.ts — DON'T mountWelcome unconditionally on first
     launch. Before, an empty keychain (no key yet) forced the
     welcome / sign-in screen on top of the editor, gating 100% of
     the app behind an account. Now the editor opens directly:
     compile + run + sim + save .vlx all work for free (they're
     upstream OSS features), and the license check still runs in
     the background just to populate state for the GraceBanner
     (which shows for invalid keys — locked, tampered, in
     soft/hard grace). Pro-only features (ESP32 QEMU download,
     agent IA) prompt for license at use time, where it actually
     matters. Matches the "try before you buy" expectation a
     desktop install creates.

  2. desktop/tauriBridge.ts — rewrite `openExternal` to try every
     known IPC path in cascade order and log via the desktop debug
     file which one worked. The previous implementation invoked
     `plugin:shell|open` with `{ path: url }`, which silently
     failed (no ACL match + wrong arg shape) and fell back to
     `window.open`, which inside a Tauri webview is a no-op for
     external URLs — the browser never opened. New cascade:
     plugin:opener|open_url (paired with tauri-plugin-opener which
     ships in this revision), then plugin:shell|open with both
     `{ path, with: null }` and `{ url }` shapes, then the
     window.__TAURI__.shell / opener high-level wrappers that
     specific Tauri 2.x flag combos expose. Each attempt logged
     via the dlog helper so the next operator can see exactly
     which path was used (or that all failed) without devtools.

  3. desktop/menu.ts — new `navigate-route` action type. Routes
     bundled in the SPA (DocsPage, ExamplesPage, AboutPage) that
     used to open velxio.dev in the system browser now navigate
     in-window via history.pushState + popstate (mirrors the
     locale-switch handler). Respects the current locale prefix
     so `/examples` from `/es/editor` lands at `/es/examples`
     instead of jumping back to English.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:57:49 -03:00
David Montero cf28b3b5ea fix(esp32): auto-enable WiFi NIC for MicroPython sketches using network module
The hasWifi auto-detection in useSimulatorStore.startBoard only matched
Arduino C++ patterns (#include <WiFi.h>, WiFi.begin). MicroPython
sketches that call `import network` or `network.WLAN(STA_IF)` were
not detected, so wifi_enabled stayed false and the backend never
attached the esp32_wifi NIC model to QEMU.

Symptom: any MicroPython ESP32 example that touches the network
module hangs in network.WLAN(STA_IF) (the constructor that triggers
esp_wifi_init internally) and the FreeRTOS task watchdog trips with
TG1WDT_SYS_RESET ~26 seconds after boot. The chip then reboot-loops.

Mirror the Pico W detector right below this one — it already handles
both Arduino and MicroPython patterns. Now ESP32 does too.

Affects 31 examples in examples-100-days.ts that use network.WLAN.
2026-05-23 22:54:10 +02:00
David Montero b09c8339ac fix(examples): use hardware I2C (not SoftI2C) in smart-ui-eyes
OSError: [Errno 19] ENODEV at ssd1306.SSD1306_I2C(...) on
100d-esp32-oled-smart-ui-eyes-animation-time-and-weather-micropython.

MicroPython SoftI2C bit-bangs GPIO directly. Velxio's ESP32 QEMU
bridge listens on the emulated I2C peripheral (registers slaves like
0x3C wokwi-ssd1306 against it) and doesn't decode bit-banged GPIO
toggles as I2C frames, so the OLED never sees any writes and
i2c.writeto() returns ENODEV on first use.

machine.I2C(0, ...) routes through the hardware I2C peripheral that
QEMU emulates, the registered slave receives the bytes, the OLED
panel updates. Same code path the other working SSD1306 MicroPython
examples on this repo already use.

API surface is identical to SoftI2C — only the constructor differs —
so the rest of the user sketch needs zero changes.
2026-05-23 18:06:42 +02:00
David Montero 76f6cd9a37 fix(avr/serial): queue RX bytes so Serial.readStringUntil sees the whole input
avr8js's usart.writeByte(value) rejects the call (returns false, drops
the byte) whenever rxBusyValue is set — and rxBusyValue stays true for
one full cyclesPerChar after each accepted call. The old serialWrite()
fed every character in a synchronous for-loop, so only the first byte
made it through and the sketch saw 'h' when the user typed 'hello\n'.

Buffer pending bytes in serialRxQueue and pump them one at a time:
- serialWrite() now just queues + kicks drainSerialRxQueue once
- drainSerialRxQueue calls writeByte on the head of the queue and only
  shifts it off if writeByte returned true (avr8js accepted it)
- usart.onRxComplete is wired to drainSerialRxQueue so the next byte
  ships as soon as the sketch's RX side actually consumed the previous
  one — matches the cyclesPerChar pacing the real chip enforces

Same handler wired in both USART setup paths (the Uno/Nano branch and
the post-loadHex Mega/ATtiny branch). TX path (onByteTransmit +
emitUartTxFrame for the oscilloscope waveform) is unchanged.
2026-05-23 17:50:19 +02:00
David Montero 0d43c5f892 fix: relax -Werror for user sketches + un-nest /* */ in robot-desktop-eyes
Two related fixes for the ESP32 Arduino-compat compile path:

(a) backend/app/services/esp-idf-template/main/CMakeLists.txt:
    Demote -Werror=comment / =parentheses / =sign-compare / =narrowing
    / =write-strings / =missing-field-initializers / =reorder back to
    plain warnings. ESP-IDF's project defaults are stricter than what
    Arduino/arduino-cli users expect, so common Arduino idioms (nested
    /* */, missing field initializers in struct literals, etc.) were
    failing builds that compile fine in the Arduino IDE. -Wall stays
    on; we just stop the abort.

(b) examples-robot-desktop.ts (robot-desktop-eyes example):
    Replace the nested /* xTaskCreatePinnedToCore( ... /* Task function. */
    ... */ block with `#if 0 / #endif` so the inner block comments
    don't terminate the outer one. Even with -Wno-error=comment the
    real-syntax-level issue (the first inner `*/` closes the outer
    comment, leaving the rest of the lines as bare code) would still
    bite, so this needs an actual code fix.
2026-05-23 16:19:56 +02:00
davidmonterocrespo24 a86a0a45bd feat(android): Digital Asset Links for TWA verification
Drops `/.well-known/assetlinks.json` so the Trusted Web Activity APK
(dev.velxio.twa, generated by bubblewrap from this same manifest.webmanifest)
can prove to Chrome that it's allowed to claim velxio.dev as its own
origin. Without this file the TWA falls back to a Custom Tab with the
URL bar visible — losing the whole "feels native" UX that TWAs
exist for.

The sha256_cert_fingerprints entry pins the production signing key
held locally as android.keystore in the velxio-twa/ build dir (NOT
in any repo). If we ever lose that key + need to re-issue, this
file has to be updated with the new fingerprint and re-deployed
BEFORE the new APK reaches users; otherwise their previously-
installed TWA verifies against an asset link that no longer matches
the APK signature and breaks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 04:42:56 -03:00
David Montero 1a6e2a23a7 fix(examples): auto-install libs for robot-desktop-eyes
Sketch fails to compile out of the box with
  fatal error: ESP32Servo.h: No such file or directory
because the ESP32Servo / U8g2 / DHT / Adafruit Unified Sensor libs
aren't part of arduino-esp32 and weren't declared on the example.

loadExample.ts already iterates `example.libraries` and runs
arduino-cli lib install for any missing entry before the user
touches Compile. Adding the four real deps the sketch needs gets
the example compiling cleanly on a fresh container without any
manual Library Manager dance.
2026-05-23 08:49:26 +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
davidmonterocrespo24 e91aaea2fd chore(manifest): make PWA mobile-friendly + better TWA copy
Three small fixes to frontend/public/manifest.webmanifest so the
Bubblewrap-generated TWA (and Add-to-Home-Screen PWA installs) feel
right on a phone:

  - orientation: landscape → any. Landscape-forced on a phone
    locks the device in side-grip whenever Velxio is foregrounded;
    the editor + simulator work fine in portrait too (the file
    explorer collapses gracefully). Tablets and desktops still
    default to landscape because they're naturally wider, so this
    only changes behaviour where the lock would actively hurt.
  - name: "Arduino Emulator" → "Circuit & Arduino Simulator".
    Matches the title tag + Open Graph copy that velxio.dev uses
    everywhere else and reflects the SPICE / ESP32 / RP2040 work
    the project has grown into since the original name was written.
  - description: was 'Free local Arduino emulator … No cloud, no
    latency.' That was true for the OSS self-host but misleading
    for an installed PWA that talks to velxio.dev. Rewritten to
    describe the actual product surface (the boards, the SPICE
    sim, "free and open source") without making a claim the live
    site can't keep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 03:38:31 -03:00
David Montero dbab794ddb test(snapshots): refresh smart-ui-eyes netlist after circuit was wired
Snapshot was written when the example had components: [] and wires:
[]. Now that 5bd541e populated the circuit (OLED + 2 buttons) and
2dea3f0 renamed the OLED pins to match wokwi-ssd1306's real
pinInfo, the netlist contains the button pull-down resistors and
floating-net autopulls. Regenerated with `vitest run -u`.
2026-05-23 08:28:08 +02:00
David Montero 2dea3f0448 fix(examples): correct SSD1306 + big-sound-sensor pin names
robot-desktop-eyes and the day-30/100-days OLED example wired the
SSD1306 OLED with SDA/SCL/VCC, but the wokwi-ssd1306 element
exposes pinInfo as DATA/CLK/VIN/GND. The mismatched names couldn't
resolve, so all three wire endpoints fell back to (0,0) of the
component and visually attached to the corner instead of the pins.

Same class of bug on the wokwi-big-sound-sensor in
robot-desktop-eyes: the element has AOUT/DOUT (no plain OUT). The
sketch uses digitalRead(SOUND_PIN), so route to DOUT.

The COMPONENT_PIN_ALIASES map in wokwiZip.ts only normalises on
.zip import — static examples have to use the real pinInfo names.
2026-05-23 08:23:15 +02:00
David Montero 5bd541e56f fix(examples): add OLED + buttons circuit to ESP32 smart-ui-eyes example
The 100d-esp32-oled-smart-ui-eyes-animation-time-and-weather-micropython
example had components: [] and wires: [] — the MicroPython code wired
an SSD1306 OLED on I2C (GPIO 21/22) plus two buttons (GPIO 14, 27) but
the circuit had nothing on the canvas, so users saw a bare ESP32 board
and the simulation was missing every peripheral the code drives.

Adds:
- wokwi-ssd1306 on I2C (3V3 / GND / SDA=21 / SCL=22)
- two wokwi-pushbuttons wired HIGH-when-pressed (3V3 → 1.l, 2.l → GPIO
  14 / 27) to match the `if pin.value(): pressed` check in main.py
2026-05-23 08:16:13 +02:00
David Montero 7d9016396e chore(sitemap): bump lastmod dates to 2026-05-23 2026-05-23 07:13:50 +02:00
davidmonterocrespo24 28826e928e feat(examples): import robot_desktop — ESP32 animated-eyes face
Pulls https://github.com/davidmonterocrespo24/robot_desktop into the
examples gallery as a real-world ESP32 + sensors project. Cozmo-style
desktop robot: SSD1306 OLED face that blinks, looks around, and shows
emotions; DHT11 weather mode triggered after 10 min idle; PIR wakeup
from sleep; LDR-driven sleep when the room goes dark; sound-triggered
reactions; and two eyebrow servos.

Ships as 34 separate files (one .ino + 33 headers / source) rather
than the usual single-sketch flatten. The face engine
(Eye / EyeTransition / EyeVariation / FaceBehavior / FaceExpression
/ FaceEmotions / BlinkAssistant / LookAssistant / …) splits
responsibility across enough classes that flattening would obscure
the design. Velxio's multi-file `files: [{ name, content }]`
mechanism handles this cleanly — the editor mounts the .ino as the
active sketch and the rest sit in the same workspace.

Pre-placed components match the original board's pin map verbatim
from Common.h:

  - SSD1306 OLED on I²C (SDA=21, SCL=22 — ESP32 default)
  - DHT11 on GPIO 15
  - PIR motion on GPIO 4
  - Big sound sensor on GPIO 2
  - Photoresistor on GPIO 34 (ADC1)
  - Right eyebrow servo on GPIO 12
  - Left eyebrow servo on GPIO 13

Arduino libraries (U8g2lib, DHT, ESP32Servo, Adafruit_Sensor) are
auto-installed by velxio's Library Manager on the first compile.

Category 'displays', difficulty 'advanced', tags cover both the
sensor list and the project's identity (cozmo / robot / animation /
eyes) so the gallery search surfaces it from multiple angles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 00:29:25 -03: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