The spiceDrivenInputs change (e81450e + f4401cc) fixed plain-INPUT-wired-to-rail
reads but BROKE the far more common INPUT_PULLUP + button-to-GND pattern: the
internal pull-up is not modeled in the netlist, so the input floated LOW and read
as permanently pressed (verified live on the stm32-bluepill-button example).
Revert all the spice-driven-input changes to the pre-fix part-seed behaviour,
which handles INPUT_PULLUP correctly. Proper fix (model the internal pull-up per
board so BOTH patterns work) is a follow-up. Keeps the Pi LED fix.
An Arduino input wired to a power rail read the wrong level: a pin tied to 5V
read LOW, and a button-to-5V read idle-HIGH / pressed-LOW. AVR inputs were never
fed the solved circuit voltage (only the ESP32 had spiceDrivenInputs), so a
bare-rail input had no driver and buttons fell back to a hardcoded active-low
pull-up seed that ignored the wiring.
Enable spiceDrivenInputs on AVRSimulator, and gate connectDigitalInputsToMcu on
a new NetlistBuilder sourcedNets set (rails, GPIO V-sources, pulls, and any net
a component card touches). Only source-backed input pins are driven from the
solve; floating nets are left to the part layer, so event-driven parts with no
SPICE model (rotary encoder, keypad, dialer, dip-switch, stepper) keep driving
their own pins instead of being forced LOW.
ESP32 digitalRead now reflects the actual circuit instead of a part-level
seed, so a button behaves like hardware — including breaking when it's
mis-wired.
- connectDigitalInputsToMcu: after each SPICE solve, threshold every ESP32
input pin's net voltage (3.3 V LVCMOS, hysteresis) and push the level into
QEMU. Only pins the MCU isn't driving as outputs are injected.
- Esp32BridgeShim advertises spiceDrivenInputs; the pushbutton / 6mm-button /
slide-switch parts skip their direct setPinState seed for such boards and
only flip the component property (pressed/value), which re-solves the
circuit. The connector then decides the level from the real wiring.
- makePinPullHandler no longer seeds the pin; it only records the pull
(netlist resistor) + requests a re-solve, so the read stays circuit-driven.
- GROUND_PIN_RE now matches bare numbered grounds (GND2, GND3) — the ESP32
DevKit element labels its second pad 'GND2', which previously floated.
Net effect: a correctly-wired INPUT_PULLUP button idles HIGH and reads LOW
pressed; a button mis-wired with GND on the wrong terminal reads stuck-LOW,
matching real silicon. AVR / RP2040 keep the legacy part-seed path.
INPUT_PULLUP / INPUT_PULLDOWN had no effect in simulation: the ESP32's
internal pull resistors live inside QEMU and were invisible to the SPICE
solver, so an input wired to a button-to-GND floated to 0 V and read LOW
even at idle. The canonical active-low button never worked.
Read the pull config straight out of the running guest: the IO_MUX
register (FUN_PU bit 8 / FUN_PD bit 7) is already exposed read-only via
qemu_picsimlab_get_internals(3), so no QEMU rebuild is needed. The worker
scans it on the 100 ms poll thread and emits gpio_pull; the bridge feeds
it to PinManager; the netlist stamps a weak 45k resistor to the rail so
idle inputs read the correct level. 45k matches the real internal pull
and is weak enough that any external driver/pull dominates.
Verified with ngspice: idle ~3.3 V (HIGH), pressed ~0 V (LOW).
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>
The user-reported 'LED with proper series resistor shows 1.84 V at
the anode but never visually lights up' had its root cause here,
not in ngspice / not in the LED brightness handler / not in any
component id naming choice. ngspice parses 'V_led-builtin_sense'
just fine; the diode conducts and the node voltage is exactly what
you'd compute by hand.
What breaks is the JS regex that scans the emitted netlist to
collect voltage-source names so CircuitSimulationService can ask
the scheduler to read their branch-current vectors:
const m = card.match(/^([Vv][_\w]*)\s/);
[_\w]* doesn't accept '-'. For a card 'V_led-builtin_sense …' the
capture is 'V_led' (truncated at the hyphen). The voltageSources
array gets the wrong name; CircuitSimulationService pushes
'i(v_led)' into extraVectorsOfInterest; ngspice has no such vector
so the readVec promise rejects silently; branchCurrents['v_led-builtin_sense']
is never populated; the LED handler in BasicParts.ts sees raw =
undefined, the SPICE-memo path is skipped, and the digital fallback
runs but only sets the LED on when the PinResolver classifies the
anode as a direct GPIO connection (which it does NOT when an
intermediate resistor is in series). Dark LED.
Fix is adding '-' to the character class. One character. All five
existing examples I previously 'fixed' by just adding a series
resistor will now light up correctly without renaming any of their
component ids. Same for any saved user project with hyphenated ids
and for the auto-generated picker ids that used to contain hyphens.
The earlier underscore-id workarounds (default canvas + picker
template) stay in place as defense in depth — they don't break
anything and they keep the SPICE side clear of avoidable special
characters.
The service is the single owner of the simulation loop. Replaces
the trio of wireElectricalSolver + connectLegacySolverToMixedMode +
connectMixedModeSchedulerToStore once G* lands.
Architecture:
- Depends on PORTS only — SimulatorStorePort, ElectricalStorePort,
MixedModeSchedulerPort. Zero coupling to useSimulatorStore /
useElectricalStore / WASM. Easy to test with fakes (and that's
what circuit-simulation-service.test.ts does).
- Single tick(): build netlist → load → solve → extract → publish.
Coalesces concurrent triggers so rapid store changes collapse to
one trailing solve.
- Domain ElectricalSnapshot type covers nodeVoltages + branchCurrents
+ pinNetMap + timeWaveforms + analysisMode + warnings. Shape
matches what the 12 existing useElectricalStore consumers read.
NetlistBuilder extension: BuildNetlistResult now reports `nets`
(every non-ground SPICE net) and `voltageSources` (every V card the
builder emitted). The service uses these to construct the full
vectorsOfInterest list — every node voltage + every branch current
— so the solver returns the data the legacy consumers want.
Scheduler addition: `setExtraVectorsOfInterest(vectors)` lets the
orchestrator add to the per-pin set. Branch currents (i(v_*))
flow through this hook.
8 service tests cover initial solve, branch current extraction,
re-solve on store change, no-spurious-solve, coalescing, .tran
waveforms, warnings forwarding, error-tolerance.
Next: C1+C2 — extract ADC injection / waveform replay into a
solver-agnostic module that just subscribes to useElectricalStore.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires can now carry a `length_cm` property. When set, the NetlistBuilder
treats them as a real resistor (0.01 ohm/cm ≈ AWG 22 copper) instead of
the legacy perfect-conductor union. Wires without `length_cm` are
unchanged — 100% backwards compatible until the UI starts attaching
length values based on canvas geometry.
Implementation:
- `WireForSpice.length_cm?: number` added to types
- Union-Find pass skips `union(a, b)` when length_cm > 0, so endpoints
end up in separate nets
- After component-card emission, scan `resistiveWires` and emit
`R_wire_<id> <netA> <netB> <ohms>` for each
- Pull-down detection runs after so the wire R counts as a DC path
Verified end-to-end with real ngspice:
- 100/100 divider at 5V → vmid = 2.5V (legacy, no wire R)
- Same with 1 cm supply wire → vmid = 2.4999 V (0.25 mV drop)
- Same with 500 cm supply wire → vmid ≈ 2.439 V (~6% drop)
5 new Phase 4 tests + 208 regression tests pass.
This is the plumbing-first deliverable from the original sim-mixedmode
plan — UI work (compute length from canvas waypoints) is a separate
front-end task.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes the gap that Phase 1b step 4 surfaced: the legacy pinNetMap was
built from board endpoints only, so the bridge from legacy solver to
MixedModeScheduler had nothing to publish for component pins like
"q1:C" — every SpiceResolvedPinResolver was stuck on FLOATING.
Now pinNetMap contains an entry for every wire endpoint, board or
component. Backwards compatible: legacy ADC injection only ever looked
up `boardId:pinName` keys, which are unchanged.
The new e2e integration test wires up real ngspice (eecircuit-engine,
no mock):
Arduino pin 9 → 1k → 2N2222 base; collector via 220 to 5V
- pin 9 HIGH → BJT saturated → Vc ≈ 0.05V → resolver emits LOW
- pin 9 LOW → BJT cut off → Vc ≈ 5V → resolver emits HIGH
Validated against the AVR_HC logic family (Phase 3). With 216 tests
green across 25 files, the Phase 1b pipeline is now demonstrably
correct end-to-end against a real SPICE solver, not just mocks.
Co-Authored-By: Claude Opus 4.7 (1M context) <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.
- Decoupled electrical simulation from the simulator store, ensuring SPICE is always active for accurate circuit analysis.
- Removed feature flag for electrical simulation, simplifying the state management.
- Preloaded SPICE engine at app start to eliminate latency during the first solve.
- Added comprehensive tests for MOSFET PWM LED behavior and NPN transistor switch functionality, ensuring correct current flow and response to pin states.
- Implemented diagnostics for floating input nodes in RC low-pass filter circuits, addressing singular matrix issues in SPICE simulations.
- Introduced active semiconductor metadata registry for better component management and simulation fidelity.
- Updated Vite configuration to force re-bundling of local wokwi-elements after component additions.
Electrical simulation is now active by default (mode='spice' instead of
'off') — users no longer need to toggle the mode on manually. The engine
lazy-loads on first solve, so there is no startup cost penalty.
Changes:
- useElectricalStore: default mode = 'spice' when ELECTRICAL_SIM_ENABLED
- subscribeToStore: ADC_PIN_MAP expanded to all 18 board types (Uno, Nano,
Mega with 16 ADC channels, ATtiny85, RP2040 GP26-29, ESP32/S3/C3 GPIO
ADCs). Voltages from SPICE solutions now inject into MCU ADC peripherals
for all boards.
- BasicParts LED: reads branchCurrents from useElectricalStore when SPICE
is active. Brightness = clamp(|I_led| / 20mA, 0, 1) instead of boolean.
Subscribes to store changes to update in real time.
- ElectricalOverlay: shows per-wire voltage labels (gold monospace on dark
pill) using buildWireNetMap() which replicates the NetlistBuilder's
Union-Find to map wireId -> netName -> nodeVoltage. Summary pill shows
net count + solve time.
- NetlistBuilder: new export buildWireNetMap() for lightweight wire-to-net
resolution without running ngspice.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>