FastLED's unity build (src/fl/build/extras+.cpp) includes
"extras/_build.cpp.hpp", but extras/ was in the excluded_dirs set so the
fragment was never copied and the merged-component build died with
'No such file or directory'. Treat extras/ as header-only instead: copy
.h/.hpp/.inl fragments so unity includes resolve, but keep compiling
nothing from it — extras/ traditionally holds repo-only content.
FastLED ships a host-test stub src/platforms/stub/Arduino.h. When another
merged library (e.g. U8g2) does #include <Arduino.h>, the stub resolved
first and broke the build: 'yield' was not declared in U8g2lib.cpp and
fl::string polluted String overload resolution inside the core WString.h.
Reuse the _SHADOW_STD_HEADERS mechanism so the stub's directory stays off
the global include path; FastLED reaches it via a file-relative include.
arduino-cli FQBNs may carry a menu-option suffix
(vendor:arch:board_id:Menu1=val1,...). The espidf lane previously derived
cdc_on_boot exclusively from the board id's boards.txt default, so a
generic-devkit board could never be built with Serial on the USB
Serial/JTAG CDC port.
- _fqbn_board_id_and_options(): split board id and menu options, keeping
the old last-segment behaviour for plain 3-part FQBNs.
- _arduino_board_flags(): CDCOnBoot=cdc overlays cdc_on_boot=1 on the
boards.txt base flags (mirroring <id>.menu.CDCOnBoot.cdc.build.
cdc_on_boot=1); the per-board cache stores only base flags, overrides
are applied on a copy.
- _arduino_variant(): board id extraction goes through the same helper so
a menu suffix no longer corrupts the variant lookup.
First user: the ESP32-C3-LCDkit board def pins
esp32:esp32:esp32c3:CDCOnBoot=cdc — the real kit has no UART bridge, its
only console is the USB Serial/JTAG port, and the esp32c3js engine now
models the always-attached host that HWCDC's connection gate requires
(esp32c3js v1.8.0).
A micropython example that ships only 'code:' landed its Python in
sketch.ino (the mode switch was right, the name was not - seen on the
SensAirShuttle ws2812 example). main.py / main.c / sketch.ino by mode.
The interface requires both fields but the Docker build never
type-checks the pro overlay's example data (vite build only), so a
missing field shipped and loadExample's unguarded .filter/.map threw -
the page surfaced as a sticky 404 on a registered example (four
SensAirShuttle examples on staging). Load an armless example as having
none instead; authoring still declares the fields required.
The Function-EV module always carries 32 MB AP-hex PSRAM and esp_lcd
allocates DSI framebuffers from it unconditionally - without it the
board's flagship peripheral cannot run. Explicit psram=disabled still
wins; the UI only sends the field after the user opens the
board-options modal. Validated: an optionless espidf compile now embeds
esp_psram.
The P4 gate ('until the engines validate it') can flip: esp32p4js
v0.3.3 models the AP hex PSRAM + MSPI1 command controller. The classic
SPIRAM chunk emitted S3-era symbols (SPEED_80M, MODE_QUAD/OCT) that do
not exist in the P4's Kconfig.spiram - it gets its own chunk: MODE_HEX
+ SPEED_200M (tree default, rev0-safe; 250M needs rev >= 3). Validated
on staging: an esp_lcd_ek79007 espidf build with psram=enabled boots in
the engine to streaming 1024x600 DSI frames.
The ESP32-C5 sensing devkit lands in the hosted editor (BME690 + BMM350
Bosch shuttles, 1.9" touch LCD, WiFi 6). Same contract as the other
Espressif cards: id matches the hosted BoardKind (esp-sensairshuttle,
in LAUNCH_ITEMS) so the ad vanishes where the real board registers and
stays suppressed during the embargo.
Two more Espressif boards land in the hosted editor; the OSS picker
advertises them like the ESP-EYE/LCDkit/C6 cards. Ids match the hosted
BoardKinds (esp-vocat, esp32-p4) so the ads vanish automatically where
the real boards register, and both ids are in LAUNCH_ITEMS so the
hosted build suppresses the ads during the embargo.
Also removes two accidental double commas that left holes in the ad
arrays.
Generalizes the esp_camera.h lesson into a table (esp_lcd_st77916,
esp_lcd_touch_cst816s, esp_lcd_gc9a01, esp_lcd_ek79007, esp32-camera)
and applies it in the arduino branch, the no-core pure-IDF fallback AND
the explicit language=espidf branch (issue #139) - the last one is where
the VoCat display example lives and previously died at the first
include. Validated on staging: the ST77916 colour-bars espidf example
now fetches espressif/esp_lcd_st77916 from the registry and compiles.
nginx 504s a cold ESP-IDF build long before it finishes; Starlette then
cancels the handler and the cancellation killed the build subprocess
MID-WRITE, leaving truncated .obj files in the persistent per-target
build cache - every later build of that target failed with
'ranlib: file truncated' until the cache was wiped (bitten twice on
staging the day the P4 lane landed). asyncio.shield lets the build run
to completion with a consistent cache; only the response is lost, and
the async /start + /status flow remains the right path for long builds.
_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.
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.
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.
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.
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.
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.
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.
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.
"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.
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).
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.
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.
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.
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).
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.
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.
- 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.
- 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- 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.
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.
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.
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.