Commit Graph

1523 Commits

Author SHA1 Message Date
David Montero Crespo a0aeb101cd test: declare `canvas` so the jsdom tests get a real 2D context
captureDisplay.test.ts runs under @vitest-environment jsdom and says in its own
header that "jsdom + the `canvas` package give a real 2D context here" — but
canvas was never a direct devDependency. jsdom lists it as optional, so npm
skipped it, getContext('2d') returned null, and five tests died on
`ctx.fillStyle` with a TypeError. That aborted the pre-deploy test gate.

Declaring it directly makes the install deterministic; the lockfile already
carried the entry. All six tests pass with it present.
2026-08-18 02:05:01 +02:00
David Montero Crespo 20fc27e680 fix(assets): Pi 4/5 photos had an opaque white background
On the landing's dark board tiles they rendered as a white slab around the
board. Keyed the near-white out to transparent and cropped to the board:
Pi 4 goes 1024x1024 -> 922x599. Pi 5 kept its framing (it had no margin) and
just gained the alpha.
2026-08-18 01:35:39 +02:00
David Montero Crespo 9fb887dd13 i18n(landing): hero board count 19 -> 35, matching the boards section
Same page was advertising two different numbers after the boards section grew.
2026-08-18 01:01:32 +02:00
David Montero Crespo 117e35e3a3 i18n(landing): boards blurb now says 35 boards / 6 architectures
The supported-hardware section listed 19 boards across 5 architectures. It now
covers STM32 (Cortex-M3/M4), Raspberry Pi 4 and 5, and the partner boards from
Pimoroni, M5Stack, Seeed Studio and DFRobot — 35 in total across 6. Updated in
all nine locales so no language advertises the old count.
2026-08-18 01:00:56 +02:00
David Montero 8abed79ad9 fix(arduino-cli): library search/install failures become one actionable sentence
arduino-cli reports errors as plain text or, with --format json, as a JSON
object on stderr. We forwarded that blob verbatim, so on a fresh install
with no library index yet (and no way through the firewall/proxy to
downloads.arduino.cc) the in-app agent told the user "the library server
returns a JSON error" ten times in a row.

humanize_cli_error() maps the known shapes — index download failed,
library zip download failed, library not found — to a single sentence
that says what to check, and keeps the raw text under raw_error. Applied
to search_libraries (rc != 0 and non-JSON stdout) and install_library.
Tests use the real arduino-cli 1.0.4 output captured offline.
2026-08-18 00:28:07 +02:00
David Montero f09489f498 fix(editor): toolbar never overlaps on a narrow bar; layout switches live in the View menu
Two things went wrong when the window was small and the AI chat docked:

1. Libraries / Output painted over the board selector. The editor zone
   has min-width: max-content, but its child .editor-toolbar-wrapper is
   an inline-size query container, and size containment makes it
   contribute zero intrinsic width — so the floor resolved to nothing,
   flexbox squeezed the zone under its buttons and the bar never got to
   wrap. Containment is now off for the in-bar mount (the wrapper keeps
   it for its standalone mobile mount); the two label collapses that
   depended on it are re-keyed to the unified-toolbar container at the
   same thresholds the canvas controls use (1010 / 905px), so the row
   degrades in step and, when it truly cannot fit, wraps instead.

2. The explorer / Code / Both / Circuit segmented toggle now hides once
   the shared bar is under 760px (its inline display moved to CSS so a
   container query can do that). Every switch it had is a checkmarked
   row in the View menu: File Explorer, and Code / Both / Circuit as a
   radio group under a View mode label. explorerOpen moves from
   EditorPage state into useEditorStore so the menu can read it.
2026-08-18 00:17:38 +02:00
David Montero 51b775997a docs(readme): weather-station hero shows the whole circuit, not a crop of the TFT
The previous GIF was a tight crop of the ILI9341 panel and rendered huge
and cut off at the top of Screenshots. This one is the landing's
estacion-meteorologica GIF cropped to the circuit (ESP32 + ILI9341 +
BMP280 + DHT22) and displayed at 520px.
2026-08-18 00:02:24 +02:00
David Montero ae1341d9c2 docs(readme): refresh for the 2026 catalog — new screenshots, 40-board table, partners footnote
- Screenshots and board art come from the velxio_docs portal (fresh
  captures of the current UI); the six 2025-era img1..6.png are gone.
- Supported Boards now covers all seven families (Arduino, Pico, ESP32,
  ESP32-S3/C3, STM32, Raspberry Pi Linux, branded boards) and says
  where each one runs: self-hosted, velxio.dev, or paid plan.
