Commit Graph

91 Commits

Author SHA1 Message Date
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 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 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 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 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 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 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 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 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 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 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 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 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 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
David Montero 737ec5c6eb feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):

  * Toolbar "Import a project from a .zip file" → Wokwi .zip only
  * File-explorer "Open .vlx file"               → Velxio .vlx only

If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.

Fix: introduce `utils/importProject.ts` as the single dispatcher.  It
sniffs the extension and routes:

  *.vlx  → importVlxFile      (writes directly to stores)
  *.zip  → importFromWokwiZip (returns a payload the caller applies,
                               so the toolbar can still trigger the
                               install-libraries modal afterwards)

Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:

  * Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
  * File-explorer "Open project (.vlx Velxio or .zip Wokwi)"

The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.

Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-22 20:55:06 +02:00
David Montero Crespo 16f168ba7f feat(board-options): per-board options modal + persistence
Adds a new BoardOptionsModal accessible from the EditorToolbar that exposes
per-board options (currently used for board-specific compile flags). Wires
the modal through:

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:50:45 -03:00
davidmonterocrespo24 18a582455c feat(pi): Phase 3.3 — Pi Zero / Pi 1 / Pi 2 armhf simulators
Closes the deferred Phase 3.3. Root-causes the Pi 2 "Attempted to
kill init" panic as `mount /dev/vda` failing with EINVAL — Debian
armmp does not have ext4 builtin (only fuseblk in /proc/filesystems).

- qemu_manager: PI_CONFIGS gains raspberry-pi-zero / -1 / -2 entries.
  All three use the armmp armhf kernel + Cortex-A7 CPU + the mmio
  virtio transport (arm-32 virt PCI fails -75 due to missing reg DT
  property). Pi Zero / Pi 1 get the small 1-core / 512 MB profile;
  Pi 2 gets 4-core / 1 GB. QEMU command builder branches on cfg.bus
  for virtio-blk-pci vs virtio-blk-device (and serial likewise).
- manifest.json: new `raspberry-pi-armhf` image_set wiring three
  assets (kernel + initramfs + zstd rootfs).
- Frontend BoardKind gains the three new kinds + an isPiBoardKind()
  helper. Replaces the eight scattered `=== 'raspberry-pi-3' ||
  === 'raspberry-pi-4' || === 'raspberry-pi-5'` branches in
  useSimulatorStore, Interconnect, loadExample, boardProtocols.
  ComponentRegistry gets three new picker entries.
- board-kinds-coverage test: ACCEPTED_UNCOVERED gains the new kinds
  (backend boards have no canvas examples).

The matching armhf build-pi-kernel.sh / build-pi-rootfs.sh changes
live in velxio-prod's scripts/ (private overlay) — the upstream
kernel build script only knows about arm64; armhf is built in the
private repo because the assets ship through the license endpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:23:48 +02:00
davidmonterocrespo24 db5e3a8623 feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS
Backend: extract per-board config into a PI_CONFIGS dict keyed by
board_type. Pi 3/4/5 share the same arm64 image set (kernel +
initramfs + rootfs) and differ only in QEMU -cpu and -m:

  raspberry-pi-3 → cortex-a53  + 1G  (BCM2837, ARMv8 64-bit)
  raspberry-pi-4 → cortex-a72  + 2G  (BCM2711, ARMv8 64-bit)
  raspberry-pi-5 → cortex-a76  + 2G  (BCM2712, ARMv8 64-bit)

PiInstance now carries board_type so the per-board lookup happens
once at start_instance time. Unknown board_type falls back to
DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for
back-compat with older clients).

Pre-warm hook walks every unique image_set in PI_CONFIGS so the
provider only downloads each set once even when several Pi models
are registered.

Frontend:
- BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'.
- BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards
  (FQBN null since they use the Pi VFS + Python toolchain like Pi 3).
- ComponentRegistry inserts two new component metadata entries
  cloning the Pi 3 board art with different thumbnail colours.
  Tag name reused so the same velxio-raspberry-pi-3 web element
  draws the board on the canvas — the 40-pin GPIO layout is
  identical across Pi 3/4/5.
- boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table
  (PI3_BCM) since the 40-pin header layout is identical.
- loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS
  ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike.
- Interconnect.isPi3Bridge() recognises all three Pi family members
  so Arduino↔Pi serial routing keeps working.
