Commit Graph

1328 Commits

Author SHA1 Message Date
David Montero 2dc0e61801 chore(sitemap): canonical trailing-slash URLs + lastmod bump 2026-06-18 22:12:36 +02:00
David Montero 1a7d4fabf4 fix(sim): re-solve when a part burns out so the open actually applies
The live solver only re-solved on component/wire/board changes, so a runtime
burnout (which changes burntComponents, not components) wouldn't rebuild the
netlist — the burnt part stayed in the circuit until something else changed.
Trigger a re-solve on burntComponents change too. The monitor skips already-
burnt parts, so this converges (one extra solve).
2026-06-18 05:10:00 +02:00
velxio-deploy 25d5e539ab chore(examples): refresh 1 thumb file(s) [auto] 2026-06-18 05:06:37 +02:00
David Montero e4aae82ca5 feat(sim): P4 slice 2 — burnt parts go open + LED joins the charred visual
- The live solver now excludes runtime-destroyed components from the netlist,
  so a burnt part actually goes OPEN: its current stops and anything it fed
  loses power (cascading failure), the way real hardware behaves once a part
  burns out. Filter is in CircuitSimulationService.runSolve (no-op when nothing
  is burnt).
- The LED's burnout now also marks it in the shared burntComponents set, so a
  burnt LED gets the same charred + smoke-badge visual (and is opened in the
  solve) as a resistor / capacitor, on top of going dark.
2026-06-18 04:57:00 +02:00
David Montero cbcf6ba81e feat(sim): P4 runtime burnout for resistors + electrolytic capacitors
Generalizes the LED's burnout to passive parts via a centralized monitor that
watches the live electrical solve. When a part is stressed past its rating for
a sustained moment it's marked "destroyed": the canvas renders it charred with a
smoke badge and a fault is logged to the output console. Clears on Reset.

Follows the Fritzing-simulator precedent (smoke-on-component) wrapped in a
first-order thermal delay so a brief inrush spike doesn't destroy a part — only
sustained overload (or a catastrophic >=3x overload, instant) does.

- runtimeBurnout.ts: pure stress (resistor power, cap voltage / reverse) + a
  thermal-delay burn decision, plus a monitor subscribed to the electrical +
  simulator stores. Resistor burns past 2x rated (the verifier already warns at
  1x for intentional teaching over-power); a cap bursts over its voltage rating
  or on reverse polarity.
- useSimulatorStore: burntComponents set + mark/clear actions; cleared on
  Reset / restartParts.
- DynamicComponent + SimulatorCanvas.css: charred filter + smoke badge.

Tests: thermal-delay decision (instant / sustained / spike / cooldown) + stress
computation (resistor power, cap over-voltage, reverse, unwired -> null).
2026-06-18 04:35:56 +02:00
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 27032a4878 chore: untrack obsolete Windows QEMU build helpers (keep local-only)
build_qemu_all.sh and build_qemu_step4.sh are stale MSYS2/MINGW64 dev
helpers (hardcoded /e/Hardware/wokwi_clon paths, output to
backend/app/services/) that built the Windows libqemu-*.dll. They are
superseded by docs/BUILD-QEMU.md (the Linux/macOS .so build from
lcgamboa/qemu) and the prebuilt / CDN binary flow, and are referenced by
no Dockerfile, CI, or deploy step. Remove from the tree and gitignore the
root-scoped paths so they stay local-only.
2026-06-16 03:51:26 +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 cd3fdded3d chore(test): prune research/exploration dirs not run by CI
The test/ tree carried a lot of one-off research, repro, and exploration
material (autosearch notes, test_100_days, test_Raspberry_Pi_Pico_W, the Intel/Z80
+ custom-chip + ePaper + micropython repro harnesses, etc.) that no GitHub Actions
workflow nor deploy.sh runs. It bloats a fresh clone for no community benefit.