- Trim the deep-dive sections that the docs portal now owns: the ESP32
  / RISC-V / Pi 3 emulation bullets, Serial Monitor, Component System,
  Wire System, Library Manager, Portable Persistence, Project Structure,
  Technologies, the doc table and Troubleshooting. Documentation is a
  single pointer to velxio.dev/docs and github.com/velxio/velxio_docs.
- Hardware Partners gains a line naming the hardware each partner
  contributed to the catalog.
2026-08-17 23:57:22 +02:00
David Montero Crespo bb46d51a3c fix(rp2040): keep the SPICE input path off the CYW43 pins
Every Pico W WiFi example in the gallery hangs at `while not
wlan.isconnected()`, with `wlan.status()` stuck at 1 (CONNECTING) and
`wlan.active()` reading True.

The CYW43439 sits on GP23/24/25/29, inside the module — there is no canvas net
for those pins. attachPioPeripheral() drives WL_HOST_WAKE (GPIO24) with
setInputValue() straight from the chip's hostWakeLevel(). When RP2040 gained
spice-driven digital inputs, the GPIO listener began seeding a pull level onto
every input pin, GPIO24 included: the CYW43 driver configures that pad as an
input, the seed landed on top of host-wake, the host stopped polling the chip,
and no association event was ever delivered.

The fault is invisible from the CYW43 side — its own harness drives a raw
rp2040js with its own FIFO/PIO plumbing and passes — which is why this looked
like a WiFi bug rather than a GPIO one.

Diagnosed by patching the sketch in the editor to print wlan.status() each
iteration: "WAIT 1 status 1 active True" repeating forever, i.e. the join was
accepted and the link never came up.
2026-08-17 22:55:45 +02:00
David Montero a27c7d61b6 chore(examples): recapture 3 thumbs that had the What's New modal painted over the canvas
cardputer-adv-m5-button, m5stack-core-hello and c6-oled-i2c were the first
slug of their capture run and got the day's announcement overlay in the shot.
The capture script (velxio-prod) now pre-marks the day's post as shown and
strips any announcement overlay before the screenshot.
2026-08-17 20:28:02 +02:00
velxio-deploy 86b939ae95 chore(examples): refresh 2 thumb file(s) [auto] 2026-08-17 20:17:41 +02:00
David Montero 7d63209d1e fix(espidf): ccache hash_dir=false so build variants share objects; log ninja/cmake timeouts
ESP-IDF compiles with -ggdb, and for debug builds ccache folds the cwd into
every cache key by default — CCACHE_BASEDIR does not exempt it (verified with
CCACHE_DEBUG on 4.11: 'Hashing CWD /var/lib/velxio-build/esp32s3/v_<hash>/
project/build'). Each build variant lives in its own dir, so every variant had
a private key space: eight esp32 variants of the SAME board each recompiled
all ~2100 IDF objects from scratch, the cache filled with dead per-directory
entries, and a cold Cardputer/S3 build (2263 steps) overran the 600s ninja
timeout on a busy box. CCACHE_NOHASHDIR=1 makes variants of the same
board/sdkconfig share objects; a cold variant becomes mostly cache hits. Only
DW_AT_comp_dir in .debug_info of a reused object can differ, which never
reaches the flashed .bin.

The timeout paths were silent server-side; they now log the build dir and how
many ninja steps completed, and the user-facing message says the partial
build is kept and a retry resumes.
2026-08-17 19:15:28 +02:00
David Montero 9118529d10 feat(intellisense): /type/ follows inheritance; extractor records bases and splits comma declarators
Measured against the user corpus, the by-resolved-type long tail was
losing what the old catch-all happened to include: for `Adafruit_SH1106G
display(...); display.setTextSize(` the class itself declares one method
(begin) and everything people call lives four levels up (Adafruit_SH110X ->
Adafruit_GrayOLED -> Adafruit_GFX -> Print). Same story for MCUFRIEND_kbv,
Adafruit_NeoMatrix and every GFX-derived display driver.

Three changes:

- The scanner records `bases` on class symbols (access/virtual keywords
  dropped, `ns::Base` -> Base, template args cut). 2,529 classes across the
  production cache now carry their bases.

- /type/{name} walks the chain breadth-first through the type index (bases
  routinely live in a different library), re-owns inherited members to the
  requested type so the editor sees one flat class, tags them with
  `inheritedFrom`, and lists the chain in `bases` -- including bases NOT in
  the cache (Print, Stream, or an embedded one like Adafruit_GFX) so the
  host can supply those itself. Depth capped at 8. Adafruit_SH1106G goes
  from 1 member to 64.

- `void a(int), b(), c(char);` declares three methods; the extractor took
  only the first. TFT_eSPI declares setTextColor/setTextSize and
  setCursor/getCursorX/... this way. Split on top-level commas after a
  complete `name(args)` group. TFT_eSPI 118 -> 173 members.

Rescan of the 1,125-entry production cache: +274 member symbols, 15
libraries gain, none lose.
2026-08-17 19:08:57 +02:00
David Montero 1a112324ab feat(intellisense): GET /api/intellisense/type/{name} — members of one class
The completion-safe long-tail path. Feeding whole library symbol sets into
the editor's candidate pool was measured to make ranking worse (U8g2 alone
injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_*
macros). What does help is answering `client.` once tree-sitter has resolved
`client` to a declared type the embedded catalog does not know -- and for
that the engine needs "the members of type T", not "the symbols of library
L". This endpoint returns exactly that: symbols whose owner is the requested
class, from whichever cached library declares it, in the same payload shape
as /symbols/ so the engine's RemoteSymbolCache stores it unchanged.