- RaspberryPi3Bridge constructor gained a boardKind parameter
  defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message
  now ships the actual board kind so the backend knows which
  PI_CONFIGS entry to use.
- useSimulatorStore.addBoard wires bridge construction for all
  three Pi family members.

Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel
package + armhf rootfs build, no change here.

Smoke-tested inside the prod container:
  Pi 4 (cortex-a72) → reached agetty login on hvc0
  Pi 5 (cortex-a76) → reached agetty login on hvc0
Both show 'aarch64' in uname -m.
2026-05-18 15:41:29 +02:00
davidmonterocrespo24 04ac1bf53b chore(tests): silence three noisy warnings in deploy-gate output
deploy.sh's vitest + pytest output was polluted with three benign
but loud warnings that buried real signal:

1. AVRSimulator.start() unconditionally read `window.__spiceDebug`.
   In node-side vitest runs `window` is undefined → ReferenceError
   → console.warn('[spice] debug dump failed', e). Logged once per
   AVR test. Guarded with `typeof window !== 'undefined'`; in
   production the browser path is unchanged.

2. pinPositionCalculator.calculatePinPosition() warned every time
   document.getElementById returned null. In node-side tests there
   is no real DOM and every wire-related test triggers the warning
   for every component. Skip the console.warn when
   import.meta.env.MODE === 'test' (vitest sets MODE=test); the
   function still returns null and production retains the
   actionable warning for unmounted components.

3. test_esp32_wifi_args.py::test_start_instance_accepts_wifi_params
   mocked asyncio.create_task with no side_effect, so the coroutine
   from self._boot(...) leaked and triggered a "coroutine never
   awaited" RuntimeWarning. Mock now closes the coroutine.

After fixes:
  frontend tests:  0 spice/pinPositionCalculator stderr lines
  backend tests:   259 passed, 15 skipped, 1 warning (starlette
                   third-party python_multipart deprecation —
                   not ours, fixed when starlette updates).
2026-05-16 22:39:43 +02:00
davidmonterocrespo24 29d76348aa feat(sim): Phase 1d #3 + #5 — WASM pre-boot on mount + delete dead wire* utils
#3: `start.ts` now kicks `scheduler.start()` (lazy-boot the WASM
engine) right when the editor mounts.  Without this, the first
solve — typically the user's first canvas edit — paid 2-5 s of
WASM init while the canvas appeared frozen.  Now the Worker boots
while the user looks at the empty canvas; by the time they wire
anything, the engine is warm.

#5: deleted three unimported dead files that pre-existing tsc -b
strict errors referenced.  Nothing in the live codebase imports
`wireOffsetCalculator`, `wirePathGenerator`, or `wireSegments` —
they were left behind by an earlier wire-routing refactor.
Removing them clears 10+ tsc errors plus the `WireControlPoint`
phantom type they relied on.

Also cleaned up an unused import in
`capacitor-charge-transient.test.ts` (leftover from F2).

1461 tests pass, vite build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:24:31 +02:00
davidmonterocrespo24 54936ef660 feat(sim): Phase 1d #2 + #9 — convergence helpers in Worker + enable LM358 subckt
#2: NgSpiceWorkerAdapter.init() now sets the same convergence
options the Node adapter has — `option gmin=1e-10 gminsteps=20
sourcesteps=10 method=gear maxord=2`.  Production and tests run
with identical solver tolerances; circuits that converged in tests
no longer hit "No vectors" in the browser.  Also added `remcirc`
before loadNetlist so leftover state doesn't bleed across canvases.

#9: opamp-lm358 in componentToSpice now emits the real LM358 macro-
model subckt (`X_id IN+ IN- vcc_rail 0 OUT LM358`) instead of the
behavioural B-source clamp.  The subckt was vendored as an asset in
Phase 2.2 and has been waiting for #2 to land — now active.

Smoke-test side effect: 67/68 → 68/68 examples converge.  The opamp
follower (`an-opamp-follower`) was the last one that didn't.

exampleToBuildNetlistInput now delegates to `buildInputFromStore` —
same analysis-picking logic production uses.  A signal-generator
circuit gets `.tran`, an MCU-driven RC step gets `.tran` with the
right τ window, plain DC gets `.op`.  No more inline analysis guess.

