The previous fix preserved display state on Stop so Resume could pick
up the multiplexed frame seamlessly — but that's Pause semantics, not
Stop. On a real Arduino, hitting the physical Stop is cutting power:
the next Run must boot from setup(), not continue at the saved PC.
User report on https://velxio.dev/example/uno-7segment :
> empieza a contar, le doy stop en el 6, le doy run y sigue desde 6
stopBoard now:
- calls sim.reset() (was sim.stop()) — CPU back to PC=0
- calls hardResetPinStates() (was the soft resetPinStates) — clears
cached states AND notifies listeners so 7-seg / NeoPixel / LCD
blank out instead of freezing on whatever was lit.
Reset and Stop are now the same cold-boot semantics; Reset still
additionally clears serial output + baud rate. The soft
resetPinStates() helper stays for internal SPICE-classification-only
paths that don't want listener fan-out.
Reporter feedback after 7aca3db: pressing Stop on the uno-7segment
example turned the 7-segment off, and pressing Start again left
random segments lit / no number at all. The previous fix made
resetPinStates() notify every listener with (pin, false) on both
Stop and Reset, which was right for Reset (full reboot) but wrong
for Stop:
- On Stop the AVR CPU is just paused. Internally it still has
PORTD=0xFF (or whatever the last drive was).
- resetPinStates blanked the pinStates cache + fan-out LOW
notifications. Display turns off, fine.
- On Start the CPU resumes from where it paused. avr8js's port
listener fires only for bits that CHANGED relative to its OWN
oldValue (which still holds the pre-stop value). If oldValue
matches the live register, no pinChange event fires for that
bit, and the display has no signal telling it to come back on.
Split the API into two methods:
resetPinStates() — soft cleanup, drops outputPins only. Used by
stopBoard. Cached pinStates and visual state
stay so the resume picks up where it left off.
hardResetPinStates() — full cleanup, drops outputPins + pinStates
and fan-outs (pin, false) to listeners.
Used by resetBoard (CPU starts at PC=0,
firmware re-drives every pin from setup()).
Updated the test helper clearAllPinManagerState to call
hardResetPinStates between tests so the same-state short-circuit in
triggerPinChange doesn't suppress fresh events.
All 32 vitest tests pass (AVRSimulator, interconnect-routing,
dual-arduino-software-serial, pin-position-rotation).
Two paired bugs that surfaced on the Reset button.
(1) 7-segment / NeoPixel / LCD freeze on last pattern after Reset.
resetPinStates() was wiping the pinStates cache + outputPins set
silently — no listener notifications fired, so visual components
that update on pinChange kept rendering whatever segments were
lit at the instant the user pressed Reset. Now we snapshot every
pin that was HIGH before clearing and fan out a synthetic
(pin, false) to each registered listener. Stateful displays
redraw cleanly to all-off; passive listeners (analog sensors,
debounce-only buttons) ignore the synthetic LOW and recover on
their next real write.
(2) Cross-board serial silently dies after pressing Reset. resetBoard
was unconditionally reassigning:
sim.onSerialData = (ch) => appendSerial(boardId, ch);
immediately after sim.reset(). The comment said "re-wire after
reset" but reset() does NOT clear that property — the new USART's
onByteTransmit chains through `this.onSerialData` which IS the
Interconnect wrapper. The reassignment destroyed that wrapper and
sibling-board UART forwarding (Uno TX → Nano RX) stopped working
until a full page reload. Same root pattern as the initSimulator
bug fixed in 5480052 — Interconnect's __icSerialHookInstalled
flag is on the live sim, so once the wrapper is blown away
nothing reinstalls it. Removed the reassignment and left a NOTE
so the next person doesn't reintroduce it.
Verified the AVRSimulator + dual-arduino-software-serial +
interconnect-routing test suites still pass (26 tests).
Cross-board UART forwarding silently broke for any project loaded
with > 1 board. User report: Arduino Uno → Arduino Nano serial echo
test where the Uno transmits fine but the Nano's Serial.available()
is never true.
Root cause traced live with chrome-devtools-mcp + temporary debug
logs in AVRSimulator.onSerialData setter and Interconnect:
1. loadProjectState → addBoard(uno) → createSimulator → sim.onSerialData = appendSerial
2. addBoard(nano) → same
3. setWires → Interconnect.updateWires → ensureSerialHook(uno)
wraps sim.onSerialData with a fan-out callback that ALSO pushes
to the Nano's RX queue. __icSerialHookInstalled flag set.
4. SimulatorCanvas mounts → useEffect calls store.initSimulator()
5. initSimulator unconditionally did:
simulatorMap.delete(boardId);
const sim = createSimulator(...); // ← brand-new sim
simulatorMap.set(boardId, sim); // ← Interconnect's wrapper is gone
The new sim's onSerialData is just appendSerial. The old sim
(where the wrapper lived) has been orphaned; Interconnect never
re-installs because its flag was on the discarded sim.
6. Run all boards → Uno.usart.onByteTransmit → this.onSerialData →
appendSerial (Uno's monitor shows TX) but no fan-out call →
Nano never receives anything.
initSimulator is a legacy single-board helper from the days when the
store only knew about one MCU. Multi-board flows already create
their sims in addBoard. Bail out early if a sim for the active
boardId already exists, so the legacy helper becomes a no-op when
the multi-board path has already done the work.
Verified the 3 related test suites still pass (AVRSimulator,
dual-arduino-software-serial, interconnect-routing).
User report: "rotating components messes up their connections" — pressing R
on a placed component visibly slid every wire endpoint off its pin tip.
Root cause: the DynamicComponent wrapper has padding:4px + border:2px on
EVERY side, so the inner web-component element sits 6 px in from the
wrapper top-left on BOTH axes. The wire layer assumed an asymmetric
(4, 6) offset, baked into:
* useSimulatorStore.updateWirePositions — store.x + 4, store.y + 6
* useSimulatorStore.recalculateAllWirePositions
— start (startComp.x + 4, startComp.y + 6)
— end (endComp.x + 4, endComp.y + 6)
* pinPositionCalculator.calculatePinPosition — inverse: (componentX - 4, componentY - 6)
Unrotated the 2 px X bias was visible only as a very-slightly-off wire,
which nobody filed. When the user rotated the component, the bias
rotated WITH it — at 90° it became a 2 px Y offset (wires hanging below
the pin), at 180° a 2 px X offset on the other side, at 270° upward. UX
read as "wires disconnected".
Verified the real CSS box via chrome-devtools-mcp against several live
components on velxio.dev (RGB LED + 3 resistors + analog joystick): all
report padding-left/top = 4 px, border-left/top = 2 px, inner offset = 6
on both axes.
Fix: use (+6, +6) at every site, single source of truth in a comment
explaining padding+border arithmetic. Updated the rotation regression
test to match the corrected math (numbers shift by 2 px on every
expectation that referenced the old offset).
Pin position math, pivot derivation and the rotate-N×90° round trip
unchanged — only the offset constant moved.
Phase 7.7 follow-up. Previously the WiFi stub returned wlan.isconnected()=False
and ntptime.settime() raised OSError — sketches degraded gracefully but
features like the TIME and WEATHER screens in the smart-ui-eyes example
showed "Sync Failed" / "API Error" instead of real-looking data.
Smart stub now:
- wlan.isconnected() returns True after the first ~2 calls (simulates a
~1 second connection ramp)
- ntptime.settime() pre-loads machine.RTC() with the host's UTC datetime
(captured at code-injection time), so localtime() returns real time
- urequests.get(url) returns a stubbed Response whose .json() decodes a
payload routed by URL substring:
"openweathermap"/"weather" → fake weather dict (temp/humidity/desc)
"ipify"/"myip" → fake public IP
"worldtimeapi" → fake ISO datetime
everything else → {}
- urequests.post/head also stubbed (return {"ok": True} / {})
- Both `urequests` and `requests` aliases registered
End result: smart-ui-eyes example shows real-looking time on TIME
screen and plausible weather data on WEATHER screen, no crashes.
Still no real internet (would need Phase 7 QEMU WiFi emulation), but
visually the example demos correctly.
Inject a compat shim into the raw-REPL prelude that replaces
sys.modules["network"] and sys.modules["ntptime"] with no-op stubs
BEFORE user main.py runs.
Why: the picsimlab QEMU fork's esp32_wifi NIC emulation handles
Arduino's lightweight WiFi.h but not MicroPython's full esp_wifi_init
path. Calling network.WLAN(STA_IF) (which is what every
network-using MP sketch does) drives the firmware to wait on
peripheral status bits QEMU never sets, eventually tripping the
FreeRTOS task watchdog (TG1WDT_SYS_RESET ~26s after boot, or
TG0WDT ~14s if the NIC is partially attached).
With the stub:
network.WLAN(STA_IF).isconnected() -> False
network.WLAN(STA_IF).connect(...) -> no-op
ntptime.settime() -> raises OSError
Sketches that already have try/except around sync_time (which is
most of the 100-days examples) now degrade gracefully: WELCOME +
EYES screens run, TIME and WEATHER screens show their fallback
behaviour, no panic, no reboot.
Doesn't affect Arduino C++ — sketches that #include <WiFi.h> use
real WiFi.begin() and the existing esp32_wifi NIC handles those fine.
A proper fix is to extend the picsimlab WiFi emulation to support
the full ESP-IDF API, but that's a multi-day project. This stub
unblocks the 31 MicroPython examples shipping with network imports.
The hasWifi auto-detection in useSimulatorStore.startBoard only matched
Arduino C++ patterns (#include <WiFi.h>, WiFi.begin). MicroPython
sketches that call `import network` or `network.WLAN(STA_IF)` were
not detected, so wifi_enabled stayed false and the backend never
attached the esp32_wifi NIC model to QEMU.
Symptom: any MicroPython ESP32 example that touches the network
module hangs in network.WLAN(STA_IF) (the constructor that triggers
esp_wifi_init internally) and the FreeRTOS task watchdog trips with
TG1WDT_SYS_RESET ~26 seconds after boot. The chip then reboot-loops.
Mirror the Pico W detector right below this one — it already handles
both Arduino and MicroPython patterns. Now ESP32 does too.
Affects 31 examples in examples-100-days.ts that use network.WLAN.
Closes the same gap as the AVR / RP2040 commits — qemu-lcgamboa's UART
transmits the byte over the WebSocket as a 'serial_output' event with no
GPIO toggle, so an oscilloscope on the ESP32 TX pin saw nothing while
real silicon would render the 8N1 frame at the configured baud rate.
Two changes inside Esp32Bridge:
* New `onPinChangeWithTime: (pin, state, timeMs) => void` callback
that hooks the oscilloscope at parity with AVRSimulator /
RP2040Simulator. The 'gpio_change' event now also flows through it
(timestamped with `performance.now()` — QEMU virtual time isn't
surfaced across the wire, but at 1× sim speed the wall-clock skew
is invisible on any practical sweep). This also fixes the broader
issue that ESP32 boards previously couldn't show ANY digital GPIO
activity on the scope.
* `emitUartTxFrame(byte, uart)` synthesizes start + 8 data LSB-first
+ stop transitions at `this.uartBaudRate` (default 115200) on the
UART0 TX pin, mapped per board variant:
esp32 / esp32-devkit-c-v4 / esp32-cam / wemos-lolin32-lite: GPIO1
esp32-s3 / xiao-esp32-s3 / arduino-nano-esp32: GPIO43
esp32-c3 / xiao-esp32-c3 / aitewinrobot-esp32c3-supermini: GPIO21
Backend doesn't expose the live baud rate so we default to 115200
(the Arduino default). Override path: bridge.uartBaudRate = N
once we surface Serial.begin's argument via a backend event.
Wire-up: `bridge.onPinChangeWithTime = getOscilloscopeCallback(boardId)`
inside the three Esp32Bridge construction sites in useSimulatorStore
(setBoardType, addBoard, changeBoard).
The SignalRouter path has been in prod through Phase 2.5 / Phase 3.3
deploys without regressions, so the temporary fallback shipped in
commit 77bf897 can come out. Closes#101.
Backend (esp32_worker.py + esp32_lib_manager.py):
- Stop emitting `ledc_update` from the 0x5000 LEDC callback and from
the polling thread. Only `ledc_duty` (channel + duty_pct) and the
GPIO matrix routing events ship now.
- Drop the channel→gpio reverse-lookup that fed the legacy event.
Frontend:
- Delete `PinManager.broadcastPwm` and `PinManager.pwmListenerPinCount`.
- Delete `makeLedcUpdateHandler` + its `channelGpioMemo`.
- Delete `Esp32Bridge.onLedcUpdate` field + the `case 'ledc_update':`
message handler + the `LedcUpdate` type.
- Strip `this.onLedcUpdate = null` from 14 test mocks.
- Rewrite the `does not call broadcastPwm` guard in
esp32-multi-servo-gpio-matrix.test.ts to assert the method itself
no longer exists on PinManager (stronger regression guard than the
spy version, and doesn't need vi).
- Remove the `PinManager.broadcastPwm fallback` describe block from
esp32-servo-pot.test.ts — every test in it exercised the deleted
fallback path.
Docs (ESP32_EMULATION.md):
- Replace `ledc_update` rows in the events / implementation tables
with the SignalRouter trio (`ledc_duty`, `gpio_routing`,
`gpio_routing_clear`).
- Update the visual flow diagram + the "why this matters" paragraph
to past-tense the broadcastPwm bug.
Tests: 1886 frontend tests pass (the previously-failing
board-kinds-coverage test that needed the new Pi Zero/1/2 kinds is
also green). Backend unit suite: 279 pass, the 11 espidf_real_paths
prereq failures are environment-dependent (need arduino-cli libs in
the local shell) and unrelated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end pipeline fixes uncovered while auditing the /examples gallery.
Each bug shipped past green unit + snapshot tests because none of those run
firmware + render LEDs. Added scripts/visual-led-test.mjs as a CDP-driven
visual harness that loads each example, runs the simulator, samples
`wokwi-led.brightness`, and asserts toggle / gradient / initial-off
invariants — exits non-zero on any regression.
Frontend simulator
- PinManager.updatePort: new optional ddrMask param. A pin is added to
`outputPins` only if the DDR bit is set, so the PORTx write that
enables INPUT_PULLUP (DDR=0, PORT=1) no longer falsely marks the pin
as MCU output. AVRSimulator now reads DDRB/C/D (0x24/0x27/0x2A on
Uno/Nano, 0x37 on ATtiny85, per-port table on Mega) and forwards it.
- AVRSimulator: pass DDR mask alongside every port-listener fire.
- BasicParts pushbutton{,-6mm}: seed pin HIGH in attachEvents so
`digitalRead()` returns HIGH while idle. avr8js doesn't auto-simulate
INPUT_PULLUP — without this the firmware reads LOW from boot and
thinks the button is permanently pressed (the "LED is always on,
pressing does nothing" UX bug).
- connectMcuEdgesToService: suppress synthetic digital edges on pins
with active PWM, AND subscribe to onPwmChange to re-tick the netlist
on duty changes. Fade-LED now produces a true gradient (6 distinct
brightness levels across a fade cycle) instead of a binary 0/full
toggle.
- CircuitSimulationService.handleMcuEdge: replace single-slot
pendingMcuEdge with a per-pin Map. Multiple pins toggling during the
same in-flight tick used to overwrite each other; now every pin's
most-recent edge replays after the tick. Fixes Traffic-Light RED→
YELLOW→GREEN sequencing.
- NetlistBuilder: new sanitizeSpiceId() helper replaces hyphens with
underscores in V-source names. ngspice's interactive `alter` command
treats `-` as an operator and silently no-ops on hyphenated source
names, so mid-simulation MCU pin transitions stopped propagating
after the first solve. MixedModeScheduler.onMcuPinChange and
CircuitSimulationService self-heal use the same sanitizer so names
stay consistent across emit/alter/lookup. Also added a regex-based
fallback in step 2 so any board pin matching `GND.\d+` canonicalises
to net "0" — ESP32-C3 dev kits expose up to 10 GND pins and the
per-board `groundPinNames` list missed several, leaving wires
floating instead of grounded.
- collectPinStates: emit V-sources only for pins in `outputPins`, not
every wired board pin. Leaves INPUT pins (analog sensors on A0,
pull-down dividers, etc.) free for the SPICE solver instead of being
shorted to 0 V by an ideal MCU V-source.
- start.ts: extended __spiceDebug to also expose outputPinsByBoard +
nodeVoltages + pinNetMapEntries for the visual harness.
- ESP32 / RP2040 / RISC-V / C3 simulators: pass `'mcu'` source flag to
triggerPinChange / setPinState so the new outputPins tracking fires
on those boards too (was AVR-only before).
- useSimulatorStore: stopBoard/resetBoard call pm.resetPinStates() so
outputPins clears between runs; Esp32Bridge.onPinChange passes the
`'mcu'` flag in all three places it's wired.
- types/board.ts: ATtiny85 FQBN `clock=internal16mhz` →
`clock=16pll` (ATTinyCore 1.5.2 renamed the option).
Backend
- esp-idf-template/main/CMakeLists.txt: skip the
`-DLED_BUILTIN=2` fallback for esp32c3 and esp32s3 targets. Both
variants already define LED_BUILTIN in pins_arduino.h via a
self-define macro (`#define LED_BUILTIN LED_BUILTIN` + `static const
uint8_t LED_BUILTIN = ...;`). Pre-defining the symbol from the
command line expanded the static-const declaration to
`static const uint8_t 2 = ...;` — a syntax error that broke every
ESP32-C3 / S3 build (`expected unqualified-id before numeric
constant`).
Examples
- examples.ts: bulk-fix 72 wire endpoints that referenced
`componentId: 'nano-rp2040'` / `'esp32-c3'` etc. (boards that don't
exist on the canvas). Replaced with `'arduino-uno'` (the canvas
board-id convention) and converted `D<n>` pin names to `GP<n>` for
Pico-style boards. Affects pico-blink, pico-i2c-scanner,
pico-i2c-rtc-read, pico-spi-loopback, c3-blink and others.
Tests
- scripts/visual-led-test.mjs: CDP-driven harness. Default suite covers
Blink (single-pin), Button (idle-OFF invariant — catches the
INPUT_PULLUP regression), Traffic-Light (multi-pin sequencing),
Fade-LED (PWM gradient — ≥3 distinct levels), RGB-LED (≥3 PWM pins
driven). Run via `npm --prefix frontend run test:visual` against a
Chrome on `:9222` + vite on `:5174` + backend on `:8001`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a new BoardOptionsModal accessible from the EditorToolbar that exposes
per-board options (currently used for board-specific compile flags). Wires
the modal through:
- types/boardOptions.ts new BoardOptions shape
- types/board.ts BoardInstance gains `boardOptions` + `spiffsFiles`
- store/useSimulatorStore.ts boardOptions persisted in loadProjectState
- components/editor/EditorToolbar.tsx button to open the modal
- components/simulator/BoardOptionsModal.{tsx,css} the modal itself
- components/simulator/SimulatorCanvas.tsx passes the options through
- utils/projectPayload.ts board options serialised in saved projects
- pages/ProjectByIdPage.tsx re-includes the by-id loader needed for
project URLs that reference boards with
their persisted options.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the deferred Phase 3.3. Root-causes the Pi 2 "Attempted to
kill init" panic as `mount /dev/vda` failing with EINVAL — Debian
armmp does not have ext4 builtin (only fuseblk in /proc/filesystems).
- qemu_manager: PI_CONFIGS gains raspberry-pi-zero / -1 / -2 entries.
All three use the armmp armhf kernel + Cortex-A7 CPU + the mmio
virtio transport (arm-32 virt PCI fails -75 due to missing reg DT
property). Pi Zero / Pi 1 get the small 1-core / 512 MB profile;
Pi 2 gets 4-core / 1 GB. QEMU command builder branches on cfg.bus
for virtio-blk-pci vs virtio-blk-device (and serial likewise).
- manifest.json: new `raspberry-pi-armhf` image_set wiring three
assets (kernel + initramfs + zstd rootfs).
- Frontend BoardKind gains the three new kinds + an isPiBoardKind()
helper. Replaces the eight scattered `=== 'raspberry-pi-3' ||
=== 'raspberry-pi-4' || === 'raspberry-pi-5'` branches in
useSimulatorStore, Interconnect, loadExample, boardProtocols.
ComponentRegistry gets three new picker entries.
- board-kinds-coverage test: ACCEPTED_UNCOVERED gains the new kinds
(backend boards have no canvas examples).
The matching armhf build-pi-kernel.sh / build-pi-rootfs.sh changes
live in velxio-prod's scripts/ (private overlay) — the upstream
kernel build script only knows about arm64; armhf is built in the
private repo because the assets ship through the license endpoint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend: extract per-board config into a PI_CONFIGS dict keyed by
board_type. Pi 3/4/5 share the same arm64 image set (kernel +
initramfs + rootfs) and differ only in QEMU -cpu and -m:
raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit)
raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit)
raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit)
PiInstance now carries board_type so the per-board lookup happens
once at start_instance time. Unknown board_type falls back to
DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for
back-compat with older clients).
Pre-warm hook walks every unique image_set in PI_CONFIGS so the
provider only downloads each set once even when several Pi models
are registered.
Frontend:
- BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'.
- BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards
(FQBN null since they use the Pi VFS + Python toolchain like Pi 3).
- ComponentRegistry inserts two new component metadata entries
cloning the Pi 3 board art with different thumbnail colours.
Tag name reused so the same velxio-raspberry-pi-3 web element
draws the board on the canvas — the 40-pin GPIO layout is
identical across Pi 3/4/5.
- boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table
(PI3_BCM) since the 40-pin header layout is identical.
- loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS
ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike.
- Interconnect.isPi3Bridge() recognises all three Pi family members
so Arduino↔Pi serial routing keeps working.
- RaspberryPi3Bridge constructor gained a boardKind parameter
defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message
now ships the actual board kind so the backend knows which
PI_CONFIGS entry to use.
- useSimulatorStore.addBoard wires bridge construction for all
three Pi family members.
Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel
package + armhf rootfs build, no change here.
Smoke-tested inside the prod container:
Pi 4 (cortex-a72) → reached agetty login on hvc0
Pi 5 (cortex-a76) → reached agetty login on hvc0
Both show 'aarch64' in uname -m.
The user reported the default editor canvas — Arduino Uno + LED +
220Ω resistor — was correctly powered (1.84 V at the LED anode,
14 mA through the diode) but the LED visual stayed dark. Only the
built-in pin-13 LED on the wokwi-arduino-uno element lit up.
Root cause: ngspice's WASM build truncates branch-current vector
keys at the first hyphen. A sense source named V_led-builtin_sense
ends up exposed under a key like v_led#branch rather than the
expected v_led-builtin_sense#branch. CircuitSimulationService and
BasicParts.ts both look up the FULL key, miss, and the LED's
brightness update treats raw as undefined → digital-fallback path
runs but the SPICE memo timestamp is fresh so HOLD keeps zero
brightness. Visible symptom: a perfectly conducting LED that never
lights.
Fix in two places:
- Default canvas (useSimulatorStore.ts): rename 'led-builtin' /
'r-builtin' to 'led_builtin' / 'r_builtin' (and the matching
wire ids).
- DynamicComponent.tsx makeNewComponent: the id template was
'metadata.id-timestamp-rand' producing hyphens for every
user-added component too. Switched to underscores, AND replace
any hyphens already in metadata.id (e.g. 'led-bar-graph') so
the prefix doesn't reintroduce the bug.
Existing saved projects whose ids contain hyphens are not migrated
here — those will keep the visual bug until either the operator
edits the components or we add a sanitisation step inside
componentToSpice + BasicParts. The next follow-up commit can add
that if you confirm this default-canvas fix works.
The default canvas (Arduino Uno + LED on pin 13) wired the LED
directly between pin 13 and GND. Two consequences:
1. Real-world: that's a short across a forward-biased diode,
blowing the LED in seconds.
2. Simulator: ngspice can't find a steady-state branch current
for an unprotected diode (returns NaN / indeterminate), so
the LED visual never lights up. Only the wokwi-arduino-uno
element's BUILT-IN LED (rendered internally by the element,
not via wire+pinManager) was visible.
Fix: insert a 220Ω resistor between pin 13 and the LED anode,
cathode straight to GND. Same circuit every introductory Arduino
book teaches. SPICE converges, LED blinks visually on the canvas.
Reported by a user trying Blink on a fresh /editor visit.
Replaces the per-peripheral ad-hoc `_ledc_gpio_map` cache with a
proper signal-routing abstraction that mirrors the ESP32 SoC's
IO_MUX + GPIO Matrix exactly. Same idea as real silicon: signal
sources (LEDC channels, RMT, MCPWM, ...) → 40-entry routing table
→ GPIO pins.
Motivation (from user bug report in
velxio.dev/project/5218f9e3-136d-43b3-bba1-6cebde21e1a4): two
ESP32 servos on a solar-tracker visibly oscillated between two
positions instead of moving smoothly when the user changed LDR
sliders. Commit 77bf897 patched it (per-channel gpio memo +
broadcast guard) but the user requested a proper hardware-fidel
architecture, not patches.
Backend:
* `app/services/signal_router.py` — SignalRouter class. Forward
index (gpio → signal_id) + reverse index (signal_id → set of
gpios). `replace_snapshot()` returns the diff for the polling-
fallback path; future C plugin hook becomes a push without
touching this code.
* `app/services/esp32_signals.py` — Signal id constants from
ESP32 TRM (LEDC HS 72-79, LS 80-87) + `ledc_signal_for_channel()`
helper.
* `app/services/esp32_worker.py` — `_ledc_gpio_map` is gone;
`_refresh_ledc_gpio_map` replaced by `_refresh_signal_routing`
which emits `gpio_routing {gpio, signal_id}` events on diff.
The 0x5000 LEDC callback and the LEDC poll thread now emit
`ledc_duty {channel, duty_pct}` (canonical, no gpio) alongside
the legacy `ledc_update {channel, duty, gpio}` for back-compat
during rollout.
Frontend:
* `simulation/SignalRouter.ts` — 1-to-1 TS mirror of the Python
class. Same forward + reverse index; same `pinsForSignal` /
`updateRouting` / `clearRouting` API.
* `simulation/esp32-signals.ts` — Signal id constants, mirror
of the Python module.
* `simulation/Esp32Bridge.ts` — new `onLedcDuty`, `onGpioRouting`,
`onGpioRoutingClear` callbacks; handlers for the new event types.
* `store/useSimulatorStore.ts` — `makeLedcDutyHandler` looks up
pins via `router.pinsForSignal(ledcSignalForChannel(channel))`
and dispatches per pin. `makeGpioRoutingHandler` /
`makeGpioRoutingClearHandler` keep the mirror in sync. Per-board
`signalRouterMap` parallels `pinManagerMap` in lifecycle.
`makeLedcUpdateHandler` (and its memo workaround from 77bf897)
stays wired for back-compat during rollout; removed in a
follow-up commit once prod is verified stable on the new path.
Tests:
* `test/backend/unit/test_signal_router.py` (20 tests) covers
update/clear semantics, idempotency, multi-pin routing,
snapshot diff, channel↔signal-id helpers, and the multi-servo
regression scenario.
* `frontend/src/__tests__/SignalRouter.test.ts` (17 tests) is the
mirror — same scenarios on the TS side.
* `frontend/src/__tests__/esp32-multi-servo-gpio-matrix.test.ts`
(6 tests) drives the end-to-end SignalRouter handler pipeline,
asserts that two servos on GPIO 13/12 via LEDC channels 0/1
move independently (no mirroring), that re-routing carries
cleanly, and — critically — that `PinManager.broadcastPwm` is
never called.
Totals: +700 LOC, 1876 frontend tests pass (was 1853), 278 backend
unit tests pass (was 259).
Docs: ESP32_EMULATION.md §9.2 rewritten with the new architecture
diagram + a runbook for adding future peripherals through the
SignalRouter.
The C plugin hook in qemu-lcgamboa that would push gpio_out_sel
writes synchronously (eliminating the polling race window entirely)
is the next step — kept as a follow-up because the polling-fallback
path here already resolves the routing before each duty event
fires, so the bug is fixed end-to-end. The plugin work removes the
race condition fundamentally.
User-reported bug (project 5218f9e3, solar-tracker with 2× ESP32
servos): when LDR values change the servos visibly oscillate between
two positions instead of moving smoothly.
Root cause in useSimulatorStore.makeLedcUpdateHandler. When the
backend emits a ledc_update with gpio=-1 (the per-channel gpio_out_sel
map isn't populated yet on the very first duty change after attach),
the handler called PinManager.broadcastPwm(duty). broadcastPwm fans
the same duty out to ALL registered PWM consumers — for a project
with two servos both subscribed in the 0.01-0.20 duty range, each
broadcast made BOTH servos mirror whichever channel was last
written. Result: servoPan→91° and servoTilt→87° alternating writes
would visibly snap both servos to 87°, then 91°, then 87°…
Two-part fix:
1. PinManager grows `pwmListenerPinCount()` — number of distinct
pins with at least one PWM consumer registered.
2. makeLedcUpdateHandler now keeps a per-board memo of
{ledc_channel → last-known-good-gpio}. On a gpio=-1 update:
- if the channel has a remembered gpio, route there;
- else, only broadcast when there's at most ONE consumer
(single-LED / single-servo setups still work);
- otherwise drop the update — the backend's GPIO out_sel poll
repopulates the map within a few ms and the next ledc_update
arrives with a real gpio.
The drop is correct because the same LEDC channel keeps emitting
duty changes every Servo.write() call (~33 Hz at 30 ms loop delay),
so missing one transient gpio=-1 frame is invisible.
Tests: 1853 pass. The existing esp32-servo-pot tests already cover
the gpio>=0 happy path; the new memo path is exercised indirectly
through that handler.
addBoard appended the new board to the boards[] array but never
touched activeBoardId. The default INITIAL_BOARD_ID points at a
board the picker injects on first load — but a fresh anonymous
session (or a project that landed in a state without that initial
board) can have activeBoardId pointing at nothing.
When the agent does add_board('arduino-uno') → compile_sketch, step
2 then fails with "no active board on the canvas" and the model
burns a turn on set_active_board.
The fix: at addBoard time, if activeBoardId doesn't resolve to any
existing board, promote the new board to active. If there IS a
valid active board, leave it alone — manual placements of additional
boards via the picker still keep focus on whatever the user was
working on.
Rotating a part with the 90° button used to leave every wire pinned to
the pre-rotation pixel coordinates — the component visually unhooked
from its cables. Two paths were missing:
1. useSimulatorStore.updateComponent only triggered updateWirePositions
for x/y changes. A rotation went through properties.rotation, so
wires never recomputed.
2. calculatePinPosition didn't know about rotation. Even when called,
it returned the unrotated offset, so the new endpoints would still
have been wrong.
3. recordRotate (undo/redo) skipped updateWirePositions on both legs,
so Ctrl+Z after a rotate left the canvas inconsistent.
Fix:
- calculatePinPosition gets a 5th `rotation` argument. When non-zero,
it finds the .dynamic-component-wrapper ancestor in the DOM, reads
its offsetWidth/Height (layout-only, immune to CSS transforms) to
locate the wrapper centre, and applies a 2D rotation matrix around
that pivot. The wrapper top-left is recovered as (componentX - 4,
componentY - 6) to match the offset convention updateWirePositions
already uses.
- updateWirePositions and recalculateAllWirePositions read the per-
component rotation and thread it through.
- updateComponent recomputes wires whenever properties.rotation
changes, mirroring the existing x/y path.
- recordRotate.execute and .undo both call updateWirePositions so
Ctrl+Z keeps the canvas coherent.
Tests (pin-position-rotation.test.ts, 6 cases): unrotated identity,
90° (left edge → bottom), 180° (point reflection), 360° round-trip,
negative angles, and a store-level integration that rotates a fake
component and asserts wires[0].start moves to the rotated coordinate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the remaining gaps in cross-board I2C so any topology of
supported boards (Uno↔ESP32, two ESP32s, Uno↔Uno↔Uno, ESP32-C3
connected to anything, etc.) works end-to-end with all I2C
components including write-only sinks (SSD1306, PCF8574, LCD-I2C).
Implementation (6 phases):
1. **BFS routing in I2CBusManager**: connectToSlave + handleExternalConnect
walk the bridge graph with a visited Set so multi-hop chains
(A↔B↔C with the device on C) resolve transparently. A new
forwarder-device shim is installed at intermediate hops so the
existing handleExternalWrite/Read/Stop machinery routes
through without per-method visited tracking.
2. **Per-peer proxy ownership in Esp32BridgeShim**: replaces the
global _proxiedAddrs Set with _proxiedByPeer Map so concurrent
bridges to the same ESP32 (e.g. wired to both Uno and Pico)
don't wipe each other's proxies on teardown. Interconnect's
per-wire teardown calls clearProxiesForPeer(peerBus) instead of
clearAllProxies.
3. **BFS-aware proxy sync**: syncProxyFromPeer now walks the peer
bus + its transitive bridges, so an ESP32 sees devices on
boards two or more hops away. _peerDeviceLookup keeps a flat
addr → device map for write-forwarding and resync.
4. **Periodic resync (250 ms)**: Esp32BridgeShim runs a setInterval
while any proxy is live, re-dumping each device with
dumpRegisters() and pushing updateProxyI2c only when an XOR-
stride hash changes. This keeps RTC time advancing visible to
ESP32 firmware without flooding the WS pipe with static
calibration dumps. Hash is primed during initial sync so the
first tick doesn't push a redundant identical buffer.
5. **Write-forwarding ProxySlave → peer**: backend ProxySlave
buffers write bytes during the transaction and emits a
`proxy_i2c_complete` event on STOP / repeated-START. Frontend
Esp32Bridge dispatches the event to a new onProxyI2cComplete
callback; the shim replays the byte sequence on the actual
peer I2CDevice via writeByte() + stop(). Makes ESP32 firmware
writes to peer SSD1306 actually repaint the OLED, peer PCF8574
latch updates, peer I2CMemoryDevice register mutations propagate.
6. **ESP32-C3 routed as bridge**: Interconnect.isBrowserSim no
longer claims c3/xiao-c3/c3-supermini — they were already
going through Esp32Bridge per the store's ESP32_RISCV_KINDS
routing, but Interconnect was treating them as browser sims
which broke proxy install. isEsp32Bridge now correctly
includes c3 family + ESP32-S3 + Arduino Nano ESP32.
Defensive: addBoard now disposes any existing shim's proxies
before overwriting simulatorMap entry so test reruns don't leak
timers.
Tests:
- 4 BFS multi-hop tests (i2c-multi-board-slave-gap.test.ts)
- 11 cross-board scenarios + per-peer + write-forward + resync
(i2c-esp32-multiboard-bridge.test.ts)
- 1 real-firmware E2E for write-forward via QEMU (compile +
load + observe proxy_i2c_complete arriving with the byte)
- New sketch fixture: esp32_i2c_write_to_peer.ino
Result: 90 test files / 1295 tests pass / 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Implemented `i2c-esp32-real-firmware.test.ts` to test ESP32 I2C communication via backend and WebSocket.
- Created `load-example-transitions.test.ts` to ensure proper loading of examples between board-less and board-based contexts.
- Added `CircuitVerificationModal.tsx` to display circuit verification results before running simulations.
- Developed `circuitVerifier.ts` to perform pre-flight checks for circuit safety, identifying potential issues like short circuits and component overloads.
- Introduced minimal ESP32 I2C master sketch `esp32_i2c_writer.ino` for testing I2C transactions.
loadMicroPythonProgram only forwarded main.py (or files[0]) to the
bridge for raw-paste injection. Any auxiliary module the project
imported (mylib.py, drivers, etc.) never reached the device, so
`import mylib` died with ModuleNotFoundError.
Build a Python prelude that writes every other .py file to the
MicroPython filesystem via raw REPL, then runs main.py in the same
paste. JSON.stringify produces an ASCII-safe Python-compatible string
literal for the file body, which keeps the prelude inside the existing
chunked-UART path Esp32Bridge already uses to feed the 128-byte FIFO.
The RP2040 path was already multi-file via sim.loadMicroPython(files),
so it stays untouched.
Reproduces with the project shared in the bug report:
https://velxio.dev/project/ac7e285c-8dc3-4d51-8751-b4aba9912f9e
Adds the foundation for canvas undo/redo. UI wiring, keyboard shortcuts,
toolbar buttons and agent-tools refactor land in follow-up commits.
useSimulatorStore.ts:
- New CanvasCommand type — { description, execute, undo }.
- HISTORY_MAX = 50 (oldest entries dropped on overflow).
- New state: history[] + historyIndex (-1 = empty).
- New APIs: pushCommand(cmd, {applyNow?}), undo, redo, canUndo, canRedo,
clearHistory.
- New "recorded" actions that wrap raw mutators with a CanvasCommand:
recordAddComponent, recordRemoveComponent, recordMove, recordRotate,
recordSetProperty, recordAddWire, recordRemoveWire, recordUpdateWire.
- recordRemoveComponent captures both the component AND any wires that
cascade with it, so undo restores both atomically.
- recordMove also re-runs updateWirePositions on undo/redo so wire
endpoints follow the component back/forward.
- setComponents and setWires (project-load / clear paths) now call
clearHistory inline — leaving stale commands pointing at IDs that no
longer exist would crash on undo.
Why custom Command pattern over zundo / travels:
- The store has 30+ ephemeral fields (simulator instances, serialOutput
growing byte-by-byte, hexEpoch counter, wireInProgress that ticks 60×/s
on drag). Snapshot/diff middleware would either burn memory tracking
them or need a fragile partialize allow-list.
- Per-op descriptions ("Add LED", "Move resistor") for tooltips come for
free with this approach; zundo/travels would need to infer them.
Tests: 15/15 in src/__tests__/undo-redo.test.ts — covers cap-at-50,
redo-truncation, cascade undo of remove-component, move/rotate/property
round trips, bulk-setter clearing.
Raw mutators (addComponent / removeComponent / updateComponent / addWire /
removeWire / updateWire) are unchanged. UI handlers can keep using them
during live drags for preview frames without spamming history; the
record* actions are what drag-end, click-finish and agent tools should
call going forward.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous fix added an ESP32-specific code path inside ili9341Simulation
to subscribe to the QEMU worker's spi_event stream. That made the LCD
work on ESP32-CAM but left the underlying issue unsolved: every other
SPI part (custom chips, future SD-card emulators, the SSD168x ePaper
already in the codebase) would also need its own per-board branching.
The right shape: every simulator exposes a `.spi` member matching the
SAME SpiBusLike interface, and SPI parts hook .spi.onByte without
caring which board they're attached to. AVRSimulator already had
this — now everything else does too.
frontend/src/simulation/SpiBus.ts (new)
Defines the contract — `onByte: (mosi) => void | null` plus
optional `completeTransfer(miso)`. Documents the single-listener
semantics that AVR has had since day one.
frontend/src/store/useSimulatorStore.ts
Esp32BridgeShim gets a lazy `.spi` getter that wraps
bridge.onSpiByte (the per-byte WS event from the QEMU worker).
completeTransfer is a no-op because the worker drives MISO via
its own _spi_response global. Covers ESP32 (Xtensa), ESP32-S3,
ESP32-CAM, ESP32-C3 — every kind that routes through Esp32Bridge.
frontend/src/simulation/RP2040Simulator.ts
Adds a lazy `.spi` getter that re-routes rp2040.spi[0].onTransmit
through the adapter. Default loopback (the prior behaviour) is
preserved when no part has accessed `.spi` yet — only consumers
that opt in see their handler invoked. Covers Pico and Pico W.
frontend/src/simulation/parts/ComplexParts.ts
ili9341Simulation no longer has an ESP32 special case. Single
code path: `simulator.spi.onByte = handler`. Works on AVR,
RP2040, all ESP32 variants. Same pattern is now available to
every future SPI part — ssd1306, sd-card, oled, etc.
The Esp32Bridge.ts spi_event field-name fix from 6afa62e (msg.data.event
instead of the non-existent msg.data.data) stays in place — that's what
makes the per-byte stream actually arrive in the bridge.
Verified: ILI9341 + ESP32-CAM gallery example renders the live webcam
preview after a hard refresh. The same simulation code works on Arduino
Uno + ILI9341 (the existing ili9341-test-sketch in example_zip).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First open-source end-to-end emulation of the AI-Thinker ESP32-CAM
in QEMU, paired with a browser webcam → firmware bridge so users can
develop camera sketches without hardware. Status: esp_camera_init()
returns ESP_OK; OV2640 chip-id verifies (PID/VER/MIDH/MIDL exactly
match the datasheet); GPIO 25 VSYNC NEGEDGE interrupt enabled by
the upstream driver. Final piece (cam_task accepting frames) is in
progress — descriptor walker fix landed in this commit.
Backend (Python/FastAPI):
- simulation.py: camera_attach/frame/detach WS handlers
- esp32_worker.py: ctypes binding to velxio_push_camera_frame +
feature-detection fallback for older DLLs
- esp32_lib_manager.py: forward camera commands to the worker stdin
- esp-idf-template/main/CMakeLists.txt: esp32-camera headers added
via add_prebuilt_library + REQUIRES driver (resolves i2c_master_*
symbols). LED_BUILTIN=2 fallback for sketches that hardcode it.
Frontend (React/TS):
- EditorToolbar.tsx: ESP32-CAM (and the rest of the ESP32 family)
added to isQemuBoard list — Run button now starts the QEMU bridge
for these boards instead of falling through to the AVR path
- useWebcamFrames.ts: getUserMedia → OffscreenCanvas →
toBlob('image/jpeg') → base64 → WS at ~10 fps
- CameraToggle.tsx: header button with status colors + frame counter
- SimulatorCanvas.tsx: render CameraToggle for esp32-cam boards
- Esp32Bridge.ts: sendCameraAttach/Frame/Detach + chunked btoa
- useSimulatorStore.ts: diagnostic log on compileBoardProgram
- components-metadata.json: regen including esp32-cam component
Submodule pointer:
- wokwi-libs/qemu-lcgamboa → ff8eee0 (camera devices commit on
davidmonterocrespo24/qemu-lcgamboa branch picsimlab-esp32)
Investigation + tests in test/test-esp32-cam/:
- 13 autosearch markdown docs (overview, SOTA, OV2640 spec, DVP/I2S
spec, build blueprint, blockers resolved, descriptor walker fix)
- 5 sketches (camera_init, sccb_probe, dma_smoke, frame_roundtrip,
webcam_demo) + 8 live + WS regression tests
- README with the user-facing flow
.gitignore:
- libqemu-*.dll.{pre-camera,new,bak} (rollback points, regenerated)
- wokwi-libs/esp32-camera/ (clone consumed by arduino-esp32 path,
not part of this repo)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The project save/load pipeline only persisted a single `board_type`, so
multi-board workspaces silently lost every board except the active one
on save, and wires referencing the dropped boards' IDs orphaned to the
canvas corner on reload. An audit of the production backup found 74/306
projects (24%) with at least one orphaned wire and 174/301 non-trivial
projects whose code was still the default Blink template — strong signal
that users save once and never re-save.
Backend
- Add `boards_json` column on `projects` with idempotent ALTER TABLE in
the lifespan migration list.
- New `FileGroup` schema + `file_groups` array on
ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat.
- `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via
`read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on
read; legacy single-list `files` only updates the active group, leaving
other boards' files intact.
- `_persist_files_from_body` honors file_groups → files → code priority.
Frontend
- `useSimulatorStore.addBoard` accepts an optional `explicitId` so
saved board IDs can be restored verbatim (wires reference IDs literally).
- New `loadProjectState({boards, fileGroups, components, wires,
activeBoardId})` action: tears down current boards, recreates from the
payload, restores file groups atomically, recalculates wire positions
on the next frame, and refreshes the Interconnect.
- `useEditorStore.replaceFileGroups` for atomic multi-group restore.
- `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through
`buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects
by synthesising a default board from `board_type`).
Auto-save (#useAutoSaveProject hook)
- 2.5s debounced silent PUT triggered ONLY when an authenticated user
has a `currentProject` with a UUID. State hash detects real changes
vs. UI-only churn; baseline is reset on project load so the just-loaded
state isn't immediately re-saved.
- `beforeunload` flush via `fetch keepalive: true` (supports PUT +
credentials, survives unload).
- Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error).
Backfill script (one-off, idempotent)
- `backend/scripts/backfill_boards_2026_05.py` populates `boards_json`
for legacy projects. Heuristic per project, based on which board IDs
the wires reference:
Case A — wires only ref 'arduino-uno' but board_type ≠ uno:
rename id→board_type and rewrite wire endpoints.
Case B — single-board normal: keep verbatim.
Case C — multi-board: recreate one board per distinct ref, infer
kind by stripping trailing -N suffix.
Also moves any flat files into the active board's group subdir.
Stdlib-only, runs from host or `docker exec`.
Docker
- `Dockerfile.standalone` now copies `backend/scripts/` into the image
so the backfill is callable via `docker exec velxio-app python
/app/scripts/backfill_boards_2026_05.py --apply`.
Verified locally on the restored production backup (363 projects):
33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans.
Re-running the script after apply skips all 363 (idempotent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Introduced EPaperPanels.ts to define configurations for various ePaper panels including dimensions, refresh rates, and controller details.
- Implemented SSD168xDecoder.ts to handle the decoding of SPI commands for the SSD168x family of ePaper displays.
- Created EPaperPart.ts to manage the simulation of ePaper panels, integrating with the existing simulator architecture and handling events.
- Added example sketches for 2.13", 2.9", 4.2", and 7.5" ePaper displays, demonstrating basic functionality and text rendering.
- Ensured compatibility with AVR, RP2040, and ESP32 platforms, with appropriate pin configurations for each.
- Introduced SVG layout dimensions for Phase 1 (B/W mono) and Phase 2 (colour) ePaper panels, detailing active areas, bezels, and pin layouts.
- Developed a phased emulation plan outlining the architecture and deliverables for different panel types, including SSD168x and UC81xx.
- Created a canonical "Hello, World!" sketch for the 1.54" ePaper panel, ensuring compatibility across ESP32, Raspberry Pi Pico, and Arduino Uno.
- Implemented a pure Python SSD168x decoder to validate SPI command sets and framebuffers against specifications.
- Added tests for compiling the hello-world sketch across supported boards and for the SSD168x protocol to ensure correct framebuffer behavior.
Fixes the user-reported bug where two RPi Pico W boards wired GP0↔GP1
running SerialPassthrough don't communicate. Replaces the broken
broadcast-style cross-board logic in addBoard (only routed AVR↔Pi3B,
ignored wires entirely, no RP2040↔anything path) with a wire-aware
Interconnect singleton.
Architecture: digital pin transitions are the lowest-common-denominator
abstraction. Each simulator's hardware peripherals (UART/I2C/SPI) and
bit-banging libraries (SoftwareSerial, software I2C) decode the
transitions naturally — propagate the pin and the protocols come for
free. For cross-process boards (ESP32 backend QEMU, Pi3B QEMU) a
byte-level shortcut is additionally enabled on hardware-UART pin
pairs to handle high-baud links over WebSocket latency.
Implementation:
- New simulation/Interconnect.ts singleton subscribes to wire/board
changes via the Zustand store. Handlers per tier: browser-sim →
pinManager.onPinChange, ESP32 → Esp32Bridge.sendPinEvent, Pi3B →
bridge.sendPinEvent. Re-entrancy guard via per-(board,pin) Set.
- New utils/boardProtocols.ts classifies pins (uart-tx, i2c-sda, etc.)
per board kind, used as optimization hint for the byte shortcut.
- types/wire.ts: added signalType field, exports WireSignalType /
WireColorMap (fixes a pre-existing TS import error in wireColors).
- Deleted the bridgeMap/simulatorMap broadcast forEach blocks in
addBoard. Initial board + future boards register with Interconnect
via setInterconnectRuntime + store subscription.
- PinManager.resetPinStates() helper for test isolation.
Tests (16 new files, 96 tests, all passing):
- Per-pair × per-protocol matrix: dual-arduino-digital,
dual-pico-digital, arduino-pico-digital, triple-pico-digital-chain,
dual-arduino-hw-uart, dual-arduino-software-serial,
arduino-pico-mixed-uart, arduino-esp32-uart, dual-esp32-uart,
pi3-pico-uart, arduino-pico-i2c, arduino-arduino-spi,
interconnect-routing, dual-arduino-multi-protocol (UART+I2C+SPI+
digital + concurrent), dual-pico-multi-protocol (UART0+UART1 alt+
I2C0+I2C1+SPI0+digital + 3-Pico star topology)
- Updated dual-pico-serial-passthrough to assert correct behaviour
- Backend test/multi_board_esp32/test_dual_esp32_serial.py for two
real QEMU instances (skip-graceful when lcgamboa lib absent)
Verified: 1107/1107 tests pass, zero regressions, vite build OK.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Modified the index file to reflect the new naming convention for Velxio components.
- Changed JSX declarations to use 'velxio-' prefix for various components.
- Updated component overrides to replace 'wokwi-' with 'velxio-' for logic gates and other components.
- Adjusted SVG generation script to use 'velxio-' prefix for BMP280 and Raspberry Pi components.
- Marked submodules as dirty in QEMU and RP2040 libraries.
- Added .prettierignore and .prettierrc.json for consistent code formatting.
- Introduced InstrumentComponent with support for Voltmeter and Ammeter, including pin information handling.
- Implement `ammeter-waveform.test.ts` to validate AC readings from a sine wave source.
- Create `capacitor-charge-transient.test.ts` to test the charging response of an RC circuit driven by a microcontroller pin.
- Introduce `esp32-rectifier-integration.test.ts` for testing rectifier behavior using QEMU and ESP32.
- Add helper functions in `esp32RectifierE2E.ts` for the rectifier test harness.
- Develop `voltmeter-waveform.test.ts` to ensure correct AC and DC readings from a sine wave source.
- Implement unit tests for waveform statistics in `waveform-stats.test.ts` to validate RMS, mean, peak, and interpolation functions.
- Create `waveformStats.ts` to provide statistical functions for time-domain waveform analysis.
- Implement `serial-batching.test.ts` to verify the behavior of `createSerialBatcher`, ensuring it coalesces multiple appends into a single flush, preserves byte order, and groups by board.
- Create `spice-rectifier-integration.test.ts` to test the end-to-end functionality of the Half-Wave Rectifier example, covering the entire simulation pipeline from input building to circuit solving.
- Add `spice-rectifier-live-repro.test.ts` to reproduce a live-app failure scenario, tracing through each layer of the simulation to identify potential failure points.
- Introduce `spice-signal-generator-tran.test.ts` to validate the behavior of the signal generator and ensure correct analysis type switching based on circuit components.
- Establish `serialBatcher.ts` to implement a batching mechanism for USART output, reducing the frequency of store updates and preventing React's maximum update depth error.
- Introduced I2C_SENSOR_MAP for pre-registering I2C sensors in the simulator store.
- Implemented I2C slave state machines for MPU6050, BMP280, DS1307, and DS3231 sensors in esp32_i2c_slaves.py.
- Added unit tests for I2C slave functionality covering BMP280, DS1307, DS3231, and I2CWriteSink.
- Updated the simulator store to handle I2C address resolution and sensor data management.
- Marked submodules as dirty in wokwi-libs for rp2040js and wokwi-elements.
- Implemented _MPU6050Slave and _BMP280Slave classes for I2C communication.
- Enhanced main function to register these sensors and handle I2C events.
- Updated sensor management to support MPU-6050, BMP280, DS1307, DS3231, SSD1306, and PCF8574.
- Added frontend examples for BMP280 weather station and SSD1306 OLED display.
- Modified Esp32Bridge to handle new I2C transaction events.
- Updated ProtocolParts to support ESP32 path for I2C devices.
- Enhanced useSimulatorStore to manage I2C transaction listeners.
Frontend WiFi detection via file-content scanning was unreliable because
fileGroups[board.activeFileGroupId] could be an empty array (not null),
bypassing the ?? fallback to editorState.files.
Fix: the ESP-IDF compiler now returns has_wifi:bool in its compile response.
The frontend stores this on the BoardInstance and uses it in startBoard()
instead of scanning file contents. The file-content scan is kept as a
fallback for boards that haven't been compiled in this session.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
startBoard() was reading useEditorStore.getState().files (the legacy global
array with the default Arduino sketch) instead of the board's specific file
group. This caused hasWifi to always be false for ESP32 boards, so QEMU
never received the -nic flag and WiFi never connected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- handleRun now auto-compiles for ESP32/QEMU boards when no firmware is
available (same behavior as AVR/RP2040 boards)
- startBoard now reloads compiledProgram into the bridge if _pendingFirmware
was lost (e.g. after a page refresh between compile and run)
- Adds Esp32Bridge.hasFirmware() helper used by the store check
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>