The Arduino Nano ESP32 starter card rendered a plain DevKit V1; the art for
it and for S3/C3/CAM/DevKitC-v4/XIAO/SuperMini has existed in public/boards/
all along. BOARD_COPIES now normalizes them into component-svgs/ and
BOARD_DEFS points each kind at its own file (w/h keep each SVG's aspect).
Below M5Stack, same pro() mechanics: each section lists only the boards the
overlay registered, so OSS builds and launch-embargoed kits render nothing.
Preferred first-run examples wired for every partner kind; Stellar/Badger
entries are required, their gallery examples use boards[] + boardFilter and
the generic blink search never matches them.
The serial monitor turns any 192.168.4.x it sees into an "Open IoT Gateway"
link, because that is the subnet QEMU's slirp leases to a connected board. The
ESP32's SoftAP default address is the same 192.168.4.1, so a captive-portal
sketch got the same link. It cannot work: an access point never receives a DHCP
lease, so it never reaches 'got_ip', so it never registers the bridge the proxy
looks up. The click landed on a 404 whose text then told the user to make sure
the sketch connected to WiFi, which is precisely what that sketch had chosen not
to do. The user is doing everything right and the product is lying twice.
Gate the link on the status every family already reports: QEMU through
wifi_status_parser, the in-browser JS engines, and the Pico W's cyw43
peripheral all emit 'got_ip'. Without it the address stays plain text.
Measured on a XIAO ESP32S3 Sense, one sketch per mode:
softAP 192.168.4.1 gateway links 1 -> 0
station 10.13.37.42 gateway links 1 -> 1 (unchanged)
The 404 body is rewritten to name softAP as the likely cause and point at a
network that exists in the simulator, instead of accusing the sketch. Covered
by a test that fails if the accusatory sentence comes back.
The stand-down check ran inside _compile_in_dir, which never received
the parameter compile() grew -- every ESP32 compile died with
NameError at runtime (ast-parse clean, scope-blind). Caught by the
staging warm-up compile minutes after deploy.
David's review of the panel-first click: the one-click "icon -> the
board's web page" flow is muscle memory and must not grow a second
click. So the badge is now a split button, the standard pattern for
primary-action-plus-related-surface:
- the WiFi glyph keeps its one-click action: with an IP it opens the
IoT gateway exactly as always (in-app iframe or new tab); before an
IP it opens the overlay's WiFi panel, since there is nothing else
to open yet.
- an attached caret is the panel's handle -- always reachable, run or
not, which is when PCAP matters most. Touch-friendly and visible,
unlike the right-click experiment it replaces.
The caret renders only when an overlay installed the panel hook; the
OSS build shows exactly the badge it always did.
The badge used to exist only once a run produced a wifiStatus, which
left no stable place to hang a WiFi panel off. It now renders from the
moment a radio-capable board is active (disconnected style before a
Run), and its click consults window.__velxio_wifi_panel_toggle__
first -- a private overlay claims it for its panel, which carries the
gateway-open action forward. OSS builds have no hook, so the badge
behaves exactly as always: nothing before an IP, gateway on click
after one.
A private overlay can install window.__velxio_custom_wifi_ssids__ — a
nullary provider naming the WiFi networks the project's access-point
parts broadcast. Three consumers, one seam:
- the compile payload gains custom_wifi_ssids; when non-empty the
ESP-IDF compiler skips SSID normalization entirely (no literal
rewrite, no channel forcing) — the sketch connects to the network
the user actually wrote, and a typo now fails truthfully. The dedup
key hashes the list so with/without-APs builds never alias.
- the MicroPython network shim redirects unknown SSIDs to the FIRST
project network instead of the hardcoded Velxio-GUEST, and its tuple
gains the trailing comma that made a one-network list a string.
- the firmware-upload note lists the project's networks and says "add
an access point part with this SSID" instead of "rebuild".
No provider installed (OSS build) = exactly the old behavior; the
issue-270 suite stays green (12/12).
Upstream esp-hosted-mcu 2.12.12 mints RPC uids and claims sync-response
table slots with no locking. Two tasks issuing sync RPCs in the same
instant cross-wire: the losing waiter re-reads a slot the winner already
cleared and asserts inside hosted_destroy_semaphore(NULL), panicking the
core the Arduino event task runs on -- so a P4 sketch associates, gets a
DHCP lease, and never sees WL_CONNECTED. Real boards dodge it by timing
jitter; the deterministic in-browser engine hits it on every run, which
is how it was found (watchpoint on the destroy's address, return address
resolved against the ELF).
Managed components are hash-checked and self-restoring, so the fix rides
the component manager's own escape hatch: after the configure that
fetches dependencies, esp32p4 arduino builds get a patched copy of
esp_hosted in components/, which overrides the managed one, and configure
runs once more to adopt it. The patch is five anchored replacements (a
portMUX around uid minting, slot claim, the waiter's read-wait-clear, and
the poster's give); if a future component bump moves the anchors, the
build proceeds UNPATCHED with a loud warning instead of guessing.
The reference diff and the full story live in velxio-prod:
project/espressif-devkits-2026-08/patches/ and STATUS.md (2026-08-26).
Declares the project.connectAgent command id and a File-menu row for it.
The row is optional+pro: only the velxio-prod overlay registers a handler
(it owns accounts, the MCP endpoint and the token flow), so bare OSS hides
the row instead of greying it out — same seam as the account.* commands.
Four Raspberry Pi kinds shipped one picture. BoardOnCanvas routed
raspberry-pi-zero, -1 and -2 straight to <RaspberryPi3>, and the picker card
did the same, both with a comment explaining that the 40-pin header is "the
canvas identity" so the art could be shared.
It cannot, for the Zero. That board is 65 x 30 mm against the Pi 3's
85 x 56, it ships its header UNPOPULATED, and it has neither Ethernet nor a
full-size USB port. Drawing it as a Pi 3 tells a student the opposite of what
is in their hand, and it is a teaching simulator. The 1B+ and the 2B really do
share the Pi 3's outline and connector positions, but not the silicon and not
the silkscreen, and those are exactly what someone reads a board photo for.
Three new illustrations, flat-vector in the house style: the palette is lifted
verbatim from Raspberry_Pi_3_illustration.svg (PCB #037e3f, gold pads #90814e,
mount rings #f9e494, HDMI #e8914f, steel #e5e5e3) and the raspberry silk is the
same path the Pico art uses, so they sit next to the Pi 3 without a seam.
Zero 2 W 650 x 314 units — bare plated header holes, RP3A0-AU package,
RF can, microSD cage, mini-HDMI, two micro-USB, CSI on the edge
1 Model B+ 884 x 578 — BCM2835 / ARM1176JZF-S, "Model B+ V1.2" silk
2 Model B 884 x 578 — BCM2836 / Cortex-A7, "Pi 2 Model B V1.1" silk
buildPi40PinHeader() was already in the tree, already carrying the BCM name
map, and used by nothing — its geometry was hardcoded to a 250x160 footprint.
It now takes the header origin, pitch and row offsets, so each board passes
the numbers ITS OWN art is drawn from and the pin tips cannot drift off the
pads. Verified by overlaying the computed positions on the rendered art: all
40 land on the drawn holes on all three boards.
Not touched: ComponentRegistry still gives these kinds tagName
velxio-raspberry-pi-3. That tag only feeds PartInspectorDialog's preview, and
the element behind it draws a crude red rectangle for the Pi 3 too — a
separate wart, and one that wants its own fix rather than three more copies.
tsc --noEmit clean across the frontend, build:docker green, and the 76 tests in
board-kinds-coverage + multi-board-integration pass.
The reporter sent his three Visuino binaries, and running them settles what his
error was. It was never the network name, and it was not his delay() theory
either.
His first binary reproduces his exact report on the in-browser engine —
`[40][E][STA.cpp:357] connect(): STA connect failed! 0x300a: ESP_ERR_WIFI_SSID`,
timestamps and all — and his latest binary connects and prints its lease, the
same `NAN WiFi stop` line and IP his real-hardware screenshot shows. Same
emulator, same day. The difference is inside the firmware: the failing build
reaches esp_wifi_connect with no usable SSID, and a sketch calling
WiFi.begin() with NO arguments on this simulator produces his error verbatim —
verified. On his real DevKit that same call works, because begin() with no
credentials reuses whatever some earlier firmware saved in NVS, and his chip's
flash had a network in it. The simulator's flash starts blank. Nothing in the
output said any of that, and it cost him days.
So the serial monitor now says it: when `0x300a: ESP_ERR_WIFI_SSID` scrolls
past, one [velxio] line explains the NVS mechanism and gives the literal fix
(`WiFi.begin("Velxio-GUEST", "")`). Wired at the serial batcher, which both
backends feed; it fires once per run, re-arming naturally because a fresh run
starts a fresh log. The delay() theory was measured and does not hold — the
same sketch with and without delay(1) both associate, 48s vs 52s on the
engine, 8.0s vs 8.2s on QEMU.
His binaries also exposed d64b6796's detection as built on sand: it keyed on
Arduino's log strings ('WiFiGeneric', 'STA.cpp'), which vanish when a build
lowers its debug level — his latest image contains neither, so the note would
have stayed silent on precisely the firmware it was written for. The markers
are now IDF driver names (net80211, phy_init, esp_wifi, wifi_init), chosen by
measuring a blink build against a WiFi build: each is absent from the former
and present in the latter. WIFI_INIT looked obvious and is in BOTH, so it is
not on the list. Checked against all three of his images: WiFi detected, his
correct SSID found, note rightly silent.
Separate finding, out of scope here: his merged images reboot-loop on the QEMU
machine (flash-init assert). His build carries CONFIG_SPIRAM=y — the core 3.x
precompiled libs enable it regardless of the PSRAM=disabled menu choice — and
our own build with PSRAM enabled fails the same way on QEMU, which models no
PSRAM chip ('PSRAM ID read error: 0x00000000'). velxio.dev routes ESP32 to the
in-browser engine, where his images run, so this affects only the self-hosted
QEMU path.
esp_hosted ships a CLI -- an esp_console REPL on the serial console --
enabled by default, and every ESP32-P4 arduino build was carrying it.
On a cloud-compiled sketch it is worse than useless: its "host>" prompt
interleaves with the user's own serial output, and its console task polls
a UART that in the emulator never blocks, pinning one HP core at
priority 23 and defeating the engine's idle fast-forward. Found while
bringing up the P4's esp_hosted WiFi companion, where it made a
mostly-idle guest emulate two orders of magnitude slower than it should.
Appended in the same esp32p4 block that pins the rev0 mask ROM, for the
same reason: it is a property of running on the emulator's terms, not a
user choice.
Two answers about the UNIHIKER M10 came from the family whose run path it
borrows rather than from the board itself.
The upgrade prompt told a free user that "Raspberry Pi emulation is a Pro
feature" about a DFRobot SBC: proBoardFeatureName named the family, so every
piFamily overlay board inherited the Pi's name. It now uses the board's own
label whenever the kind is registered; the OSS families keep theirs.
loadExample gated the guest-script branch on isPiBoardKind(), which reads a
registry the overlay only fills at mount. A direct /example/<id> link could
load before that: the branch was skipped and the editor opened on the board
group's Arduino blink default instead of the example's script.py. vfsFiles is
itself the declaration that this board's code is a guest script, so the
branch no longer consults the registry.
The server-side gate message names the two families instead of one board,
since the QEMU-Linux lane also carries boards that are not Raspberry Pis.
Test covers both, with the unregistered-kind race reproduced.
`Fail if components-metadata.json is out of date` has been red on every push
for days. The diff it printed was one line — the last one:
-}
+}
\ No newline at end of file
The committed JSON ends with a newline; the generator wrote
JSON.stringify(...) with none, so `git diff` was never empty and no content
change could ever clear it. The generator now writes the trailing newline,
which makes a fresh run byte-identical to what is committed.
Verified by running the real generator (needs the wokwi-elements clone, and
NODE_PATH pointing at frontend/node_modules): 156 components, `git diff`
empty.
The last ten: the thermal trio (DC motor, heater block, SSR) and the seven
DFRobot Gravity parts — CO, ECG, soil moisture, industrial pH, the C4001
radar, the HuskyLens and the DF2301Q voice recogniser.
That completes the shop window: every part the hosted overlay merges is now
advertised in OSS, thumbnail and all, instead of a self-hoster having no way
to learn it exists. Each card still retires itself in the hosted build.
DS18B20, the three Maxim thermocouple/RTD front-ends and the ILI9341+touch
panel are as free and as online-only as the four added a moment ago; OSS just
showed nothing for them. Same card, same pill, same self-retiring contract.
The overlay's parts are free — they just live in the hosted build. OSS showed
nothing at all for the SCD41, the BME688, the BME280 and the RGB Encoder
Wheel, so a self-hoster could not tell they exist. They get the same ad card
the M5Stack matrices and the Seeed boards already have: real thumbnail, ONLINE
pill, link to the editor. Each one vanishes on its own in the hosted build,
where the registry has the real component.
The QEMU libraries left the public `qemu-prebuilt` release: they are
GPL-2.0 derivatives of QEMU, and distributing one obliges us to hand the
recipient the corresponding source, so binary and source now live in one
place -- velxio.dev's gated endpoint, where the qemu-source-<commit>
archive sits beside them.
Six documents still pointed at the release. Two of them shipped runnable
commands that now 404: the deploy-to-container curl pair in the WiFi
notes, and the e2e README's claim about where CI gets its libraries
(that workflow had in fact moved to the gated endpoint some time ago --
only the README was stale).
Also corrects two things that were already wrong before today:
QEMU_RELEASE_URL is documented with the release as its default, but
Dockerfile.standalone defaults it to empty and reaches the binaries via
VELXIO_LICENSE_KEY; and the asset inventory omitted the macOS, Windows
and esp32s3_rev0_rom entries.
`frontend/src/pro/` is a gitignored COPY some dev setups drop in for a local
pro build. `src/**/__tests__/**` picked it up, so every pro test ran twice —
once from the source at ../../pro/, once from a snapshot — and any file the
snapshot reads from outside frontend/ resolves against velxio/ instead of
pro/. That is how pi-vision-example.test.ts started failing the deploy gate
on `velxio/guest-shims/velxio_screen.py`, a path that has never existed, for
a file nobody had touched.
A clean OSS clone has no src/pro, so the exclude is a no-op there.
The symbol server assumed one library layout: the content-addressed cache,
where an entry dir name IS its content hash. A host without that cache --
Velxio Desktop's sidecar, and any self-host -- has its libraries where
arduino-cli put them, under plain folder names with the version inside
library.properties, and got a 404 for every request. The editor there falls
back to the engine's embedded catalog, so `client.` on a library the user
installed himself resolves to nothing.
Both layouts are now sources, tried best-first. The only real difference is
mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its
name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by
a cheap stat-only stamp (file count, total size, newest mtime) in the same
`<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact
rather than merely long-lived. Cache entries keep their existing keys, so no
deployed scan is invalidated.
Where the sketchbook is: an explicit override, then the sketchbook env vars,
then arduino-cli itself (`config get directories.user`, probed once and
remembered) -- guessing ~/Arduino is wrong on the platforms whose default is
~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the
pro overlay points arduino-cli at a symlink to the cache root, and scanning
it twice under two shapes buys nothing.
Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside
`libraries/` and never inside it, so nothing Velxio writes shows up in
`arduino-cli lib list`.
The type index now keys on a fingerprint of the source's entry keys instead
of the entry count, which is exact for both layouts (an in-place upgrade
changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
Reported in velxio#271: the guide pointed at commit 822927b6 of
lcgamboa/qemu, which exists in no repository, and told the reader to
recover the canonical SHA by grepping Dockerfile.standalone, which
records no SHA. It also promised the result would match our prebuilts
byte-for-byte.
Three more errors nobody had reached yet, because the bad commit id
stopped them first:
- configure was invoked with --enable-shared-lib, an option QEMU
does not have. The library comes from relinking the qemu-system-*
objects without system_main, which build_libqemu-esp32.sh does.
- The dependency list omitted libgcrypt, libslirp, libfdt, zlib and
jq, the meson==1.2.3 pin, tomli (configure dies without it on
Python < 3.11) and the empty libiconv.a stub glibc hosts need.
- The ROM list omitted esp32s3_rev0_rom.bin, the S3's own.
Rewritten around what the release CI actually runs. States up front
that an upstream clone cannot build what Velxio runs -- upstream has
Espressif's plain -M esp32s3, not the esp32s3-picsimlab variant the
worker binds its callbacks to, nor the GPSPI2 and ESP32-CAM device
models -- and points at the corresponding-source archive published
next to the binaries on the download endpoint.
A manifest-scoped compile only sees the libraries the project declares, so
it dies on the first undeclared transitive dependency — a header the user
had no reason to name. The scan-all retry exists to rescue exactly that,
and when it succeeds nothing is lost. When it fails as well, we returned
the SCOPED failure, which describes a library only our own scoping made
missing.
Today a hand-written SSD1306 sketch was reported as
Adafruit_GFX.h:12:10: fatal error: Adafruit_I2CDevice.h: No such file
while the retry had already resolved BusIO and reached the real bug, an
`#include <Adafruit_SSD1306>` with no .h. The user went looking for a
missing library that was installed all along.
The retry runs in the superset environment, so its error is the sketch's
own in both compilers. Prefer it whenever it carries text; the scoped
result still stands in when it does not.
observe() is called once per emulated instruction, and nearly every one of
those is a step forward inside the same code — not a backward branch, not a
long jump. It was doing the loop-close bookkeeping first anyway: two range
updates and a counter on fields of a foreign object, then the test that would
have told it none of that was needed yet.
The forward case now returns after one comparison and at most one field write.
The loop's lowest PC is only updated where it can actually change — a backward
branch IS the loop top — so nothing about what counts as an idle spin moves.
Measured on the RP2350 host (BadgeOS, MicroPython on the M33), together with a
one-read-per-turn change on the caller's side: +11% instructions per CPU
second, four rounds out of four.
IdleSpinDetector reported a spin from two signals: the same backward branch
closing over and over, and no GPIO change while it did. A bytecode interpreter
matches both — MicroPython's dispatch closes the same loop millions of times
and touches no pin — and eliding it does not skip the work, it only races the
simulated clock ahead of work that is really happening. On the RP2350 that cost
the Badger 2350 its frame budget: BadgeOS executed 500 k guest instructions per
second of guest time where the emulator can do four million.
Two more things now have to hold before a loop counts as idle: its body must be
narrower than 128 bytes, and one turn of it must take fewer than 24
instructions. A delay() that polls the timer is a handful of instructions in a
handful of bytes; a dispatch loop is neither.
Both are ceilings, so this can only ever elide LESS than before — the RP2040's
own delay() elision is unaffected.
Typing anything into the /examples search box threw "Cannot read
properties of undefined (reading 'map')" and blanked the page. The
haystack the search matches against is built from
`example.components.map(...)`, and `components` was undefined on 27
examples.
`components` is required by `ExampleProject`, so this reads as
impossible — but the gallery is assembled from overlay example modules
whose factories end in `as unknown as ExampleProject`, and the compiler
stops checking there. The field is a promise the type makes and cannot
keep, so the three places that read it now cope with its absence.
CircuitPreview is the one that matters most: it renders as the fallback
whenever an example's thumbnail 404s, so it meets exactly the examples
nobody has generated art for yet.
fqbnForLanguage landed on the single-board Run path and on
compileBoardForFlash, but compileAllBoards still read BOARD_KIND_FQBN
directly — so a canvas with more than one board, or any run that goes
through the multi-board path, still refused an ESP32-C5 kit with 'no FQBN
configured' while its ESP-IDF project built fine on its own.
Probed against staging the same way as the previous commit: lvgl.h and
esp_lvgl_port.h both died at "No such file or directory", so a visitor
could not paste a single screen of esp-dev-kits UI code. Declared only
when the source includes the header, so no other build pays for it.
A header that lives in a registry component and is not on
_MANAGED_COMPONENT_INCLUDES dies at "No such file or directory": the
project template ships no idf_component.yml, so nothing fetches it. That
list covered five display/camera components, which left the pieces the
official Espressif board demos are actually made of unavailable —
verified by compiling a one-include probe for each against staging:
led_strip.h FAIL (the C3-LCDkit knob_panel's WS2812 driver)
iot_button.h FAIL (its BOOT/knob-press handling)
iot_knob.h FAIL (its EC11 quadrature decoder)
esp_lcd_touch_gt911 (the P4 devkit's panel) and esp_codec_dev (every
audio demo on the VoCat and the P4) are added on the same grounds.
Anyone pasting code from esp-dev-kits or esp-bsp hit this on the first
line. Detection is unchanged — the dependency is declared only when the
source actually includes the header.
Tests: ManagedComponentDetectionTests in test_espidf_pure_mode.py, incl.
negative cases (a user's own my_led_strip.h must not match).
BOARD_SUPPORTS_ESPIDF is a literal set of OSS board kinds and it gates
both the toolbar's ESP-IDF option and setBoardLanguageMode, so no overlay
board could ever be switched into ESP-IDF: loadExample called
setBoardLanguageMode(id, 'espidf') for an `languageMode: 'espidf'` gallery
example, the guard returned early, and the example opened in Arduino mode
with the toolbar saying "Arduino C++". It stayed invisible because the
backend sniffs `app_main` and compiles those sketches as pure IDF anyway —
until a board with no Arduino FQBN at all hit it, and the run died in the
browser at "No FQBN for board kind" before any request left the page.
- ProBoardDef.supportsEspIdf registers the kind into BOARD_SUPPORTS_ESPIDF,
the same way supportsMicroPython already did for MicroPython.
- ProBoardDef.espidfFqbn + BOARD_KIND_ESPIDF_FQBN + fqbnForLanguage give the
IDF lane a target for a board whose Arduino FQBN is null (the ESP32-C5
kits: no arduino-esp32 core supports the C5, and the backend refuses that
target for Arduino on purpose, but idf.py builds it fine). Both compile
sites resolve through it instead of reading BOARD_KIND_FQBN directly.
- The language select shows when EITHER alternative mode exists (the P4 is
ESP-IDF but not MicroPython, so it had no select at all), hides options
the board does not support, and hides Arduino for a board with no FQBN.
- addBoard no longer seeds an Arduino-less board into 'arduino', where it
could never compile and the select would show a value it does not carry.
Regression test: overlay-espidf-mode.test.ts.
Auditing the catalog for parts in the same situation as the DHT22 and the
HC-SR04 turned up three things.
Two more models own a real pad and were not declared: a matrix keypad's
COLUMNS (the QEMU worker even keeps a `_keypad_cols_owned` set, idles them
HIGH and drives them on each scan) and an ePaper panel's BUSY line. Neither is
broken with plain wiring today, only because the connector's older
`sourcedNets` gate happens to skip a net nothing else sits on — put a pull-up
on a keypad column and that gate stops holding, which is exactly how a
level-shifted HC-SR04 lost its echo. Their DC / CS / RST stay absent on
purpose: the host drives those.
Ownership is therefore no longer "is this a single-wire sensor?" but "which
fields does this model declare as pads it drives", which also lets a field
hold a LIST of pads (the keypad's columns).
And `pushbutton-6mm` did nothing at all, on every board. Three parts hand
control of their pin to the electrical solve rather than seeding it; two of
them are modelled by SPICE and the 6mm variant was not, so it stood down for a
circuit that never modelled it. It is the same switch as the full-size one, so
it now shares that mapper through a MAPPER_ALIASES table. Verified in a real
browser: on production the button visibly depresses while the sketch keeps
reading `released`; with this build it reads PRESSED and returns.
The last part is the guard: a test reads the parts themselves and fails if any
part that defers to SPICE has no model, so the next variant is covered the day
it is written. It catches pushbutton-6mm when the alias is removed.
Two additions, both for peripherals that model the far end of the wire
rather than watch it arrive bit by bit.
feedWord() now carries which state machine wrote the word, where the host
plumbing knows it, so a peripheral can tell one program from another.
consumesTxWords() is the new one that matters: it says "I have this word,
the PIO does not need to shift it out". A Pimoroni Unicorn's panel spends
about 300,000 PIO cycles per refresh in the program's BCD delay loops, and
every one of them is wasted once the pixels have been read out of the FIFO
word. Unlike inDiscardableWriteData(), the peripheral is still fed the
word — that is the whole point of it.
The cost test asserted milliseconds per pin, and a deploy gate that happened to
be running two test shards read 8.19 where an idle box reads 1.96 — so it
failed the deploy over the machine's mood rather than over the code.
Worse, the number it watched was the wrong one. Tracing a pin on a shared
ground rail costs O(node) because the answer is a property of the whole node:
a canvas with more legs on that rail HAS a bigger node, and a higher per-pin
cost, by design. The hazard the index actually removes is different — the
first cut re-scanned every wire on the canvas at every frame, so circuitry
that had nothing to do with the traced net still slowed it down.
So the test now grows the canvas with UNRELATED clusters (their own breadboard,
their own rail — a board pad is where a walk stops, so they share no node) and
asserts that tracing the same little net does not get slower. Best-of-seven, so
it measures the code and not a scheduling hiccup. Indexed it reads ~0.6-0.9x
across an eightfold canvas; with the cache disabled it reads 7.8x and fails,
which is the negative control this threshold was set from.
"Which sensors drive their own wire" was written down three times: a set of
type names in the ESP32 bridge, a wiring map in the store, and a union type
plus per-type branches in the overlay's hub. Nothing tied them together, so a
third single-wire sensor meant finding all three, and missing one is silent —
the same shape of mistake that cost an HC-SR04 its echo pin in the first place.
simulation/sensorModels is now the list: the sensor type, its data pin, its
properties and the extra pins its model also drives. The store's
SENSOR_COMPONENT_MAP IS that declaration, the bridge re-exports the ownership
predicate instead of restating the names, and the overlay's hub derives its
union from it and gates sensorFromRecord on the shared check. What stays
per-device is the MODEL — the waveform each sensor answers with — which is
real behaviour and belongs next to the code that produces it.
Ownership now walks the extra-pin fields a model DECLARES rather than any
field that happens to end in _pin, so a stray property cannot take a pad, and
a model that grows a second line is covered by its declaration alone.
BOARD_PIN_GROUPS is keyed by the BoardKind union, so a board registered by the
overlay could never appear in it and fell through to `default` — 5 V, ground
pads named GND/GND.1/GND.2, supply pads named 5V/VCC. Every RP2350 and
ESP32-family pro board is a 3.3 V part whose supply pad is called 3V3, so the
netlist powered it from the wrong rail and did not recognise its supply pad at
all. Nothing errors; the numbers are simply wrong.
Caught on the Pimoroni Pico Plus 2 W: a potentiometer across 3V3 and GND read
2.50 V at half travel instead of 1.65 V, because the divider was solving
against a 5 V rail. The same 1.5x error was sitting under every LED current,
pull-up and divider on every pro board.
boardPinGroupFor() consults the pro registry before falling back, and a board
declares its pads through ProBoardDef.power. The OSS table is untouched, so no
existing board changes behaviour.
Found while wiring the ADC seam: enabling the SPICE analog path for a pro board
is what finally made the wrong rail visible.
Review pass over 8f148e8, closing what was still copied or only half true.
- `upsertSensorRecords` is THE merge for sensor records, exported from the
ESP32 bridge: same kind on a pin merges per field (the part and the store
each know one half of an HC-SR04), a different kind replaces outright, so
a DHT22 swapped for an HC-SR04 cannot inherit a stale echo_pin that the
ownership guard would then keep. The seven overlay bridges carried their
own copy of the old replace-by-pin loop — the exact copy-six-times trap
this codebase has hit before — and now call this one.
- `sensorRecordOwnsPin` is likewise the one answer to "does this record own
that pad", so the overlay's pre-connect path and the QEMU bridge cannot
drift apart.
- connectDigitalInputsToMcu forgets its cached level for a pin a part owns.
Keeping it meant that once the part let go (rewire, unmount, detach) the
next solve could find the circuit agreeing with a stale memory and never
re-emit, leaving the guest at whatever the part last drove.
- traceBoardGpio's `boardId` was a filter applied AFTER the walk had already
picked a pad, so a net reaching two boards' GPIOs answered null for the
second board. It is now a preference the node collection honours, with a
test that fails without it.
Hardening pass on 57325aa, which fixed the reported HC-SR04 divider but did it
by returning the first answer any branch produced. Five topologies people
really draw break under that rule, and each one now has a test that fails on
57325aa and passes here:
- a ground tie point almost always carries something else (a button's
pull-down). Reaching a rail has to END the walk, or a grounded pin resolves
to whatever GPIO that pull-down hangs off. Same through a breadboard rail;
- two custom chips on one wire stopped sharing a chipbus net key, because a
nested frame answered with its own neighbour's per-endpoint key before the
root could mint the net-canonical one;
- a chip one hop away ended the search before a real GPIO on the same node
was ever looked at;
- a node first reached deep, where the hop budget cut it short, was never
re-walked when a shorter route arrived.
So the walk now models what a net IS. It collects the whole NODE (every pin
joined without crossing a component: the wires, and a breadboard strip's
holes), then judges the set: a driven pad wins outright; a supply pad means the
node IS the rail and the walk stops there; only then does it cross a passive to
the next node, where a rail is the weaker answer that lets a level-shifted
sensor resolve. Wires, components and breadboard groups are indexed per store
array, and a driven pad short-circuits collection — the common case is now
0.08 ms/pin on a 300-wire canvas, better than before this work started, with
the shared-rail worst case at ~2 ms.
Second half: a part that models its own line keeps it, on every board.
connectDigitalInputsToMcu thresholds the solved node into every input pin, and
its `sourcedNets` gate only ever protected part-driven lines by accident — it
holds while no component sits on the net, and stops the moment the user adds
the pull-up or the divider real hardware needs. Then the connector pins the
line an HC-SR04 or a DHT22 is answering on. simulation/partPinOwnership records
what each part drives (implicitly, through the simulator it is handed) and the
connector skips those pins; the ESP32 bridge answers the same question for
sensors the backend or an engine emulates, through a new optional `ownsPin`
seam. Parts SPICE can describe never claim anything, so a mis-wired button
still reads stuck — that property is why spiceDrivenInputs exists.
Also: setSensors merges per FIELD, so the coarser registration path can no
longer drop the finer one's echo_pin; the ESP32 bridge's guard is narrowed to
the single-wire sensor types, since an ePaper's DC pin or a keypad row must
stay drivable; and the Mega and Pico W pin maps get the supply-pad guard the
other boards have, where '5V' parsed as D5 and '3V3' as D3.
Verified in a browser against a local build of this tree, driving the real app:
HC-SR04 and DHT22, direct wiring and level-shifted, on Uno (avr8js), Pico
(rp2040js), ESP32 (esp32js and QEMU) and ESP32-S3 (esp32s3js) — see
scripts/sensor-wiring-matrix.mjs in velxio-prod.
An HC-SR04 on an ESP32-S3 answered "No echo" forever the moment its ECHO
went through the 1k/2k2 divider a real 5 V sensor needs, and worked again
when the divider came out (reported on Discord, project esp32-s3-distance).
Nothing about the divider is wrong: three separate resolvers could not see
past it.
1. The wire walk treated a net as a chain. Arriving at the divider's tap it
hopped straight across the lower resistor, hit GND, and answered "tied to
a rail" (-1) — never looking at the OTHER wire on that same tap, where
GPIO41 was. A rail was also allowed to end the search, so a plain pull-up
drawn before the signal wire hid the GPIO behind it the same way. The walk
now explores the whole node (same depth, one shared visited set) and
treats a supply pad as the weakest answer: remembered, returned only when
no driven pin was found anywhere.
2. The walk moved to simulation/PinTrace so the store can ask the same
question. It pre-registered backend sensors with its own single-wire
lookup — one wire, far end must be a board — so ECHO (landing on a
resistor) resolved to nothing. That entry REPLACES the part's, keyed by
pin, so the echo pin was silently dropped and the QEMU worker fell back to
TRIG+1: it pulsed GPIO22, which nobody was reading. Both the data pin and
the extra pins now resolve through the shared walk, so a sensor behind a
breadboard strip or a divider registers like any other.
3. Esp32Bridge.sendPinEvent now refuses a pad a registered sensor owns, the
guard the in-browser engines already have. connectDigitalInputsToMcu
thresholds the solved node, and `sourcedNets` does not hold it back once
real components sit on the net — the divider makes ECHO exactly that, and
it solves at ~0 V because nothing models the sensor's output.
Test covers the reported circuit end to end: the walk, and the echo_pin that
reaches the start_esp32 payload.
A Visuino-built `Generated.ino.merged.bin` was reported as failing to run in
Velxio. It runs fine. The screenshot shows the ESP32 bootloader completing and
the sketch executing; what it also shows is
[E][STA.cpp:357] connect(): STA connect failed! 0x300a: ESP_ERR_WIFI_SSID
[W][WiFiGeneric.cpp:900] setTxPower(): Neither AP or STA has been started
which is WiFi association, not firmware loading — and reads as "the emulator is
broken" to someone who has no reason to know the difference.
The emulated radio broadcasts four networks and nothing else: Velxio-GUEST,
PICSimLabWifi, Espressif, MasseyWifi, all open. Sketches compiled here never
have to know, because espidf_compiler rewrites their SSID literal on the way to
the emulator. Firmware that arrives already built skips that step by
construction — there is no compile to rewrite anything — so it hunts for the
network it was built for and never finds it.
Confirmed on the real emulator rather than reasoned about: the same sketch
built with SSID "MyHomeNet" reports FAILED, and with "Velxio-GUEST" connects on
192.168.4.x. Same board, same path, only the string differs.
So the upload path now reads the image and, when it carries the Arduino WiFi
stack but names none of the reachable networks, prints one line saying which
networks exist and why a compiled-elsewhere binary cannot be fixed up
automatically. When the image already names one it says nothing: a note nobody
needs is noise, and noise is how people learn to ignore the console. The
markers are deliberately narrow for the same reason — `esp_wifi` alone appears
in images that never touch the radio.
The SSID list moves to EMULATED_WIFI_SSIDS in types/board.ts. It was written by
hand inside the MicroPython prelude string, and a second hand-written copy is
precisely what #268 was made of.
Not closed by this: the reporter's exact error is ESP_ERR_WIFI_SSID from
esp_wifi_connect, meaning the SSID handed to the stack was invalid rather than
merely absent from the air — forcing an empty SSID here produces a different
message from a different line (STA.cpp:387, "SSID too long or missing!"). That
may be his Visuino configuration or his core version; it needs his binary to
tell, which he offered to send.
Export a project to .zip, start a new project, import the zip: the circuit
comes back and the board does not. Two reporters; the second noticed it "only
works with wokwi components", which is the shape of it — the passives are real
Wokwi part types and survive, the board was not one.
The bug was in the EXPORT, not the import. wokwiZip knew four boards and wrote
`wokwi-arduino-uno`/`uno` for anything else, while the connections kept the
board's real canvas id. The attached repro says so in six lines: parts declare
an `uno`, connections name `esp32`.
Of those four, only two round-tripped. 'arduino-mega' was missing from the
export's board-id test, so a Mega's wires named `arduino-mega` while its part
was `mega`. 'raspberry-pi-pico' was tested against 'nano-rp2040', an id nothing
has been issued since boards became instances, and the import mapped it back to
the same dead string. Nobody noticed because the module had no tests.
"Velxio has four boards" was written down five times: two tables in the export,
one in the import, a hand-copied union in the import dispatcher, and BoardType
in the store — which setBoardType already ignored, casting to BoardKind to do
its job. All five are gone, plus BOARD_FQBN and BOARD_LABELS, two more from the
same era that nothing imported.
A board may claim a Wokwi part type only when a Wokwi element draws it. That is
not a preference: connections carry PIN NAMES, and the names a wire carries are
whatever the element on the canvas calls its pins. Those four render through
wokwi-elements, so an Uno wire says `GND.2`, `A4`, `5V` and the file opens over
there. Every other board renders through a velxio-* element with our names —
ours calls a pin `34` where Wokwi's esp32-devkit-v1 calls it `D34` — so
claiming a Wokwi board for one would produce a file that loads in Wokwi and
attaches every wire to a pin that does not exist. They travel as
`board-velxio-<kind>`, which also round-trips the overlay's kinds, runtime
strings this module cannot enumerate.
Nothing is coerced any more. An unrecognised diagram returns a null board and
says so instead of answering 'arduino-uno'; a second board in the diagram is
reported rather than dropped; wires naming a part the diagram lacks are called
out, which is what the reporter's own file now does when you import it.
Two more halves surfaced only by driving the real editor:
The board was still missing after import, because setBoardType re-kinds the
ACTIVE board and a new project has none — nothing to re-kind, so the circuit
arrived without its chip exactly as reported. The importers now add a board
when there is none, activate it (addBoard promotes but does not sync the flat
legacy fields), and retarget the wires to the id the board actually got.
And the export read `boardType`, the flat mirror, which only tracks the active
board through setActiveBoardId/setBoardType and reports every Raspberry Pi as
an 'arduino-uno' on purpose. It now reads the board itself, so the kind and the
canvas id come off one object and cannot disagree.
Adversarial review of the fix caught six more, every one reproduced.
The worst was not a loss but a corruption, and it was mine: a canvas with two
boards wrote BOTH boards' wires against the exported board's part id, because
for every non-Arduino family that id IS the first-of-kind canvas id. The second
board's components came back attached to the first board's pins — a different
circuit, from a file that was internally consistent, so nothing complained. My
first attempt at a dangling-wire warning could not see it either: it allowed
the board KIND as a defined id, which is precisely the id a second board
writes. The check now reads the raw diagram, the export leaves other boards'
wires out rather than mis-addressing them, and it says what did not travel.
The rest: isKnownBoardKind used `in`, so 'toString' and '__proto__' passed a
guard whose input comes from files people send each other; the importer's
extension list admitted only the Arduino four, so a MicroPython project came
back with an empty editor; the exporter never wrote libraries.txt though the
importer has always read it and offered to install what it finds; and the
unknown-board message claimed the circuit landed on the current board without
retargeting its wires there.
41 tests, over every board kind, which is the assertion this module never had:
it was written when Velxio had four boards and nothing failed when it grew to
thirty. Verified in a real browser through the reporter's flow — build the
circuit, File > Export, new project, import — for arduino-uno (unchanged),
arduino-mega and raspberry-pi-pico (both previously broken) and esp32/esp32-s3.
Before and after are identical in all five.
Still lossy, and out of scope here: a .zip carries one board, so a multi-board
project must be saved as .vlx to survive whole. Wire waypoints, one shade of
green, and board-level state (boardOptions, SPIFFS, languageMode) do not travel
either.
ADC_PIN_MAP and ADC_PIN_TO_GPIO are keyed by the BoardKind union. An overlay
board's kind is a runtime string that can never join that union, so
connectAnalogInputsToMcu hit `if (!adcPins) continue;` at all three sites for
every pro board: a divider, an LDR, an op-amp output wired to one solved
correctly in SPICE and then had nowhere to go. Parts that inject their own value
(a potentiometer, through partUtils) were unaffected, which is exactly what made
this easy to miss — analog looked like it worked.
A pro board now declares its pads through ProBoardDef.adcPins, and pad->GPIO
resolution consults its own pinToNumber first. Both fall back to the existing
tables untouched, so no OSS board changes behaviour.
The pad NAME matters, not the GPIO: on the Pimoroni Pico Plus 2 W the analog
pads are silked 26/27/28 and tied to GP40/41/42 through 1k, so the two disagree
by design.