examples-analog.test.ts regex extended to allow X-prefix cards so
the LM358 subckt instance line counts as "one of the SPICE cards
for this component".

1461 tests pass across 105 files (28 pre-existing skips, none
introduced by this commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:17:06 +02:00
davidmonterocrespo24 33570d7690 feat(sim): Phase 1d #1 — gallery smoke test imports real examples + shared helper
Replaces the manual "open each example in browser" step from the
post-migration plan with an automated test that:

  • Imports `analogExamples` and `digitalExamples` from the real
    `data/examples-*.ts` modules — new gallery entries pick up the
    test automatically.
  • Uses the same `stripBrandPrefix` + board-filter logic that
    production `loadExample.ts` uses, via the new shared helper
    `utils/exampleToBuildNetlistInput.ts`.  Single source of truth:
    if the wokwi/velxio prefix rule ever changes, both production
    and the smoke test track it.
  • Runs each example through `solveInput` (Phase 1c F2 helper)
    against the same ngspice WASM production uses.

`loadExample.ts` refactored to call `stripBrandPrefix` instead of
inlining the regex (two call sites converged on the helper).

Result against the gallery:
  • 67/68 examples converge cleanly.
  • 1 known regression: `an-opamp-follower` (LM358 follower) — the
    same case `examples-analog-live.test.ts` already skips.  Item
    #2 (.op convergence helpers in NgSpiceWorkerAdapter) targets it.

The smoke test now serves as the safety net for the remaining
post-migration work — it'll flag if a future fix breaks examples
that converge today.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:11:28 +02:00
David Montero Crespo 95f2aa9a9f fix(loadExample): clear currentProject before mutating stores
Critical data-loss bug. Repro:

  1. User opens a saved project at /<username>/<slug>. The page sets
     useProjectStore.currentProject = { id, slug, ownerUsername, ... }.
     Auto-save kicks in and starts watching simulator/editor stores.
  2. User clicks the "Examples" link, picks an example, hits Run.
  3. loadExample mutates useSimulatorStore (setComponents, setWires,
     addBoard, removeBoard) and useEditorStore (loadFiles).
  4. Auto-save sees the change. Its eligibility check finds
     currentProject still pointing at the user's saved project (we
     never touched useProjectStore). It debounces a
     PUT /api/projects/<old-id> with the EXAMPLE's components/wires/
     files. The user's saved project is overwritten with the example
     contents.

The URL changing to /editor isn't enough — useProjectStore is store
state, not router state. ProjectPage / ProjectByIdPage set it on
mount; nothing clears it when the user navigates away.

Fix: loadExample calls useProjectStore.getState().clearCurrentProject()
BEFORE the simulator/editor mutations. autoSaveImpl is subscribed to
useProjectStore via subscribe((s, prev) => ... reset() if id changed),
and Zustand notifies subscribers synchronously inside set(), so the
reset (projectId=null, baseline hash=null) runs in the same tick.
Every subsequent setComponents/setWires/loadFiles fires onChange in
the hook, which now sees projectId=null and returns early. No PUT
ever goes out.

The reset is order-sensitive: it must run BEFORE the store mutations
or the hook would already have queued a save with the old projectId
before we cleared. Comment in the source spells this out so it
doesn't get reordered in a future refactor.

In-flight saves are not affected: buildSavePayload() snapshots state
before its `await updateProject(...)`, so a save that started right
before the example load still sends the user's pre-example state to
the right project. Worst case: the save completes after clear, and
the hook quietly returns idle.

Build verified (vite OSS+pro, 285 SEO pages).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:02:05 -03:00
David Montero Crespo b4ab742456 feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.

New: utils/vlxFile.ts
  - buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
    editor + simulator stores.
  - triggerDownloadVlx({ name? }) — anchor-click download with a safe
    filename. Returns the filename actually used.
  - parseVlxFile(File) — async reader + validator. Checks
    format === "velxio-project", version <= 1, and the required
    arrays/objects are present. Throws VlxParseError with a human-
    readable message on any issue.
  - importVlxFile(File) — convenience wrapper that parses AND calls
    useSimulatorStore.loadProjectState() with the result.

Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).

lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.

components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.

Verified with both builds:
  - OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
    3 buttons (New, Open, Save).
  - OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
    SaveProjectModal as before. Open .vlx still works (independent
    button, not part of the save flow).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:33:00 -03:00
