feat: electrical simulation via ngspice-WASM (eecircuit-engine)
Adds full SPICE-accurate electrical simulation to Velxio, behind a lazy-
loaded ⚡ toolbar toggle. Arduino / ESP32 / RP2040 sketches now co-simulate
with real analog behaviour: correct voltages on wires, real I–V curves on
LEDs, working potentiometers, NTC thermistors read by analogRead(), PWM
driving RC filters, transistors, op-amps, diodes, MOSFETs, etc.
Engine: eecircuit-engine (ngspice compiled to WebAssembly). Main bundle
stays at 2.4 MB; the 20 MB SPICE chunk only loads when the user activates
electrical mode. Disabled at build time via VITE_ELECTRICAL_SIM=false.
Frontend additions:
- simulation/spice/: SpiceEngine wrapper + lazy entry, NetlistBuilder with
UnionFind over wires, componentToSpice mapping (24 metadataIds incl.
real part numbers: 2N2222, 2N3055, BC547, IRF540, 2N7000, 1N4148,
1N4007, 1N4733, LEDs, NTC, op-amp ideal), CircuitScheduler with
debounced coalescing, AVRSpiceBridge for quasi-static co-simulation.
- store/useElectricalStore: Zustand slice, feature-flag aware.
- components/analog-ui/: ⚡ toolbar toggle + SVG voltage overlay.
- components/components-instruments/: Voltmeter, Ammeter probes.
- 62 tests (spice-*, netlist-builder, component-to-spice, instruments).
Sandbox (test/test_circuit/): 47-test validation sandbox that proved
the approach (hand-rolled MNA baseline + ngspice pipeline) before
porting to the app. Kept as reference.
Docs: docs/wiki/circuit-emulation-*.md (13 engineering pages covering
architecture, solvers, components, AVR bridge, gotchas, performance,
integration plan, API reference, appendix) + electrical-simulation-
user-guide.md (end-user facing).
Reference plan: test/test_circuit/plan/phase_8_velxio_implementation.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:11:54 +07:00
|
|
|
/**
|
|
|
|
|
* CircuitScheduler — debounces electrical solve requests coming from UI
|
|
|
|
|
* interactions (wire edits, property edits, pin changes) and dispatches
|
|
|
|
|
* them to the SPICE engine.
|
|
|
|
|
*
|
|
|
|
|
* Design notes:
|
|
|
|
|
* - Single instance per app (module-level singleton).
|
|
|
|
|
* - `requestSolve()` is safe to call frequently; solves are rate-limited.
|
|
|
|
|
* - While a solve is in flight, further requests coalesce into a single
|
|
|
|
|
* trailing solve so we never miss the latest edit.
|
|
|
|
|
* - Exposes `onResult` hooks so the store can subscribe.
|
|
|
|
|
*/
|
|
|
|
|
import type { BuildNetlistInput, ElectricalSolveResult } from './types';
|
|
|
|
|
import { buildNetlist } from './NetlistBuilder';
|
|
|
|
|
import { runNetlist } from './SpiceEngine.lazy';
|
|
|
|
|
|
|
|
|
|
type Listener = (result: ElectricalSolveResult) => void;
|
|
|
|
|
|
|
|
|
|
interface QueuedRequest {
|
|
|
|
|
input: BuildNetlistInput;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DEFAULT_DEBOUNCE_MS = 50;
|
|
|
|
|
|
|
|
|
|
class CircuitScheduler {
|
|
|
|
|
private pending: QueuedRequest | null = null;
|
|
|
|
|
private inFlight = false;
|
|
|
|
|
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
private listeners = new Set<Listener>();
|
|
|
|
|
private debounceMs = DEFAULT_DEBOUNCE_MS;
|
|
|
|
|
|
|
|
|
|
setDebounceMs(ms: number): void {
|
|
|
|
|
this.debounceMs = Math.max(0, ms);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onResult(cb: Listener): () => void {
|
|
|
|
|
this.listeners.add(cb);
|
|
|
|
|
return () => this.listeners.delete(cb);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Request a solve with the given NetlistBuilder input. Coalesces and
|
|
|
|
|
* debounces. The most recent request always wins.
|
|
|
|
|
*/
|
|
|
|
|
requestSolve(input: BuildNetlistInput): void {
|
|
|
|
|
this.pending = { input };
|
|
|
|
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
|
|
|
this.debounceTimer = setTimeout(() => this.drain(), this.debounceMs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Force an immediate solve (bypass debounce). Returns when done. */
|
|
|
|
|
async solveNow(input: BuildNetlistInput): Promise<ElectricalSolveResult> {
|
|
|
|
|
this.pending = { input };
|
|
|
|
|
if (this.debounceTimer) {
|
|
|
|
|
clearTimeout(this.debounceTimer);
|
|
|
|
|
this.debounceTimer = null;
|
|
|
|
|
}
|
|
|
|
|
return this.drain();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async drain(): Promise<ElectricalSolveResult> {
|
|
|
|
|
this.debounceTimer = null;
|
|
|
|
|
if (this.inFlight) {
|
|
|
|
|
// Will be picked up once the in-flight solve finishes
|
|
|
|
|
return this.waitForNextResult();
|
|
|
|
|
}
|
|
|
|
|
const req = this.pending;
|
|
|
|
|
if (!req) {
|
|
|
|
|
return noopResult('no pending request');
|
|
|
|
|
}
|
|
|
|
|
this.pending = null;
|
|
|
|
|
this.inFlight = true;
|
|
|
|
|
|
2026-04-18 05:27:18 +07:00
|
|
|
const { netlist, pinNetMap } = buildNetlist(req.input);
|
feat: electrical simulation via ngspice-WASM (eecircuit-engine)
Adds full SPICE-accurate electrical simulation to Velxio, behind a lazy-
loaded ⚡ toolbar toggle. Arduino / ESP32 / RP2040 sketches now co-simulate
with real analog behaviour: correct voltages on wires, real I–V curves on
LEDs, working potentiometers, NTC thermistors read by analogRead(), PWM
driving RC filters, transistors, op-amps, diodes, MOSFETs, etc.
Engine: eecircuit-engine (ngspice compiled to WebAssembly). Main bundle
stays at 2.4 MB; the 20 MB SPICE chunk only loads when the user activates
electrical mode. Disabled at build time via VITE_ELECTRICAL_SIM=false.
Frontend additions:
- simulation/spice/: SpiceEngine wrapper + lazy entry, NetlistBuilder with
UnionFind over wires, componentToSpice mapping (24 metadataIds incl.
real part numbers: 2N2222, 2N3055, BC547, IRF540, 2N7000, 1N4148,
1N4007, 1N4733, LEDs, NTC, op-amp ideal), CircuitScheduler with
debounced coalescing, AVRSpiceBridge for quasi-static co-simulation.
- store/useElectricalStore: Zustand slice, feature-flag aware.
- components/analog-ui/: ⚡ toolbar toggle + SVG voltage overlay.
- components/components-instruments/: Voltmeter, Ammeter probes.
- 62 tests (spice-*, netlist-builder, component-to-spice, instruments).
Sandbox (test/test_circuit/): 47-test validation sandbox that proved
the approach (hand-rolled MNA baseline + ngspice pipeline) before
porting to the app. Kept as reference.
Docs: docs/wiki/circuit-emulation-*.md (13 engineering pages covering
architecture, solvers, components, AVR bridge, gotchas, performance,
integration plan, API reference, appendix) + electrical-simulation-
user-guide.md (end-user facing).
Reference plan: test/test_circuit/plan/phase_8_velxio_implementation.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:11:54 +07:00
|
|
|
const t0 = performance.now();
|
|
|
|
|
let result: ElectricalSolveResult;
|
|
|
|
|
try {
|
|
|
|
|
const cooked = await runNetlist(netlist);
|
|
|
|
|
const nodeVoltages: Record<string, number> = { '0': 0 };
|
|
|
|
|
for (const name of cooked.variableNames) {
|
|
|
|
|
if (name.startsWith('v(')) {
|
|
|
|
|
const net = name.slice(2, -1);
|
|
|
|
|
const v = cooked.dcValue(name);
|
|
|
|
|
if (Number.isFinite(v)) nodeVoltages[net] = v;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const branchCurrents: Record<string, number> = {};
|
|
|
|
|
for (const name of cooked.variableNames) {
|
|
|
|
|
if (name.startsWith('i(')) {
|
|
|
|
|
const src = name.slice(2, -1);
|
|
|
|
|
const i = cooked.dcValue(name);
|
|
|
|
|
if (Number.isFinite(i)) branchCurrents[src] = i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
result = {
|
|
|
|
|
nodeVoltages,
|
|
|
|
|
branchCurrents,
|
|
|
|
|
converged: true,
|
|
|
|
|
error: null,
|
|
|
|
|
solveMs: performance.now() - t0,
|
|
|
|
|
submittedNetlist: netlist,
|
2026-04-18 05:27:18 +07:00
|
|
|
pinNetMap,
|
feat: electrical simulation via ngspice-WASM (eecircuit-engine)
Adds full SPICE-accurate electrical simulation to Velxio, behind a lazy-
loaded ⚡ toolbar toggle. Arduino / ESP32 / RP2040 sketches now co-simulate
with real analog behaviour: correct voltages on wires, real I–V curves on
LEDs, working potentiometers, NTC thermistors read by analogRead(), PWM
driving RC filters, transistors, op-amps, diodes, MOSFETs, etc.
Engine: eecircuit-engine (ngspice compiled to WebAssembly). Main bundle
stays at 2.4 MB; the 20 MB SPICE chunk only loads when the user activates
electrical mode. Disabled at build time via VITE_ELECTRICAL_SIM=false.
Frontend additions:
- simulation/spice/: SpiceEngine wrapper + lazy entry, NetlistBuilder with
UnionFind over wires, componentToSpice mapping (24 metadataIds incl.
real part numbers: 2N2222, 2N3055, BC547, IRF540, 2N7000, 1N4148,
1N4007, 1N4733, LEDs, NTC, op-amp ideal), CircuitScheduler with
debounced coalescing, AVRSpiceBridge for quasi-static co-simulation.
- store/useElectricalStore: Zustand slice, feature-flag aware.
- components/analog-ui/: ⚡ toolbar toggle + SVG voltage overlay.
- components/components-instruments/: Voltmeter, Ammeter probes.
- 62 tests (spice-*, netlist-builder, component-to-spice, instruments).
Sandbox (test/test_circuit/): 47-test validation sandbox that proved
the approach (hand-rolled MNA baseline + ngspice pipeline) before
porting to the app. Kept as reference.
Docs: docs/wiki/circuit-emulation-*.md (13 engineering pages covering
architecture, solvers, components, AVR bridge, gotchas, performance,
integration plan, API reference, appendix) + electrical-simulation-
user-guide.md (end-user facing).
Reference plan: test/test_circuit/plan/phase_8_velxio_implementation.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:11:54 +07:00
|
|
|
};
|
|
|
|
|
} catch (err) {
|
|
|
|
|
result = {
|
|
|
|
|
nodeVoltages: {},
|
|
|
|
|
branchCurrents: {},
|
|
|
|
|
converged: false,
|
|
|
|
|
error: String(err instanceof Error ? err.message : err),
|
|
|
|
|
solveMs: performance.now() - t0,
|
|
|
|
|
submittedNetlist: netlist,
|
2026-04-18 05:27:18 +07:00
|
|
|
pinNetMap,
|
feat: electrical simulation via ngspice-WASM (eecircuit-engine)
Adds full SPICE-accurate electrical simulation to Velxio, behind a lazy-
loaded ⚡ toolbar toggle. Arduino / ESP32 / RP2040 sketches now co-simulate
with real analog behaviour: correct voltages on wires, real I–V curves on
LEDs, working potentiometers, NTC thermistors read by analogRead(), PWM
driving RC filters, transistors, op-amps, diodes, MOSFETs, etc.
Engine: eecircuit-engine (ngspice compiled to WebAssembly). Main bundle
stays at 2.4 MB; the 20 MB SPICE chunk only loads when the user activates
electrical mode. Disabled at build time via VITE_ELECTRICAL_SIM=false.
Frontend additions:
- simulation/spice/: SpiceEngine wrapper + lazy entry, NetlistBuilder with
UnionFind over wires, componentToSpice mapping (24 metadataIds incl.
real part numbers: 2N2222, 2N3055, BC547, IRF540, 2N7000, 1N4148,
1N4007, 1N4733, LEDs, NTC, op-amp ideal), CircuitScheduler with
debounced coalescing, AVRSpiceBridge for quasi-static co-simulation.
- store/useElectricalStore: Zustand slice, feature-flag aware.
- components/analog-ui/: ⚡ toolbar toggle + SVG voltage overlay.
- components/components-instruments/: Voltmeter, Ammeter probes.
- 62 tests (spice-*, netlist-builder, component-to-spice, instruments).
Sandbox (test/test_circuit/): 47-test validation sandbox that proved
the approach (hand-rolled MNA baseline + ngspice pipeline) before
porting to the app. Kept as reference.
Docs: docs/wiki/circuit-emulation-*.md (13 engineering pages covering
architecture, solvers, components, AVR bridge, gotchas, performance,
integration plan, API reference, appendix) + electrical-simulation-
user-guide.md (end-user facing).
Reference plan: test/test_circuit/plan/phase_8_velxio_implementation.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:11:54 +07:00
|
|
|
};
|
|
|
|
|
} finally {
|
|
|
|
|
this.inFlight = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const cb of this.listeners) cb(result);
|
|
|
|
|
|
|
|
|
|
// If new requests arrived while we were solving, drain them now.
|
|
|
|
|
if (this.pending) {
|
|
|
|
|
// microtask: re-run so we don't recurse synchronously
|
|
|
|
|
setTimeout(() => this.drain(), 0);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private waitForNextResult(): Promise<ElectricalSolveResult> {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const off = this.onResult((r) => {
|
|
|
|
|
off();
|
|
|
|
|
resolve(r);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function noopResult(reason: string): ElectricalSolveResult {
|
|
|
|
|
return {
|
|
|
|
|
nodeVoltages: {},
|
|
|
|
|
branchCurrents: {},
|
|
|
|
|
converged: true,
|
|
|
|
|
error: reason,
|
|
|
|
|
solveMs: 0,
|
|
|
|
|
submittedNetlist: '',
|
2026-04-18 05:27:18 +07:00
|
|
|
pinNetMap: new Map(),
|
feat: electrical simulation via ngspice-WASM (eecircuit-engine)
Adds full SPICE-accurate electrical simulation to Velxio, behind a lazy-
loaded ⚡ toolbar toggle. Arduino / ESP32 / RP2040 sketches now co-simulate
with real analog behaviour: correct voltages on wires, real I–V curves on
LEDs, working potentiometers, NTC thermistors read by analogRead(), PWM
driving RC filters, transistors, op-amps, diodes, MOSFETs, etc.
Engine: eecircuit-engine (ngspice compiled to WebAssembly). Main bundle
stays at 2.4 MB; the 20 MB SPICE chunk only loads when the user activates
electrical mode. Disabled at build time via VITE_ELECTRICAL_SIM=false.
Frontend additions:
- simulation/spice/: SpiceEngine wrapper + lazy entry, NetlistBuilder with
UnionFind over wires, componentToSpice mapping (24 metadataIds incl.
real part numbers: 2N2222, 2N3055, BC547, IRF540, 2N7000, 1N4148,
1N4007, 1N4733, LEDs, NTC, op-amp ideal), CircuitScheduler with
debounced coalescing, AVRSpiceBridge for quasi-static co-simulation.
- store/useElectricalStore: Zustand slice, feature-flag aware.
- components/analog-ui/: ⚡ toolbar toggle + SVG voltage overlay.
- components/components-instruments/: Voltmeter, Ammeter probes.
- 62 tests (spice-*, netlist-builder, component-to-spice, instruments).
Sandbox (test/test_circuit/): 47-test validation sandbox that proved
the approach (hand-rolled MNA baseline + ngspice pipeline) before
porting to the app. Kept as reference.
Docs: docs/wiki/circuit-emulation-*.md (13 engineering pages covering
architecture, solvers, components, AVR bridge, gotchas, performance,
integration plan, API reference, appendix) + electrical-simulation-
user-guide.md (end-user facing).
Reference plan: test/test_circuit/plan/phase_8_velxio_implementation.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:11:54 +07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Module-level singleton
|
|
|
|
|
export const circuitScheduler = new CircuitScheduler();
|