Resolution: a class-name -> cache-entry index built once over the library
cache (each per-entry scan is already disk-cached), persisted under
.symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry
production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed
after a few hours or when the cache grows. Many classes are declared by more
than one library -- the real one plus forks and bundles vendoring a copy
(PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather
Station", AccelStepper inside "Arduino Learning Board") -- so the pick is:
the library named after the class, else the one declaring the most members
of it (vendored copies are trimmed), else alphabetical.

Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 18:51:08 +02:00
David Montero 0e107fafe6 feat(editor): M5Stack section (Cardputer ADV, Core) ahead of STM32 in the starter dialog
The New Project dialog now lists an M5Stack section — M5 Cardputer ADV first,
then M5Stack Core — between ESP32 and STM32. Both kinds are overlay-registered,
so the section only renders when the overlay mounted (an OSS build shows no
empty block). Selecting a card loads the board's on-LCD hello example
(cardputer-adv-hello / m5stack-core-m5-helloworld), which also provides the
card thumbnail.

The sections builder is extracted as buildStarterSections(defs) and covered
by a unit test that pins the display order.
2026-08-17 18:50:37 +02:00
David Montero 15126cd27a fix(intellisense): access labels apply to statements that open a nested brace
_scan_text applied `public:` / `private:` / `protected:` only inside
handle_statement, which the `{` branch skipped whenever the statement opened
a nested enum, struct or class. So `public:\n typedef enum { ... }` -- the
first thing in AccelStepper's class body, and a common idiom -- took the enum
branch, the label was never seen, the class stayed in its default private
access, and every method after the enum was dropped. AccelStepper shipped 4
of its public methods; DallasTemperature, SoftWire, MFRC522_I2C, RTCZero
shipped none.

Labels are now consumed before the branch. Rescanned all 1,125 entries of
the production library cache: 82 libraries gain members (+1,532 total,
DallasTemperature 0 -> 57), and the 20 that "lose" one or two were leaking
private/protected members past the old check (PMS::loop is under private:,
hd44780::waitReady under protected:) -- precision, not regression.
2026-08-17 18:47:46 +02:00
velxio-deploy 8f412ef8bf chore(examples): refresh 20 thumb file(s) [auto] 2026-08-17 17:16:57 +02:00
David Montero Crespo fadefcc748 fix(examples): the Bluetooth LED example ignored every command
It printed "Bluetooth Control Ready" and then did nothing, no matter what
you sent — David typed `on` and `off` into the Serial Monitor and neither
reached it.

It reads UART(0), and on this port UART0 IS the REPL's channel: the code
runs through the raw REPL, so bytes typed into the monitor are consumed
there and uart.read() never sees them. `UART(0, 9600)` also retunes the
very port the monitor is talking to at 115200.

Upstream drives this from a phone through an HC-05, where "Bluetooth" is
only the transport and the ESP32 just sees serial. Velxio has no HC-05 on
the canvas, so the Serial Monitor is that link — read sys.stdin through a
select.poll (100 ms timeout, so the loop yields instead of spinning).

Verified in the app: `on` -> "Received: on / LED ON" with the LED lit,
`off` -> "Received: off / LED OFF" with it dark.
2026-08-17 15:30:39 +02:00
David Montero Crespo 08c9459c71 fix(examples): the common-emitter amplifier never left cutoff
analogRead printed a flat 1021-1023 on every loop — the collector pinned at
Vcc. Measured in the running app:

  base 0.481 V   emitter 0.001 V   collector 4.994 V

0.481 V is below the ~0.65 V a 2N2222 needs, so the transistor never
conducted and the "amplifier" was an open switch.

The bias divider was fine on paper (47k/10k -> 0.88 V). What broke it was
the 10k "coupling resistor" from the signal generator, whose own comment
claimed it let the generator "wiggle the biased base without DC-shifting
it". It does the opposite: a generator with 0 V offset is ground at DC, so
that resistor sits in parallel with Rb2 —

  10k || 10k = 5k  ->  Vb = 5 * 5/(47+5) = 0.481 V

which is exactly the measured value. Replace it with a 1 uF coupling
capacitor (159 ohm at the generator's 1 kHz: passes the signal, blocks the
DC that was wrecking the bias), and take Rb2 to 15k so the collector sits
near mid-rail — the operating point the sketch's own comment promises and
the only one with room to swing both ways.

The netlist snapshot could never have caught this: the text was exactly
what the author intended. So the new test asserts the PHYSICS — bias in the
active region, collector off both rails, and a collector that actually
swings — resolving node names through the builder's pin map rather than
hardcoding `n2`, and adding the 5 V rail the board supplies in the app.
2026-08-17 15:21:24 +02:00
David Montero Crespo 5545089389 fix(compile): lock the build DIRECTORY, not the FQBN — concurrent compiles swapped binaries
`_prepare_persistent_project_dir` keys the persistent build dir on
(idf_target, variant), but the lock was keyed on the FQBN. Several FQBNs
resolve to one variant — arduino-esp32's boards.txt gives both `esp32` and
`esp32cam` build.variant=esp32 — so those two were free to build in the SAME
directory at the same time.

Measured against staging, compiling both concurrently:

  esp32     285504 bytes  sha f65ca909…  contains its own marker
  esp32cam  285504 bytes  sha f65ca909…  contains the OTHER sketch's marker

Byte-identical firmware, and the esp32cam caller got the esp32 sketch's
binary. In the product that is two gallery tabs open at once: the first tab
prints the second example's serial log and does nothing itself. It made
esp32-pwm-led-rgb look broken (dark LED, every SPICE node 0 V) when the
example is fine — it was running someone else's program.

Key the lock on the same identity as the directory. Boards with genuinely
separate build dirs still compile in parallel up to the semaphore cap; an
unresolvable FQBN falls back to the default pair, which is the safe
direction (serialising more only costs throughput).

Also in this commit: battery-voltage-monitor ships the 9 V battery its own
description promises. The divider's top end was wired to nothing, so A0 read
0 V and the sketch printed "Battery: 0.0 V / WARNING: Battery low!" forever.
2026-08-17 09:12:26 +02:00
David Montero Crespo 76e656ea8d test(config): let overlay tests import the engine packages
A pro test lives at ../../pro/frontend/src/pro/..., outside this package,
so Node resolution walks up from THERE and never reaches this project's
node_modules — importing 'esp32js' dies with "Cannot find package". The
config already carries the other wiring overlay tests need (the include
glob, the fs.allow, @pro); this is the missing piece for one that drives
a real engine.

Guarded by existsSync per package: a pure-OSS clone has none of them
installed and no test imports them (the pro glob matches nothing there),
so each missing entry is simply left unaliased instead of pointing at a
path that doesn't exist.
2026-08-17 05:43:18 +02:00
David Montero Crespo 979a95fef9 test(examples): audit every shipped component value against physical ranges
The night-light LDR shipped a 0.001 ohm dark resistance for months and
nothing complained: the netlist built, ngspice converged, the part
rendered, and the example just quietly read 4095 forever. A wrong value
is invisible, so check them all.

Parses every resistor / pot / photoresistor / capacitor / inductor value
in the gallery with the real parser and asserts it lands in a physically
sane range. The failure message names both SPICE suffix traps, because
they are the ones that bite: M is milli (mega is "Meg"), and F is femto
and is deliberately not stripped, so "10uF" would parse as 10 fF rather
than the 10 uF intended.

Verified the check actually bites: putting "1M" back makes it fail with
`dark = "1M" -> 0.001 — LDR dark resistance (ohm) out of range`. A second
case asserts the audit visits a meaningful number of components, so the
test can't silently degrade into a no-op.

Swept the rest of the catalogue while here (all example files, the pro
overlay, and the 49 100-days diagrams): no other value hits either trap.
2026-08-17 03:47:39 +02:00
David Montero Crespo 9b0b4bc038 fix(examples): night-light LDR shorted AO to VCC — "1M" is milliohms in SPICE
100d-auto-night-light shipped `dark: "1M"` meaning 1 megaohm. The value
parser follows the SPICE convention where M is MILLI and mega is `Meg`,
so the dark resistance became 0.001 ohm. With the divider's top leg a
short, AO sat at VCC: `ldr.read()` returned 4095 on every sample, the
sketch's `light < 500` branch never ran, and the lamp LED never lit.
Dragging the lux control changed nothing either, because every lux value
still divides a short (measured n1 = 3.299999996 V, which is exactly what
100k / (100k + 0.00011) predicts).

The netlist snapshot had frozen the bug in place:

  - R_night-ldr_ldr vcc_rail n1 0.00011111111111111112
  + R_night-ldr_ldr vcc_rail n1 111111.11111111111

Diagram now says "1Meg" (regenerated from test/test_100_days), and the
mapper clamps a sub-1k dark resistance to the documented 1Meg default
with a console warning naming the milli/mega trap — a photoresistor that
reads as a short is always this mistake, and it fails in the most
confusing way possible: the part looks alive, it just never moves.
2026-08-17 03:39:47 +02:00
David Montero Crespo 5c95ade35f fix(sim): stop dropping sibling pin edges — only bit 0 of a multi-pin write reached the canvas
A sketch that drives several pins between yields lost every pin but the
first. The 4-bit binary counter example (the most-viewed one in the
gallery) blinked bit 0 and left bits 1-3 dark while the firmware counted
0-15 correctly on serial.

Sequence: pin 2's edge arrives first, finds no V-source (the pin was not
in outputPins when the netlist was built), self-heals via handleMcuEdge
and rebuilds. The pin 3/4/5 edges of that same iteration land while the
rebuild solve is in flight, so they queue in pendingMcuEdges. The drain
then looked them up in the loaded context, found no V_uno_3/4/5, and
dropped them. Those pins never got a source, so their LEDs kept the
build-time current (~0) forever — and since the LED part prefers SPICE
current over the digital fallback, they stayed dark for good.

