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).
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.
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.
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.
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.
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.
boardPinToNumber had two failure shapes across the ESP32 family, both silent.
A pad the mapper cannot name returns null, and a null pin means the wire
connects to nothing: the part sits on the canvas, the sketch drives a dead
pin, and the only trace is one console.warn from WirePin.
- DevKit V1 RX0/TX0. Unlike RX2/TX2, which have numeric twins at the same
coordinate ('16'/'17', be24243e), these have none, so they were the only
route to GPIO3 and GPIO1 and a wire to either did nothing.
- S3 DevKitC 40/41/42/45/46/47/48, rejected by a GPIO ceiling of 39 shared
with the classic ESP32. The ceiling is now per family.
- Nano ESP32 RX0/TX1/B0/B1. Its map named them D0/D1, pads the element does
not draw, leaving GPIO44 and GPIO43 unreachable.
- Reset pads (RST) on the S3, C3 and Nano, now -1 like the classic board's
EN rather than null.
The second shape is worse, because the pad does something wrong instead of
nothing: the numeric fallback is parseInt(pinName, 10), and parseInt('3V', 10)
is 3. The Wemos Lolin32 Lite's supply pad resolved to GPIO3, and the XIAO S3,
XIAO C3, Nano ESP32 and Aitewin C3 SuperMini resolved 3V3 to GPIO3 and 5V to
GPIO5 — a wire to the rail drove a real pin. Those branches had no power
guard at all; the shared POWER_PAD_RE replaces the ad-hoc startsWith checks.
Same reason the Wemos SPICE group now lists the '3V' pad it actually draws:
it declared '3V3', a name that board does not expose.
Every expected value is cross-checked against the `target` field of the
upstream wokwi-boards board.json, which is what surfaced this: the pad data
in PR #165 (naweiss) generates that field, and diffing it against our tables
turned up the whole class. Six of the ten ESP32-family boards now match
upstream exactly.
Left deliberately: the UART pad numbers (S3 TX/RX are GPIO43/44 and C3
TX/RX are GPIO21/20, not the classic 1/3) and the DevKitC V4 flash block
(D0-D3/CMD/CLK are GPIO6-11). Unlike the above, those pads resolve today, so
changing them moves an existing wire to a different pin.
The test walks every pad of every ESP32-family board and fails on either
shape. Pad lists are copied from Esp32Element.ts, as in
pin-inspector-layout.test.ts, because vitest runs in the node environment.
Co-authored-by: naweiss <naweiss@g.jct.ac.il>
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.
"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.
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).
- 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.
A wire endpoint naming a pin the board element does not expose silently
resolved to nothing and rendered anchored at the board corner. A full
audit (all gallery examples + every saved project on velxio.dev) found 15
examples and 28 user projects doing it — the same electrical pin spelled
differently per surface: GND.1 on the classic ESP32 (element exposes
GND/GND2), 3.3V vs 3V3, GND2 vs GND.2, D13 vs 13 vs GP13, A0-A2 vs
GP26-28 on the Pico, 36/39 vs VP/VN, bare 5V vs VBUS/VIN.
calculatePinPosition now tries those equivalences AFTER the exact match
and the two existing fallbacks miss (so no current resolution changes),
healing old saves, .vlx imports and agent-authored wires without a data
migration. The Pico element gains A0/A1/A2 alias pads on the ADC pins
(RX2/16 dual-entry precedent) and a GP25 pad at the onboard LED silk so
pico-blink-style wires have an anchor. The two examples that were
outright wrong (microsd-card-esp32 GND.1, pico-serial-echo TX) now use
the element's real pin names.
Netlist snapshots (166) untouched; verifier + builder suites green.
deleteFileGroup left editorStore.activeGroupId dangling at the removed
group: Monaco (and agent writes) kept editing a group compile never reads,
so a stale same-named sketch.ino shipped instead of the code on screen
(the 'leftover template Blink built for 35 minutes' failure). The pointer
now re-targets a surviving group, and compile re-points the editor when
the two pointers diverge (with a visible warning log).
compiledProgram is no longer serialised into boards_json: the firmware
blob was 99.99% of a typical ESP32 save (a bare board already ~1.04 MB),
pushing every real project against the request-size ceiling, and a
restored binary is stale the moment the code differs. Loaders still
accept it from old rows.
Two faults from the same audit. Every wire from the Core's M-Bus header
resolved to null: the kind does not start with 'esp32', so boardPinToNumber
fell through every branch and a part wired to the header never saw a signal,
with nothing anywhere saying why. A dedicated branch (like the Cardputer's)
maps the bare-number pads, sends 5V/GND/3V3/BAT to -1, and three new tests
pin it — the board had zero test coverage before.
And LEDC events now carry the carrier frequency. The engines have always
known it; LedcDuty just had no field for it, so a speaker listener could only
ever play a canned note — the Core's piano spent its life on 660 Hz. The duty
handler stashes it per pin in the PinManager (getPwmFreq), a side table
rather than a new callback parameter so the existing two- and three-arg PWM
listeners stay untouched.
useProjectStore.currentExampleId — stamped by loadExample, dropped the
moment a real project (or a blank workspace) takes over. The compile
request and the run event carry it as example_id, analytics only, so
'which examples get compiled and run most' becomes answerable.
Both Buy renderers (picker hover panel + Part Inspector) now append
utm_source=velxio&utm_medium=simulator&utm_campaign=datasheet with the
component id in utm_content, so vendors can attribute the referral.
trackProductPageClick fires the GA4 event and dispatches a
'velxio:product-page-click' CustomEvent — an overlay build can persist
the click server-side without this tree knowing about it.
tailAfterStream sliced result.stdout at the live-buffer offset, but the
buffer interleaves stdout+stderr in arrival order while result.stdout is
stdout-only plus a joining newline — offsets never line up, so the flush
could start mid-line ('f/esptool_py && ...'). Instead, the poll loop now
emits one final onProgress(state:'done') with the completed buffer and
the existing delta logic flushes the tail perfectly aligned. tailAfterStream
is gone.
With the success-path reprint gone, the lines that land between the
final running-poll and job completion (esptool image creation, the
binary-size summary) silently vanished. tailAfterStream() appends
result.stdout past the streamed offset, noise-filtered, before the
structured analysis — skipped once the live buffer hits its 256 KB
tail-trim cap, where offsets stop lining up.
ESP-IDF builds sprayed the compilation console with configure chatter
(-- cmake status, git-metadata probes that 'fatal' on the synthesized
project tree, IDF component-manager NOTEs, Python env checks) and then
reprinted the entire stdout a second time when parseCompileResult ran
over the final result after the live stream had already shown it.
- isNoiseBuildLine(): pattern list for build-system chatter, applied to
the live stream deltas and to the final-result parsing.
- parseCompileResult(streamedLive): skips the stdout reprint on success;
on failure re-emits only error/warning lines so they still get the
red/yellow markers the plain live stream lacked.
Compiler errors, FAILED blocks, ninja [N/M] progress, esptool output and
binary-size summaries all still show. The full log stays available
server-side.
Component picker: registry entries that ARE boards (the injected Raspberry
Pi family) no longer render as component cards — clicking one dropped a
dead 40-pin canvas prop that never boots (that is how "add a Pi 4" looked
like it placed a Pi 3). The boards row is the only path now, and it gains
Raspberry Pi Zero / 1 / 2 cards (Pi-3 illustration, PRO pill, same gate):
their QEMU profiles already existed in qemu_manager; the canvas renders
them through the Pi-3 element and BoardOnCanvas/store route them like any
Pi kind.
Part inspector: a board whose canvas node is NOT a registered custom
element (the Pi family's plain <img> wrapper) now clones the live node for
the preview instead of giving up — the Pi 3/4/5 pin map was completely
missing before. Wide boards with 40+ pins (Mega, Pi family) render
ROTATED 90° so their long pin rows become side columns with vertical room
(new PINS_ART_ROTATED_H box), and pinInspectorLayout splits a dual-line
header pair to OPPOSITE gutters the way printed pinout posters do — 40
interleaved labels never fit one gutter. Existing 18 layout tests pass.
esp-insights/esp-rainmaker print their version banner to stderr during the
CMake configure; without git metadata in the component tree it reads
"Project commit: HEAD-HASH-NOTFOUND" and the stderr classifier's default
painted it as a red error. Users read it as a failed build (it is not).
Classify "project commit:" lines as info.
File names may now carry '/' paths; the explorer renders each board group
as a collapsible folder tree (folders first, files in group order, CSS-var
indentation because .fe-file-item pins padding with !important). New-folder
buttons on the board header and per-folder hover actions (new file here,
new subfolder, delete with confirm when files are inside; a folder holding
every file of the group cannot be deleted - groups keep >= 1 file).
Model: names are the source of truth; only EMPTY folders are tracked
(folderGroups per group) so they survive until a file lands in them, then
materialise into the name prefix. Round-trips through .vlx (optional
folderGroups field, laxly validated), the auth stash draft, and
loadProjectState. Examples gain boards[].files so a single board can ship
a full multi-file tree (first entry = main sketch).
Backend: the arduino-cli sketch writer now creates subdirectories for
'/'-pathed names and REJECTS names that resolve outside the sketch dir
('..' traversal) - previously it crashed on the former and wrote outside
the tmp dir on the latter.
On real hardware your code lives in flash, not on the microSD; a sketch
listing the card should see its data (csv, json, txt, wav...), not its
own source. buildProjectSdImage now skips .ino/.h/.hpp/.c/.cpp/.py from
the workspace auto-copy. Explicit uploads are exempt - an upload always
lands whatever its extension. Panel hint updated; tests cover both the
exclusion and the upload exemption (15 green).
The SD panel only knew about uploads; everything the GUEST wrote to the
FAT card (photos, logs) stayed invisible inside the bridge. New
readFat16Image() parses the card image back (BPB-driven, real cluster
chains, VFAT long names, root dir - the inverse of buildFat16Image but
general enough for FatFs-written cards), and the panel gains a live
'Card contents' section reading the bridge's readSdImage() with a
per-file Save (download) button and a Refresh. Round-trip covered in
fat-image.test.ts (16 green).
Three-part fix for the library-contamination class of ESP32 compile
failures (a user's project kept pulling unrelated libraries because 79%
of projects compile with an empty manifest, forcing scan-all resolution
over ~1000 shared libraries):
1. Name-affinity ranking in _find_library_for_header: the provider whose
name matches the header stem wins (exact normalized match > esp32
arch declaration > contains-stem, alphabetical only as tie-break).
Previously first-alphabetical won, so any stray library shipping a
generic header (Servo.h has 6 providers) could hijack the build.
2. Manifest auto-migration: scan-all compiles now report the libraries
the build really used (manifest_incomplete +
manifest_suggested_libraries on the compile result); the editor folds
single-candidate suggestions into the board's declared manifest after
a green build, so the NEXT compile runs scoped. One green build = one
migrated project; empty-manifest scan-all behaviour is untouched.
3. Install-declares: the Wokwi import library modal records successful
installs in the active board's manifest, and .vlx export/import now
round-trips board.libraries.
Tests: test_espidf_library_ranking.py (9) + library-manifest.test.ts (19).
A ProBoardDef can declare builtInSdCsPin, and the store already built a FAT
card from the project files for it - but there was no way to put YOUR files
on that card: the upload panel only existed for the microsd-card component
and for parts with metadata.sdSlot, and boards have no properties bag.
Boards now carry sdFiles (same shape the SD panel persists everywhere else):
- BoardInstance.sdFiles, consumed by the store's Run path (card component
still wins when one is on the canvas);
- the board inspector shows the SD Card panel whenever the board kind
declares builtInSdCsPin, and routes the panel's onChange to updateBoard;
- .vlx round-trips it (serialisableBoard + payload type), and gallery
examples can pre-load the slot via ExampleBoard.sdFiles, applied by
loadExample the way the panel would.
First user: the XIAO ESP32S3 Sense (CS=GPIO21), whose board def and example
land in the pro overlay.
The Arduino Uno came out wrong: 18 pins on the top header, 13 on the bottom,
and SIX x values shared between the two. The column detector accepted any two
pads sharing an x, so those pairs were read as columns and most of the board
was thrown into the side gutters with leader lines fanning across the art.
A column now needs three pads AND small gaps between them (no more than 40%
of the body), which is what a real header looks like; rows get the same guard
on x. The Uno lands as two headers, top and bottom, with no side gutters at
all - covered by a test using its real 31-pin table.
The ReSpeaker's left side carries 13 labels: at the minimum spacing that is
208px against a 190px body, so the column ran past both ends and the first
and last labels were cut off by the preview's clip. The gutters now grow to
whatever the labels actually occupy - the box follows the content.
Three things the dialog got wrong in use:
1. Missing pins. Classification was nearest-edge with an "interior" bucket,
and interior pins printed no label - only a dot with a tooltip. The
ReSpeaker Lite showed it: its XIAO socket is two INSET columns (x=50.5 and
126.5 on a 178-wide body) plus two breakout columns, so most of its pins
were called interior and the part looked like it was missing them. The
classifier now finds the LINES first - pins sharing an x are a column,
pins sharing a y a row - assigns each line to the side of the body it sits
on however far inset it is, and joins label to dot with a leader. Only a
lone pad falls back to nearest-edge. Every pin gets a printed label; that
is now the contract, covered by a ReSpeaker test with its real table.
2. The panel was cut off on the Datasheet tab until some unrelated event
re-rendered it. The datasheet loads asynchronously, so the one-shot
measure placed the dialog while it was still short and it then grew off
screen. A ResizeObserver repositions on any size change, and the tab body
caps its own height so long markdown scrolls inside the tab.
3. Flicker when the pointer left and re-entered. The preview effect depended
on componentProperties and pinInfo by identity - both are rebuilt on every
parent render (pinInfo is a getter) - so the element was torn down and
remounted constantly. It now depends on stable digests of them.
An SSD1306 has 8 pins along a 240px header. Horizontal labels need ~36px
each = 288px, so they overflowed the art box and rendered outside the dialog,
over the header and the canvas. Top/bottom labels are now vertical text - what
printed pinout diagrams do - which needs only the side spacing, and the gutter
grows to fit. The preview also clips, so nothing can escape again.
The right-click dialog was a 250px column: every pin as a text row (an ESP32
means 30 rows of scrolling), a few editors, Rotate/Delete. The picker's info
card (datasheet, brand, Buy) was a separate surface you could only reach by
hovering the catalogue. One dialog now serves both:
- ~560px, two panes: live preview + actions on the left, Properties/Datasheet
tabs on the right. The Datasheet tab IS the picker card's body -
ComponentInfoPanel was split into an embeddable ComponentInfoBody (+
useComponentDoc, and its .cip-* styles moved to their own stylesheet) so
both surfaces render one implementation.
- "Show pins" (off by default): the preview is the REAL web component,
mounted and scaled, with pin markers at their true pinInfo positions and
labels flanking the side each pin lives on. pinInspectorLayout is pure
geometry - nearest-edge classification, per-side label stacking with a
minimum spacing and leader lines when nudged, interior pins as
dot+tooltip - driven only by pinInfo plus the measured element size, so
every part and board, present or future, works with no per-part data.
Dots are tinted by the pin's declared signal (i2c/spi/power/...).
- Clicking a pin keeps the old contract (onPinSelect -> handlePinClick), so
wires start with the same undo, colors and running-state gating.
- The two open gestures finally agree on coordinates: right-click always
passed viewport coords while touch passed canvas-local ones; the dialog is
now portaled to body in viewport space and the touch site converts.
- Keybind capture, the SD Card panel, the two-step delete, Escape and
click-outside all migrate unchanged. Mobile keeps the full-sheet layout.
Geometry covered by pin-inspector-layout.test.ts with coordinate tables
copied verbatim from the shipped parts (ESP32 two-column board, LED, an
interior-pin case).
Tres cambios que van juntos porque atacan la misma queja: "anado un
elemento y a veces ni veo donde se anadio".
1. Donde cae. Ya se anclaba a la esquina visible, pero la cascada que evita
que se apilen iba indexada por components.length: seguia avanzando
aunque movieras o borraras piezas, asi que las caidas se alejaban cada
vez mas de donde estabas mirando. Ahora toma el primer hueco LIBRE desde
la esquina, bajando en diagonal; si apartas la ultima, la siguiente
recupera su sitio. Extraido a utils/dropSlot con 8 tests, incluido el
caso de "la apartaron" y el tope para no salirse de la vista.
2. Que se vea. El recien anadido queda seleccionado, y la seleccion pasa de
un borde discontinuo quieto a un caminito de hormigas. El movimiento es
lo que capta el ojo en un canvas lleno; un borde fijo se pierde. Va en
un pseudo-elemento por fuera del cuerpo, sin robar clicks ni tapar el
dibujo, y se queda quieto si el sistema pide menos animacion.
3. Clicks. El izquierdo SELECCIONA y ya esta; antes abria el panel de
propiedades, o sea que no podias ni senalar una pieza sin comerte un
popup que luego habia que cerrar. Propiedades y pines pasan al click
derecho, que es donde va lo deliberado. En tactil se mantiene tocar ->
panel, que ahi no hay boton derecho.
Auditoria de "la Pi tiene todo lo de la placa real" con cuatro huecos
encontrados y cerrados:
1) GPIO de entrada en modo Linux: GPIO_IN respondia VAL 0 fijo (stub de
la fase 2), asi que GPIO.input() leia 0 eternamente aunque el canvas
empujara el nivel. El backend guarda ahora el ultimo nivel por pin
(set_pin_state lo escribe) y GPIO_IN contesta de ahi. Los flancos
(SET) siguen llegando al guest como antes.
2) UART del header hacia otra placa: el shim del rootfs ya hablaba
`UART <port> TX <hex>` / RX_REQ, pero sin modelo de esclavo el
backend tragaba los bytes. Ahora TX sin esclavo se emite al canvas
(uart_tx) y RX_REQ sin esclavo drena la cola que llena pi_uart_rx —
el mismo protocolo de siempre, sin ops nuevas.
3) El escaner de esclavos I2C/SPI/UART estaba doblemente muerto:
clasificaba por numero fisico de pin ('3','5','19'...) cuando el
elemento expone GPIOxx, y su unico llamador era RaspberryPiWorkspace,
que el terminal unificado reemplazo. Acepta ambos nombres y corre en
onBooted del store.
4) boardPinToNumber solo mapeaba los pines de la 3/4/5; la Zero, 1B+ y
2B (mismo header de 40 pines, mismo elemento) se quedaban sin mapa.
Dos piezas que faltaban para que pi-to-arduino-led-control fuera algo
mas que un guion imprimiendo lo que "habria enviado".
1) classifyPin no reconocia los pads del header por su nombre. Se llaman
GPIO14 / GPIO15 en el dibujo de la placa y en todos los cables de los
ejemplos, pero solo se aceptaban numeros fisicos: parseInt('GPIO14')
daba NaN, el pin no clasificaba como nada y el Interconnect nunca
construia la ruta. Ahora se acepta el prefijo GPIO/BCM y la numeracion
fisica sigue funcionando.
2) Seam de serie para placas que no tienen ni simulador ni bridge: el
motor de navegador corre el Python de la Pi en la propia pestana.
registerSerialSink(placa, fn) recibe los bytes que le llegan y
feedBoardSerialOut(placa, ch) anuncia los que envia, que es lo que el
enrutado por cables ya sabia repartir.
QEMU-Linux boards used three competing file surfaces (workspace group
with a meaningless sketch.ino/libraries.json, the VFS panel with its
Upload button, and the Pi workspace's own editor). Now they behave like
every other board:
- the editor file group defaults to script.py for ANY Pi-family kind
(kind-based check via isPiBoardKind, not the old raspberry-pi- string)
- the libraries.json manifest row is hidden for Pi boards
- EditorPage always renders Monaco; the RaspberryPiWorkspace swap is
gone
- the bottom serial panel renders the interactive xterm (PiTerminal,
now seeded with session history) for running Pi boards
- example vfsFiles load into the editor group (single source of truth);
the run path uploads the group into the guest home
- Run on a booted guest re-runs without the 45 s reboot (Ctrl-C +
re-upload + run); starting a Pi board pops the terminal open
- piSyncAndRunScript/piRerunScript exported from the store; auto-run
now applies to the whole family (guestHome/autoRun overridable)
- ProBoardDef.autoRun: after boot (+guestSetup) the VFS uploads itself
and the command runs, so a single Run click boots, uploads and starts
the user's script (same UX as compiled boards)
- ProBoardDef.guestHome: VFS home dir override ('/root' for guests that
log in as root); those boards drop the historic hello.sh sample
- upload sequence extracted to utils/piUpload (shared by the VFS panel
button and autoRun)
- serial monitor strips DEL/C0 control echoes (backspace showed tofu)
El bump global a zIndex 3 (efecto iman del Round Display) puso TODAS las
placas por encima de TODOS los componentes: una resistencia al lado de un
Arduino quedaba oculta tras la placa en cualquier ejemplo normal (se veian
los cables, no el cuerpo). Ahora isBoardSeated (misma matematica del snap,
umbral 0.5px) decide: posada -> z3 encima de su zocalo, como el XIAO fisico
apilado en el shield; libre -> z0 debajo de los componentes, como siempre.
El iman coloca la placa exactamente en el asiento, asi que al capturarla
salta al frente y al arrancarla vuelve abajo.
Como el iman de la breadboard pero para PLACAS: un componente puede declarar en
su elemento (estilo regla 6a, igual que pinInfo) que lleva un zocalo:
get boardSocket(): { anchorPin, accepts }
y una placa cuyo boardKind case con accepts, arrastrada cerca, se posa de golpe
con su pad anchorPin sobre el homonimo del zocalo. Un solo ancla basta porque
ambas rejillas comparten paso — esa es la gracia de un zocalo. Arrastrarla mas
alla de la tolerancia la suelta, sin estado que recordar.
Enganchado en los dos caminos de arrastre de placas (raton y tactil). El
overlay privado declara el zocalo sin que este repo sepa que existe.
boardPinToNumber no tenia rama para 'cardputer-adv', asi que caia al return
null del final: cada cable a su cabecera EXT o al Grove Port A se conectaba a
nada. La pieza quedaba en el lienzo, el sketch leia un pin muerto y nadie
avisaba de nada.
La rama compartida de esp32 tampoco habria servido: recorta en 39 y este board
saca G40 en la cabecera. El S3 tiene 48 GPIOs.
Las dos ramas habian divergido: master llevaba el modo lenguaje ESP-IDF puro
(#139) y v3.2 la ruta de compilacion IDF v5.5 para toda la familia ESP32 mas
los arreglos de venv/toolchain. Ambas tocaban espidf_compiler.py.
Los dos lados son ejes ORTOGONALES y se conservan enteros:
- use_idf5 / arduino_mode (v3.2): que arbol IDF usa el build (5.5 vs 4.4) y
si cabe Arduino-como-componente.
- pure_idf (master): el modo LENGUAJE que elige el usuario; sus ficheros son
las fuentes del componente main con su propio app_main().
Resolucion:
- _build_env acepta los tres. Un build IDF puro fuerza arduino_mode a falso:
la plantilla CMake mete el componente arduino-esp32 en cuanto existe
ARDUINO_ESP32_PATH, asi que dejarlo puesto compilaba el core de Arduino en
un build que no tiene sketch. Lo cazaron los tests de master.
- VELXIO_PURE_SKETCH solo con pure_idf, nunca con arduino_mode a falso a
secas: un target sin core arduino-esp32 sigue entregando un SKETCH al
traductor legacy y no debe tomar la rama del glob puro.
- La identidad del build-dir suma los dos tokens (|idf:N|ard:N y |lang:pure):
ningun par de esas combinaciones puede compartir un build/ configurado.
- La cadena de escritura de fuentes queda pure_idf -> arduino_mode -> legacy.
- sdkconfig: render de v3.2 (con target/use_idf5) mas el filtrado de simbolos
CONFIG_ARDUINO* de master cuando el build es puro.
test/backend/unit/test_espidf_compiler.py: los 7 tests que ya estaban rotos en
v3.2 (AttributeError: idf5_path, fixture sin actualizar desde que se anadio la
seleccion de IDF) vuelven a pasar.
Verificado: backend 293 pasan / 0 fallan (v3.2 traia 7 rotos); frontend 2268
pasan / 0 fallan en los dos shards.
Adds a third entry to the board language selector next to Arduino C++
and MicroPython: ESP-IDF. In this mode the user writes a plain ESP-IDF
project — app_main() entry point, FreeRTOS + driver APIs — and the
backend compiles it through the same ESP-IDF toolchain it already uses
for ESP32 Arduino sketches, just without the arduino-esp32 component.
Backend:
- CompileRequest.language ('espidf') threaded through the sync + async
compile paths and folded into the dedup job key (language='arduino'
and omitted hash identically so old clients keep dedupping).
- espidf_compiler: pure_idf flag. User files are written into main/
as-is (no Arduino.h wrap, no velxio_compat.h, Arduino library
resolution skipped), ARDUINO_ESP32_PATH is dropped from the build env
and VELXIO_PURE_SKETCH raised so the template CMake compiles the
user's own sources via a glob branch. Pure builds get their own
persistent build-dir variant through the eff_hash fold.
- QEMU WiFi compat for IDF-style code: esp_wifi.h/esp_wifi_init
detection sets has_wifi, and literal #define SSID/PASS plus
wifi_config_t designated initializers are normalized to the QEMU AP.
- CONFIG_ARDUINO_* lines are stripped from sdkconfig.defaults in pure
mode (the symbols don't exist without the arduino component).
Frontend:
- LanguageMode gains 'espidf'; BOARD_SUPPORTS_ESPIDF covers the ESP32
family (Xtensa, S3, C3). Toolbar shows the option only for those.
- Switching modes seeds a main.c blink skeleton (app_main + gpio
driver), mirroring the MicroPython main.py flow.
- compileCode sends language='espidf'; run/stop paths are unchanged
(the QEMU worker consumes the same merged flash image).
- New gallery example: esp32-idf-blink (LED + resistor on GPIO 2).
Tests: unit coverage for the build-env switch, IDF wifi normalization,
job-key variance, file-group seeding and the new example; verified
end-to-end in a container from the prod image (pure build produces a
bootable flash image; Arduino-mode build unchanged, same variant hash).
A wire to a pro board's Dx pad (XIAO 'D2', etc.) did nothing because
isBoardComponent only knew the OSS board-id list, so the electrical/sensor
chain skipped the board endpoint. It now also returns true for any
proBoardRegistry kind, so boardPinToNumber (which already consults the pro
def's pinToNumber) resolves the Dx name. Fixes the numeric-alias-only
workaround in the XIAO examples.
registerProBoards() lets a hosted overlay ship boards outside the OSS tree as
data-only definitions: registration patches the exported BoardKind maps
(labels / FQBN / MicroPython) so every existing read site keeps working, and
the sites a map can't cover consult the registry — canvas render (custom
element or overlay render fn), BOARD_SIZE, pin-name mapping, picker list +
descriptions + tag, ESP32 family routing, in-browser simulator construction
and firmware load (structural ProBoardSimulator contract, duck-typed PIO
attach/detach), built-in bridge sensors, and a CS-gated built-in microSD
(sdCsPin -> sd_card.cs_pin worker config). Esp32Bridge additionally gains the
esp32-c6 machine type + TX pin (public chip knowledge — the C6 compile path
already ships) and a generic sendKey() for built-in matrix keyboards.
registerProExamples() appends gallery examples at runtime; the board ONLINE
ads recompute at render so registration hides them. OSS behavior without an
overlay is unchanged — the registry is dead code, same as the other seams.
Fixes the reported breadboard wiring UX ("requerimos un vocabulario"):
Vocabulary implemented (breadboardOccupancy.ts, pure + unit-tested):
- 1 hole = 1 wire. A hole already holding a visible wire can't start a
new one — clicking it SELECTS that wire. This is the core fix: wires
running hole-to-hole across the board were impossible to select
because the pin overlays swallowed every click and silently started a
new wire (so the top horizontal rail wire was un-deletable).
- Same 5-hole strip / rail = one net. When a new wire end lands in an
occupied hole (a seated leg or another wire), it shifts to the
NEAREST FREE hole of the same group — electrically identical, the
real-world "bridge to the next hole in the row". Never crosses strips.
Two selection bugs behind the symptom:
- Click on a wire lying over the breadboard BODY now selects the wire
instead of opening the breadboard's 830-hole property dialog (that
list popping over everything was the "se sobrepone la lista de todos
los puntos" report). Guarded so the bubbled canvas click doesn't
re-toggle the fresh selection.
- Click on a hole occupied by a wire selects the wire (handlePinClick),
so wires anchored in holes are reachable at all.
Jumper colors (like a real kit — a board of identical green wires is
unreadable, "se ven todos verdes"):
- Power-rail holes mandate red (tp./bp. = +) / black (tn./bn. = −).
- Other breadboard holes get a random jumper-palette color on manual
draw; red and black are reserved for rails.
- jumperColorForId gives agent/deterministic callers a stable per-wire
color across reloads.
Tests: breadboard-occupancy.test.ts (12) — findWireAtHole (skips seating
wires, topmost wins), resolveFreeHole (same-strip shift, no cross-strip,
rail shift, passthrough), color policy (rails, palette determinism,
red/black reserved).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three router bugs found by replaying a real agent session (reloj_3333) where
wires ran straight across a seated 4-digit display. Each fix is covered by a
regression test built from the failing geometry.
Endpoint inside an obstacle no longer drops the whole obstacle
--------------------------------------------------------------
Breadboard strips under a seated display start INSIDE its inflated bbox, so
the "rects containing an endpoint are dropped" rule deleted the display as
an obstacle for every wire leaving those strips — 15 wires crossed it end to
end. The rect is now carved instead: an escape corridor (ROUTE_MARGIN wide)
from the endpoint to the chosen edge, with the rest of the body still
blocking. Side blocks overlap the endpoint's row by 1px, or the strict
segment-hit test leaves the row as a free seam straight across the body.
Overlapping rects escape in ONE shared direction
------------------------------------------------
Seated resistors overlap heavily (19px pitch, ~66px inflated boxes). When
each containing rect picked its own nearest edge, the corridors pointed
different ways and walled each other off — A* found no exit, fell back to
the direct elbow, and the wire crossed the display anyway. The escape
direction is now chosen once against the UNION of containing rects and
every carve uses it, so the corridors chain into a continuous exit.
Null route materialises the CHECKED elbow
------------------------------------------
routeAroundObstacles returns null when the PREVIEW elbow (longer-axis-first)
is clear — but the re-route pass stored empty waypoints, which the renderer
expands as the horizontal-first corner: a DIFFERENT elbow the router never
validated. Three wires shipped crossing a display whose checked route was
clean. The pass now materialises previewElbow explicitly, exactly like
finishWireCreation always did.
Verified E2E: the same agent prompt that produced 15 crossings now builds
the ESP32 clock with ZERO wire segments crossing the display body.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>