velxio/frontend/src/simulation/spice/CircuitSimulationService.ts

437 lines
17 KiB
TypeScript
Raw Normal View History

feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
/**
* CircuitSimulationService — the orchestration layer that owns the
* simulation loop.
*
* Responsibilities (single, well-defined):
* 1. Listen to canvas state via an injected SimulatorStorePort.
* 2. Build the SPICE netlist via NetlistBuilder.
* 3. Drive the scheduler (loadCircuit + resolveDc / resolveTran).
* 4. Extract every voltage / branch current / waveform from the
* scheduler's last SolveResult and publish to the
* ElectricalStorePort so the 12 downstream consumers (ADC
* injection, instruments, overlays) keep working.
* 5. Coalesce concurrent solves: if one is in flight, mark a
* pending re-solve for after.
*
* Single source of truth: this service replaces the trio of
* - wireElectricalSolver (legacy)
* - connectLegacySolverToMixedMode (bridge)
* - connectMixedModeSchedulerToStore (Phase 1c step 1)
*
* Architecture:
* - Depends on PORTS only (SimulatorStorePort, ElectricalStorePort,
* MixedModeSchedulerPort). Easy to test with fakes.
* - No useSimulatorStore / useElectricalStore imports in this file
* — those bindings live in the wiring file (start.ts).
* - No SPICE-engine knowledge — that's in the adapters.
*/
import { buildInputFromStore } from './storeAdapter';
fix(spice+pipeline): LED visualization, INPUT_PULLUP, ESP32-C3, PWM fade, examples 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>
2026-05-19 07:47:45 +07:00
import { buildNetlist, sanitizeSpiceId } from './NetlistBuilder';
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
import type { TimeWaveforms } from './types';
feat(digital-gate-engine): evaluate logic gates on the event-driven kernel Board-less digital circuits (logic gates + switches + LEDs) run today as ngspice analog B-sources, which is fragile for deep logic: a 4-bit ripple adder re-solves but never lights its result LEDs live. This adds an event-driven digital motor that reuses the multichip-bus settle kernel, so the same engine that boots a Z80 over a chip bus evaluates a gate network exactly and instantly. Phases 0-2 (project/digital-gate-engine/), all behind ?digitalgates=on (default OFF — flag off is byte-for-byte the old behaviour): - digitalGateEngine.ts: buildDigitalNetwork(components, wires) does union-find over the wires (merging pass-through resistors), identifies the rail/gnd from the signal-generator, registers drivers (rail STRONG-1, gnd 0, pull resistors PULL, slide-switch as a pass-gate) and event-driven gates (reusing the LogicGateParts boolean semantics), settles on busKernel, and exposes setSwitch / readLed / netOf. Tolerant of both the raw example `type` and the store `metadataId`. Returns {ok:false} for any non-primitive, so mixed/analog circuits stay entirely on ngspice. - digitalGateController.ts + a SimulatorCanvas useEffect: when the flag is on and the circuit is all-digital, rebuild from the store on switch-toggle / load (rAF-coalesced) and paint the wokwi-led DOM. CircuitSimulationService.tick() skips the SPICE solve for all-digital circuits when the flag is on, so the two motors never fight over the LEDs. Tests: digitalgate-kernel (22 — single gates -> half/full adder -> 4-bit adder/subtractor -> exhaustive ADD 256 -> mux/decoder/comparator/parity/ multiplier) and digitalgate-engine-examples (6 — the real gallery data for and/or/xor/not + the full adder/subtractor). Verified live: ?digitalgates=on lights the adder's result LEDs that the SPICE path leaves dark. Full suite 2117 pass / 5 pre-existing unrelated fails. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 06:21:33 +07:00
import { digitalGatesEnabled, isAllDigital } from '../digital/digitalGateEngine';
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
/** What the service needs from the simulator store. */
export interface SimulatorStorePort {
getState(): {
components: Array<{ id: string; metadataId: string; properties: Record<string, unknown> }>;
wires: Array<{
id: string;
start: { componentId: string; pinName: string };
end: { componentId: string; pinName: string };
}>;
boards: Array<{ id: string; boardKind: string; pinStates?: Record<string, unknown> }>;
/** Components destroyed at runtime (P4) — excluded from the netlist so a
* burnt part actually goes open. Optional for non-store ports. */
burntComponents?: Set<string>;
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
};
subscribe(listener: (state: unknown, prev: unknown) => void): () => void;
}
/** What the service publishes to (the legacy electrical store, in our case). */
export interface ElectricalStorePort {
/** Atomically write a complete solve snapshot. */
publish(snapshot: ElectricalSnapshot): void;
}
/** Domain-level solve result, decoupled from SolverPort details. */
export interface ElectricalSnapshot {
/** SPICE net name → scalar voltage (V). For .tran: last sample. */
nodeVoltages: Record<string, number>;
/** V-source name (without leading V) → scalar branch current (A). */
branchCurrents: Record<string, number>;
/** "componentId:pinName" → SPICE net name (from NetlistBuilder). */
pinNetMap: Map<string, string>;
/** Which analysis produced this. */
analysisMode: 'op' | 'tran' | 'ac';
/** Per-sample waveforms — present only for .tran. */
timeWaveforms?: TimeWaveforms;
/** Convergence warnings from the solver. */
warnings: string[];
/** Nets backed by a real source/element (see NetlistBuilder). Gates which
* MCU input pins connectDigitalInputsToMcu may drive from the solve. */
sourcedNets: Set<string>;
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
}
/** What the service needs from the scheduler. */
export interface MixedModeSchedulerPort {
loadCircuit(netlist: string, pinNetMap: Map<string, string>): Promise<void>;
resolveDc(): Promise<void>;
resolveTran(step: string, stop: string): Promise<void>;
/**
* The last SolveResult the scheduler produced. Used by the service
* to extract waveforms / branch currents without going around the
* scheduler.
*/
getLastResult(): import('./ports/SolverPort').SolveResult | null;
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
/**
* MCU pin transition → alter the matching V source + re-resolve.
* Domain-level event; the scheduler maps state+vcc → volts and
* issues the alter.
*/
onMcuPinChange(boardId: string, pinName: string, state: boolean, vcc: number): Promise<void>;
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
/**
* Allow the service to request extra vectors of interest before
* the solve runs (branch currents, internal nets). Optional —
* implementations may ignore it if they don't optimise.
*/
setExtraVectorsOfInterest?(vectors: readonly string[]): void;
}
export interface ServiceOptions {
/** Pre-existing pin states for board pins (from PinManager). */
collectBoardPinStates: (
boardId: string,
boardKind: string,
wires: SimulatorStorePort['getState'] extends () => infer S
? S extends { wires: infer W }
? W
: never
: never,
) => Record<string, unknown>;
}
export class CircuitSimulationService {
private inFlight = false;
private pending = false;
fix(spice+pipeline): LED visualization, INPUT_PULLUP, ESP32-C3, PWM fade, examples 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>
2026-05-19 07:47:45 +07:00
// Per-pin pending edge buffer. Keyed by `${boardId}|${pinName}`. A single
// slot would be overwritten when several pins toggle during the same
// in-flight tick (Traffic Light: red→yellow→green hand off within ~µs of
// each other) — the LATEST edge would win, dropping the rest. With a Map
// every pin's most-recent edge is preserved and replayed after the tick.
private pendingMcuEdges = new Map<
string,
{ boardId: string; pinName: string; state: boolean; vcc: number }
>();
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
/**
* Last loaded circuit context — used by `handleMcuEdge` to extract
* the right vectors from `scheduler.getLastResult()` after an
* `alter + resolveDc` without rebuilding the netlist.
*/
private loadedContext: {
pinNetMap: Map<string, string>;
nets: string[];
voltageSources: string[];
analysisKind: 'op' | 'tran' | 'ac';
sourcedNets: Set<string>;
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
} | null = null;
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
fix(circuit-sim): stop() guard + drop edges with no V-source post-rebuild Two related bugs that surfaced as "Vitest worker exited unexpectedly / Timeout terminating forks worker" on the circuit-simulation-service test file. Bug 1 — tick() recursively re-schedules itself in its finally block. After afterEach disposes the scheduler via __resetMixedModeScheduler(), those re-scheduled ticks throw "call loadCircuit first", get caught by the console.warn, and the finally schedules ANOTHER tick. Infinite Promise loop survives until the worker OOMs. Fix: add CircuitSimulationService.stop() that flips a `stopped` flag short-circuiting tick() + handleMcuEdge(). The test harness now tracks each started service in _activeServices and calls stop() in afterEach alongside the existing unsubscribe sweep. Bug 2 — when an MCU edge fires on a pin that's NOT wired into any net (buildNetlist skips it because netLookup returns null), handleMcuEdge sees hasSource=false, self-heals by queueing the edge + tick(). The rebuild still doesn't emit the V-source (no wire), so tick.finally replays the edge → self-heal again → tick again → infinite loop AT RUNTIME, not just in tests. A user toggling a digital pin without a wire freezes the whole circuit simulation. Fix: in tick.finally's pendingMcuEdges replay loop, check whether the rebuilt netlist now contains a V-source for each pending edge's pin. If not, drop the edge silently — a future canvas tick triggered by adding the wire will pick it up via the normal subscription path. Also fix the "coalesces an edge with an in-flight full solve" test fixture: simpleBoardWithBoard leaves pin 9 unwired, so V_uno_9 was never emitted and the test was racing the (now-bounded) self-heal rebuild. Replaced with an inline fixture wiring pin 9 → resistor → GND, mirroring the wired fixture used by the alter+republish test right above it. Full vitest --shard 1/2 + 2/2 pass cleanly (1886 tests, 22-29s per shard) with no worker-exit warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:49:29 +07:00
/** Set by `stop()`. Once true, `tick()` and `handleMcuEdge()`
* short-circuit so a service whose owner has unsubscribed can't
* keep re-scheduling solves against a disposed scheduler. */
private stopped = false;
fix(perf): un-freeze the editor during fast-toggling simulations (ESP32 clock) Running a multiplexed 4-digit 7-segment clock on ESP32/QEMU froze the browser for minutes after Run — evaluate probes waited 40-90 s, and before the first fixes the sim WebSocket eventually died (code 1006) with the page never recovering. CPU-profiled on staging; four compounding per-GPIO-edge costs, in profile order: updateComponentState minted a new components array per edge ------------------------------------------------------------ The store setter rebuilt `components` (and one properties object) on EVERY edge even when the state didn't change. The breadboard is direct-wired to 13 board pins, so segment toggles produced thousands of store sets per second; every subscriber re-rendered each time, and the canvas subscription effect (deps: [components, ...]) re-subscribed all pin listeners in a loop. Now a no-op guard returns prevState unchanged, and breadboards are treated as self-managed (they have no visual on/off state to echo). CompilationConsole re-rendered every log line per editor render ---------------------------------------------------------------- The post-compile console holds hundreds of lines; each render called Date.toLocaleTimeString per line (~0.2 ms each — it builds a fresh Intl formatter every call). Profile: 162 s of self time in LogLine over a 337 s window, in ~150 ms tasks. LogLine is now memoized (entries are immutable), timestamps go through one shared Intl.DateTimeFormat, and the console itself is React.memo'd against parent re-renders. Per-edge full SPICE re-solves ------------------------------ PinManager requested a FULL netlist rebuild+solve on every 'mcu' edge. Now only the edge that newly classifies a pin as MCU-output triggers the rebuild (that's what emits the pin's V-source); steady-state updates flow through connectMcuEdgesToService's per-pin coalesced alterSource path. The start.ts resolve hook is trailing-throttled (33 ms) for the other per-edge callers (RP2040, custom chips), the service's pending-edge queue drains on a 33 ms gap timer instead of replaying back-to-back, and new edges arriving inside the gap queue instead of soloing a solve. STM32 / Pi reverse pin-name mappings added to connectMcuEdgesToService so those boards keep fine-grained updates now that the full-tick storm is gone (PA0/PC13-style and GPIO-style names never matched before). wokwi-7segment re-rendered per segment write --------------------------------------------- element.values now flushes at most every 8 ms per display (trailing write guaranteed), instead of re-rendering the 32-shape SVG per edge. Also: CLN (colon) pin support for 7-segment clock faces — wired CLN now drives colon/colonValue in both the attachEvents path and the QEMU onPinStateChange path; it was silently ignored, so clock colons never lit. Verified on staging with the failing project: main-thread probes drop from 40-90 s waits (324 long tasks, 52.6 s blocked in 150 s) to 5-11 ms (2 long tasks, 179 ms), display shows 12:00 with the colon blinking at 1 Hz from the first seconds after Run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 06:15:59 +07:00
/** Trailing timer for draining `pendingMcuEdges`. Replaying queued
* edges IMMEDIATELY after a solve creates a back-to-back solve loop
* under sustained toggling (a multiplexed display keeps 8-13 pins
* hot, so the queue never empties) — the solver runs at 100% duty
* and the UI starves. One drain per gap keeps last-state-wins per
* pin while bounding total solve rate. */
private drainTimer: ReturnType<typeof setTimeout> | null = null;
private static readonly EDGE_DRAIN_GAP_MS = 33;
/** Drain pendingMcuEdges after a short gap (coalescing: one timer). */
private scheduleEdgeDrain(): void {
if (this.stopped || this.drainTimer !== null) return;
this.drainTimer = setTimeout(() => {
this.drainTimer = null;
if (this.stopped) return;
const edges = Array.from(this.pendingMcuEdges.values());
this.pendingMcuEdges.clear();
const ctx = this.loadedContext;
for (const edge of edges) {
const expected = `v_${sanitizeSpiceId(edge.boardId)}_${sanitizeSpiceId(edge.pinName)}`.toLowerCase();
const hasSource = ctx?.voltageSources.some(
(vs) => vs.toLowerCase() === expected,
);
if (!hasSource) continue;
void this.handleMcuEdge(edge.boardId, edge.pinName, edge.state, edge.vcc);
}
}, CircuitSimulationService.EDGE_DRAIN_GAP_MS);
}
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
constructor(
private readonly simStore: SimulatorStorePort,
private readonly electricalStore: ElectricalStorePort,
private readonly scheduler: MixedModeSchedulerPort,
private readonly options: ServiceOptions,
) {}
fix(circuit-sim): stop() guard + drop edges with no V-source post-rebuild Two related bugs that surfaced as "Vitest worker exited unexpectedly / Timeout terminating forks worker" on the circuit-simulation-service test file. Bug 1 — tick() recursively re-schedules itself in its finally block. After afterEach disposes the scheduler via __resetMixedModeScheduler(), those re-scheduled ticks throw "call loadCircuit first", get caught by the console.warn, and the finally schedules ANOTHER tick. Infinite Promise loop survives until the worker OOMs. Fix: add CircuitSimulationService.stop() that flips a `stopped` flag short-circuiting tick() + handleMcuEdge(). The test harness now tracks each started service in _activeServices and calls stop() in afterEach alongside the existing unsubscribe sweep. Bug 2 — when an MCU edge fires on a pin that's NOT wired into any net (buildNetlist skips it because netLookup returns null), handleMcuEdge sees hasSource=false, self-heals by queueing the edge + tick(). The rebuild still doesn't emit the V-source (no wire), so tick.finally replays the edge → self-heal again → tick again → infinite loop AT RUNTIME, not just in tests. A user toggling a digital pin without a wire freezes the whole circuit simulation. Fix: in tick.finally's pendingMcuEdges replay loop, check whether the rebuilt netlist now contains a V-source for each pending edge's pin. If not, drop the edge silently — a future canvas tick triggered by adding the wire will pick it up via the normal subscription path. Also fix the "coalesces an edge with an in-flight full solve" test fixture: simpleBoardWithBoard leaves pin 9 unwired, so V_uno_9 was never emitted and the test was racing the (now-bounded) self-heal rebuild. Replaced with an inline fixture wiring pin 9 → resistor → GND, mirroring the wired fixture used by the alter+republish test right above it. Full vitest --shard 1/2 + 2/2 pass cleanly (1886 tests, 22-29s per shard) with no worker-exit warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:49:29 +07:00
/**
* Permanently stop the orchestration loop. After `stop()`, the
* in-flight solve still completes (its Promise was already
* scheduled), but no further `tick()` or replay of pending
* `handleMcuEdge` will fire.
*
* Why this exists: the `tick()` finally-block recursively
* re-schedules itself when `pendingMcuEdges` is non-empty, and
* each iteration calls `scheduler.resolveDc()`. If the test
* fixture (or a future production caller) disposes the scheduler
* via `__resetMixedModeScheduler()` without telling the service,
* those re-scheduled ticks throw "call loadCircuit first", get
* caught, and the finally schedules ANOTHER tick — infinite
* Promise loop in the event queue that survives until the worker
* OOMs. Calling `service.stop()` in the test's afterEach (or any
* teardown path) breaks the loop.
*/
stop(): void {
this.stopped = true;
this.pending = false;
this.pendingMcuEdges.clear();
fix(perf): un-freeze the editor during fast-toggling simulations (ESP32 clock) Running a multiplexed 4-digit 7-segment clock on ESP32/QEMU froze the browser for minutes after Run — evaluate probes waited 40-90 s, and before the first fixes the sim WebSocket eventually died (code 1006) with the page never recovering. CPU-profiled on staging; four compounding per-GPIO-edge costs, in profile order: updateComponentState minted a new components array per edge ------------------------------------------------------------ The store setter rebuilt `components` (and one properties object) on EVERY edge even when the state didn't change. The breadboard is direct-wired to 13 board pins, so segment toggles produced thousands of store sets per second; every subscriber re-rendered each time, and the canvas subscription effect (deps: [components, ...]) re-subscribed all pin listeners in a loop. Now a no-op guard returns prevState unchanged, and breadboards are treated as self-managed (they have no visual on/off state to echo). CompilationConsole re-rendered every log line per editor render ---------------------------------------------------------------- The post-compile console holds hundreds of lines; each render called Date.toLocaleTimeString per line (~0.2 ms each — it builds a fresh Intl formatter every call). Profile: 162 s of self time in LogLine over a 337 s window, in ~150 ms tasks. LogLine is now memoized (entries are immutable), timestamps go through one shared Intl.DateTimeFormat, and the console itself is React.memo'd against parent re-renders. Per-edge full SPICE re-solves ------------------------------ PinManager requested a FULL netlist rebuild+solve on every 'mcu' edge. Now only the edge that newly classifies a pin as MCU-output triggers the rebuild (that's what emits the pin's V-source); steady-state updates flow through connectMcuEdgesToService's per-pin coalesced alterSource path. The start.ts resolve hook is trailing-throttled (33 ms) for the other per-edge callers (RP2040, custom chips), the service's pending-edge queue drains on a 33 ms gap timer instead of replaying back-to-back, and new edges arriving inside the gap queue instead of soloing a solve. STM32 / Pi reverse pin-name mappings added to connectMcuEdgesToService so those boards keep fine-grained updates now that the full-tick storm is gone (PA0/PC13-style and GPIO-style names never matched before). wokwi-7segment re-rendered per segment write --------------------------------------------- element.values now flushes at most every 8 ms per display (trailing write guaranteed), instead of re-rendering the 32-shape SVG per edge. Also: CLN (colon) pin support for 7-segment clock faces — wired CLN now drives colon/colonValue in both the attachEvents path and the QEMU onPinStateChange path; it was silently ignored, so clock colons never lit. Verified on staging with the failing project: main-thread probes drop from 40-90 s waits (324 long tasks, 52.6 s blocked in 150 s) to 5-11 ms (2 long tasks, 179 ms), display shows 12:00 with the colon blinking at 1 Hz from the first seconds after Run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 06:15:59 +07:00
if (this.drainTimer !== null) {
clearTimeout(this.drainTimer);
this.drainTimer = null;
}
fix(circuit-sim): stop() guard + drop edges with no V-source post-rebuild Two related bugs that surfaced as "Vitest worker exited unexpectedly / Timeout terminating forks worker" on the circuit-simulation-service test file. Bug 1 — tick() recursively re-schedules itself in its finally block. After afterEach disposes the scheduler via __resetMixedModeScheduler(), those re-scheduled ticks throw "call loadCircuit first", get caught by the console.warn, and the finally schedules ANOTHER tick. Infinite Promise loop survives until the worker OOMs. Fix: add CircuitSimulationService.stop() that flips a `stopped` flag short-circuiting tick() + handleMcuEdge(). The test harness now tracks each started service in _activeServices and calls stop() in afterEach alongside the existing unsubscribe sweep. Bug 2 — when an MCU edge fires on a pin that's NOT wired into any net (buildNetlist skips it because netLookup returns null), handleMcuEdge sees hasSource=false, self-heals by queueing the edge + tick(). The rebuild still doesn't emit the V-source (no wire), so tick.finally replays the edge → self-heal again → tick again → infinite loop AT RUNTIME, not just in tests. A user toggling a digital pin without a wire freezes the whole circuit simulation. Fix: in tick.finally's pendingMcuEdges replay loop, check whether the rebuilt netlist now contains a V-source for each pending edge's pin. If not, drop the edge silently — a future canvas tick triggered by adding the wire will pick it up via the normal subscription path. Also fix the "coalesces an edge with an in-flight full solve" test fixture: simpleBoardWithBoard leaves pin 9 unwired, so V_uno_9 was never emitted and the test was racing the (now-bounded) self-heal rebuild. Replaced with an inline fixture wiring pin 9 → resistor → GND, mirroring the wired fixture used by the alter+republish test right above it. Full vitest --shard 1/2 + 2/2 pass cleanly (1886 tests, 22-29s per shard) with no worker-exit warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:49:29 +07:00
}
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
/** Run one solve cycle, coalescing concurrent triggers. */
async tick(): Promise<void> {
fix(circuit-sim): stop() guard + drop edges with no V-source post-rebuild Two related bugs that surfaced as "Vitest worker exited unexpectedly / Timeout terminating forks worker" on the circuit-simulation-service test file. Bug 1 — tick() recursively re-schedules itself in its finally block. After afterEach disposes the scheduler via __resetMixedModeScheduler(), those re-scheduled ticks throw "call loadCircuit first", get caught by the console.warn, and the finally schedules ANOTHER tick. Infinite Promise loop survives until the worker OOMs. Fix: add CircuitSimulationService.stop() that flips a `stopped` flag short-circuiting tick() + handleMcuEdge(). The test harness now tracks each started service in _activeServices and calls stop() in afterEach alongside the existing unsubscribe sweep. Bug 2 — when an MCU edge fires on a pin that's NOT wired into any net (buildNetlist skips it because netLookup returns null), handleMcuEdge sees hasSource=false, self-heals by queueing the edge + tick(). The rebuild still doesn't emit the V-source (no wire), so tick.finally replays the edge → self-heal again → tick again → infinite loop AT RUNTIME, not just in tests. A user toggling a digital pin without a wire freezes the whole circuit simulation. Fix: in tick.finally's pendingMcuEdges replay loop, check whether the rebuilt netlist now contains a V-source for each pending edge's pin. If not, drop the edge silently — a future canvas tick triggered by adding the wire will pick it up via the normal subscription path. Also fix the "coalesces an edge with an in-flight full solve" test fixture: simpleBoardWithBoard leaves pin 9 unwired, so V_uno_9 was never emitted and the test was racing the (now-bounded) self-heal rebuild. Replaced with an inline fixture wiring pin 9 → resistor → GND, mirroring the wired fixture used by the alter+republish test right above it. Full vitest --shard 1/2 + 2/2 pass cleanly (1886 tests, 22-29s per shard) with no worker-exit warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:49:29 +07:00
if (this.stopped) return;
feat(digital-gate-engine): evaluate logic gates on the event-driven kernel Board-less digital circuits (logic gates + switches + LEDs) run today as ngspice analog B-sources, which is fragile for deep logic: a 4-bit ripple adder re-solves but never lights its result LEDs live. This adds an event-driven digital motor that reuses the multichip-bus settle kernel, so the same engine that boots a Z80 over a chip bus evaluates a gate network exactly and instantly. Phases 0-2 (project/digital-gate-engine/), all behind ?digitalgates=on (default OFF — flag off is byte-for-byte the old behaviour): - digitalGateEngine.ts: buildDigitalNetwork(components, wires) does union-find over the wires (merging pass-through resistors), identifies the rail/gnd from the signal-generator, registers drivers (rail STRONG-1, gnd 0, pull resistors PULL, slide-switch as a pass-gate) and event-driven gates (reusing the LogicGateParts boolean semantics), settles on busKernel, and exposes setSwitch / readLed / netOf. Tolerant of both the raw example `type` and the store `metadataId`. Returns {ok:false} for any non-primitive, so mixed/analog circuits stay entirely on ngspice. - digitalGateController.ts + a SimulatorCanvas useEffect: when the flag is on and the circuit is all-digital, rebuild from the store on switch-toggle / load (rAF-coalesced) and paint the wokwi-led DOM. CircuitSimulationService.tick() skips the SPICE solve for all-digital circuits when the flag is on, so the two motors never fight over the LEDs. Tests: digitalgate-kernel (22 — single gates -> half/full adder -> 4-bit adder/subtractor -> exhaustive ADD 256 -> mux/decoder/comparator/parity/ multiplier) and digitalgate-engine-examples (6 — the real gallery data for and/or/xor/not + the full adder/subtractor). Verified live: ?digitalgates=on lights the adder's result LEDs that the SPICE path leaves dark. Full suite 2117 pass / 5 pre-existing unrelated fails. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 06:21:33 +07:00
// The digital-gate engine owns all-digital board-less circuits when
// ?digitalgates=on; skip the SPICE solve so the two motors don't fight over
// the LEDs. Flag off (default) -> isAllDigital is never consulted.
if (
digitalGatesEnabled() &&
isAllDigital((this.simStore.getState() as { components: unknown[] }).components as never[])
) {
return;
}
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
if (this.inFlight) {
this.pending = true;
return;
}
this.inFlight = true;
try {
await this.runSolve();
} catch (err) {
// eslint-disable-next-line no-console
console.warn('[circuit-sim] solve failed:', err);
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
} finally {
this.inFlight = false;
fix(circuit-sim): stop() guard + drop edges with no V-source post-rebuild Two related bugs that surfaced as "Vitest worker exited unexpectedly / Timeout terminating forks worker" on the circuit-simulation-service test file. Bug 1 — tick() recursively re-schedules itself in its finally block. After afterEach disposes the scheduler via __resetMixedModeScheduler(), those re-scheduled ticks throw "call loadCircuit first", get caught by the console.warn, and the finally schedules ANOTHER tick. Infinite Promise loop survives until the worker OOMs. Fix: add CircuitSimulationService.stop() that flips a `stopped` flag short-circuiting tick() + handleMcuEdge(). The test harness now tracks each started service in _activeServices and calls stop() in afterEach alongside the existing unsubscribe sweep. Bug 2 — when an MCU edge fires on a pin that's NOT wired into any net (buildNetlist skips it because netLookup returns null), handleMcuEdge sees hasSource=false, self-heals by queueing the edge + tick(). The rebuild still doesn't emit the V-source (no wire), so tick.finally replays the edge → self-heal again → tick again → infinite loop AT RUNTIME, not just in tests. A user toggling a digital pin without a wire freezes the whole circuit simulation. Fix: in tick.finally's pendingMcuEdges replay loop, check whether the rebuilt netlist now contains a V-source for each pending edge's pin. If not, drop the edge silently — a future canvas tick triggered by adding the wire will pick it up via the normal subscription path. Also fix the "coalesces an edge with an in-flight full solve" test fixture: simpleBoardWithBoard leaves pin 9 unwired, so V_uno_9 was never emitted and the test was racing the (now-bounded) self-heal rebuild. Replaced with an inline fixture wiring pin 9 → resistor → GND, mirroring the wired fixture used by the alter+republish test right above it. Full vitest --shard 1/2 + 2/2 pass cleanly (1886 tests, 22-29s per shard) with no worker-exit warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:49:29 +07:00
if (this.stopped) return;
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
if (this.pending) {
this.pending = false;
void this.tick();
fix(spice+pipeline): LED visualization, INPUT_PULLUP, ESP32-C3, PWM fade, examples 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>
2026-05-19 07:47:45 +07:00
} else if (this.pendingMcuEdges.size > 0) {
fix(perf): un-freeze the editor during fast-toggling simulations (ESP32 clock) Running a multiplexed 4-digit 7-segment clock on ESP32/QEMU froze the browser for minutes after Run — evaluate probes waited 40-90 s, and before the first fixes the sim WebSocket eventually died (code 1006) with the page never recovering. CPU-profiled on staging; four compounding per-GPIO-edge costs, in profile order: updateComponentState minted a new components array per edge ------------------------------------------------------------ The store setter rebuilt `components` (and one properties object) on EVERY edge even when the state didn't change. The breadboard is direct-wired to 13 board pins, so segment toggles produced thousands of store sets per second; every subscriber re-rendered each time, and the canvas subscription effect (deps: [components, ...]) re-subscribed all pin listeners in a loop. Now a no-op guard returns prevState unchanged, and breadboards are treated as self-managed (they have no visual on/off state to echo). CompilationConsole re-rendered every log line per editor render ---------------------------------------------------------------- The post-compile console holds hundreds of lines; each render called Date.toLocaleTimeString per line (~0.2 ms each — it builds a fresh Intl formatter every call). Profile: 162 s of self time in LogLine over a 337 s window, in ~150 ms tasks. LogLine is now memoized (entries are immutable), timestamps go through one shared Intl.DateTimeFormat, and the console itself is React.memo'd against parent re-renders. Per-edge full SPICE re-solves ------------------------------ PinManager requested a FULL netlist rebuild+solve on every 'mcu' edge. Now only the edge that newly classifies a pin as MCU-output triggers the rebuild (that's what emits the pin's V-source); steady-state updates flow through connectMcuEdgesToService's per-pin coalesced alterSource path. The start.ts resolve hook is trailing-throttled (33 ms) for the other per-edge callers (RP2040, custom chips), the service's pending-edge queue drains on a 33 ms gap timer instead of replaying back-to-back, and new edges arriving inside the gap queue instead of soloing a solve. STM32 / Pi reverse pin-name mappings added to connectMcuEdgesToService so those boards keep fine-grained updates now that the full-tick storm is gone (PA0/PC13-style and GPIO-style names never matched before). wokwi-7segment re-rendered per segment write --------------------------------------------- element.values now flushes at most every 8 ms per display (trailing write guaranteed), instead of re-rendering the 32-shape SVG per edge. Also: CLN (colon) pin support for 7-segment clock faces — wired CLN now drives colon/colonValue in both the attachEvents path and the QEMU onPinStateChange path; it was silently ignored, so clock colons never lit. Verified on staging with the failing project: main-thread probes drop from 40-90 s waits (324 long tasks, 52.6 s blocked in 150 s) to 5-11 ms (2 long tasks, 179 ms), display shows 12:00 with the colon blinking at 1 Hz from the first seconds after Run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 06:15:59 +07:00
// Deferred drain (not an immediate replay): the drain itself
// re-checks each pin against the freshly-rebuilt V-source list,
// dropping edges for pins that still aren't wired into any net —
// replaying those would self-heal again → re-tick → loop forever.
this.scheduleEdgeDrain();
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
}
}
}
/**
* Handle an MCU pin transition. Uses the WASM solver's
* `alterSource` to update the relevant voltage source in place
* and re-resolve — no netlist rebuild — then publishes the new
* voltages to useElectricalStore.
*
* Coalesces with the canvas-change tick: if a full solve is in
fix(spice+pipeline): LED visualization, INPUT_PULLUP, ESP32-C3, PWM fade, examples 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>
2026-05-19 07:47:45 +07:00
* flight, the edge is queued per-pin (Map keyed by `boardId|pinName`)
* and replayed after the tick completes — last-state-wins PER pin,
* so different pins toggling during the same tick don't drop each
* other's edges.
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
*/
async handleMcuEdge(boardId: string, pinName: string, state: boolean, vcc: number): Promise<void> {
fix(circuit-sim): stop() guard + drop edges with no V-source post-rebuild Two related bugs that surfaced as "Vitest worker exited unexpectedly / Timeout terminating forks worker" on the circuit-simulation-service test file. Bug 1 — tick() recursively re-schedules itself in its finally block. After afterEach disposes the scheduler via __resetMixedModeScheduler(), those re-scheduled ticks throw "call loadCircuit first", get caught by the console.warn, and the finally schedules ANOTHER tick. Infinite Promise loop survives until the worker OOMs. Fix: add CircuitSimulationService.stop() that flips a `stopped` flag short-circuiting tick() + handleMcuEdge(). The test harness now tracks each started service in _activeServices and calls stop() in afterEach alongside the existing unsubscribe sweep. Bug 2 — when an MCU edge fires on a pin that's NOT wired into any net (buildNetlist skips it because netLookup returns null), handleMcuEdge sees hasSource=false, self-heals by queueing the edge + tick(). The rebuild still doesn't emit the V-source (no wire), so tick.finally replays the edge → self-heal again → tick again → infinite loop AT RUNTIME, not just in tests. A user toggling a digital pin without a wire freezes the whole circuit simulation. Fix: in tick.finally's pendingMcuEdges replay loop, check whether the rebuilt netlist now contains a V-source for each pending edge's pin. If not, drop the edge silently — a future canvas tick triggered by adding the wire will pick it up via the normal subscription path. Also fix the "coalesces an edge with an in-flight full solve" test fixture: simpleBoardWithBoard leaves pin 9 unwired, so V_uno_9 was never emitted and the test was racing the (now-bounded) self-heal rebuild. Replaced with an inline fixture wiring pin 9 → resistor → GND, mirroring the wired fixture used by the alter+republish test right above it. Full vitest --shard 1/2 + 2/2 pass cleanly (1886 tests, 22-29s per shard) with no worker-exit warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:49:29 +07:00
if (this.stopped) return;
fix(spice+pipeline): LED visualization, INPUT_PULLUP, ESP32-C3, PWM fade, examples 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>
2026-05-19 07:47:45 +07:00
const pinKey = `${boardId}|${pinName}`;
fix(perf): un-freeze the editor during fast-toggling simulations (ESP32 clock) Running a multiplexed 4-digit 7-segment clock on ESP32/QEMU froze the browser for minutes after Run — evaluate probes waited 40-90 s, and before the first fixes the sim WebSocket eventually died (code 1006) with the page never recovering. CPU-profiled on staging; four compounding per-GPIO-edge costs, in profile order: updateComponentState minted a new components array per edge ------------------------------------------------------------ The store setter rebuilt `components` (and one properties object) on EVERY edge even when the state didn't change. The breadboard is direct-wired to 13 board pins, so segment toggles produced thousands of store sets per second; every subscriber re-rendered each time, and the canvas subscription effect (deps: [components, ...]) re-subscribed all pin listeners in a loop. Now a no-op guard returns prevState unchanged, and breadboards are treated as self-managed (they have no visual on/off state to echo). CompilationConsole re-rendered every log line per editor render ---------------------------------------------------------------- The post-compile console holds hundreds of lines; each render called Date.toLocaleTimeString per line (~0.2 ms each — it builds a fresh Intl formatter every call). Profile: 162 s of self time in LogLine over a 337 s window, in ~150 ms tasks. LogLine is now memoized (entries are immutable), timestamps go through one shared Intl.DateTimeFormat, and the console itself is React.memo'd against parent re-renders. Per-edge full SPICE re-solves ------------------------------ PinManager requested a FULL netlist rebuild+solve on every 'mcu' edge. Now only the edge that newly classifies a pin as MCU-output triggers the rebuild (that's what emits the pin's V-source); steady-state updates flow through connectMcuEdgesToService's per-pin coalesced alterSource path. The start.ts resolve hook is trailing-throttled (33 ms) for the other per-edge callers (RP2040, custom chips), the service's pending-edge queue drains on a 33 ms gap timer instead of replaying back-to-back, and new edges arriving inside the gap queue instead of soloing a solve. STM32 / Pi reverse pin-name mappings added to connectMcuEdgesToService so those boards keep fine-grained updates now that the full-tick storm is gone (PA0/PC13-style and GPIO-style names never matched before). wokwi-7segment re-rendered per segment write --------------------------------------------- element.values now flushes at most every 8 ms per display (trailing write guaranteed), instead of re-rendering the 32-shape SVG per edge. Also: CLN (colon) pin support for 7-segment clock faces — wired CLN now drives colon/colonValue in both the attachEvents path and the QEMU onPinStateChange path; it was silently ignored, so clock colons never lit. Verified on staging with the failing project: main-thread probes drop from 40-90 s waits (324 long tasks, 52.6 s blocked in 150 s) to 5-11 ms (2 long tasks, 179 ms), display shows 12:00 with the colon blinking at 1 Hz from the first seconds after Run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 06:15:59 +07:00
// Queue while a solve is in flight OR while the drain gap timer is
// armed. Without the second condition, every edge landing in the gap
// between solves would start an immediate solve of its own and the
// gap would only apply to the queued leftovers — under a sustained
// storm that's still ~1 solve per 2 edges instead of 1 per gap.
if (this.inFlight || this.drainTimer !== null) {
fix(spice+pipeline): LED visualization, INPUT_PULLUP, ESP32-C3, PWM fade, examples 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>
2026-05-19 07:47:45 +07:00
this.pendingMcuEdges.set(pinKey, { boardId, pinName, state, vcc });
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
return;
}
if (!this.loadedContext) {
// No circuit loaded yet — kick a full tick. The edge will
// appear in board.pinStates during runSolve.
void this.tick();
return;
}
fix(spice+pipeline): LED visualization, INPUT_PULLUP, ESP32-C3, PWM fade, examples 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>
2026-05-19 07:47:45 +07:00
// Self-heal: if this is the FIRST edge on a pin that wasn't classified
// as MCU-output when the netlist was built, the matching V-source
// doesn't exist — alterSource would be a silent no-op. Trigger a
// full rebuild instead; collectPinStates now sees the pin in
// outputPins and emits the V-source, so the next edge alters normally.
const expected = `v_${sanitizeSpiceId(boardId)}_${sanitizeSpiceId(pinName)}`.toLowerCase();
const hasSource = this.loadedContext.voltageSources.some(
(vs) => vs.toLowerCase() === expected,
);
if (!hasSource) {
this.pendingMcuEdges.set(pinKey, { boardId, pinName, state, vcc });
void this.tick();
return;
}
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
this.inFlight = true;
try {
// alter + resolveDc internally; the scheduler's
// onMcuPinChange covers both steps.
await this.scheduler.onMcuPinChange(boardId, pinName, state, vcc);
this.publishFromLastResult();
} catch (err) {
// eslint-disable-next-line no-console
console.warn('[circuit-sim] mcu-edge solve failed:', err);
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
} finally {
this.inFlight = false;
if (this.pending) {
this.pending = false;
void this.tick();
fix(spice+pipeline): LED visualization, INPUT_PULLUP, ESP32-C3, PWM fade, examples 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>
2026-05-19 07:47:45 +07:00
} else if (this.pendingMcuEdges.size > 0) {
fix(perf): un-freeze the editor during fast-toggling simulations (ESP32 clock) Running a multiplexed 4-digit 7-segment clock on ESP32/QEMU froze the browser for minutes after Run — evaluate probes waited 40-90 s, and before the first fixes the sim WebSocket eventually died (code 1006) with the page never recovering. CPU-profiled on staging; four compounding per-GPIO-edge costs, in profile order: updateComponentState minted a new components array per edge ------------------------------------------------------------ The store setter rebuilt `components` (and one properties object) on EVERY edge even when the state didn't change. The breadboard is direct-wired to 13 board pins, so segment toggles produced thousands of store sets per second; every subscriber re-rendered each time, and the canvas subscription effect (deps: [components, ...]) re-subscribed all pin listeners in a loop. Now a no-op guard returns prevState unchanged, and breadboards are treated as self-managed (they have no visual on/off state to echo). CompilationConsole re-rendered every log line per editor render ---------------------------------------------------------------- The post-compile console holds hundreds of lines; each render called Date.toLocaleTimeString per line (~0.2 ms each — it builds a fresh Intl formatter every call). Profile: 162 s of self time in LogLine over a 337 s window, in ~150 ms tasks. LogLine is now memoized (entries are immutable), timestamps go through one shared Intl.DateTimeFormat, and the console itself is React.memo'd against parent re-renders. Per-edge full SPICE re-solves ------------------------------ PinManager requested a FULL netlist rebuild+solve on every 'mcu' edge. Now only the edge that newly classifies a pin as MCU-output triggers the rebuild (that's what emits the pin's V-source); steady-state updates flow through connectMcuEdgesToService's per-pin coalesced alterSource path. The start.ts resolve hook is trailing-throttled (33 ms) for the other per-edge callers (RP2040, custom chips), the service's pending-edge queue drains on a 33 ms gap timer instead of replaying back-to-back, and new edges arriving inside the gap queue instead of soloing a solve. STM32 / Pi reverse pin-name mappings added to connectMcuEdgesToService so those boards keep fine-grained updates now that the full-tick storm is gone (PA0/PC13-style and GPIO-style names never matched before). wokwi-7segment re-rendered per segment write --------------------------------------------- element.values now flushes at most every 8 ms per display (trailing write guaranteed), instead of re-rendering the 32-shape SVG per edge. Also: CLN (colon) pin support for 7-segment clock faces — wired CLN now drives colon/colonValue in both the attachEvents path and the QEMU onPinStateChange path; it was silently ignored, so clock colons never lit. Verified on staging with the failing project: main-thread probes drop from 40-90 s waits (324 long tasks, 52.6 s blocked in 150 s) to 5-11 ms (2 long tasks, 179 ms), display shows 12:00 with the colon blinking at 1 Hz from the first seconds after Run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 06:15:59 +07:00
this.scheduleEdgeDrain();
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
}
}
}
private async runSolve(): Promise<void> {
const state = this.simStore.getState();
// P4: a runtime-destroyed part is excluded from the netlist so it actually
// goes open — its current stops and anything it fed loses power (cascading
// failure), the way real hardware behaves once a component burns out.
const burnt = state.burntComponents;
const liveComponents =
burnt && burnt.size > 0 ? state.components.filter((c) => !burnt.has(c.id)) : state.components;
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
const snap = {
components: liveComponents,
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
wires: state.wires,
boards: state.boards.map((b) => ({
id: b.id,
boardKind: b.boardKind,
pinStates: this.options.collectBoardPinStates(
b.id,
b.boardKind,
state.wires as never,
) as never,
})),
};
const input = buildInputFromStore(snap as Parameters<typeof buildInputFromStore>[0]);
const { netlist, pinNetMap, nets, voltageSources, sourcedNets } = buildNetlist(input);
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
// Tell the scheduler exactly which vectors we want — every net
// voltage + every branch current.
const extraVectors: string[] = [];
for (const net of nets) extraVectors.push(`v(${net})`);
for (const vs of voltageSources) extraVectors.push(`i(${vs.toLowerCase()})`);
this.scheduler.setExtraVectorsOfInterest?.(extraVectors);
await this.scheduler.loadCircuit(netlist, pinNetMap);
if (input.analysis.kind === 'tran') {
await this.scheduler.resolveTran(input.analysis.step, input.analysis.stop);
} else {
await this.scheduler.resolveDc();
}
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
// Cache the load context so handleMcuEdge can publish without
// re-running buildInputFromStore + buildNetlist.
this.loadedContext = {
pinNetMap,
nets,
voltageSources,
analysisKind: input.analysis.kind,
sourcedNets,
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
};
this.publishFromLastResult();
}
/**
* Build an ElectricalSnapshot from the scheduler's last SolveResult
* + the cached load context. Publishes to useElectricalStore.
*/
private publishFromLastResult(): void {
const ctx = this.loadedContext;
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
const result = this.scheduler.getLastResult();
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
if (!ctx || !result) return;
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
const nodeVoltages: Record<string, number> = {};
const branchCurrents: Record<string, number> = {};
let timeWaveforms: TimeWaveforms | undefined;
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
for (const net of ctx.nets) {
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
const vec = result.vectors.get(`v(${net})`);
if (vec && vec.real.length > 0) {
nodeVoltages[net] = vec.real[vec.real.length - 1]!;
}
}
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
for (const vs of ctx.voltageSources) {
const vec = result.vectors.get(`i(${vs.toLowerCase()})`);
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
if (vec && vec.real.length > 0) {
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
// Convention: useElectricalStore.branchCurrents keys use the
// lower-case V-source name (e.g. "v_led1_sense"). LED handler
// and Ammeter both read this shape.
branchCurrents[vs.toLowerCase()] = vec.real[vec.real.length - 1]!;
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
}
}
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
if (ctx.analysisKind === 'tran' && result.timeAxis.length > 0) {
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
const nodes = new Map<string, number[]>();
const branches = new Map<string, number[]>();
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
for (const net of ctx.nets) {
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
const vec = result.vectors.get(`v(${net})`);
if (vec && vec.real.length > 0) nodes.set(net, Array.from(vec.real));
}
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
for (const vs of ctx.voltageSources) {
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
const vec = result.vectors.get(`i(${vs.toLowerCase()})`);
if (vec && vec.real.length > 0) branches.set(vs.toLowerCase(), Array.from(vec.real));
}
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
timeWaveforms = { time: Array.from(result.timeAxis), nodes, branches };
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
}
this.electricalStore.publish({
nodeVoltages,
branchCurrents,
feat(sim): Phase 1c D1+D2 — MCU edges drive scheduler.alterSource + republish 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>
2026-05-16 01:26:09 +07:00
pinNetMap: ctx.pinNetMap,
analysisMode: ctx.analysisKind,
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
timeWaveforms,
warnings: result.warnings,
sourcedNets: ctx.sourcedNets,
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
});
}
/**
* Mount the service: subscribe to store changes + run one initial
* solve. Returns an unsubscribe handle.
*/
start(): () => void {
const unsubscribe = this.simStore.subscribe((next, prev) => {
const n = next as ReturnType<typeof this.simStore.getState>;
const p = prev as ReturnType<typeof this.simStore.getState>;
if (
n.components !== p.components ||
n.wires !== p.wires ||
n.boards !== p.boards ||
// P4: a part burning out (or a Reset un-burning it) changes which
// components are in the netlist, so re-solve to apply the open.
n.burntComponents !== p.burntComponents
) {
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) 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>
2026-05-16 00:28:26 +07:00
void this.tick();
}
});
void this.tick();
return unsubscribe;
}
}