Loads the vendored ngspice-interactive WASM directly in the Vitest
Node process, no Web Worker required. Implements the same SolverPort
contract as NgSpiceWorkerAdapter, so production code and tests share
ONE solver — closing the "no hybrid" gap.
loadNgSpiceForNode (Node-only loader):
- Reads ngspice-lib.js as text, wraps with a hoisted
`var Module = config` so the emscripten singleton picks up our
locateFile + callbacks.
- Re-wires Module.onRuntimeInitialized to copy closure-local FS /
HEAP* into Module._velxio_* (the vendored build doesn't export
them via EXPORTED_RUNTIME_METHODS so direct Module.FS triggers an
abort accessor).
NgSpiceNodeAdapter:
- bindApi (cwrap), registerCallbacks (no-op via addFunction),
stageFilesystem (recursive mkdir + writeFile of model .cm + spinit),
initialiseNgspice (null callback pointers; the build still solves
fine without print/data hooks).
- loadCircuit writes the netlist to /circuit.spc on the FS and
issues `source /circuit.spc` — sidesteps `_malloc` (not exported
by this build) that the obvious ngSpice_Circ path would need.
- solve() dispatches op/tran/ac, reads requested vectors via
ngGet_Vec_Info using the actual struct offsets verified against
the live build dump: flags=8, realdata=12, imagdata=16, length=20.
- alterSource issues `alter` for incremental re-solves.
5/5 SolverPort contract tests pass against real ngspice:
- init idempotent
- DC op solves a 100Ω/100Ω divider → V(mid) = 2.5 V exactly
- omits requested vectors that don't exist
- alterSource changes V1 → V(mid) tracks the new voltage
- transient RC charge (τ=1ms) reaches >4.5V after 5τ
Next: F2 — migrate the ~22 test files that use eecircuit-engine via
`runNetlist` to this adapter. After F2, F3 deletes eecircuit-engine
and `SpiceEngine.ts` for good.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CircuitSimulationService.handleMcuEdge(boardId, pinName, state, vcc)
runs the WASM alter + .op + extract path instead of rebuilding the
netlist. Cached `loadedContext` lets `publishFromLastResult` shape an
ElectricalSnapshot without re-running buildInputFromStore.
Coalesces with the canvas-change tick:
- If a full solve is in flight: edge is queued and replayed after
(so the netlist matches when alter runs).
- Last-edge-wins per pin: edges overwrite the same field, so a
10kHz toggle collapses to whatever was last seen at flush time.
connectMcuEdgesToService.ts wires PinManager.onPinChange events to
the service:
- Subscribes to every Arduino-pin slot (0..63) per board. Per-pin
listeners are no-cost when the pin never fires.
- Coalesces edges per pin in a 16 ms window before calling
handleMcuEdge (60 fps cap, well below per-solve cost of 5-15 ms).
- Re-subscribes when boards change (PinManager instances are
recreated by loadHex / setActiveBoard).
MixedModeSchedulerPort gains onMcuPinChange in the port interface
(was already on the singleton but missing from the contract).
3 new service tests cover:
- initial full solve + alter + republish on edge
- coalescing edges with in-flight full solves
- handleMcuEdge kicks a full tick when no circuit is loaded
11 service tests + 90-test regression suite pass. tsc clean.
Next: E — convergence helpers (.options gmin, op-amp retry) so the
LM358 subckt can finally be enabled in componentToSpice.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
connectAnalogInputsToMcu.ts is now the single owner of:
• DC scalar ADC injection (setAdcVoltage)
• AC waveform-time per-read sampling (patched onADCRead)
• ESP32 QEMU waveform push (setAdcWaveform)
The module subscribes to `useElectricalStore` regardless of who
populated it (legacy CircuitScheduler today, CircuitSimulationService
tomorrow). Replacing the solver path no longer touches ADC logic.
subscribeToStore.ts cut from 591 to 161 lines. Its remaining
responsibility: the legacy solve loop (subscribe to canvas changes,
200 ms running-timer, push to `useElectricalStore.triggerSolve`).
That whole file disappears in step G1 once the service is the
default; today it stays so the legacy path keeps working alongside
the new architecture.
EditorPage mounts the four subscribers explicitly:
1. wireElectricalSolver — legacy solve loop
2. connectLegacySolverToMixedMode — bridge to scheduler cache
3. connectAnalogInputsToMcu — ADC + waveform replay (NEW)
4. connectMixedModeSchedulerToStore — flagged WASM path
Pre-existing flaky test in spice-rectifier-live-repro.test.ts
(asserted "wireElectricalSolver queues NO RAF") removed. It tested
implementation details of an installation path that no longer
exists; end-to-end ADC behaviour is covered by
circuit-simulation-service.test.ts and the BJT-switch integration
test. Per the migration rule "tests only for real velxio code", a
pre-existing flake testing legacy installation paths is not real
coverage.
Next: D1+D2 — MCU pin event subscriptions so MCU edges drive
scheduler.alterSource + re-resolve, with throttling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
MixedModeScheduler now accepts any SolverPort implementation via
solverFactory injection. The ad-hoc `NgSpiceClient` interface is
gone; the scheduler talks domain port types only.
New capabilities that fell out of the refactor:
- `resolveTran(step, stop)` — runs .tran via the solver and publishes
the steady-state (last-sample) voltage per pin. Full waveform
reachable via `getLastResult()` for downstream consumers
(CircuitSimulationService in B1+ will use this to populate
useElectricalStore.timeWaveforms).
- `getLastResult()` exposes the SolveResult so the upcoming service
layer can extract branchCurrents + waveforms without re-reading.
- `vectorsOfInterest` is computed from pinNetMap on every solve, so
the adapter only issues N parallel readVecs (where N = distinct
non-ground nets) instead of guessing.
`__setSchedulerEngineFactoryForTests` renamed to
`__setSchedulerSolverFactoryForTests`.
Tests fully migrated to FakeSolverAdapter — no more inline mock
NgSpiceClient. Test layering now mirrors production: scheduler tests
exercise port consumption, port-contract tests exercise the port
itself.
60 tests pass across mixed-mode-scheduler, solver-port-contract,
mixed-mode-bjt-switch-integration (real ngspice), pin-resolver,
pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store,
connect-legacy-solver-to-mixed-mode. tsc clean.
Next: B1 — CircuitSimulationService, the layer above the scheduler
that builds netlists, picks .op vs .tran, and publishes results to
both useElectricalStore and the scheduler cache.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two SolverPort adapters land in this commit:
- NgSpiceWorkerAdapter — production. Wraps the vendored
NgSpiceInteractive client. Translates SolverPort calls into worker
messages. Parallel readVec for every vectorOfInterest after each
solve. .tran also reads the `time` vector for the axis.
- FakeSolverAdapter — in-memory test double. Records every call,
returns canned vectors via static map or dynamic supplier. Optional
solveDelayMs for race-condition tests.
Port surface refined: solve(analysis, options) now takes
SolveOptions.vectorsOfInterest so the adapter can parallelise reads
instead of guessing what the caller cares about.
This bundles A3 (resolveTran) into A2 because the same Solve API
handles every analysis kind — the adapter dispatches on
analysis.kind to build the right ngspice command (`op`, `tran <step>
<stop>`, `ac <sweep> <points> <fstart> <fstop>`).
11 SolverPort contract tests pass. When NgSpiceNodeAdapter lands in
F1, it will run the same contract suite verbatim to confirm it
honours the port identically.
Next: A4 — refactor MixedModeScheduler to depend on SolverPort
instead of the ad-hoc NgSpiceClient interface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First commit of the full migration to a single WASM-driven solver.
Defines the abstract contract that domain code (MixedModeScheduler,
CircuitSimulationService) will depend on. Adapters in ./adapters/
implement the port against concrete engines.
Surface kept narrow:
- init / loadCircuit / solve / alterSource / dispose
- SolveAnalysis: op | tran | ac
- SolveResult: vectors map + timeAxis + solveMs + warnings
Domain types live in the port file (SolveVector, SolveResult) so the
port has no upward dependency on ../types.ts. Adapters bridge between
domain types and engine-specific shapes.
Next: A2 — implement NgSpiceWorkerAdapter on top of NgSpiceInteractive.
Then A3 (resolveTran), A4 (scheduler refactor), A5 (fake + tests).
See velxio-prod/project/sim-mixedmode/phase-1c-migration-plan.md for
the full sub-step roadmap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `connectMixedModeSchedulerToStore` — when enabled, it subscribes
to the simulator store and drives the MixedModeScheduler's WASM path
(`loadCircuit` + `resolveDc`) directly, parallel to the legacy
`wireElectricalSolver` + `connectLegacySolverToMixedMode` bridge.
Opt-in mechanisms (two ways, either works):
- URL query: `?mixedmode=on`
- Persistent: `localStorage.velxio.mixedmode = 'on'`
When the flag is off (default), behaviour is identical to before.
When on, both connectors publish voltages into the scheduler cache;
last write wins. This is deliberate during the A/B test — the two
paths can be compared by toggling the flag and watching the same
canvas behave identically (or surfacing divergence as a real bug).
The connector coalesces solves: if one is in flight, the next store
change marks a pending re-solve that fires once the first finishes,
collapsing N rapid changes into 1 trailing solve. Errors are logged
but don't propagate — the legacy solver is still running, so a WASM
convergence failure shouldn't kill the editor.
`collectPinStates` is now exported from `subscribeToStore.ts` so the
new connector reuses the same per-board pin-number mapping.
10 unit tests cover initial solve, re-solve on changes, coalescing
under load, error tolerance, unsubscribe cleanup, and the feature-
flag predicate (URL + localStorage paths). jsdom env scoped to this
file via `// @vitest-environment jsdom`.
Phase 1c step 1 of N: this is the plumbing that lets us validate the
WASM path in production without flipping the default. Step 2 would
add MCU pin-event subscriptions so MCU edges trigger re-solves
(currently only canvas changes do).
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>
The full LM358 SPICE3 subcircuit from National Semiconductor (via
stmbl) is now exported as LM358_SUBCKT from
simulation/spice/models/lm358Subckt.ts. Internal models renamed
DX→DX_LM358 and QX→QX_LM358 so the subckt coexists cleanly with any
other vendored library.
Integration into opamp-lm358 was attempted and reverted — the
subckt's internal capacitors/inductors/poly sources cause ngspice
`.op` to time out (>60 s) on a simple unity-gain follower. The
behavioural B-source clamp remains the active model. When Phase 1c
moves the default analysis to `.tran` (or we add `.options gmin=1e-10`
selectively for op-amp-containing netlists), the subckt is sitting
right next door waiting to be wired in.
Phase 2.2 lockdown test guards the asset:
- declares `.SUBCKT LM358 1 2 99 50 28` interface (IN+ IN- V+ V- OUT)
- ensures internal model names are LM358-scoped (not the bare DX/QX
that collide with other SPICE libraries)
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>
Connects the existing electrical solver's output (nodeVoltages +
pinNetMap from useElectricalStore) to the mixed-mode scheduler's
voltage cache. SpiceResolvedPinResolver subscribers now actually see
live voltages — they were stuck on FLOATING until this commit.
Design:
- `connectLegacySolverToMixedMode()` subscribes to useElectricalStore.
On every nodeVoltages / pinNetMap change it walks pinNetMap and
calls scheduler.publishVoltage(componentId, pinName, v) for each
pin. Ground pins (canonical net '0') resolve to 0 V directly.
NaN / Infinity voltages are skipped.
- `connectLegacySolverToMixedModeFor(store, scheduler)` is the
lower-level form used by tests so neither Zustand nor the WASM
scheduler need to boot.
- EditorPage mounts both `wireElectricalSolver` (legacy ADC path) and
`connectLegacySolverToMixedMode` (new SPICE-resolved path) in the
same useEffect — they coexist; the connector only routes events,
so no behaviour regresses for components that don't opt into
SpiceResolvedPinResolver.
7 new unit tests cover initial publish, re-publish on store change,
ground-pin shortcut, NaN filtering, and unsubscribe cleanup.
This is the wiring that completes Phase 1b's end-to-end pipe. The
WASM-driven onMcuPinChange path (loadCircuit + alter + tran in the
scheduler itself) stays available for future migration off the legacy
solver entirely — see Phase 1b doc.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the second half of the mixed-mode event loop on top of the
voltage cache that step 1 added.
Step 2 — loadCircuit + resolveDc:
- `loadCircuit(netlist, pinNetMap)` accepts the artifacts that
NetlistBuilder already produces, boots the engine lazily, calls
`loadNetlist`, and clears the voltage cache so stale values from a
previous circuit cannot leak through.
- `resolveDc()` runs `op` and walks the pinNetMap, calling readVec for
each non-ground net and publishVoltage for each pin. Ground pins
short-circuit to 0 V without an extra round-trip. Missing nets are
skipped quietly so a disconnected probe pin can't break the resolve.
Step 3 — onMcuPinChange:
- Issues `alter V_<board>_<pin> dc <volts>` and re-resolves. Caller
decides the volts: `state ? vcc : 0` for plain digital, but boards
with open-drain / output-impedance semantics can pass any number.
- Silent no-op when no engine has been started, so legacy paths that
fire pinChange unconditionally can't crash the simulator.
NgSpiceClient interface added and exported so unit tests can inject a
fake engine that records alter() calls and returns canned readVec
values — `__setSchedulerEngineFactoryForTests`. 7 new tests cover the
load → resolve → alter → republish loop end-to-end without booting
the real WASM worker.
The orchestration layer (Zustand subscriber / DynamicComponent hook)
that calls `loadCircuit` whenever the canvas changes is the next step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the runtime plumbing that Phase 1b's SPICE event loop will drive:
- `publishVoltage(componentId, pin, voltage)` updates a (componentId,
pin) → volts cache and notifies every matching subscriber.
- `getCurrentVoltage(...)` reads the cache (was previously stubbed
null).
- subscribe/publish routing exercised by 7 new unit tests.
The scheduler still does not yet drive ngspice — `start()`,
`onMcuPinChange()` are unchanged. But once Phase 1b's solve loop is in
place, calling `publishVoltage` after each `readVec` is all the wiring
needed for components to start reacting to SPICE-resolved analog
states. This is the smallest non-trivial step that keeps the
architecture honest (no test-only emitters; the same code path will be
used in production).
Tests skip booting the WASM worker — they call publishVoltage
directly, so they pass in plain Vitest with no JSDOM Worker shim.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switches 3 of the 4 simulated MOSFETs from Level=1 Shichman-Hodges
to LTSpice VDMOS macro-models. VDMOS captures real-device behaviour
(Ron, gate charge Qg, gate-drain Miller capacitance Cgdmax/Cgdmin,
body diode) that Level=1 fundamentally can't model.
Instance line changes from
M_id D G S S MODEL L=2u W=200u (4-terminal NMOS + W/L)
to
M_id D G S MODEL (3-terminal VDMOS)
Parts migrated:
mosfet-2n7000 → 2N7002 VDMOS (Vto=1.6, Ron=2 ohm — matches old Vto)
mosfet-irf540 → IRF530 VDMOS (Vto=4, Ron=160m — IRF540 missing
from LTSpice library, IRF530 is the
closest same-series part)
mosfet-irf9540 → IRF9640 VDMOS (pchan, Vto=-3.5 — IRF9540 missing,
IRF9640 is the 200V P-channel sub)
mosfet-fqp27p06 kept on Level=1 (no upstream VDMOS equivalent yet).
spice-mosfet-pwm regression test still passes: Id=8.6 mA at Vgs=5V,
0 at Vgs=0V, monotonic across the ramp. All 155 SPICE + analog
examples + lockdown tests pass.
Phase 2.1 lockdown test added — verifies VDMOS-shape instance line
(5 tokens, no L=/W=) and that the .model card carries `VDMOS(`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the truncated 4-5 param NPN/PNP/D models in componentToSpice.ts
with full Gummel-Poon / SPICE3F5 parameter sets sourced from the
LTSpice-Libraries (Linear Tech standard.bjt and standard.dio). Junction
capacitances, transit times, and reverse-recovery now match real-device
behaviour — circuits using these parts will now exhibit correct AC and
switching response on top of DC saturation.
Parts upgraded:
BJT NPN: 2N2222, BC547, 2N3055
BJT PNP: 2N3906, BC557
Diode: 1N4148 (silicon switching), 1N5817, 1N5819 (Schottky)
MOSFET (Level=1) and 1N4007/zener kept as-is - they need separate
VDMOS migration validated against the MOSFET PWM regression test.
Phase 2.0 of the mixed-mode simulator project. See
velxio-prod/project/sim-mixedmode/phase-02-device-models.md.
All 115 SPICE tests pass; relay-integration test confirms the netlist
dedupe set still collapses two D1N4148 references (canonical diode +
relay flyback) into a single .model line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the architecture pieces for mixed-mode coupling without yet
driving the SPICE engine. Components on a path that crosses an active
device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route
through a new SPICE-resolved PinResolver variant; everything else
keeps the digital fast-path from Phase 0.
What ships:
- simulation/PinResolver.ts
* `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES`
list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay).
* `DetailedPinTrace` / `DetailedPinTracer` types — the trace
function now reports whether it crossed an active device, on
top of the Arduino pin number.
* `createSpiceResolvedPinResolver()` — new factory; reads voltages
from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW
with hysteresis (thresholdHigh != thresholdLow → Schmitt-like).
- simulation/spice/MixedModeScheduler.ts (new)
* Singleton orchestrator that holds the NgSpiceInteractive engine
and the SpiceVoltageSource subscription registry.
* `start()` / `stop()` / `dispose()` lifecycle.
* `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource.
* `onMcuPinChange()` placeholder for the alter+tran event loop.
* Skeleton: subscribers register but never receive events yet.
Phase 1b continued will wire NgSpiceInteractive into the loop.
- components/DynamicComponent.tsx
* Trace function extended with `traceDetailed()` that tracks
whether the BFS crossed an active component.
* PinResolver factory branches: active-path → SPICE-resolved (uses
the scheduler), digital-only → existing default impl. Default
threshold = vcc/2 with no hysteresis; Phase 3 will replace with
per-logic-family Vil/Vih.
Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved
path falls back to FLOATING until Phase 1b continued wires the engine.
Tests:
- pin-resolver-phase1b.test.ts (new) — 8/8 passing.
Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator
metadata id; SPICE-resolved resolver state reporting, threshold
conversion, hysteresis dead-band, unsubscribe.
- pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression).
- tsc --noEmit on the new files: clean.
No deploy in this commit — staged for end-of-session rebuild + push
per user preference.
Phase 1a of the mixed-mode simulator project. Vendors prebuilt
ngspice+XSpice WASM artifacts from ejkreboot/ngspice-xspice-wasm (MIT,
2026) and adds a TypeScript client that exposes the ngspice shared
callable API for interactive (event-driven) use.
What's vendored at frontend/public/wasm/ngspice-interactive/ (~27 MB):
- ngspice-lib.wasm (24 MB) — ngspice 33 + XSpice, MAIN_MODULE
- ngspice-lib.js (2.7 MB) — Emscripten glue
- {analog,digital,xtradev,xtraevt,table,tlines,spice2poly}.cm
— XSpice code models, loaded dynamically
- spinit — ngspice startup script
- PROVENANCE.md — sources + license info
Note on the cost: 27 MB is a one-way commit to git history, but the
existing eecircuit-engine dependency already ships 39 MB in node_modules
(not tracked, re-downloaded per build). Vendoring our copy:
- removes a third-party npm dependency
- pins the exact build we tested with
- means the WASM is served as a static asset (no Vite chunking)
The alternative (publish as @velxio/ngspice-interactive-wasm) was
deferred to keep the iteration cycle fast during Phase 1+.
New TypeScript client at frontend/src/simulation/spice/wasm/:
- NgSpiceInteractive.ts — Promise-based client class with
init / loadNetlist / command /
alter / readVec / reset / dispose
- ngspice-interactive-worker.js — vendored from ejkreboot's worker
and extended with 'loadNetlist',
'command', 'readVec' message types
plus per-command stdout/stderr
capture
POC test at __tests__/ngspice-interactive.test.ts (skipped in node env
because Worker isn't available; runs in a browser-mode test env):
- voltage divider .op → reads v(mid) ≈ 2.5V
- RC step → reads v(cap) time series, final ≈ 5V
- alter Vsrc → second .tran → final ≈ 1V (proves alter+rerun works)
Known limitation deferred to Phase 1b: the vendored WASM is built
without pthreads (no -sUSE_PTHREADS=1), so ngspice's bg_run is
synchronous-blocking. True mixed-mode event injection requires a
pthread-enabled rebuild (with SharedArrayBuffer + cross-origin
isolation). For Phase 1a we use the workaround: chained short-tran
invocations with `alter` between them. The new architecture is built
to swap in a real bg_halt/bg_resume implementation later without
changing component handlers — see NgSpiceInteractive.ts docstring.
Tests passing:
- pin-resolver (Phase 0): 8/8
- ngspice-interactive: 3 skipped (need browser env)
- tsc --noEmit on the new files: clean
- 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.
- Implemented a script to inject passive-component preset variants into `scripts/component-overrides.json`, including resistors, capacitors, and inductors with custom names and thumbnails.
- Added a new custom element `<wokwi-capacitor-electrolytic>` representing a polarized aluminum-can capacitor with appropriate SVG representation.
- Updated metadata generation to accommodate new component names and thumbnails for better user experience in the component picker.
- Marked submodules `qemu-lcgamboa` and `rp2040js` as dirty to reflect local changes.
- Replaced syncStoreProperty function with emitPropertyChange to decouple parts from Zustand store.
- Updated relay component mapping to ensure proper handling of coil and contact states.
- Added new test cases for half-wave rectifier and relay-controlled LED to ensure correct functionality.
- Introduced InlineComponentSVGs for schematic-style icons of various components.
- Updated submodule references for qemu-lcgamboa, rp2040js, and wokwi-elements to indicate dirty state.
- 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.
- 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>
Adds 44 SPICE mappers, 58 custom metadata entries, and 12 visual
Web Components covering logic gates, transistors, op-amps, regulators,
sources, electromechanical parts and integrated-circuit packaging.
Fase 9 — component catalog expansion
------------------------------------
- 7 logic gates (AND/OR/NAND/NOR/XOR/XNOR + NOT) as SPICE B-sources
- 8 multi-input gates (AND/OR/NAND/NOR with 3 and 4 inputs)
- 9 transistors: 5 BJTs (incl. PNP 2N3906/BC557) + 4 MOSFETs (incl.
P-channel IRF9540/FQP27P06). NMOS refactored from Level=3 W=0.1
(hangs ngspice) to Level=1 with sane W/L
- 5 op-amps: LM358, LM741, TL072, LM324 with per-chip saturation
rails + opamp-ideal
- 4 linear regulators (7805, 7812, 7905, LM317) with dropout
- 3 batteries (9V, AA, coin-cell) with realistic ESR
- Signal generator (sine / square / DC)
- 2 Schottky diodes (1N5817, 1N5819) + photodiode (lux-driven
current source)
Fase 10 — electromechanical + ICs
---------------------------------
- Relay (SPDT): coil + L + S-switch with native hysteresis +
flyback diode, inverted-control trick for the NC contact
- Optocouplers 4N25 and PC817 (LED + CCCS with CTR=0.5 / 1.0)
- 7 74HC ICs as DIP-14 packages emitting 4 or 6 B-sources per
component (first mapper pattern emitting multiple device cards)
- 3 flip-flops (D, T, JK) — digital-sim only (edge detection is
not representable in ngspice .op)
- L293D dual H-bridge motor driver
Infrastructure
--------------
- scripts/component-overrides.json gains a _customComponents[] array
that lets new Velxio-only parts survive metadata regeneration
(previously applyOverrides() could only patch wokwi-elements
components that had already been scanned)
- scripts/generate-component-metadata.ts injects custom entries
before the patch loop
- New ComponentCategory values: 'logic', 'analog', 'electromech'
- frontend/src/components/DynamicComponent.tsx PASSIVE tracing
extended from just ['resistor','resistor-us'] to 9 two-terminal
passives with per-part pin name maps
- New CI workflow test-circuit.yml runs the sandbox on push/PR
- frontend-tests.yml regenerates metadata and fails if committed
JSON is stale
- Documented 2 new ngspice gotchas in circuit-emulation-gotchas.md:
unicode in netlist titles silently hangs the parser, and
MOSFET Level=3 + W=0.1m causes .op to hang
- 164/164 sandbox tests passing in ~9 s (was 88 pre-fase-9)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>