Commit Graph

1200 Commits

Author SHA1 Message Date
David Montero 1d65badcae feat(ci): create GitHub Release + tag on each announce so the release link resolves 2026-06-11 19:20:49 +02:00
David Montero 3fe7de2d25 fix(ci): rework discord-release-notify
- Reorder: send Discord FIRST; commit CHANGELOG + version bump ONLY on
  a successful announce, so a failed send never consumes a version.
- Raise max_tokens for the deepseek-v4-flash reasoning model (6000/4000)
  so reasoning+content fit and content is never empty.
- Guard against empty content (don't POST an empty Discord message).
- Add workflow_dispatch (manual re-fire) with an optional base ref input.
2026-06-11 19:12:52 +02:00
David Montero Crespo 603c6b861f
Merge pull request #228 from davidmonterocrespo24/release
Release
2026-06-11 10:02:43 -03:00
David Montero 937b912c45 chore(ci): call deepseek-v4-flash instead of deepseek-chat for release notes 2026-06-11 14:58:24 +02:00
David Montero Crespo 6ac425c2fb
Merge pull request #227 from davidmonterocrespo24/fix/release-version-autobump
fix(ci): auto-increment release version on each Discord announce (baseline 3.0.0)
2026-06-11 09:56:08 -03:00
David Montero 200875720c fix(ci): auto-increment release version + baseline 3.0.0
Seed the release branch with the corrected discord-release-notify workflow
and version baseline BEFORE the next master->release merge, so that merge
announces v3.0.0 and the workflow bumps the patch (3.0.0 -> 3.0.1 -> ...)
on every subsequent merge instead of repeating the same version.

A direct push (not a pull_request) does not trigger the announce workflow,
so this is safe to land here ahead of the merge.
2026-06-11 14:54:43 +02:00
David Montero f27303264f fix(ci): auto-increment release version on each Discord announce; baseline 3.0.0
The Discord release-notify workflow read the version from
frontend/package.json but never wrote it back, so every merge to release
announced the SAME version (the CHANGELOG ended up with two "[2.0.1]"
entries). Now, after generating the CHANGELOG and before announcing, the
workflow bumps the PATCH in frontend/package.json and commits it alongside
the CHANGELOG to release. Each merge advances the counter:
3.0.0 -> 3.0.1 -> 3.0.2 ...

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

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

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

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

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

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

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

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

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

Pre-existing since 4cb5748 (custom-chip first-class circuit nodes).
2026-06-11 04:19:35 +02:00
velxio-deploy 2408ccde54 chore(examples): refresh 1 thumb file(s) [auto] 2026-06-11 04:18:32 +02:00
David Montero Crespo a2174b316e
Merge pull request #224 from davidmonterocrespo24/feat/microsd-card-storage
feat(microsd): SD-over-SPI card storage for AVR, RP2040 and ESP32
2026-06-10 23:01:38 -03: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 Crespo 41ffa9a0c3
Merge pull request #223 from davidmonterocrespo24/fix/esp32-mbedtls-psk-wificlientsecure
fix(esp32): enable mbedTLS PSK so WiFiClientSecure/HTTPClient link
2026-06-10 16:19:20 -03:00
David Montero 0daa45df76 fix(esp32): enable mbedTLS PSK so WiFiClientSecure/HTTPClient link
arduino-esp32's WiFiClientSecure/ssl_client.cpp wraps its ENTIRE body
(start_ssl_client, ssl_init, send_ssl_data, ...) in
  #if !defined(MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED) ... #else <body> #endif
ESP-IDF's mbedtls defaults the PSK key-exchange modes OFF, so the object
compiled empty and any sketch using WiFiClientSecure — including
HTTPClient.begin(url), which links the secure client even for http:// —
failed to link with "undefined reference to start_ssl_client". Commenting
out begin() let the optimizer drop the unused client, which is why it
"compiled when commented".

- sdkconfig.defaults.in: enable the PSK key-exchange ciphersuites
  (CONFIG_MBEDTLS_PSK_MODES + the four KEY_EXCHANGE_*PSK), matching
  arduino-esp32's own sdkconfig.
- espidf_compiler: ESP-IDF only seeds sdkconfig from sdkconfig.defaults when
  sdkconfig is ABSENT. Persistent build dirs live in the build volume and
  keep a stale sdkconfig across image rebuilds, so the new CONFIG_* would
  never reach kconfig. Drop the generated sdkconfig when the rendered
  defaults change so it re-seeds on configure.
- test: assert the rendered sdkconfig enables PSK.

Verified end-to-end: the reported WiFi+HTTPClient sketch now compiles to a
1.1 MB binary (was a hard link error before).
2026-06-10 20:50:48 +02:00
David Montero Crespo c3810f16f5
Merge pull request #222 from davidmonterocrespo24/fix/esp32-bundle-mkspiffs
fix(esp32): bundle mkspiffs so uploaded SPIFFS files are actually fla…
2026-06-10 15:09:33 -03:00
David Montero 7bdf28ed76 fix(esp32): bundle mkspiffs so uploaded SPIFFS files are actually flashed
The SPIFFS upload feature (GH #162) is wired end-to-end — Board Options
uploads -> spiffs_files -> espidf_compiler._build_spiffs_image() ->
_merge_flash_image() at the partition offset — but the mkspiffs binary it
shells out to was never bundled in the image. _locate_mkspiffs() returns
None, _build_spiffs_image() logs "mkspiffs not found ... files will be
ignored" and returns None, and the merged flash ships with a blank SPIFFS
region. The firmware's SPIFFS.begin() then fails to mount, auto-formats,
and the uploaded files never appear.

Install the arduino-esp32 0.2.3 flavor of mkspiffs (the exact build
referenced by the arduino-esp32 2.0.17 package index, matching the SPIFFS
on-disk format SPIFFS.begin() expects) into the espidf-builder stage at the
path the runtime already searches (IDF_TOOLS_PATH/tools/mkspiffs/*/mkspiffs/).
No code change needed — _build_spiffs_image() works once the binary exists.

LittleFS is a separate follow-up (arduino-esp32 2.0.17's index does not ship
mklittlefs; needs sourcing the matching tool + FS-type selection in the
compiler).
2026-06-10 19:53:21 +02:00
David Montero Crespo 31dafdac4a
Merge pull request #221 from davidmonterocrespo24/fix/rp2040-realtime-idle-elision
fix(rp2040): keep delay()-based sketches real-time on slower hosts
2026-06-10 14:07:28 -03:00
David Montero 50823e9d49 fix(rp2040): keep delay()-based sketches real-time on slower hosts
The RP2040 core (125 MHz Cortex-M0) is ~8x heavier to emulate than the
AVR. The run loop used a FIXED per-frame cycle budget, and arduino-pico
delay() busy-waits the timer (no WFI), so a host that cannot sustain
125M instr/s rendered a 1s blink every 4-5s (sim ran in slow motion).

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 22:46:21 -03:00
David Montero 02c8ad756d feat(library-manager): collapse to a single unified tab with state-aware row actions
Remove the 3 tabs (In project / Search / Installed). One list now: browse your
installed + custom libraries by default, search the index when you type. Each
row is state-aware:
  + Add to project   — installs if needed, then declares it on the active board
  In project (toggle) — click to remove from this board's manifest
  Uninstall / Remove  — free the cache / remove your custom upload
'Install' is folded into 'Add to project' (install-on-add) for simplicity. The
per-board manifest (board.libraries) stays the compile scope. The pro custom-zip
upload button still injects into .lib-modal-header. The in-modal velxio.json
editor tab is gone (the manifest is shown by the explorer's libraries.json file).
2026-06-08 15:51:34 +02:00
David Montero e14a8bba78 feat(P2.1h): Library Manager 'Installed' list reads the cache, not the global volume
list_installed_libraries() now honors VELXIO_FALLBACK_SKETCHBOOK (pro overlay) so
GET /api/libraries/list enumerates the content-addressed cache instead of the
shared global dir — the Installed tab survives the global volume's retirement
(audit finding #3). Unset (OSS self-host) -> default sketchbook, unchanged.
2026-06-08 06:14:17 +02:00
David Montero 3fe1766dc8 feat(P2.1h): no-manifest + scan-all-retry resolve from the content-addressed cache, not the global volume
The last paths that read the shared global /root/Arduino/libraries: a compile
with NO manifest (libraries=null — any from-scratch/anon sketch; the manifest is
never auto-derived from #includes) and the incomplete-manifest scan-all retry
(which re-enters compile unscoped). Both fell through to the global dir,
bypassing the cache entirely (a cached lib still failed when global was gone).

Now, when no scope is materialized, point the library search at the cache: the
cache root is itself a valid Arduino libraries dir (each <name@ver-sha> child is
a library), exposed via env (pro overlay sets them):
  - arduino-cli: ARDUINO_DIRECTORIES_USER = VELXIO_FALLBACK_SKETCHBOOK (whose
    libraries/ -> cache root) when scope_dir is None.
  - ESP-IDF: _find_arduino_libraries_dir() prefers VELXIO_FALLBACK_LIBRARIES_DIR.
Unset (OSS self-host) -> legacy global, unchanged.

Also strip a trailing @version from manifest names (_bare_lib_names): norm_name
fused 'ArduinoJson@6.21.5' -> 'arduinojson6215' (cache miss -> global scan-all);
the per-board boards_json manifest is the path that still carries @version.
2026-06-08 06:02:55 +02:00
David Montero 7674b7b15e feat(examples): declare library manifests for the two lib-using AVR examples (P2.4-arduino examples)
lcd-hello -> ['LiquidCrystal'], uno-servo -> ['Servo']. These were the only
non-ESP32 gallery examples using a USER library without a manifest; loading +
compiling them now sends the library scope (resolved from the content-addressed
cache) instead of falling back to the global scan-all. Every other non-ESP32
example is core-only (Wire/SPI are core-bundled; the RP2040 core bundles Servo,
so pico-servo needs no manifest) or already declared its libraries.
2026-06-08 04:24:02 +02:00
David Montero 1aae505aa7 fix(P2.2-sec): visibility-gate cross-tenant custom-library resolution
The compile scope resolved a project OWNER's per-user custom libraries for ANY
project_id a requester supplied, with no visibility check — so a requester who
knew a victim's PRIVATE project_id + custom-lib name could compile a binary
against the victim's private uploaded library. Replace the ungated
get_project_owner hook with resolve_compile_owner(project_id, requester_id),
which returns the owner ONLY when the requester IS the owner OR the project is
shareable (public/unlisted); otherwise None -> the caller falls back to the
requester's OWN store. Fails closed on any error.

Also gate the server-side manifest fallback by the same rule (symmetry): a
private project's declared library NAMES are no longer read into a non-owner's
compile scope. The owner's own compile and shared/embed compiles of public/
unlisted projects keep their backend-authoritative scope unchanged.
2026-06-08 00:36:29 +02:00
David Montero d3f06265ac fix(P2.1f): isolate scoped library reads via ARDUINO_DIRECTORIES_USER, not --libraries
Empirically, arduino-cli's --libraries ADDS to the search path — the global
sketchbook is STILL scanned, so the prior commit did NOT isolate reads from the
shared global volume. Point ARDUINO_DIRECTORIES_USER at the scratch sketchbook
instead (its <scratch>/libraries becomes the ONLY user-library dir); cores +
board-manager URLs live in the DATA dir and are untouched.

In-container functest (AVR uno): a cache-only lib compiles via the scope (never
in global); a global-only lib NOT in the manifest is INVISIBLE to the scoped
compile and only recovers via the scan-all retry (manifest_incomplete=True) —
proving the global volume is no longer scanned for a manifest-scoped build.
2026-06-07 23:27:44 +02:00
David Montero 2ee443470d feat(P2.1f): scope AVR/RP2040/ATtiny library reads to the content-addressed cache
arduino-cli compiles now read libraries from a per-compile --libraries dir of
symlinks materialized by the pro overlay (owner store -> cache -> legacy), via
the existing materialize_library_scope hook, instead of scanning the shared
mutable global volume. --libraries only overrides the USER library search path,
so cores + board-manager URLs (RP2040 earlephilhower, ATTinyCore) are untouched.

Mirrors the ESP-IDF P2.1e graceful fallback: an incomplete manifest (a needed or
transitive lib not declared) makes the scoped compile miss a header; we retry
ONCE scan-all (no --libraries) and surface manifest_incomplete, so a partial
manifest degrades to legacy behavior instead of hard-failing.

Dedup correctness: the manifest + owner are resolved ONCE (shared
_resolve_compile_scope) and folded into the /compile/start dedup key AND threaded
into the build, so two owners with identical sketch+board but different custom
libs never coalesce to one another's job, and the key never diverges from the
bytes the build uses. Owner folded only when a manifest applies (index-only
compiles keep cross-owner dedup).
2026-06-07 23:12:24 +02:00
David Montero 02d396318f feat(P2.1): install index libraries by WARMING the shared cache (not the global dir)
New OSS warm_library hook; /api/libraries/install now calls it (with the
requester id for the anon policy) instead of mutating the shared global
libraries volume — so that volume stops growing and can be retired (P2.5).
Falls back to the legacy arduino-cli global install when no overlay is loaded
(OSS self-host parity).
2026-06-07 22:33:05 +02:00
David Montero 97f390719f feat(P2.2c): show per-user custom libs in the Library Manager + autocomplete
The Library Manager Installed tab + the velxio.json add-autocomplete now merge
the user's per-user custom uploads (getCustomLibraries -> GET /api/pro/libraries/
custom) with the shared global index list, so users can see and reuse their own
uploads (which live in the per-user store, not the global list). A custom lib's
button removes it via the per-user delete endpoint (not arduino-cli uninstall,
which would not find it). Degrades to [] for OSS/anon.
2026-06-07 19:58:13 +02:00
David Montero cc40bda3eb feat(P2.2): owner falls back to requester + auto-declare uploaded custom lib
- compile.py: owner_id = project owner ELSE the requester (so an unsaved
  compile resolves the libs the user just uploaded, which are their own);
  threaded requester_id into _run_compile from both call sites.
- LibraryManagerModal: on a custom .zip upload, auto-add the lib to the active
  board's velxio.json + show the Project tab, so the compile resolves it via the
  owner per-user path (the upload now lands in the per-user store, not the
  shared dir, so it must be declared to be found).
2026-06-07 17:20:12 +02:00
David Montero 6d5f6b01a4 feat(P2.2): thread project owner_id into compile scope materialization
So a scoped compile can resolve the project OWNER's per-user custom libraries
(not the requester's) — a shared/embed/anon compile of someone else's project
still finds that owner's uploaded libs.

- core/hooks.py: new get_project_owner hook; materialize_library_scope gains an
  opaque owner_id param (no-op default unchanged).
- espidf_compiler.compile/_attempt: thread owner_id to the materializer.
- compile.py: resolve owner via get_project_owner(project_id), pass to compile.

Additive: the OSS image (no overlay) ignores owner_id; index libs still resolve
from the cache. Foundation for per-user custom-lib storage (P2.2a write side).
2026-06-07 17:12:08 +02:00
David Montero 810d8e4b51 feat(espidf): per-compile library-scope materialization hook (P2.1e read repoint)
A scoped ESP32 compile can now resolve libraries from a per-compile directory
provided by an overlay (the manifest's libs symlinked from a content-addressed
cache, with a legacy-dir fallback) instead of the single shared global volume.

- core/hooks.py: register_materialize_library_scope / materialize_library_scope
  (no-op default -> None, so the OSS image keeps its single scan-all dir).
- espidf_compiler: _attempt(allowed) calls the hook, folds the returned content
  token into the build-variant eff_hash (a content change gets a clean build
  dir), passes libraries_dir to _compile_in_dir (arduino_libs = libraries_dir or
  _find_arduino_libraries_dir()), and removes the throwaway dir after. The
  graceful scan-all fallback (allowed=None) keeps using the default dir, so the
  worst case of any materializer failure is fall-back-to-legacy (no break).
2026-06-07 16:37:37 +02:00
David Montero 96b8ca309e feat(library-manifest): one velxio.json per board, grouped with its code
Moved the velxio.json entry out of a single top-level row (ambiguous about
which board it applied to) into EACH board's file group, next to that board's
sketch. Each board now shows its own velxio.json with its own declared-library
count; clicking it switches to that board and opens the Library Manager on its
list. Makes the per-board manifest model unambiguous.
2026-06-07 07:17:00 +02:00
David Montero 8617d3b224 feat(library-manifest): per-board manifests + autocomplete
Library manifests are now PER-BOARD (each board carries its own velxio.json),
so two boards in one project can use different (even conflicting) libraries
without clashing — the multi-board extension of the no-clash guarantee.

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

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

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

Safe: a core-only example sends null (legacy scan-all); a stale/incomplete
manifest degrades to scan-all via the backend graceful fallback, never a wrong
build. Ignored by the backend for non-ESP32 (arduino-cli) boards. Example
manifests were completed (incl. transitive deps) in c671c9b.
2026-06-07 00:10:15 +02:00
David Montero 472973f05b fix(esp32): auto-retry once on transient infrastructure build failures
Per-variant build dirs fixed the cross-compile staleness, but the FIRST build of
a cold variant can still occasionally hit ESP-IDF nested-build flakiness (cmake /
bootloader / managed-components / sdkconfig). These are infrastructure failures,
never user code, and clear on a retry once the variant dir is warmer. Retry the
attempt once when the failure matches infrastructure markers (not a user-sketch
or missing-library error). Cheap via ccache + ninja incremental.
2026-06-06 21:55:25 +02:00
David Montero c996ca3717 fix(esp32): per-variant persistent build dirs instead of wiping one shared dir
Replaces the wipe-on-change approach (which left ESP-IDF's nested bootloader /
managed-components build in a broken state under rapid reconfigure -
intermittent 'managed_components_list.temp.cmake: No such file'). Each distinct
configuration (board options x resolved library set, via the variant key the
caller already computes) now gets its OWN persistent project dir with its OWN
build/, never wiped or reconfigured for a different config:

  - same config  -> same dir -> warm ninja incremental + ccache (fast iterate);
  - different config -> different dir -> isolated, consistent, no staleness,
    no nested-build breakage;
  - the scoped vs scan-all fallback attempts land in different dirs, so the
    double-compile no longer corrupts a shared build/.

Variant dirs are LRU-bounded (_MAX_BUILD_VARIANTS per target); the global ccache
warms a fresh/evicted variant in seconds. Cleans up the old single-project
layout on first run. Fixes the cross-compile staleness for legacy AND manifest
compiles, and the fallback regression.
2026-06-06 21:13:42 +02:00
David Montero 33fbc03429 fix(esp32): reset build dir on library-set change via options hash, not mid-compile wipe
Supersedes the mid-_compile_in_dir build/ wipe (161deb9): wiping build/ AFTER
materializing libs but right before cmake left ESP-IDF's config half-regenerated
during the fallback's scoped->scan-all double-compile, intermittently failing
with 'sdkconfig.h: No such file'.

Instead fold the effective library set (the manifest, else the sketch's
non-core external includes) into the per-attempt build-dir hash, so a changed
lib set — or the scan-all fallback after a scoped attempt — resets the
persistent build/ at _prepare_persistent_project_dir time (before any cmake).
That is the existing, well-tested early-wipe path, so the configure is always
clean. Same lib set across compiles keeps the warm ccache/ninja cache; core-only
sketches share one dir (core headers filtered out of the token) so they never
trigger a spurious wipe.

Fixes both the original cross-compile staleness (intermittent cmake-configure
failures + stale-object false positives) and the fallback regression.
2026-06-06 20:53:18 +02:00
David Montero 161deb93e2 fix(esp32): wipe persistent build/ when the resolved library set changes
The persistent per-target build/ caches ESP-IDF's cmake configuration, ninja's
incremental graph and ccache-backed objects, all assuming a stable component
set. When consecutive compiles on the same dir have a DIFFERENT resolved
user_libs set (a different project/user, or a different library manifest) that
cache is inconsistent and produces two real failures:

  - cmake reconfigure intermittently fails ('cmake configure failed') even
    though each manifest compiles fine on a clean dir;
  - ninja/ccache reuse a previous compile's objects/headers, letting a
    now-absent library slip through as a false-positive success against a lib
    the current sketch/manifest no longer includes.

Fingerprint the materialized user_libs/ (sorted relative paths + sizes) and,
when it changes vs the last compile on this dir, wipe build/ to force a clean
configure. ccache (enabled) refills the objects so the rebuild stays cheap.
No-op on the ephemeral path and the first compile. Fixes both symptoms; will be
superseded by the fully ephemeral per-compile workspace (P1).
2026-06-06 20:32:26 +02:00