The drop was deliberate: it stops the self-heal -> tick -> self-heal loop
for a pin wired to nothing. Replace the blanket drop with a per-pin
guard: heal a pin at most once per netlist, then drop as before. The
guard re-arms whenever a rebuild changes the V-source list, so a pin that
becomes drivable later still heals.

Verified in the browser on binary-counter-leds; two regression tests
cover both sides (sibling pin heals, unwired pin does not loop).
2026-08-16 20:02:58 +02:00
David Montero 11f1a7ef53 fix(examples): series resistors on the RGB mixer, refreshed netlist snapshots
The RGB colour mixer wired the LED's three legs straight to GPIO25/26/27 —
examples-led-resistor.test.ts fails the build over exactly that, and it is
right to: a bare LED on a pin is a short through the die. Each leg now has
its own 220R.

The 100-days netlist snapshots were recorded when 36 of those examples had
no canvas at all, so every one of them snapshotted as an empty circuit.
Re-recorded now that they have real parts.
2026-08-16 05:48:59 +02:00
David Montero 00da0c6937 fix(examples): 100 Days of IoT — 33 canvases, real pins, honest boards
The 100-days series is imported from a repo that ships circuit photos, not
machine-readable diagrams, so every example whose canvas nobody had drawn by
hand loaded as a bare board: 36 of 49 had components: [] / wires: []. They
compiled and ran and showed nothing — the LDR night light printed 0 forever
because GPIO34 was floating, and there was no LED on GPIO2 to see either way.

