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: Arduino Nano connected to an Uno-TX wire received bytes
but displayed them poorly, and pressing Stop then Run "killed" the
serial link until the page reloaded.
Two paired bugs in the cross-board serial path:
(1) drainSerialRxQueue was only ever re-fired from usart.onRxComplete,
which itself only fires AFTER a successful delivery. If the very
first delivery attempt fails (rxEnable=false because the sketch
hasn't reached Serial.begin yet — extremely common when one board
starts emitting bytes before the receiving board's setup() runs)
nothing re-kicks the queue and every subsequent byte from the
sibling board sits in serialRxQueue indefinitely. Adding a
per-frame drain attempt (no-op when queue is empty or rxBusyValue
is set, so cost is negligible) makes the link self-heal across
cold-start races and Serial.end()/begin() toggles.
(2) stop() never cleared serialRxQueue. On Run after Stop the new
USART would re-drain the previous run's leftovers into the fresh
sketch before its setup() ran, corrupting the first bytes the
user saw on the receiving side. Clearing the queue in stop() —
same place we already clear scheduledPinChanges — keeps each Run
a clean slate.
Verified 52 existing tests still pass (dual-pico-serial-passthrough,
dual-arduino-software-serial, interconnect-routing, avr-uart-tx
-waveform, serial-batching, AVRSimulator, pin-position-rotation).
avr8js's usart.writeByte(value) rejects the call (returns false, drops
the byte) whenever rxBusyValue is set — and rxBusyValue stays true for
one full cyclesPerChar after each accepted call. The old serialWrite()
fed every character in a synchronous for-loop, so only the first byte
made it through and the sketch saw 'h' when the user typed 'hello\n'.
Buffer pending bytes in serialRxQueue and pump them one at a time:
- serialWrite() now just queues + kicks drainSerialRxQueue once
- drainSerialRxQueue calls writeByte on the head of the queue and only
shifts it off if writeByte returned true (avr8js accepted it)
- usart.onRxComplete is wired to drainSerialRxQueue so the next byte
ships as soon as the sketch's RX side actually consumed the previous
one — matches the cyclesPerChar pacing the real chip enforces
Same handler wired in both USART setup paths (the Uno/Nano branch and
the post-loadHex Mega/ATtiny branch). TX path (onByteTransmit +
emitUartTxFrame for the oscilloscope waveform) is unchanged.
avr8js intercepts the transmitted byte at the UDR0 register and never
toggles the corresponding GPIO. Real ATmega328P / ATmega2560 hardware
drives PD1 / PE1 with a start bit, 8 data bits LSB-first, and a stop bit
at the configured baud rate the moment TXEN is set. An oscilloscope
probe on D1 therefore showed nothing in Velxio while the same probe in
the real world would resolve the UART frame.
Synthesize the frame from the inside of `onByteTransmit`:
* Read `usart.baudRate`, `usart.bitsPerChar`, `usart.parityEnabled`,
`usart.parityOdd`, `usart.stopBits` so unusual configurations stay
accurate (avr8js already exposes these as public getters).
* Build the bit list start + data(LSB first) + parity? + stopBit(s).
* For each transition vs. previous state (initial = idle HIGH), call
`onPinChangeWithTime(1, state, timeMs)` where
`timeMs = (cpu.cycles + i * cyclesPerBit) / 16_000`. Same
simulator-time clock the existing port-listener path uses, so the
scope draws the UART waveform cycle-accurately alongside other GPIO
activity.
Also hook `onConfigurationChange` to detect TXEN flipping 0→1 and seed
the scope baseline at idle HIGH; without that, the very first byte's
start bit transition would be invisible because the scope's pre-first-
sample default is LOW.
Both USART construction sites (initial setupSimulation around line 423,
re-init after stop around line 749) get the same hook.
Covered by `__tests__/avr-uart-tx-waveform.test.ts` (5 cases): idle seed,
byte with internal transitions, 0xFF edge case, TXEN-disabled no-op,
bit-period timing.
ATtiny85 (AVRSimulator + collectPinStates + connectAnalogInputsToMcu + SimulatorCanvas + Attiny85Element + examples):
- Add attiny85AdcConfig with correct register addresses (ADMUX=0x27,
ADCSRA=0x26, ADCSRB=0x23, ADCL=0x24, ADCH=0x25, DIDR0=0x34, adcInterrupt=0x08).
Without this, analogRead() polled the wrong address forever and the
firmware hung on first ADC read.
- Add attiny85Timer0Config + instantiate AVRTimer so OVF fires at the
ATTinyCore-expected ~1.024 ms cadence. delay() advance is still blocked
on avr8js TIFR auto-clear semantics (separate upstream issue, see
ATTINY85_TIMER0_UPSTREAM_ISSUE.md in velxio-prod test plan).
- Map ATtiny85 ADC channels to PB-style pin names (PB5/PB2/PB4/PB3 -> 0..3)
in connectAnalogInputsToMcu so SPICE node voltages reach the right ADC
channel.
- Recognise /^PB\d+$/ in collectPinStates.pinNameToArduinoPin so wires
named "PB1" emit v_attiny85_pb1 V-source and the LED responds to MCU
writes. Previously every PB-wire returned -1 and SPICE saw no source.
- SimulatorCanvas: subscribe pin 1 (PB1) for the built-in LED on the
attiny85 board kind (Digispark convention), instead of falling through
to the pin-13 default.
- Attiny85Element: remove the hand-drawn "yellow LED" circle that was
floating above the chip. The bare DIP-8 has no on-board LED; examples
wire a real wokwi-led + resistor instead.
- examples.ts: add a real wokwi-led + 220 Ohm wokwi-resistor + wires to
attiny85-blink, and add missing series resistors to attiny85-button-led
and attiny85-ntc-sensor. attiny85-pwm-fade was already correct.
Custom-chip pipeline (CustomChipPart + simulatorBridges):
- Add a requestAnimationFrame loop that calls instance.tickTimers() every
frame in CustomChipPart. Chips that register vx_timer_create (e.g. an
i8080 stepping its core, or a sensor publishing samples) had timers
added to the queue but nothing fired them; tickTimers was dead code.
- Gate the ESP32 backend path with detectSimulatorKind(sim)==='esp32'.
The previous `typeof sim.registerSensor === 'function'` check matched
AVR and RP2040 simulators too (they expose registerSensor for I2C
sensor proxies), routing client-side chips to a non-existent ESP32
worker on those boards.
- Replace direct simulator.usart.writeByte calls in avrUartTx with a
JS-level FIFO + setTimeout(1ms) drainer. avr8js writeByte drops bytes
under burst load (a chip emitting print_string lost ~99% of bytes via
non-immediate, or kept only the last byte via immediate). The drainer
attempts one non-immediate write per tick and retries on RXC busy /
RXEN off. Added a guard for ATtiny85 (no USART -> would queue forever).
End-to-end verified: i8080-banner-streamer now prints the boot banner
followed by "uptime ticks: 0xNN" lines stepping every ~50 ms, executing
real Intel 8080 instructions inside the WASM chip.
Co-Authored-By: Claude Opus 4.7 <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>
deploy.sh's vitest + pytest output was polluted with three benign
but loud warnings that buried real signal:
1. AVRSimulator.start() unconditionally read `window.__spiceDebug`.
In node-side vitest runs `window` is undefined → ReferenceError
→ console.warn('[spice] debug dump failed', e). Logged once per
AVR test. Guarded with `typeof window !== 'undefined'`; in
production the browser path is unchanged.
2. pinPositionCalculator.calculatePinPosition() warned every time
document.getElementById returned null. In node-side tests there
is no real DOM and every wire-related test triggers the warning
for every component. Skip the console.warn when
import.meta.env.MODE === 'test' (vitest sets MODE=test); the
function still returns null and production retains the
actionable warning for unmounted components.
3. test_esp32_wifi_args.py::test_start_instance_accepts_wifi_params
mocked asyncio.create_task with no side_effect, so the coroutine
from self._boot(...) leaked and triggered a "coroutine never
awaited" RuntimeWarning. Mock now closes the coroutine.
After fixes:
frontend tests: 0 spice/pinPositionCalculator stderr lines
backend tests: 259 passed, 15 skipped, 1 warning (starlette
third-party python_multipart deprecation —
not ours, fixed when starlette updates).
The mixed-mode migration's endgame. After this commit there is ONE
SPICE solver path in the codebase — the vendored ngspice WASM via
SolverPort, behind both NgSpiceWorkerAdapter (production browser) and
NgSpiceNodeAdapter (Vitest Node). Zero hybrids; zero legacy left to
maintain.
Deleted production files:
• simulation/spice/CircuitScheduler.ts (200ms-poll legacy)
• simulation/spice/SpiceEngine.ts (eecircuit-engine wrap)
• simulation/spice/SpiceEngine.lazy.ts (lazy code-split)
• simulation/spice/subscribeToStore.ts (legacy solve loop)
• simulation/spice/connectLegacySolverToMixedMode.ts (bridge)
• simulation/spice/connectMixedModeSchedulerToStore.ts (feature flag)
Deleted tests (no longer cover any live code):
• connect-legacy-solver-to-mixed-mode.test.ts
• connect-mixed-mode-scheduler-to-store.test.ts
• spice-rectifier-live-bootstrap.test.ts
Migrated 6 tests off the deleted `circuitScheduler.solveNow` API to
the new `__tests__/helpers/solveInput.ts` (same shape, backed by
NgSpiceNodeAdapter).
`useElectricalStore` rewritten as a pure state container:
• setSolveResult(snapshot) — atomic publish from the service
• paused / setPaused — UI control unchanged
• reset — project unload
• REMOVED: triggerSolve, solveNow, setDebounceMs, scheduler hook
• REMOVED: dependency on SpiceEngine.lazy preload
EditorPage now mounts a single `startSimulation()` from
`simulation/spice/start.ts`, which constructs
CircuitSimulationService + ADC bridge + MCU edge bridge. Four
useEffect calls collapsed to one.
`circuitVerifier.ts` (production) and `runNetlist.ts` use an
environment-aware factory: Web Worker in browser, in-proc WASM in
Node tests. `/* @vite-ignore */` keeps the Node adapter chain
(node:fs, node:url) out of the browser bundle while still letting
Node resolve it dynamically.
Removed `eecircuit-engine` from package.json dependencies.
`collectPinStates` extracted to its own module so the service doesn't
depend on the (now deleted) subscribeToStore.ts.
Verification:
• 1392/1392 tests pass across 103 files (28 pre-existing skips).
• `tsc --noEmit` clean.
• `vite build` succeeds (27 s, only the existing chunk-size
warning that pre-dates this work).
Phase 1c — COMPLETE.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Implement HD44780Decoder for decoding I2C commands to HD44780-compatible LCDs.
- Add bmp280_bridge_reader.ino to read BMP280 chip_id and status registers via I2C.
- Create i2c_scanner_multi.ino to scan I2C addresses and report responding devices.
- Introduce lcd_i2c_hello.ino to demonstrate basic LCD functionality with I2C.
- Implement pcf8574_bidirectional.ino to test bidirectional communication with PCF8574.
- Add pico_i2c_master_reader.ino for reading BMP280 from a Raspberry Pi Pico.
- Create rtc_lcd_clock.ino to display time from a DS1307 RTC on an I2C LCD.
- 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 `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.
- Added board-agnostic sensor registration methods in RP2040Simulator.
- Enhanced ComplexParts to handle LEDC PWM duty updates for ESP32.
- Updated ProtocolParts to check if the simulator handles sensor protocols natively, delegating to backend if applicable.
- Introduced pre-registration of sensors in useSimulatorStore for ESP32 to prevent race conditions.
- Added tests for ESP32 DHT22 sensor registration flow, ensuring proper delegation and fallback mechanisms.
- Created tests for ESP32 Servo and Potentiometer interactions, verifying PWM subscriptions and ADC handling.
- Implemented ATtiny85 visual component with DIP-8 layout and built-in LED.
- Added Raspberry Pi Pico W web component using official SVG and pin mapping.
- Created RISC-V Board visual component with SOP-20 style and built-in LED.
- Introduced RiscVCore class for minimal RV32I ISA interpreter with memory model.
- Developed RiscVSimulator class for CH32V003-compatible simulator with UART and GPIO support.
- Updated subproject commits for qemu-lcgamboa, rp2040js, and wokwi-elements to dirty state.
- Add useOscilloscopeStore with ring-buffer sample storage and channel management
- Add onPinChangeWithTime callback to AVRSimulator (fires on every bit transition with cycle-derived timestamp)
- Add onPinChangeWithTime callback to RP2040Simulator (fires on GPIO state change)
- Wire oscilloscope callbacks in useSimulatorStore (initSimulator + setBoardType)
- Create Oscilloscope React component with canvas-based waveform rendering
- Add oscilloscope panel to EditorPage (resizable bottom panel, same as SerialMonitor)
- Add 'Scope' toggle button to SimulatorCanvas toolbar
Co-authored-by: davidmonterocrespo24 <47928504+davidmonterocrespo24@users.noreply.github.com>
- Introduced ArduinoMega component for rendering in the simulator.
- Updated SimulatorCanvas to handle Arduino Mega board type.
- Enhanced AVRSimulator to support ATmega2560 architecture, including PWM pin mapping and port management.
- Modified PinManager to accommodate Mega's non-linear pin mapping.
- Updated boardPinMapping utility to include Mega analog pins.
- Adjusted Wokwi import/export functionality to recognize and handle Arduino Mega.
- Updated useSimulatorStore to initialize AVRSimulator with the correct board variant.
- Implemented SerialMonitor component to display serial output and allow user input.
- Enhanced AVRSimulator to handle USART communication and transmit serial data.
- Updated useSimulatorStore to manage serial output state and toggle visibility of the Serial Monitor.
- Added example Arduino sketches for serial communication, including Serial Echo and Serial LED Control.
- Introduced I2CBusManager to manage virtual I2C devices and integrated with AVRSimulator.
- Removed outdated WOKWI_LIBS.md and replaced with updated documentation.
- Added ARCHITECTURE.md to describe project structure and data flow.
- Created SETUP_COMPLETE.md for installation and configuration instructions.
- Implemented automatic update script for Wokwi libraries.
- Updated frontend components to utilize local Wokwi libraries.
- Enhanced AVRSimulator to manage peripherals more efficiently.
- Added example screenshot generation instructions for better documentation.
- Updated components metadata and ensured proper integration with Vite.
- Added PWM duty cycle tracking and callback registration to PinManager.
- Introduced methods for handling analog voltage injection and callbacks.
- Updated updatePort method to notify digital pin listeners.
- Improved listener management with clearAllListeners method.
feat: Expand BasicParts with new components
- Registered new components: 6mm Pushbutton, Slide Switch, DIP Switch 8, LED Bar Graph, and 7-Segment Display.
- Implemented event handling for each component to interact with the AVR simulator.
feat: Introduce ComplexParts with advanced components
- Added RGB LED with PWM support for color mixing.
- Implemented Potentiometer and Slide Potentiometer for analog input.
- Created Photoresistor Sensor to simulate light levels.
- Developed Analog Joystick for two-axis control and button press.
- Added Servo motor simulation with pulse width modulation.
- Implemented Buzzer using Web Audio API for sound generation.
- Created LCD 1602 and 2004 simulations with command/data processing.
- Updated components-metadata.json with new generation timestamp.
- Added event handling for button presses and releases in DynamicComponent.
- Improved ExamplesGallery with new styles for placeholders and previews.
- Introduced LCD 20x4 display example with corresponding code and wiring.
- Enhanced SimulatorCanvas to subscribe components to pin changes.
- Implemented PartSimulationRegistry for managing component simulation logic.
- Added basic and complex parts simulation including pushbuttons, LEDs, and LCDs.
- Created utility functions for capturing canvas previews and generating SVG previews for example projects.
- Implemented a comprehensive backend test suite in `test_compilation.py` to validate the Arduino CLI installation, AVR core presence, compilation service, and API endpoint functionality.
- Created a frontend test suite in `simulation.test.ts` to test the `PinManager` and `AVRSimulator` components, ensuring proper functionality and integration.
- Introduced new components for wire management in the simulator, including `PinOverlay`, `WireInProgressRenderer`, `WireLayer`, and `WireRenderer`, enhancing the visual wiring system.
- Developed utility functions for pin position calculations and wire color management, ensuring accurate connections and visual representation.
- Added types for wire management in `wire.ts`, defining structures for wire endpoints, control points, and signal types.
- Added SimulatorCanvas component for rendering the simulator interface.
- Integrated Wokwi components (Arduino, LED, Resistor, Pushbutton, Potentiometer) into the simulator.
- Created PinManager to handle pin state changes and notifications.
- Developed AVRSimulator class for emulating Arduino Uno functionality.
- Implemented hex file loading and compilation service.
- Added CSS styles for the simulator interface.
- Established Zustand stores for managing editor and simulator states.
- Created utility functions for parsing Intel HEX format.
- Set up Vite configuration for the frontend project.
- Added batch scripts for starting backend and frontend servers, and updating Wokwi libraries.