David Montero Crespo 81ac2283c6 fix(canvas): wires follow components on rotation
Rotating a part with the 90° button used to leave every wire pinned to
the pre-rotation pixel coordinates — the component visually unhooked
from its cables. Two paths were missing:

  1. useSimulatorStore.updateComponent only triggered updateWirePositions
     for x/y changes. A rotation went through properties.rotation, so
     wires never recomputed.
  2. calculatePinPosition didn't know about rotation. Even when called,
     it returned the unrotated offset, so the new endpoints would still
     have been wrong.
  3. recordRotate (undo/redo) skipped updateWirePositions on both legs,
     so Ctrl+Z after a rotate left the canvas inconsistent.

Fix:

  - calculatePinPosition gets a 5th `rotation` argument. When non-zero,
    it finds the .dynamic-component-wrapper ancestor in the DOM, reads
    its offsetWidth/Height (layout-only, immune to CSS transforms) to
    locate the wrapper centre, and applies a 2D rotation matrix around
    that pivot. The wrapper top-left is recovered as (componentX - 4,
    componentY - 6) to match the offset convention updateWirePositions
    already uses.
  - updateWirePositions and recalculateAllWirePositions read the per-
    component rotation and thread it through.
  - updateComponent recomputes wires whenever properties.rotation
    changes, mirroring the existing x/y path.
  - recordRotate.execute and .undo both call updateWirePositions so
    Ctrl+Z keeps the canvas coherent.

Tests (pin-position-rotation.test.ts, 6 cases): unrotated identity,
90° (left edge → bottom), 180° (point reflection), 360° round-trip,
negative angles, and a store-level integration that rotates a fake
component and asserts wires[0].start moves to the rotated coordinate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:03:32 -03:00
David Montero Crespo 71616e580d Add end-to-end tests for ESP32 I2C functionality and circuit verification
- Implemented `i2c-esp32-real-firmware.test.ts` to test ESP32 I2C communication via backend and WebSocket.
- Created `load-example-transitions.test.ts` to ensure proper loading of examples between board-less and board-based contexts.
- Added `CircuitVerificationModal.tsx` to display circuit verification results before running simulations.
- Developed `circuitVerifier.ts` to perform pre-flight checks for circuit safety, identifying potential issues like short circuits and component overloads.
- Introduced minimal ESP32 I2C master sketch `esp32_i2c_writer.ino` for testing I2C transactions.
2026-05-12 16:55:15 -03:00
David Montero Crespo a097601a73 Add HD44780Decoder and various I2C sketches
- Implement HD44780Decoder for decoding I2C commands to HD44780-compatible LCDs.
- Add bmp280_bridge_reader.ino to read BMP280 chip_id and status registers via I2C.
- Create i2c_scanner_multi.ino to scan I2C addresses and report responding devices.
- Introduce lcd_i2c_hello.ino to demonstrate basic LCD functionality with I2C.
- Implement pcf8574_bidirectional.ino to test bidirectional communication with PCF8574.
- Add pico_i2c_master_reader.ino for reading BMP280 from a Raspberry Pi Pico.
- Create rtc_lcd_clock.ino to display time from a DS1307 RTC on an I2C LCD.
2026-05-12 14:26:33 -03:00
David Montero Crespo c20a7498fc feat: add touch-friendly components for improved mobile usability
- Implemented PinPickerDialog for selecting pins on touch devices.
- Added SelectionActionBar for managing selected items with touch actions.
- Created WireModeBanner to provide feedback during wire creation.
- Introduced useTouchDevice utility for detecting coarse pointer input.
- Refactored WireLayer to utilize useIsCoarsePointer for touch detection.
2026-05-08 11:12:26 -03:00
David Montero Crespo 694a2f4073 fix(frontend): polyfill crypto.randomUUID for non-secure contexts
crypto.randomUUID() is only exposed on secure contexts (HTTPS, localhost,
127.0.0.1, ::1). When Velxio is self-hosted and accessed via a LAN IP over
plain HTTP (e.g. http://192.168.31.139:3080/), crypto.randomUUID is
undefined and any code path that calls it throws TypeError.

This silently broke ESP32 simulation start for self-hosters: the frontend
generates a UUID for the WS client_id when Run is clicked; the throw
rejected the promise before reaching the WS connect, so the backend
never got the start request — no worker spawned, logs empty, simulation
"didn't start" with no visible error.

Same root cause would also break the multi-file editor (createFile,
createFileGroup) on the same LAN-HTTP self-host setup, just less
observably.

Add a single generateUUID() helper that:
  1. Uses crypto.randomUUID() when available (secure context fast path).
  2. Falls back to crypto.getRandomValues() — which IS available in
     non-secure contexts — to build a v4 UUID by hand.
  3. Final fallback to Math.random() if even that is missing
     (defensive — Web Crypto getRandomValues has been universal for
     years).

Replace all 6 crypto.randomUUID() call sites:
  - frontend/src/simulation/Esp32Bridge.ts (2 sites — getTabSessionId)
  - frontend/src/store/useEditorStore.ts   (4 sites — file IDs)

Reported by a self-hoster on OrangePi 5B accessing Velxio via LAN IP.
DevTools console showed:
  TypeError: crypto.randomUUID is not a function
    at Ph (...) at wh.connect (...) at startBoard (...)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 13:41:30 -03:00
David Montero Crespo 6a88375bc0 feat: persist multi-board projects + add auto-save
The project save/load pipeline only persisted a single `board_type`, so
multi-board workspaces silently lost every board except the active one
on save, and wires referencing the dropped boards' IDs orphaned to the
canvas corner on reload. An audit of the production backup found 74/306
projects (24%) with at least one orphaned wire and 174/301 non-trivial
projects whose code was still the default Blink template — strong signal
that users save once and never re-save.

Backend
- Add `boards_json` column on `projects` with idempotent ALTER TABLE in
  the lifespan migration list.
- New `FileGroup` schema + `file_groups` array on
  ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat.
- `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via
  `read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on
  read; legacy single-list `files` only updates the active group, leaving
  other boards' files intact.
- `_persist_files_from_body` honors file_groups → files → code priority.

Frontend
- `useSimulatorStore.addBoard` accepts an optional `explicitId` so
  saved board IDs can be restored verbatim (wires reference IDs literally).
- New `loadProjectState({boards, fileGroups, components, wires,
  activeBoardId})` action: tears down current boards, recreates from the
  payload, restores file groups atomically, recalculates wire positions
  on the next frame, and refreshes the Interconnect.
- `useEditorStore.replaceFileGroups` for atomic multi-group restore.
- `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through
  `buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects
  by synthesising a default board from `board_type`).