Canvases for 33 of them (LDR, pots, MQ gas sensors, HC-SR04, PIR, 4x4 keypad,
joystick, servos, relays with a lamp on the contact, A4988 + stepper, 7-segment,
10-segment bar graph, RGB, DHT22, indicator LEDs), each wired to the pins its
own sketch uses. LED chains are active HIGH (GPIO -> 220R -> anode, cathode ->
GND); the Smart Home canvas had all four wired the other way round, so led.on()
turned them off.

AQI gains the three analog sensors its code reads (GPIO34/33/35) plus the dust
sensor's LED — without them every reading was 0 and the AQI sat at "0.0 Good".

Board-level repairs in the sources (each commented in place):
  - Pin("LED") is a Pico idiom; on ESP32 it is not a pin at all, and on Pico W
    it drives the CYW43 LED, which no board element renders. Now real GPIOs
    with a real LED on the canvas.
  - Battery monitor read ADC(0) (10-bit, single-channel) on a 12-bit per-pin ADC.
  - Irrigation used GPIO3/GPIO1 — UART0 on this board, so it killed the console.
  - Joystick+servo used another board's ADC0/servo pins.
  - Temperature LED indicator shipped the DC-motor sketch by mistake upstream;
    replaced with the listing from the project's own README (DHT22 + 3 LEDs).
  - GPIO6 (internal flash) and GPIO0 (strap) are not on the DevKit header, so
    those wires anchored at the board corner.

