Commit Graph

1650 Commits

Author SHA1 Message Date
David Montero Crespo 010a112bc8 feat(compile): ESP32-P4 and ESP32-C5 targets in the espidf lane
_idf_target mapped only c3/c6/s3 and defaulted everything else to
'esp32', so a P4 fqbn compiled with the XTENSA toolchain and
-DARDUINO_ESP32P4_DEV. Adds p4/c5 predicates, IDF5-target membership,
0x2000 bootloader offsets, the riscv toolchain requirement, PSRAM held
off until the engines validate it, P4 CPU-freq drops (its kconfig
choices do not overlap the classic set), a rev0 sdkconfig pin for the P4
(the in-browser engine runs the only published mask ROM and v3 firmware
reads a flash descriptor rev0 never wrote), and an honest refusal for C5
arduino builds (no core exists). Validated on staging: an esp32p4
arduino sketch now produces a merged image with the riscv32 toolchain
that the esp32p4js engine accepts.
2026-08-19 06:35:33 +02:00
David Montero Crespo c4860d4f23 feat(boards): widen esp32Family with p4/c5 - loud failure on the absent QEMU path
The overlay's ESP32-P4 (and the coming C5) run ONLY on their in-browser
engines: neither chip has a QEMU machine anywhere. toQemuBoardType now
throws a clear error for those families instead of booting the wrong
machine and wedging on the first register.
2026-08-19 04:42:24 +02:00
David Montero Crespo 149fbee1e9 feat(boards): ESP-EYE ad card + declarative builtInCamera on ProBoardDef
Overlay boards with a camera now declare builtInCamera on their def (bool,
or { maxFrameBytes } for drivers with tight frame buffers) and the canvas
CameraToggle keys off it - the hardcoded esp32-cam / xiao-esp32s3-sense
kinds keep working unchanged.
2026-08-19 02:50:49 +02:00
David Montero Crespo 9733047a9f feat(boards): ESP32-C3-LCDkit ad card (online-only)
The knob kit joins the overlay board program (Espressif partner batch);
OSS builds show the shop-window card until the pro overlay registers the
real kind.
2026-08-19 02:33:33 +02:00
David Montero Crespo bd1613a9e2 feat(micropython): ship v1.28.0 firmware for the whole ESP32 family
The esp32/s3/c3 MicroPython bins were v1.20.0 (April 2023) — built by
micropython.org on IDF 4.4, three IDF generations behind the compile
pipeline and the engines' own validation corpus (both IDF 5.5), and two
behind the C6 bin (already v1.28). That drift is what broke I2C on the
S3: the engines decode the IDF 5.x driver sequences, and the v1.20 image
drove them the 4.4 way.

All four families now ship v1.28.0 (2026-04-06, IDF 5.x). Remote URLs
and cache keys move together, so a browser holding the old bin in
IndexedDB misses the new key and fetches v1.28 on its own. The new
images all declare 4 MB flash — padToFlashSize's floor exactly (the old
S3 bin declared 8 MB).

Validated cold on each engine (boot -> REPL, i2c.scan finds an attached
slave, WLAN scan/connect/DHCP; S3 additionally quad-PSRAM with a
multi-MB heap) and in the app (scan [60] on esp32/s3/c3, the ssd1306
gallery example drawing). Companion engine release: esp32s3js v1.16.0
(I2C interrupts on both cores, quad-PSRAM data plane). Pin.toggle() and
three years of upstream fixes come along; the v1.20 Pin.toggle note in
the editor seed is now history only.

Residual risk, flagged not fixed: the OSS QEMU self-host path (no JS
engines) now boots v1.28 under QEMU — unvalidated here, velxio.dev does
not exercise it.
2026-08-19 00:57:43 +02:00
David Montero Crespo 4eb8060dcb fix(sensors): illumination sliders are logarithmic, like the quantity they set
The lux slider was linear 0-1000, but an LDR's response (and light
perception) is logarithmic: in the night-light example the LED's whole
on/off behaviour lived below ~20 lux — 2% of the slider's travel — and the
remaining 98% did nothing, reading as "the sensor barely works". The
physics were right (the divider model puts the example's 500-count
threshold at ~20 lux, sensible for a night light); the CONTROL was wrong.