Auto-save (#useAutoSaveProject hook)
- 2.5s debounced silent PUT triggered ONLY when an authenticated user
  has a `currentProject` with a UUID. State hash detects real changes
  vs. UI-only churn; baseline is reset on project load so the just-loaded
  state isn't immediately re-saved.
- `beforeunload` flush via `fetch keepalive: true` (supports PUT +
  credentials, survives unload).
- Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error).

Backfill script (one-off, idempotent)
- `backend/scripts/backfill_boards_2026_05.py` populates `boards_json`
  for legacy projects. Heuristic per project, based on which board IDs
  the wires reference:
    Case A — wires only ref 'arduino-uno' but board_type ≠ uno:
             rename id→board_type and rewrite wire endpoints.
    Case B — single-board normal: keep verbatim.
    Case C — multi-board: recreate one board per distinct ref, infer
             kind by stripping trailing -N suffix.
  Also moves any flat files into the active board's group subdir.
  Stdlib-only, runs from host or `docker exec`.

Docker
- `Dockerfile.standalone` now copies `backend/scripts/` into the image
  so the backfill is callable via `docker exec velxio-app python
  /app/scripts/backfill_boards_2026_05.py --apply`.

Verified locally on the restored production backup (363 projects):
33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans.
Re-running the script after apply skips all 363 (idempotent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 13:43:33 -03:00
David Montero Crespo 2ac94aed24 fix(loadExample): update filename logic for Arduino-style boards to ensure correct file extension 2026-04-28 20:40:48 -03:00
David Montero Crespo 7f2014bef7 Add ESP32 chip demos and comprehensive tests for I2C, SPI, and UART interactions
- Implemented `esp32_spi_chip_demo.ino` to demonstrate SPI communication with a 74HC595 shift register.
- Created `esp32_uart_chip_demo.ino` for UART loopback testing with ROT13 transformation.
- Added Python tests for compiling chips and sketches, ensuring valid WASM output and successful compilation for various board families.
- Developed end-to-end tests for ESP32 with custom chips using I2C and SPI, validating synchronous communication through the backend.
- Introduced GPIO bridge tests to verify serial communication and GPIO state changes.
- Ensured all tests validate the expected behavior of the custom chips and their interaction with the ESP32 firmware.
2026-04-28 19:24:39 -03:00
David Montero Crespo fa170a082a Add shared validators and test configurations for Velxio projects
https://github.com/kritishmohapatra/100_Days_100_IoT_Projects
- Introduced `_lib.py` containing shared validators for board support and static source analysis for MicroPython projects.
- Added `conftest.py` to configure pytest for the test suite, simplifying import paths.
- Created `NOT_SUPPORTED.md` files for two projects indicating they cannot be emulated in Velxio due to lack of source code.
- Implemented unit tests for the unsupported projects to verify the presence of the NOT_SUPPORTED marker and source preservation.
2026-04-28 00:36:05 -03:00
David Montero Crespo 8e769f8a4e feat(multi-board): add wire-aware cross-board interconnect router
Fixes the user-reported bug where two RPi Pico W boards wired GP0↔GP1
running SerialPassthrough don't communicate. Replaces the broken
broadcast-style cross-board logic in addBoard (only routed AVR↔Pi3B,
ignored wires entirely, no RP2040↔anything path) with a wire-aware
Interconnect singleton.

Architecture: digital pin transitions are the lowest-common-denominator
abstraction. Each simulator's hardware peripherals (UART/I2C/SPI) and
bit-banging libraries (SoftwareSerial, software I2C) decode the
transitions naturally — propagate the pin and the protocols come for
free. For cross-process boards (ESP32 backend QEMU, Pi3B QEMU) a
byte-level shortcut is additionally enabled on hardware-UART pin
pairs to handle high-baud links over WebSocket latency.

Implementation:
- New simulation/Interconnect.ts singleton subscribes to wire/board
  changes via the Zustand store. Handlers per tier: browser-sim →
  pinManager.onPinChange, ESP32 → Esp32Bridge.sendPinEvent, Pi3B →
  bridge.sendPinEvent. Re-entrancy guard via per-(board,pin) Set.
- New utils/boardProtocols.ts classifies pins (uart-tx, i2c-sda, etc.)
  per board kind, used as optimization hint for the byte shortcut.
- types/wire.ts: added signalType field, exports WireSignalType /
  WireColorMap (fixes a pre-existing TS import error in wireColors).
- Deleted the bridgeMap/simulatorMap broadcast forEach blocks in
  addBoard. Initial board + future boards register with Interconnect
  via setInterconnectRuntime + store subscription.
- PinManager.resetPinStates() helper for test isolation.

Tests (16 new files, 96 tests, all passing):
- Per-pair × per-protocol matrix: dual-arduino-digital,
  dual-pico-digital, arduino-pico-digital, triple-pico-digital-chain,
  dual-arduino-hw-uart, dual-arduino-software-serial,
  arduino-pico-mixed-uart, arduino-esp32-uart, dual-esp32-uart,
  pi3-pico-uart, arduino-pico-i2c, arduino-arduino-spi,
  interconnect-routing, dual-arduino-multi-protocol (UART+I2C+SPI+
  digital + concurrent), dual-pico-multi-protocol (UART0+UART1 alt+
  I2C0+I2C1+SPI0+digital + 3-Pico star topology)
- Updated dual-pico-serial-passthrough to assert correct behaviour
- Backend test/multi_board_esp32/test_dual_esp32_serial.py for two
  real QEMU instances (skip-graceful when lcgamboa lib absent)

Verified: 1107/1107 tests pass, zero regressions, vite build OK.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 19:47:40 -03:00
David Montero Crespo 5bf3a3d5ed feat(metrics): add usage analytics dashboard with country tracking
Track per-user, per-project, and per-board usage to inform pricing tier
decisions. Adds an admin dashboard with KPIs (DAU/WAU/MAU, totals,
success rate), time-series charts for compiles/runs, board family +
FQBN breakdowns, "board diversity" pie chart (key freemium signal),
top users/projects, and per-country breakdown via Cloudflare's
CF-IPCountry header. Admin can now also view private projects.

Backend:
- New UsageEvent table (append-only event log with user_id, project_id,
  event_type, board_fqbn/family, country, error_kind, duration_ms)
- Aggregate counters on User (total_compiles/runs/errors, last_active,
  signup_country, last_country) and Project (compile/run/update counts,
  last_compiled/run timestamps) kept in sync by MetricsService for O(1)
  dashboard reads
- 10 admin endpoints under /api/admin/metrics/{overview, timeseries,
  boards, board-diversity, top-users, top-projects, countries,
  users/{id}, projects/{id}}
- POST /api/metrics/run for client-side run telemetry
- Country detection via cf-ipcountry header (no DB / no API calls)
- Auto-migrations in lifespan for legacy DBs

Frontend:
- recharts-powered Dashboard tab with KPI cards and 4 charts
- New Boards tab with per-family + per-FQBN breakdown
- Country column with flag emoji on Users tab
- Top countries card on Dashboard
- compileCode now forwards project_id; Run button reports via WS

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 19:47:40 -03:00
davidmonterocrespo24 4708d54195 fix(examples): re-create board when loading an Arduino example after an analog one
Loading an analog-only example removes every board. The single-board
branch of loadExample then called setBoardType, which only maps over
existing entries in boards[] and silently did nothing when the array
was empty — components rendered but no board. Fall back to addBoard +
setActiveBoardId when there are no boards.
2026-04-23 05:22:35 +02:00
David Montero Crespo 692af00258 style: update ESLint rules and clean up code formatting across multiple files 2026-04-21 17:14:27 -03:00
David Montero Crespo 53efc226a3 Merge branch 'master' into feature/electrical-simulation-ngspice
# Conflicts:
#	deploy/entrypoint.sh
#	frontend/src/components/examples/ExamplesGallery.tsx
2026-04-21 17:11:26 -03:00
David Montero Crespo 212ecd1bcb refactor: rename components and update prefixes to 'velxio-' for consistency
- Modified the index file to reflect the new naming convention for Velxio components.
- Changed JSX declarations to use 'velxio-' prefix for various components.
- Updated component overrides to replace 'wokwi-' with 'velxio-' for logic gates and other components.
- Adjusted SVG generation script to use 'velxio-' prefix for BMP280 and Raspberry Pi components.
- Marked submodules as dirty in QEMU and RP2040 libraries.
- Added .prettierignore and .prettierrc.json for consistent code formatting.
- Introduced InstrumentComponent with support for Voltmeter and Ammeter, including pin information handling.
2026-04-21 16:45:45 -03:00
David Montero Crespo eaf3fffd36 Refactor code structure for improved readability and maintainability 2026-04-21 12:59:59 -03:00
David Montero Crespo 00a15c6f76 feat: add 'circuits' category to ExampleProject interface
fix: increase timeout for compilation requests to 180 seconds

refactor: call recalculateAllWirePositions after loading examples

chore: update subproject commit for rp2040js to dirty state

chore: update subproject commit for wokwi-elements to dirty state
2026-04-16 08:39:47 -03:00
David Montero Crespo 09d048e20f feat: add ATtiny85 support with examples and simulation tests; update pin mapping and AVRSimulator logic 2026-04-15 02:46:33 -03:00
David Montero Crespo 71fa6eae4a Merge branch 'master' into feature/shareable-urls
# Conflicts:
#	.gitignore
2026-04-08 00:03:59 -03:00
David Montero Crespo 9761aad0be feat: enhance Arduino library handling by detecting external libraries and creating IDF components, add tests for library resolution logic 2026-04-07 14:59:51 -03:00
David Montero Crespo 8c68879ce7 feat: add share functionality for projects and examples
- Implemented ExampleLoaderPage to load examples by ID from the URL.
- Added ExampleLoaderPage route to App component.
- Created ShareModal for sharing project links with visibility toggle.
- Updated UserProfilePage to include share button for user projects.
- Enhanced ExamplesGallery with a copy link button for examples.
- Introduced utility function loadExample to streamline example loading and library installation.
- Updated project visibility management in useProjectStore.
- Added styles for new components and buttons.
- Updated .gitignore to include Arduino compilation byproducts.
2026-03-30 19:00:33 -03:00