Left without a canvas on purpose: the EEPROM and hotspot demos are serial-only,
and the NTP clock needs a MAX7219 element Velxio does not have yet.
2026-08-16 05:34:48 +02:00
David Montero Crespo 1b04e910ce fix(editor): quick suggestions stay alive inside snippet placeholders 2026-08-15 21:31:01 +02:00
David Montero aed1796a8f fix(wires): stop board-corner wire anchors — alias fallbacks for pin-name spellings
A wire endpoint naming a pin the board element does not expose silently
resolved to nothing and rendered anchored at the board corner. A full
audit (all gallery examples + every saved project on velxio.dev) found 15
examples and 28 user projects doing it — the same electrical pin spelled
differently per surface: GND.1 on the classic ESP32 (element exposes
GND/GND2), 3.3V vs 3V3, GND2 vs GND.2, D13 vs 13 vs GP13, A0-A2 vs
GP26-28 on the Pico, 36/39 vs VP/VN, bare 5V vs VBUS/VIN.

calculatePinPosition now tries those equivalences AFTER the exact match
and the two existing fallbacks miss (so no current resolution changes),
healing old saves, .vlx imports and agent-authored wires without a data
migration. The Pico element gains A0/A1/A2 alias pads on the ADC pins
(RX2/16 dual-entry precedent) and a GP25 pad at the onboard LED silk so
pico-blink-style wires have an anchor. The two examples that were
outright wrong (microsd-card-esp32 GND.1, pico-serial-echo TX) now use
the element's real pin names.

Netlist snapshots (166) untouched; verifier + builder suites green.
2026-08-15 20:08:47 +02:00
David Montero Crespo fe8ac7e4e3 fix(editor): modals above the agent side panel (z 8000 -> 9000)
With the agent panel open, the library/install/verification modals were
partially covered on the right, hiding the close button and actions.
2026-08-15 08:48:23 +02:00
David Montero Crespo cbd116ce19 fix(editor): fixedOverflowWidgets so marker hovers render above the canvas
A hover wider than the editor pane slid under the simulator canvas
(sibling stacking context) and could not be read.
2026-08-15 08:32:33 +02:00
David Montero d78266011d chore(dev): expose core stores on window in dev builds for the QA harness
DEV-guarded (import.meta.env.DEV): the platform-bugs Playwright harness
drives store paths (property updates, group switches) the way the agent
tools do, which raw DOM access cannot reach. Production bundles never
ship it.
2026-08-15 08:29:39 +02:00
David Montero 941df93cb3 fix(editor): reconcile active file-group pointers; stop persisting compiledProgram
deleteFileGroup left editorStore.activeGroupId dangling at the removed
group: Monaco (and agent writes) kept editing a group compile never reads,
so a stale same-named sketch.ino shipped instead of the code on screen
(the 'leftover template Blink built for 35 minutes' failure). The pointer
now re-targets a surviving group, and compile re-points the editor when
the two pointers diverge (with a visible warning log).

compiledProgram is no longer serialised into boards_json: the firmware
blob was 99.99% of a typical ESP32 save (a bare board already ~1.04 MB),
pushing every real project against the request-size ceiling, and a
restored binary is stale the moment the code differs. Loaders still
accept it from old rows.
2026-08-15 08:29:39 +02:00
David Montero b58ecd1c37 fix(espidf): generate .ino forward declarations + link the task-WDT API
The custom sketch.ino.cpp path never ran arduino-cli's ctags prototype
pass, so the universal Arduino idiom (helpers defined after loop()) died
with "'foo' was not declared in this scope" on every ESP32 build. A
conservative generator (skips templates, default args, methods,
function-pointer params) injects prototypes before the first definition
with a #line directive so error line numbers stay put. 8 unit tests.

