Three stacked defects made ANY non-knob property change invisible to a
running sim (values restored from saved projects, the agent's
set_component_property, the property dialog):
1. DynamicComponent assigned element properties without firing the events
part simulators bind to — dispatch input/change + dispatchSensorUpdate
on real value changes (applied-value guard breaks the echo loop).
2. Parts received the shared legacy AVR simulator regardless of board —
route attachEvents to the simulator of the board the part is wired to
(getBoardSimulator), so direct analog injection works on non-AVR boards.
3. The SPICE->ADC push stomped un-modeled parts: a net with no SPICE
element gets an auto pull-down, solves to 0 V, and overwrote the
part's own injected voltage every solve (the dead analog-joystick).
Only sourced nets are pushed now.
Verified end-to-end: pot curve exact to the LSB (value 100/300/600 ->
ADC 400/1201/2401 on GPIO35), joystick idle at centre 2047, full-scale
4095 after a store write.
Board supply pins split into the main logic rail plus aux rails by real
voltage (ESP32/XIAO VIN,5V -> aux_rail_5 at 5 V; 5V-Arduino 3.3V pin ->
aux_rail_3v3; Pico VBUS/VSYS -> 5 V). An external 5 V supply wired to VIN
used to read 3.3 V (ideal vcc_rail source clamped the whole net — customer
isolated it with a voltmeter). Aux rails join positiveSourceNets and the
relay nominal-voltage check; the voltmeter and hover overlay read them.
Also: ratings entry for the 4-pin ssd1306-i2c-4pin (supply pin VCC) and a
VCC alias on the 8-pin entry, so its power check works at all.
The unified-toolbar responsiveness work gave .unified-toolbar
container-type: inline-size, which makes it the containing block for
position:fixed descendants. The three modals rendered inside the toolbar
(library manager, install-libraries, circuit verification) shrank to the
toolbar's 89px box: pinned to the top, header and close button clipped
offscreen. createPortal(document.body) makes them immune to ancestor
containment. The library manager also gets a fixed tall height
(min(86vh, 1000px)) instead of a bouncy 80vh cap.
GET /api/intellisense/symbols/{Name@Version} scans the content-addressed
library cache entry's headers (dependency-free C++ scanner) into an editor
symbol table, disk-cached per entry and served with long cache headers.
Plain OSS hosts without a lib cache 404 and the editor degrades gracefully.
The tracked sitemap.xml committed in 3ff984a1 was a stale working-tree
regen from BEFORE df19778a retired the in-app /docs routes; it carried 15
/docs URLs that now 301 to the docs portal (which ships its own
sitemap-index, declared in robots.txt since 720a5a1). Harmless in prod —
build:docker regenerates the file at image build — but noisy in the repo
and wrong as a source of truth. 193 URLs (22 routes + 171 examples).
These binaries are loaded at runtime from /firmware/ (cyw43 installer,
RP2350 simulator, C6 bridge) and were living only in the prod working
tree — a fresh checkout would 404 them. Sitemap regenerated 2026-08-13.
velxio.dev/docs/ ships its own Astro sitemap-index (2142 URLs) but nothing
referenced it — the main sitemap dropped the retired in-app /docs entries
on purpose (df19778a) and robots.txt only listed the app + projects
sitemaps, so crawlers had no discovery path to the new portal.
C3/S3/C6 default to CONFIG_BT_BLE_50_FEATURES_SUPPORTED with 4.2 OFF, so
arduino-esp32's Bluedroid BLE library (esp_ble_gap_config_adv_data_raw,
esp_ble_gap_start_advertising) died at link with undefined references —
esp32c3-ble-advertise never compiled. Enabling 42+50 together matches
arduino-esp32's own per-target sdkconfigs; on classic ESP32 these flags
are already the Kconfig defaults. Found by the nightly gallery smoke.
The dead-branch pruner marked any assumed-live branch as 'taken', so the
#else of an UNKNOWN condition was skipped. GxEPD2 guards its GFX base
class with '#if ENABLE_GxEPD2_GFX (default 0) ... #else #include
<Adafruit_GFX.h>' — the pruned #else silently dropped the Adafruit_GFX
dependency and every GxEPD2 e-paper example failed with 'fatal error:
Adafruit_GFX.h: No such file'. Found by the nightly gallery compile smoke.
'taken' is now set only by PROVABLY-true conditions (all identifiers in
the known-true set), keeping the 2026-08-03 ESPAsyncWebServer behaviour:
provably-dead branches still prune, provably-true branches still kill
their #else.
The ESP32 examples now default to the in-browser JS engines, whose lwIP
serves the sketch's HTTP server from the simulation tab itself. Opening
the IoT gateway in a new tab backgrounds that tab, the browser throttles
its timers, and the in-chip server can't answer before the proxy's
timeout — the exact failure the Pico W iframe path already solved.
- WifiStatus grows an inBrowser flag (set by the overlay's JS bridges);
the serial-monitor link and the WiFi badge now open the in-app iframe
for any board that carries it, not just pi-pico-w.
- /api/gateway polls ~1s before answering 404: the net bridge dials on
'got_ip', which races the sketch printing its URL, so a fast click
used to land before the bridge registered.
Live E2E on the in-browser ESP32 engine surfaced two freezes:
- i2c.scan() stalls on the engine's I2C controller (first probed address
has no device and the transaction never completes). The bmp180 shim now
probes 0x76/0x77 directly via readfrom_mem.
- BlynkLib's blocking socket.connect() froze the demo when the second
outbound connection stalled. The vendored BlynkLib now sets a 10s
timeout before connect, and main.py catches OSError and falls back to
the offline no-op Blynk (probe-then-reconnect removed — one bounded
connection instead of two).
2c4c93a2 gave 13 of the repaired 100-days examples real canvases, so
their netlists are no longer empty. Regenerated with vitest -u; every
change is the expected kind (components and rails appearing, plus the
micropython-watch button rewiring).
- 10 Pico W projects now emit boardType pi-pico-w so the RPI_PICO_W
firmware (network + CYW43) boots; the plain-pico build has no network
module and every Wi-Fi example died on import.
- Entry points: projects whose alphabetically-first file was a library
(BlynkLib.py) or a host script (analyze.py) now ship a real main.py.
- All Blynk projects probe blynk.cloud with a 5s timeout and fall back
to an offline no-op object — a plain connect() blocks forever in the
emulated network and froze the demos.
- DHT11 -> DHT22 across six projects (the emulator speaks DHT22) and
the OLED projects use hardware I2C instead of bit-banged SoftI2C.
- 11 more examples get a wired canvas (DHT22, SSD1306, LCD1602 I2C,
membrane keypad, joystick, heart-beat sensor, LEDs): 13/49 total.
- IoT Atmospheric Monitoring (ESP32+Blynk): ship the missing bmp180.py
driver (BMP180 API over the emulated BMP280), switch SoftI2C -> hardware
I2C(0) so the emulator decodes the bus, survive an unreachable Blynk
Cloud, and add the BMP280 sensor + wiring to the canvas.
- AQI ESP: ship the missing ssd1306.py driver, DHT11 -> DHT22 (the
emulated waveform), local AQI fallback when the Flask server is absent,
and add the OLED + DHT22 canvas with wiring.
- All 49 descriptions now credit Kritish Mohapatra's upstream repo
(github.com/kritishmohapatra/100_Days_100_IoT_Projects).
Generated by test_100_days/_emit_examples_data.py in velxio-prod (which
now supports per-project velxio_diagram.json canvases).
The drag handler computed the split percentage against
getBoundingClientRect().width (border box), but the panels' percentage
widths resolve against the content box. Overlays that reserve space by
padding .app-container (docked side panels) made the handle jump left on
click and track offset from the cursor. Measure against clientWidth minus
padding, with the origin shifted by border + left padding.
The documentation moved to the Starlight portal served statically at
/docs/ (nginx shadows the old SPA routes and 301s the legacy slugs).
The header Documentation link becomes a full-page <a> so it reaches
nginx instead of client-routing into the removed pages.
Board DATA like spiPanelId: the bridge that supports the declared type
instantiates the model at the given address and setBatteryVoltage drives it.
First consumer is the M5Stack Core's IP5306.
Vendor autodetects read the panel's RDDID with the display's own CS and
reject or misdetect the board on the bus's idle 0xFF. The answer is board
DATA (which CS, which id byte), not bridge code, so it moves to the def:
the M5Stack Core is {14, 0xE3}, the Cardputer ADV {37, 0x85}. Bridges that
support the probe consume it generically.
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.
The main component had them; the merged user-libraries component did not, so
a warning INSIDE a vendor library was still fatal. M5Stack's MahonyAHRS.cpp
(fast inverse square root, a float read through a long*) fails under
-Werror=uninitialized, which made installing the vendor's own library kill
every build using it. Library code the user cannot edit must not stop their
build; the diagnostics still print as warnings.
The library's fast inverse square root reads a float through a long*, GCC
flags y as used uninitialized, and -Werror turns a vendor library the user
cannot edit into a failed build. Demoted to a warning like its siblings.
A private build can now register a per-component buy URL at boot
(fetched from its backend), superseding the datasheet front-matter.
loadDoc merges the override, so both Buy buttons, the UTM decoration
and the click tracking pick it up unchanged — and a component with no
authored datasheet gets a Buy button too (empty body, buy set).
Two of M5Stack's own published Cardputer examples fail to build here and
compile fine in the Arduino IDE: SSHClient passes an int to a %lu, and
mic_wav_record has a local GCC thinks may be used uninitialized. ESP-IDF
promotes both categories to errors, which is right for IDF itself and wrong
for a sketch a user pasted from the vendor's repository — the emulator looks
broken for code that is not.
format and maybe-uninitialized join the existing relaxation list, which
already carries comment, parentheses, sign-compare and the rest for exactly
this reason. They stay warnings, so the diagnostic is still printed; -Wall is
untouched.
The proWebFlash seam gains an optional flashMicroPython(files) method
plus an 'uploading' progress phase. MicroPython boards enable the
Flash menu when the overlay implements it — no compile step needed,
the modal collects the workspace group's files (same path as the Run
flow) and explains the two-step install+upload. toFirmwareVariant is
exported so the flasher shares the emulator's per-chip offset table.
compiledProgram holds the 'micropython-loaded' sentinel in MicroPython
mode, not a flash image — atob() blew up with an encoding DOMException
before the port chooser even opened. The menu item now disables with an
explanatory tooltip and the modal shows the limitation instead of the
Connect button.
appendHardwareSerial pushes real-UART bytes through the existing
per-frame serial batcher (no SerialMonitor changes), and
lib/proHardwareSerial lets the overlay intercept a board's serial
input while a physical monitor is attached — TX and RX switch to the
hardware together, detaching hands the tab back to the simulator.
Also adds @types/w3c-web-serial (esptool-js references it but only
dev-depends on it).
The board context menu's Flash item and FlashModal gain a second
backend: a Web Serial flasher the pro overlay can install via
lib/proWebFlash.ts (installWebFlashImpl). Without an overlay the seam
is inert — web builds keep the desktop-only behavior. esptool-js is
declared here because the overlay has no package.json of its own; OSS
code never imports it (verified absent from the vite bundle).
The announcement fetch now fires 250ms after editor mount — early
enough to race the dynamically imported pro overlay chunk that
registers the real news source. Losing the race silently fell back to
the OSS self-host proxy, which on the cloud deployment returns [] (its
hairpin fetch of its own public feed does not survive the round trip),
so cold-cache visitors — every incognito window — got no announcement
at all. getNextNews() in a pro build now waits, bounded at 8s, for
registerNewsSource() before picking a source; OSS builds resolve the
gate immediately and behave exactly as before.
The corner toast went unread. The announcement is a modal again — what
makes it viable this time is sequencing, not stacking: a tiny gate
(lib/newsGate.ts) lets EditorPage hold the pristine-visit starter
dialog until the news flow is done (nothing to show, or modal closed),
so only one startup overlay is ever on screen and news always comes
first. The pending decision is bounded at 2.5s so a dead feed cannot
hold the starter dialog hostage; reading time is unbounded.
The modal is deliberately a touch wider than the starter dialog
(880px vs 840px) and the news fetch delay drops 4s -> 250ms since the
starter now waits on it. Toast markup and styles removed; Help >
What's new keeps reopening the last delivered post.
With the AI chat docked, the canvas header ran out of room and the Camera
and Microphone buttons overlapped the icons before them (visible as
"…Camera" colliding on a XIAO-S3-Sense board). Serial and Scope already
collapse to icon-only via the `@container unified-toolbar` query
(font-size: 0 hides the label, the fixed-size SVG stays), but Camera/Mic
carried their layout in INLINE styles, and an inline font-size beats the
class rule so the query could never reach them.
Move the two toggles' static layout into a shared `.canvas-cam-mic-btn`
class (only state-driven styles — background, border, colour, the pulse
animation — stay inline) and add that class to the same collapse rules as
Serial, in both the container query and the mobile media query. Also drop
the header button gap from 8px to 4px once the toolbar is narrow so all
four (Serial/Camera/Mic/Scope) pack in without overlapping.
Side effect: the toggles now share Serial's 28px height, so the toolbar row
lines up (they were a touch shorter before).
b02aab9a picked up frontend/src/pro — a symlink to a private-overlay
checkout that only exists on one machine; every other clone got a
dangling link. The ignore patterns had a trailing slash, which matches
directories but not symlinks — that is exactly how it slipped past.
Drop the slash so both forms stay ignored.
New lib/newsEvents.ts registry (same pattern as newsSource.ts): OSS
default is a no-op sink; an extension overlay can register one to
forward the signals to its own analytics. NewsAnnouncer reports the
toast impression and modal opens (toast vs Help menu); NewsMarkdown
grows an optional onInteract callback fired on body link clicks and
YouTube preview plays — the admin authoring preview passes nothing, so
authoring never counts as engagement.
c201f5d9 guarded the property-sync effect, but the mount effect has its
own raw Object.entries(properties) loop that runs first and assigned the
saved imageData: '' onto the fresh element — so the firstUpdated
putImageData crash survived. Same guard, second call site.
Saved projects carry imageData: '' in the SSD1306's properties (the
generated metadata lists it as a text prop). The property-sync effect
assigned that string onto the web component, replacing the element's
live ImageData and crashing wokwi-elements' firstUpdated with
'putImageData: parameter 1 is not of type ImageData' on every project
load. Only a real ImageData may pass through now.
The bridge was a passive sniffer that never drove the ACK bit. That
worked by accident: avr8js seeds a pin's external input value to 0, so
when TinyWireM released SDA for the ACK slot the master read 0 (ACK)
and kept transmitting. Commit eb3b04ef (seed INPUT_PULLUP pins to their
resting level) now sets that external value HIGH at init, so every ACK
slot read back NACK and TinyWireM aborted each transmission right after
the address byte: 239 address frames, zero data bytes, zero STOPs, and
a permanently blank SSD1306 on every ATtiny85 + Tiny4kOLED project.
Fix: capture each byte's ACK through the bus master callbacks (the
bridge owns the master slot on the ATtiny85) and hold SDA's external
input low for the whole ACK slot. Timing matters: TinyWireM strobes the
USI with USICS1|USICLK (avr8js clockSrc 3), which samples DI at the top
of the FALLING USITC write, one toggle after the 9th rising edge, so
the drive is released only on the falling edge that closes the slot.
Unowned addresses now read back NACK like real hardware.
Regression test replays TinyWireM's exact register sequence (from the
ATTinyCore disassembly) against a real AVRSimulator.
The canonical MicroPython connect idiom is `while not wlan.isconnected():
pass` — a pure Python busy-wait, and most gallery WiFi examples use it.
The emulated CPU only fast-forwards through WAITI idles, so a busy-wait
pins the sim at real execution speed (~2% of the chip in a browser tab).
Measured on the classic-ESP32 engine with a marker-bracketed harness: the
connect+DHCP phase costs ~1.5M busy instructions under a sleepy poll loop
and ~194M under the busy-wait — same firmware, same radio, 130x apart.
At browser speed that is close to a minute of wall clock for the connect
alone, which reads as "WiFi hangs" and got diagnosed as exactly that.
The proxy now sleeps 20ms per unanswered isconnected()/status() poll,
turning the user's busy loop into an idle loop without touching their
code. Re-measured end-to-end with the real prelude: 226.8M -> 64.3M busy
steps, and the made-up-SSID example connects with a 10.13.37.x lease on
the in-browser engine.
Deploy verification of the #262 fix caught a backend split the issue
could not see. The reporter runs the OSS Docker image, where MicroPython
executes under the QEMU worker — there the real driver works and their
fix verification (DHCP on 192.168.4.x, DNS, TCP) holds. But velxio.dev
routes a plain ESP32 MicroPython run to the in-browser esp32js engine,
and the engines' P4 WiFi (which serves Arduino fine) does not yet cover
MicroPython's v1.20 / esp-idf-4.2 vintage esp_wifi blob: handing it the
real module stalls esp_wifi_init with NO output — observed live as four
minutes of dead serial after the raw-REPL paste, strictly worse than the
stub's fake-but-responsive answers.
So the prelude now asks the bridge: an overlay bridge that keeps the
MicroPython run in-browser declares micropythonWifiSupported=false and
keeps the stub; the QEMU path (every OSS install, and ?esp32sim=qemu in
the overlay) gets the real driver + SSID-rewrite proxy. When an engine
gains MicroPython WiFi it flips its declaration and this file needs no
further change.
Property is duck-typed and optional; the plain OSS Esp32Bridge does not
define it, and undefined means supported — OSS behavior is exactly the
reporter's verified environment.
The serial monitor turns printed device URLs into IoT-gateway links —
but only when the sketch printed a full http:// URL. That matched the
Arduino idiom and missed MicroPython's: `print("Open:",
wlan.ifconfig()[0])` emits a bare "192.168.4.15", which rendered as
plain text. The address is unreachable from the host by design (it only
exists inside the emulated network), so exactly the users who just
gained real WiFi were handed an IP with no way to open it.
The scheme is now optional; a negative lookahead keeps longer
dotted strings (192.168.4.15.99) from half-matching. ifconfig() tuples
linkify every same-subnet address they contain — all of them route to
the same gateway root, so the extra links are harmless.
Follow-up to the #262 fix. Handing MicroPython the real driver exposed a
population the stub had been quietly carrying: sketches that connect to
an SSID that simply does not exist in the emulated radio. The AP tables
(QEMU esp32_wifi_ap.c and the esp32*js engines — same four networks in
both) associate by MAC/channel, so an unknown SSID scans to nothing and
`while not isconnected():` spins forever. Most 100-days gallery examples
ship their author's home SSID, so "real driver, no shim" would have
frozen them on the connect loop.
So when a NIC is attached the prelude now injects a thin proxy around
the REAL network module: connect() to any SSID outside the emulated set
is redirected to Velxio-GUEST (announced on serial, so nobody debugs a
connection they didn't make); known SSIDs pass through untouched; every
other attribute delegates to the native module. Result: made-up-SSID
sketches get genuine DHCP/DNS/TCP through SLIRP instead of the old fake
answers, and the reporter's PICSimLabWifi repro is never rewritten.
Wrapper semantics (rewrite, passthrough, module-attr delegation, and the
`from network import WLAN` form) are exercised against an instrumented
fake module; both generated preludes are syntax-checked.
The metadata auto-scan sweeps every wokwi-elements board into the
registry, and boards Velxio does not emulate surface as component cards
that promise support we don't have. Filtered in the picker, not deleted
from the metadata: four saved projects already contain the element and
must keep rendering.
Reported in #262: on an emulated ESP32, MicroPython's WLAN.scan() always
returned [], ifconfig() reported 10.0.2.15/10.0.2.2/10.0.2.3, and
outbound sockets raised EHOSTUNREACH — while Arduino WiFi.h on the same
board and container scanned, connected on 192.168.4.x and completed real
HTTP requests. Two independent faults, both needed for the failure.
1. The MicroPython prelude injected a stub `network` module into
sys.modules unconditionally, so user code never reached the firmware
driver. The stub's hardcoded ifconfig() happens to be QEMU's SLIRP
defaults, which is why the symptom read so convincingly as a routing
bug.
2. hasWifi survived a language switch. It is a property of the compiled
sketch, set only by arduino-cli, but setBoardLanguageMode kept it — so
a board compiled once as Arduino carried that verdict into a later
MicroPython run. The source-scanning fallback only fires when hasWifi
is undefined, so it never got a chance, and the backend attaches the
`-nic` device only when wifi_enabled is true.
The stub is NOT removed, which is where this departs from the patch in
the issue. It exists for a reason the report didn't have: without a NIC,
`network.WLAN(STA_IF)` hangs forever waiting on peripheral status bits
QEMU never sets, tripping the FreeRTOS watchdog after ~26s. Deleting it
would trade a fake-but-working network for a hung board. It is now
conditional — real driver when a NIC is attached, stub when none is —
so both paths are honest.
Also kept: the ntptime and urequests stubs, which the issue's diff would
have dropped along with `network` (it removed the whole prelude block).
Those back 29 call sites in the 100-days gallery examples, which have no
emulated internet to reach regardless of the NIC.
The detector is now one shared function instead of two copies that could
disagree — the stub decision and the -nic decision MUST match. Folded in
`from network import WLAN`, which matched neither previous pattern: such
a sketch was judged WiFi-less and hung on the missing peripheral. The
existing tests re-implemented the detection inline, so they only ever
proved that copy self-consistent; the new ones call the shipped function.
The Arduino section listed only Uno and Mega — Nano and Nano ESP32
existed as first-class boards (kinds, FQBNs, examples) but could only be
reached through the gallery or the board dropdown. Same for the
ESP32-CAM, which now sits in the ESP32 section next to its DevKit
sibling. The camera board's starter is the webcam demo rather than a
blink: the board exists to show the sensor, and a bare LED sketch would
be a misleading first run.
The post-deploy thumbnail bot captured examples for boards still hidden
behind launch control and pushed the files here; the images are blank
canvases but the filenames name unannounced products. The capture leak
is fixed at the source (the gallery's launch filter now checks
boardType/boardFilter), so these cannot regenerate.