The photoresistor and photodiode lux sliders now run in log position
space: value = min + 10^(p·log10(span+1)) - 1, anchored exactly on min
and max (the +1 shift gives the log axis a zero). Mid-travel lands in the
tens of lux, and the night-light toggles near the middle of the track
instead of at its first pixels. Same mapping on the component's own
on-canvas slider. The model, the examples and the reported lux values are
untouched — only where a given lux sits on the track changed.

Slider controls gain an opt-in `scale: 'log'`; everything else stays
linear.
2026-08-18 22:42:25 +02:00
velxio-deploy 6bd173bba7 chore(examples): refresh 1 thumb file(s) [auto] 2026-08-18 22:31:43 +02:00
David Montero 33ee270ae1 compile: arduino-cli builds no longer queue behind ESP-IDF builds
One Semaphore(2) gated every compile, so while two ESP32 builds ran (cmake +
ninja, minutes on a cold cache) an Arduino Uno blink sat 'compiling' for as
long as they took. Two lanes now: heavy (ESP-IDF, still 2 — the VPS load
constraint is real) and light (arduino-cli boards, 3), plus a console line
when a job is queued so the wait is visible instead of looking like a hung
build. The per-target lock that keeps concurrent builds out of one ESP-IDF
build dir is unchanged.
2026-08-18 22:08:54 +02:00
David Montero Crespo d7a4b5e4eb feat(micropython): install libraries from the micropython-lib index (#214)
A MicroPython library in Velxio IS a .py file in the project, copied onto
the board's filesystem before main.py runs. That mechanism worked, but
getting the file meant hunting GitHub by hand — the gap issue #214 is
about. Now the Library Manager fills it, in both halves:

Backend: /api/micropython-libs proxies the official micropython-lib
package index (the same one `mip` uses on real hardware). /search filters
the index; /fetch resolves a package plus its declared deps into .py
files. The proxy is what makes this safe to expose: only the official
index is reachable (the name must exist in index.json, files come from its
content-addressed store), source only (.py, never .mpy bytecode), path-
safe, and capped at 24 files / 256 KB per install. In-process TTL cache
over the index.

Frontend: the same Library Manager modal, mode-aware. On a MicroPython
board the search hits the index and "Add to project" writes the package's
files into the board's workspace (upsert — re-adding is the upgrade path),
visible in the explorer and travelling with the project. Before typing it
shows featured picks (ssd1306, umqtt.*, urequests) and the modules already
frozen into the firmware (neopixel, dht, onewire, ds18x20, machine,
network) labeled "built into the firmware", so searching for one answers
"just import it" instead of an empty list.

The file-materialization prelude now creates package directories: umqtt
ships as umqtt/simple.py, and MicroPython's open() does not mkdir, so the
prelude died with ENOENT on any packaged module.

Also ships a gallery example ("ESP32 + MicroPython: use an external
library") that documents the file-is-the-library mechanism with the
ssd1306 driver on the wired OLED.
2026-08-18 21:50:10 +02:00
David Montero 9936d1956c flash: compile before flashing, stale-build warning, i18n dialog, entitlement seam (#207)
"Flash to real board" no longer needs a prior Compile. The menu item is
enabled for every Arduino/ESP-IDF board (MicroPython keeps its own
gating) and the Flash dialog compiles first when the board has no program
or its code changed since the last build — compiler output streams into
the dialog's console, a red build ends in an error banner there with a
"Recompile & flash" retry, and only a green build reaches the flasher.
Web Serial's port picker needs a user gesture, so the web flow asks the
overlay to grant the port at the click (new WebFlashImpl.preparePort)
before the compile that may take minutes.

Stale detection: compileBoardProgram() now records a fingerprint of the
sources + language + build options (BoardInstance.compiledSourceHash,
utils/sourceFingerprint.ts); the dialog warns and rebuilds when the live
sources differ. utils/boardCompile.ts holds the compile request builder
shared with the toolbar (same files, same options).

The whole dialog and the menu hints are translated (editor.flash.* and
editor.canvas.flashHint.* in the 9 locales); before, every string was
hard-coded English.

Entitlement seam for #207 (desktop "upload to board"): lib/proWebFlash.ts
gains installHardwareFlashGate(); OSS default allows, and when a gate
blocks, the dialog shows a paid-license panel with a "See plans" link
instead of the port picker.
2026-08-18 20:21:57 +02:00
David Montero Crespo c21bfca220 fix(editor): the workspace panel stacks its actions, and the toolbar stops growing a hole
Two places where the editor bar spent width on nothing.

The file explorer put its label and its buttons on one line: WORKSPACE next
to new/open/save, and every board section's name next to rename / new file /
new folder plus the status dot. Between them those set a floor under the
panel's width, and the board name — the one thing a user reads there — got
whatever was left ("M5 Cardput..." in a 150px pane). Both headers now stack:
the label owns its row, the actions sit above (board) or below (workspace),
and the status dot rides in the actions row, where it costs the name
nothing. The actions row keeps its height reserved whether or not the
buttons are showing, so hovering a section never shoves the files below it
down. Default pane width 150 -> 124px; the freed px go to the code editor.

The toolbar's center slot is a flexible spacer that pins the right icons to
the far right, and nothing renders into it (no caller passes centerSlot). So
whenever the bar had width to spare — closing the docked AI chat is exactly
that — the spacer ate it: 191px of black between Reset and Libraries on a
1600px window. Empty, it no longer grows, so the editor controls stay packed
and the surplus collects at the one boundary that already carries a divider,
in front of the canvas controls (which belong over the canvas pane).
2026-08-18 18:42:38 +02:00
David Montero 13a19c127a prerender: initialise i18n for SSR; example pages mounted under their route
Now that SSR bodies are actually injected, two more prerender defects
showed: every t() rendered as its key (i18next was never initialised in
entry-server — the English bundle is imported now, plus the overlay's
"pro" namespace on pro builds, with an OSS stub), and every example
detail page rendered "404 Example not found" because ExampleDetailPage
was mounted without a Route so useParams() was empty. Verified locally:
/examples/blink-led SSRs breadcrumb, h1, description and JSON-LD; the
gallery and editor summary render translated copy.
2026-08-18 18:35:01 +02:00
David Montero 2c43c638a1 prerender: SSR bodies were never injected; examples gallery renders on the server
replaceRootSeo() swaps the static #root-seo fallback for the page's SSR
body by string search. The old regex required a <script> right after the
block, but Vite hoists the module script into <head> in the built
index.html, so it never matched: every prerendered page (landings, /v3,
/about, all 171 example pages) shipped the HOMEPAGE's fallback body —
same <h1>, same nav — under its own <head>. Two hundred near-duplicate
bodies, and none of the landings' actual content (h1, sections, FAQ)
ever reached the crawler.

ExamplesGallery passes a server snapshot to useSyncExternalStore so
/examples SSRs instead of falling back to meta-only.
2026-08-18 18:10:36 +02:00
David Montero e1fc833f61 seo: starter examples are "New <board> project" pages
data/starters.ts maps eight free boards to the gallery example that opens
the editor with the board and a working blink (blink-led, nano-blink,
mega-blink, esp32-blink-led, esp32s3-blink-led, c3-blink, pico-blink,
attiny85-blink). ExampleEditorPage gives those ids an action-shaped
title/description ("New ESP32 project — free online ESP32 simulator")
while the gallery keeps the sketch title; the prerender writes
dist/starters.json so the prod app shell can serve the same head in the
raw HTML. This is the Wokwi "New ESP32 Project" shape: a URL of its own,
canonical of its own, that lands inside the product with the board loaded
— a candidate for brand sitelinks with usage intent.
2026-08-18 17:38:13 +02:00
David Montero 6f19cf9068 seo: /v2 and /v2-5 canonical to the home and out of the sitemap
Superseded release pages that were taking two brand sitelink slots (5.9k
impressions, 29 clicks). They still render for old links; hreflang is
only emitted for a page that is its own canonical (prerender + useSEO).
2026-08-18 17:29:54 +02:00
David Montero 18b133311e editor: tolerate the trailing slash now that /editor is prerendered; nav links to /editor/
A fresh load of /editor is 301'd to /editor/ by nginx (prerendered dir),
while client-side navigation still lands on /editor. The three exact
pathname comparisons (header active state, header share button, starter
dialog gate) now ignore the trailing slash, and the header nav links to
/editor/ so crawled internal links match the canonical with no redirect
hop.
2026-08-18 17:22:47 +02:00
David Montero 54e25cfed5 seo: seo-routes.json lists every SEO route with metadata, not only the prerendered ones
So the prod app shell can give a route that has metadata but (yet) no
prerendered file its own head, self canonical and hreflang instead of the
homepage's.
2026-08-18 17:21:12 +02:00
David Montero 4da00840c6 seo: own-locale canonical + hreflang for localized pages; /editor prerendered
- useSEO: for the static SEO routes (marketing pages, examples gallery,
  editor, home) the canonical is now the page's OWN locale variant and the
  head lists every locale variant as rel=alternate hreflang plus x-default
  on the locale-less English URL. /es/ and /zh-cn/ were declaring
  canonical=/ while Google indexed and ranked them on their own — an
  unstable contradiction. UGC pages (projects, profiles, example detail
  pages) are the same English content under every prefix and keep a single
  locale-less canonical with no hreflang.
- prerender-seo.mjs emits the same hreflang set into each prerendered page
  and writes dist/seo-routes.json (path/title/description) so the prod app
  shell can serve the /<locale>/<route> variants (which have no static file)
  with the route's own head.
- /editor gets seoMeta and a static SSR summary component, so it is
  prerendered: nginx now serves /editor/ as a real page and 301s /editor to
  it. Both forms answered 200 with the homepage head before, and Google
  indexed /editor and /editor/ as two pages.
2026-08-18 16:50:39 +02:00
David Montero 1b735c9c30 seo/analytics: real GA attribution, tight robots.txt, no build-date lastmod, GA only on velxio.dev
- trackClickCTA sent the originating page as the event parameter `source`,
  a reserved traffic-source name in GA4: every landing CTA click rewrote the
  session's acquisition to landing / arduino-simulator / rpi-simulator with
  medium (not set), which put 16% of sessions in the Unassigned channel.
  Renamed to cta_source (register it as a custom dimension in GA4 if the
  click_cta report should keep breaking down by page).
- robots.txt is one group with no blank lines (some parsers end a group at
  a blank line, orphaning the Disallow rules); /login, /register and /admin
  are no longer disallowed — they carry noindex, and a page must be
  crawlable for that tag to take effect. /project/ and /api/ stay out.
- The static sitemap no longer stamps every URL with the build date as
  <lastmod>; an all-identical, ever-changing lastmod is discarded as
  unreliable, so omitting it is the honest option.
- The GA4 tag loads only on velxio.dev / www.velxio.dev / tauri.localhost:
  self-hosted copies of the app (there are >180 hostnames reporting into
  the property) no longer pollute velxio.dev's analytics.
2026-08-18 16:04:48 +02:00
David Montero Crespo 189ba355a9 feat(boards): a board can ship the code it opens with
The editor seeded a freshly placed board from its FAMILY: the Arduino
LED_BUILTIN blink, the Pico Pin(25) MicroPython blink, or the Raspberry
Pi RPi.GPIO script. None of those run on a board that has no such LED,
no RPi.GPIO and its own vendor library, so the first Run failed on a
board the user had not touched yet.

ProBoardDef gains defaultFiles (per language mode, plus 'python' for the
QEMU-Linux guests) and defaultLibraries; addBoard and setBoardLanguageMode
prefer the board's own seed over the family default, and the seeded
libraries land in the board's compile manifest.

Also fix the MicroPython family fallback: it recognised an ESP32 by
'esp32' appearing in the group id, so an overlay ESP32 whose kind carries
no such substring (m5stack-core, cardputer-adv) was seeded the Pico's
Pin(25) blink - a dead pin on both. It now consults the board's declared
esp32Family too.
2026-08-18 15:02:13 +02:00
velxio-deploy 0a0badd10f chore(examples): refresh 2 thumb file(s) [auto] 2026-08-18 09:18:51 +02:00
velxio-deploy 3b213e9e02 chore(examples): refresh 20 thumb file(s) [auto] 2026-08-18 09:01:45 +02:00
David Montero b9f663f02e fix(editor): the toolbar strip's label collapse is measured too, and each layout is judged by its own widths
Follow-up to 534b2ff. Dropping the strip to its own bar was now measured,
but two things still produced a wrapped bar on the XIAO example at 1440
with the chat docked:

- The first collapse tier (Serial / Camera / Mic / Scope / Add / Libraries
  labels -> icons) was still a fixed 1010px container query, so on the
  1060px own-bar the labels stayed and the bar wrapped in two. That tier is
  now the unified-toolbar--compact class, set by the same measurement:
  labels on the brand row if they fit, icons on the brand row if those fit,
  else the own bar — labelled if the full bar holds it, icons otherwise.
  The finer tiers (board pill 905, count/zoom 835, view toggle 760/1140)
  stay as container queries.
- Each candidate layout is measured IN that layout: the zones carry their
  own container queries (the view-mode toggle hides on a narrow row and
  reappears on the wide bar), so a width read on the brand row said the
  bar would fit when it would not.
2026-08-18 05:49:45 +02:00
David Montero 534b2ff21b fix(editor): the header measures whether the toolbar strip fits instead of guessing by breakpoint
With the AI chat docked at 1440px, a board with many canvas controls (XIAO:
display + camera + mic + scope) wrapped the strip INSIDE the header — the
canvas controls orphaned on a second line next to a black hole — because
the drop-to-own-bar fallback fired on media-query thresholds (1040 / 1060 /
1400px by chat state) that were only right for one board's strip width.

AppHeader now measures: natural single-row width of the strip (each zone at
its content width) against the room left next to the brand + menus, and
sets app-header--strip-below when it does not fit. Always measured in the
inline layout (class removed, measured, restored before paint) so the
below-state's margins and uncollapsed labels never bias the decision, and
there is nothing to oscillate. Re-measured via ResizeObserver on the strip
host, the brand block and the zones (plus a MutationObserver for the
portaled canvas controls), deferred a frame so a toggle never re-enters
delivery. The three media-query blocks collapse into one class rule; a
1000px window without the chat now keeps one row when the collapsed strip
fits, instead of being forced to two.
2026-08-18 05:21:21 +02:00
David Montero 7fc131d4fb feat(editor): compact file explorer — less indent, smaller icons and rows, 150px default
The explorer rows carried a 26px indent plus 22px file icons, 5px vertical
padding and 12px names in a 165px pane: a lot of margin for a sidebar
whose only job is listing a handful of files, and every px of it was taken
from the code editor. Now: root files indent 16px (12px per nested level),
file icons render at 16px like the folder icon, rows are 3px/6px with an
11px name, the board header actions are 18px, and the header buttons 20px
with a title that truncates before the actions do. The pane defaults to
150px (min 100) and the account footer keeps the first name visible at that
width.
2026-08-18 04:54:49 +02:00
David Montero e71c78f734 fix(canvas): boards added from the picker land in the visible corner like components
Boards were placed 420 world-px right of the ACTIVE board plus 60/30 px per
board already on the canvas: a fixed world offset that ignored pan/zoom (the
new board could land off-screen) and kept marching further out with every
board added, even after the earlier ones had been moved away or removed.
Components had already been fixed to start at the visible top-left and take
the first free slot (utils/dropSlot); boards now go through the same rule,
and both count each other as parked items so they cascade over the same
slots.
2026-08-18 04:39:24 +02:00
David Montero 990cced209 feat(editor): the Save button is the auto-save indicator; drop the header pill
The header rendered a "Saved" / "Unsaved changes" text pill next to the
menus whenever a project was loaded. It ate width in the one row that has
to hold the whole editor toolbar, and it duplicated a signal the Save
button in the workspace panel can carry by colour alone.

The pill is gone. FileExplorer takes the AutoSaveState and colours its Save
button: green saved, orange unsaved changes, pulsing while saving, red on
failure; grey (unchanged) with no project loaded. The tooltip carries what
the pill used to say plus the last-save time. Nine locales get the four new
strings under editor.fileExplorer with English fallbacks.

AppHeader no longer takes autoSave; EditorPage passes it to FileExplorer
instead. No pro overlay code referenced the pill or the prop.
2026-08-18 02:26:35 +02:00
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