IDF 5.5 renamed ESP_TASK_WDT to the EN/INIT pair; the v4.4 template's =n
became EN=n, compiling the whole implementation out — every sketch using
esp_task_wdt_add/reset (Firebase's client does internally) failed at
link. The v5 fixup now emits EN=y INIT=n: the API links, the watchdog
never auto-starts under emulation. v4.4 semantics untouched.
2026-08-15 08:29:39 +02:00
David Montero d2648f5a66 fix(sim): programmatic property changes reach the running simulation
Three stacked defects made ANY non-knob property change invisible to a
running sim (values restored from saved projects, the agent's
set_component_property, the property dialog):
1. DynamicComponent assigned element properties without firing the events
   part simulators bind to — dispatch input/change + dispatchSensorUpdate
   on real value changes (applied-value guard breaks the echo loop).
2. Parts received the shared legacy AVR simulator regardless of board —
   route attachEvents to the simulator of the board the part is wired to
   (getBoardSimulator), so direct analog injection works on non-AVR boards.
3. The SPICE->ADC push stomped un-modeled parts: a net with no SPICE
   element gets an auto pull-down, solves to 0 V, and overwrote the
   part's own injected voltage every solve (the dead analog-joystick).
   Only sourced nets are pushed now.
Verified end-to-end: pot curve exact to the LSB (value 100/300/600 ->
ADC 400/1201/2401 on GPIO35), joystick idle at centre 2047, full-scale
4095 after a store write.
2026-08-15 08:29:39 +02:00
David Montero 44782a419a fix(spice): per-voltage supply rails — VIN/5V no longer clamped to the 3.3V logic rail
Board supply pins split into the main logic rail plus aux rails by real
voltage (ESP32/XIAO VIN,5V -> aux_rail_5 at 5 V; 5V-Arduino 3.3V pin ->
aux_rail_3v3; Pico VBUS/VSYS -> 5 V). An external 5 V supply wired to VIN
used to read 3.3 V (ideal vcc_rail source clamped the whole net — customer
isolated it with a voltmeter). Aux rails join positiveSourceNets and the
relay nominal-voltage check; the voltmeter and hover overlay read them.
Also: ratings entry for the 4-pin ssd1306-i2c-4pin (supply pin VCC) and a
VCC alias on the 8-pin entry, so its power check works at all.
2026-08-15 08:29:39 +02:00
David Montero Crespo c550b29f9e fix(editor): portal toolbar modals to body — container queries broke their overlays
The unified-toolbar responsiveness work gave .unified-toolbar
container-type: inline-size, which makes it the containing block for
position:fixed descendants. The three modals rendered inside the toolbar
(library manager, install-libraries, circuit verification) shrank to the
toolbar's 89px box: pinned to the top, header and close button clipped
offscreen. createPortal(document.body) makes them immune to ancestor
containment. The library manager also gets a fixed tall height
(min(86vh, 1000px)) instead of a bouncy 80vh cap.
2026-08-15 08:19:30 +02:00
David Montero Crespo 63a91a08ef feat(backend): intellisense symbol server for the library long tail
GET /api/intellisense/symbols/{Name@Version} scans the content-addressed
library cache entry's headers (dependency-free C++ scanner) into an editor
symbol table, disk-cached per entry and served with long cache headers.
Plain OSS hosts without a lib cache 404 and the editor degrades gracefully.
2026-08-15 06:46:55 +02:00
David Montero Crespo 7c3b1c6e24 feat(editor): compile-diagnostics seam — publish raw build output for editor markers 2026-08-15 06:44:35 +02:00
David Montero Crespo 55bc03d281 feat(editor): intellisense registry seam for the pro completion engine 2026-08-15 06:13:25 +02:00
David Montero 5b82299793 chore(seo): regenerate sitemap from current seoRoutes — drops the 15 stale /docs entries
The tracked sitemap.xml committed in 3ff984a1 was a stale working-tree
regen from BEFORE df19778a retired the in-app /docs routes; it carried 15
/docs URLs that now 301 to the docs portal (which ships its own
sitemap-index, declared in robots.txt since 720a5a1). Harmless in prod —
build:docker regenerates the file at image build — but noisy in the repo
and wrong as a source of truth. 193 URLs (22 routes + 171 examples).
2026-08-15 06:11:55 +02:00
David Montero 3ff984a155 chore(assets): ship BadgeOS 2.0.2 factory images + MicroPython firmware for esp32c6/rp2040w/rp2350-arm; refresh sitemap lastmod
These binaries are loaded at runtime from /firmware/ (cyw43 installer,
RP2350 simulator, C6 bridge) and were living only in the prod working
tree — a fresh checkout would 404 them. Sitemap regenerated 2026-08-13.
2026-08-15 06:10:16 +02:00
David Montero 720a5a1dd2 chore(seo): declare the docs portal sitemap in robots.txt
velxio.dev/docs/ ships its own Astro sitemap-index (2142 URLs) but nothing
referenced it — the main sitemap dropped the retired in-app /docs entries
on purpose (df19778a) and robots.txt only listed the app + projects
sitemaps, so crawlers had no discovery path to the new portal.
2026-08-15 05:57:04 +02:00
David Montero 0f279c901b fix(espidf): enable BLE 4.2 legacy APIs on BLE5-only SoCs
C3/S3/C6 default to CONFIG_BT_BLE_50_FEATURES_SUPPORTED with 4.2 OFF, so
arduino-esp32's Bluedroid BLE library (esp_ble_gap_config_adv_data_raw,
esp_ble_gap_start_advertising) died at link with undefined references —
esp32c3-ble-advertise never compiled. Enabling 42+50 together matches
arduino-esp32's own per-target sdkconfigs; on classic ESP32 these flags
are already the Kconfig defaults. Found by the nightly gallery smoke.
2026-08-15 04:19:21 +02:00
David Montero 0d2e73ee00 fix(espidf): unknown #if must not prune its #else in the include scanner
The dead-branch pruner marked any assumed-live branch as 'taken', so the
#else of an UNKNOWN condition was skipped. GxEPD2 guards its GFX base
class with '#if ENABLE_GxEPD2_GFX (default 0) ... #else #include
<Adafruit_GFX.h>' — the pruned #else silently dropped the Adafruit_GFX
dependency and every GxEPD2 e-paper example failed with 'fatal error:
Adafruit_GFX.h: No such file'. Found by the nightly gallery compile smoke.