Keep only the dirs CI actually exercises:
  - test/backend/      (backend-unit-tests.yml, backend-e2e-tests.yml, deploy.sh)
  - test/esp32_cam/    (backend-e2e-tests.yml)
  - test/test_circuit/ (test-circuit.yml)

No workflow, script, or Dockerfile referenced any removed path.
2026-06-15 17:10:41 +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 7db9e41278 test: drop Pico W backend tests (moved to the pro overlay)
test/backend/unit/test_picow_inbound_gateway.py and
test/backend/integration/test_picow_net_bridge.py imported
app.services.picow_net, which moved to the private overlay in the open-core
split. They now live under pro/backend/tests/ and run against the overlay.
Removing them here unbreaks the OSS pytest collection (and the deploy gate).
2026-06-15 08:54:16 +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 Crespo 3b8266a75f
Merge pull request #244 from davidmonterocrespo24/fix/picow-gateway-browser-robustness
Fix/picow gateway browser robustness
2026-06-13 22:13:35 -03: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 bb4d06cc7a fix(picow): make the IoT gateway robust to real browsers
Two real-world failures hit the Pico W gateway that the headless e2e
(node fetch, tiny request, fast timing) didn't surface:

- A browser sends KILOBYTES of headers (cookies, User-Agent, sec-*).
  Forwarded verbatim, the request overran the chip's small recv()
  (e.g. recv(1024)); lwIP then RST the connection on close-with-unread-
  data, crashing blocking-socket sketches with ECONNRESET. Now we forward
  a MINIMAL request (method, path, Host, Connection: close, and
  Content-Type/Length for bodies) — nothing a tiny server can choke on.

- After a gateway request completed we dropped the connection immediately,
  so a late chip segment (retransmitted FIN / trailing ACK) no longer
  matched and fell through to the chip-initiated NAT, which RST it. Add a
  short TIME_WAIT: keep the connection briefly and re-ACK late segments so
  the chip closes cleanly, never RSTing it.
2026-06-14 03:11:54 +02:00
David Montero Crespo 319decf1bd
Merge pull request #243 from davidmonterocrespo24/feat/picow-iot-gateway
Feat/picow iot gateway
2026-06-13 21:37:58 -03: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 173cc3ea36 feat(picow): IoT gateway — proxy browser HTTP into the chip's server
ESP32 web-server examples are reachable from the browser via
/api/gateway/<client_id>/ (QEMU slirp hostfwd). The Pico W server lives
in the browser-side lwIP, so there was no inbound path: visiting the
chip's IP did nothing.

Add the mirror of tcp_nat.py: tcp_inbound.TcpInbound originates a TCP
connection INTO the chip over the WebSocket bridge (SYN -> SYN+ACK ->
ACK -> request -> response -> FIN), so the backend can fetch a page the
sketch serves on 10.13.37.42:80 and hand it back to the browser.

- bridge.py routes chip TCP segments addressed to a gateway-opened
  connection to TcpInbound (before the chip-initiated NAT, which would
  RST them); exposes http_into_chip() + ensure_chip_mac() (primes the
  chip's gateway ARP).
- iot_gateway.py: same /api/gateway/<client_id>/ route now falls through
  to the Pico W bridge when there's no ESP32 instance, builds a raw
  HTTP/1.1 request, and parses the chip's response. Same plan gate, same
  URL shape — the browser sees no difference between ESP32 and Pico W.

Validated end to end (real RP2040 emulator serving an HTTP page ->
gateway returns it) plus 6 unit tests for the TCP state machine,
response parsing and ARP priming.
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 Crespo b3558a4c42
Merge pull request #242 from davidmonterocrespo24/feat/picow-internet-bridge
Feat/picow internet bridge
2026-06-13 03:00:13 -03: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 Crespo 6345ba5fab
Merge pull request #241 from davidmonterocrespo24/fix/littlefs-utf8-truncation
fix(micropython): write LittleFS files with UTF-8 byte length
2026-06-12 23:11:04 -03:00