Saved velxio_circuit wires sering berbentuk {start,end} tanpa id/color/
waypoints. Filter lama (w && w.start && w.end && w.id) membuang semua
wire sehingga rangkaian lesson kosong. Relaksasi filter ke start+end saja,
lalu normalisasi id/color/waypoints, dan panggil recalculateAllWirePositions
agar wire ter-render.
New release
Conflicto resuelto en CHANGELOG.md: la seccion [Unreleased] de master queda
arriba y debajo siguen las versiones ya publicadas ([3.0.3] .. [3.0.0]) que
solo existen en release. frontend/package.json se fusiono solo y conserva el
3.0.4 que dejo el autobump de release, no el 3.0.0 de master.
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.
A sketch that includes BTstackLib on a CYW43 board fails with arduino-pico's
static_assert: "This library needs Bluetooth enabled. Use the 'Tools->IP/
Bluetooth Stack' menu in the IDE to enable it." There is no such menu here, so
it reads as a setting we forgot to expose. What is actually true is simpler:
the CYW43439's Bluetooth controller is not emulated — only its WiFi side is —
so there is nothing to switch on.
Appends that, leaving the compiler's own diagnostic intact above it because the
developer still needs the real text. Deliberately a separate helper from
humanize_cli_error, which REPLACES arduino-cli's own failures; this one only
adds context under a compiler message.
Worth knowing for whoever picks up the BT work: this is the honest-failure
milestone. Making a BT sketch compile at all means flipping this board's FQBN
to ipbtstack=ipv4btcble, which swaps liblwip.a for liblwip-bt.a and costs every
sketch on the board ~71 KB flash and ~18 KB RAM — and leaves it reaching
HCI_STATE_WORKING while advertising to nobody. The mechanism is mapped in
project/pimoroni-pico-plus-2w/STATUS.md.
MicroPython over the QEMU worker stopped loading: every project failed with
"the board stopped answering while writing _vlx_main.py", the board sitting
idle at a healthy `>>>`.
The uploader shipped in 664c3581 waits for the board's end-of-execution marker
before sending the next step — that wait is the whole point, it replaced a
fixed delay that pushed 64 bytes every 150 ms. The marker never arrived. Raw
REPL answers each statement with `OK` <EOT> traceback <EOT> `>` and not one
newline in it, and the UART buffer flushed only on newline, carriage return, a
period or 256 bytes. The answer sat in the buffer; the uploader timed out
waiting for something the backend was holding.
Nothing had needed that answer before: the old uploader pasted the whole
program and never listened. EOT is a message boundary in this protocol, so the
buffer treats it as one.
Found by forcing a MicroPython project down the QEMU path with ?esp32sim=qemu —
the residual risk 664c3581 flagged and could not exercise, because velxio.dev
routes every supported ESP32 kind to the in-browser engines. Self-hosters from
this repo have no engines and always take this path, so it broke for exactly
the people the #219 fix was written for.
`WiFi.begin()` panicked the guest: `Guru Meditation Error: Core 0 panic'ed
(LoadStorePIFAddrError)`, EXCVADDR 0x60033c00. Reported against a self-hosted
dev setup, and it did not reproduce on velxio.dev — which routes every
supported ESP32 kind to the in-browser engines. The engines are not in this
repo, so anyone self-hosting from it always runs QEMU, which is where the crash
lives. It was verified away on a path the reporter cannot take.
0x60033c00 is the WiFi MAC at its APB alias, and the fork only instantiates
that device when a NIC is attached (`qemu_find_nic_info(TYPE_ESP32_WIFI)` in
hw/xtensa/esp32.c). The worker attached one only when `wifi_enabled` was true,
so a regex over the user's source decided whether the machine had a radio at
all. Reproduced against the real worker: same sketch, same firmware, only the
flag differs — true connects on 192.168.4.x, false panics exactly as reported.
Two things were wrong, and the second is why the first was fatal.
The scan was too narrow. Arduino mode read only the entry sketch while pure-IDF
mode read every file, so `#include <WiFi.h>` in a project header was judged
WiFi-less. Verified against the live backend: main sketch -> has_wifi=true, the
same include one file away -> false. Both branches read the whole project now.
The frontend also trusted a false over its own all-files scan; the two signals
are OR'd, because a stale or narrower false is the direction that hurts.
But no scan should have been load-bearing. A real ESP32 has a radio whether or
not the sketch uses it, so the emulated one does too: the NIC is attached
unconditionally on machines that model a MAC. A misread sketch now fails to
connect instead of faulting. Only those machines — the S3 machine models no
radio (hw/xtensa/esp32s3.c never looks for the NIC), so handing it one would
leave an unconsumed netdev. `wifi_enabled` still gates the host forward, which
exposes the guest's server and does belong to the sketch.
Measured before making it unconditional, on a sketch that never touches WiFi:
boot time, guest-clock-vs-real-time and container CPU unchanged over 4 paired
runs. The AP's beacon timer runs on QEMU_CLOCK_REALTIME — the reason -icount is
off for Xtensa, b0fe5b5d — but idle beacons cost nothing measurable. One host,
Linux x86-64, one sim at a time; small effects would not show.
With the radio always present, the MicroPython `network` stub loses its reason
to exist on this path: it was there because `WLAN(STA_IF)` hangs forever with no
NIC behind it. Keeping the old condition would put the stub in front of a
working device whenever the scan misread the sketch — #262, from the other
direction. It now depends only on whether the backend models a radio.
And when this class of fault does happen, the dump says "memory" and nothing
else; a week went into #260 for that reason. LoadStorePIFAddrError is always an
unmapped peripheral, so the worker now says so in the serial monitor, naming
the WiFi MAC when the address is in its page.
test_esp32_wifi_args re-implemented the arg logic in its own `_simulate_args`,
so it would have passed unchanged through this entire bug — the same trap
1800f572 called out for the detector. The decision is now `wifi_nic_arg()` and
the test calls it.
Verified end to end against the QEMU worker: the multi-file WiFi project
compiles with has_wifi=true and connects, and a WiFi-less sketch still boots and
runs with the radio attached.
The CYW43439 emulator is stateful — bus handshake, backplane window, SDPCM
sequence and credit, event mask, link state — and attachPioPeripheral is
idempotent, called once at board-add. So the chip's state SURVIVED every later
Run while the guest driver started over from scratch. The second Run had a
fresh driver talking to a chip mid-conversation: the handshake "passed" against
stale registers, IOCTL replies came back zeroed (WiFi.macAddress() read all
00s), and the first call that actually waited on the chip — WiFi.begin(), or
the second IOCTL of any kind — blocked forever.
A rebuilt MCU means a rebooted board, so the chip is power-cycled with it: the
peripheral is replaced wholesale (it owns its network bridge and its whole
register model, so there is no partial-reset path to get wrong) right before
the FIFO hooks are re-installed on the new PIO.
Also: overlay board simulators now get onSimulationStart. The Pimoroni Pico
Plus 2 W and Badger 2350 carry the same CYW43439 die on an RP2350 simulator,
which is not an RP2040Simulator, so they never had their network bridge opened
— WiFi that reached an IP and could then reach nothing.
Verified on the RP2350 side with a harness that boots a real arduino-pico
binary twice in one simulator (pro overlay); before the fix run 2 hung with a
zeroed MAC, after it both runs associate.
A MicroPython library in Velxio is a .py file copied onto the board before
main.py runs. It got there inlined into the program as ONE Python string
literal:
with open("mqtt_as.py",'w') as _f:
_f.write("...36 KB of escaped source on one line...")
so compiling the program asked for one CONTIGUOUS allocation the size of the
file. Issue #219 is that bill coming due on a PSRAM-less ESP32: 36407 bytes of
mqtt_as.py, `MemoryError: memory allocation failed, allocating 36410 bytes`.
Never the firmware version the thread suspected.
The v1.28 bump (bd1613a9) made that single allocation fit, so the reported
message is gone — but the mechanism was not, and what it costs today is worse
than the error was. Measured on the reporter's own project, on velxio.dev and
on staging: 216 s pinned in the raw REPL before the first line ran (the paste
went out at 64 bytes per 150 ms, ~426 B/s), and then `OSError: WiFi Out of
Memory`, because 36 KB of heap was still held by the compiled literal when
esp_wifi came up. The project could not work.
Nothing is inlined now. Each file is written by a sequence of small executions
(open / write(b'...') x N / close) — the shape mpremote's `fs cp` uses against
real hardware — so peak guest RAM is a few dozen bytes per step whatever the
library weighs. The program is written as a file too and started with execfile,
whose lexer streams off flash: a big sketch compiles in the same memory a small
one does, and it still runs in __main__ exactly as the pasted code did.
Flow control is the other half. One burst is in flight at a time, sized to fit
the 128-byte RX FIFO both backends model, and what releases the next is the
board's own end-of-execution marker — proof it consumed the last one, instead
of a delay chosen by hoping. A step can only overflow one burst on a
pathologically long file path, and those few bursts fall back to timed pacing
rather than corrupt the stream. The hundreds of protocol exchanges stay out of
the serial monitor; a progress line goes in their place, and a board-side
failure mid-upload is reported instead of starting a program whose library
never arrived.
One implementation, shared by the QEMU bridge and the in-browser engines. The
hand-ported second copy is why this bug lived in both paths, and the pro
overlay now re-exports this module instead of keeping its own.
Also fixes a latent corruption: sanitizeForRepl's inline-comment rule used to
run over that one-line literal, so a ` #` inside a library file made it strip
non-ASCII from everything after it. Library bytes are never tokenized now.
Verified on staging with the reporter's project (main.py + mqtt_as.py): upload
complete in 36.5 s, then "Got reliable connection / Connecting to broker /
publish 0". The WiFi OOM is gone with the heap it was competing for.
Not exercised live: the OSS QEMU MicroPython path — velxio.dev routes every
supported ESP32 kind to the in-browser engines, so staging cannot reach it. It
runs the same session and is covered by the unit tests, and its 800/200 ms
handshake timings are preserved rather than unified with the engines' 400/150.
The ESP32 and STM32 shims only offered `feedUart(uart, data: string)` and
`serialWrite(text)`, both of which run the string through TextEncoder. That
is UTF-8, so every byte >= 0x80 leaves as TWO bytes. Fine for the text
protocols the seam was written for (NMEA out of the GPS module, AT chatter);
silently fatal for anything framed.
A part answering a binary protocol has no way to spot it. A ten-byte reply of
AA 55 04 00 FF FD 01 FD 55 AA
reaches the guest as fifteen bytes,
C2 AA 55 04 00 C3 BF C3 BD 01 C3 BD 55 C2 AA
and no parser recovers from that: the sync word is gone, the length field
disagrees with the frame, the checksum covers bytes that were never sent. The
symptom is a device that answers and a sketch that times out anyway, with
nothing in the console — which is what makes it expensive to find. Protocols
whose replies happen to be ASCII pass straight through, so the seam looks
fine right up until a sync word is 0xAA.
Both shims now also expose `sendSerialBytes(bytes: number[], uart = 0)`,
forwarding to the bridge method of the same name without encoding. feedUart
keeps its text contract and its callers; its doc comment now says so.
The test pins the difference rather than the implementation: same frame down
both paths, asserting the text one mangles it and the raw one does not, plus
a round trip of all 256 byte values.
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>
An overlay board was invisible to the build-options model: its kind is not in
the OSS family sets, so isEsp32Family() said no, the Board Options menu never
appeared, and every compile went out with board_options: null - the
conservative family default of CONFIG_SPIRAM=n.
That is wrong for a module that always ships PSRAM. The ESP32-S3-EYE carries
8 MB of octal PSRAM and esp_camera allocates its frame buffer with
MALLOC_CAP_SPIRAM, so its camera examples compiled cleanly and then died at run
time with 'cam_dma_config(509): frame buffer malloc failed' - a hardware fact
surfacing as a mysterious driver error.
Boards now declare defaultBoardOptions, sent when the project saved none of its
own. ONLY the declared keys are sent: the backend backfills the rest from its
own defaults, which are deliberately not the modal's (huge_app vs min_spiffs),
so a board that gains one declared knob does not silently get repartitioned.
Boards that declare nothing keep sending null and build bit-for-bit as before.
Also route the family predicates through the registry, so overlay ESP32 boards
finally get the Tools menu they were missing.
arduino-esp32 3.x keeps most of its requires PRIVATE, so a sketch that
includes esp_lcd_panel_io.h dies with 'No such file or directory' -
while the SAME header resolves fine as soon as any user library is
present, because the merged user-libs component requires esp_lcd. That
made the failure look random.
Detect the IDF-tree components a sketch includes and add them to main's
REQUIRES, the same shape as the registry managed-component table next to
it. Guarded on the component existing in the IDF tree, so older IDF
generations are unaffected.
Found writing the ESP32-S3-EYE camera example, and it matters beyond
that board: esp_lcd, esp_cache, esp_ldo_regulator and esp_psram are
exactly what an S3/P4 sketch reaches for once it drives a panel.
David's kit is the ESP32-S3-EYE, not the ESP-EYE the project had been
built against - different product, and only this one has a screen. Card
art follows the real board: camera and function buttons on the main
board, the orange SUB silk band, then the 240x240 panel.