'taken' is now set only by PROVABLY-true conditions (all identifiers in
the known-true set), keeping the 2026-08-03 ESPAsyncWebServer behaviour:
provably-dead branches still prune, provably-true branches still kill
their #else.
2026-08-15 04:14:00 +02:00
David Montero Crespo 8391cc7fa8 fix(gateway): keep in-browser WiFi boards reachable through the IoT gateway
The ESP32 examples now default to the in-browser JS engines, whose lwIP
serves the sketch's HTTP server from the simulation tab itself. Opening
the IoT gateway in a new tab backgrounds that tab, the browser throttles
its timers, and the in-chip server can't answer before the proxy's
timeout — the exact failure the Pico W iframe path already solved.

- WifiStatus grows an inBrowser flag (set by the overlay's JS bridges);
  the serial-monitor link and the WiFi badge now open the in-app iframe
  for any board that carries it, not just pi-pico-w.
- /api/gateway polls ~1s before answering 404: the net bridge dials on
  'got_ip', which races the sketch printing its URL, so a fast click
  used to land before the bridge registered.
2026-08-14 19:45:39 +02:00
David Montero efabac0a05 fix(compile): strip @version/@wokwi pin suffixes in manifest name matching 2026-08-14 18:20:27 +02:00
velxio-deploy a3e93e08ca chore(examples): refresh 1 thumb file(s) [auto] 2026-08-14 18:18:49 +02:00
David Montero Crespo f28ca56548 fix(examples): 100-days — bound Blynk connect, avoid i2c.scan in bmp180 shim
Live E2E on the in-browser ESP32 engine surfaced two freezes:
- i2c.scan() stalls on the engine's I2C controller (first probed address
  has no device and the transaction never completes). The bmp180 shim now
  probes 0x76/0x77 directly via readfrom_mem.
- BlynkLib's blocking socket.connect() froze the demo when the second
  outbound connection stalled. The vendored BlynkLib now sets a 10s
  timeout before connect, and main.py catches OSError and falls back to
  the offline no-op Blynk (probe-then-reconnect removed — one bounded
  connection instead of two).
2026-08-14 17:59:11 +02:00
David Montero 34001f4748 test(examples): 100-days netlist snapshots follow the batch repair
2c4c93a2 gave 13 of the repaired 100-days examples real canvases, so
their netlists are no longer empty. Regenerated with vitest -u; every
change is the expected kind (components and rails appearing, plus the
micropython-watch button rewiring).
2026-08-14 17:28:27 +02:00
David Montero Crespo 2c4c93a21b fix(examples): 100-days batch repair — pico-w firmware, entry points, canvases
- 10 Pico W projects now emit boardType pi-pico-w so the RPI_PICO_W
  firmware (network + CYW43) boots; the plain-pico build has no network
  module and every Wi-Fi example died on import.
- Entry points: projects whose alphabetically-first file was a library
  (BlynkLib.py) or a host script (analyze.py) now ship a real main.py.
- All Blynk projects probe blynk.cloud with a 5s timeout and fall back
  to an offline no-op object — a plain connect() blocks forever in the
  emulated network and froze the demos.
- DHT11 -> DHT22 across six projects (the emulator speaks DHT22) and
  the OLED projects use hardware I2C instead of bit-banged SoftI2C.
- 11 more examples get a wired canvas (DHT22, SSD1306, LCD1602 I2C,
  membrane keypad, joystick, heart-beat sensor, LEDs): 13/49 total.
2026-08-14 17:22:14 +02:00