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';
|
2026-05-16 00:28:26 +07:00
|
|
|
import type { TimeWaveforms } from './types';
|
|
|
|
|
|
|
|
|
|
/** 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> }>;
|
|
|
|
|
};
|
|
|
|
|
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[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 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;
|
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>;
|
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 }
|
|
|
|
|
>();
|
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';
|
|
|
|
|
} | null = null;
|
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,
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
/** Run one solve cycle, coalescing concurrent triggers. */
|
|
|
|
|
async tick(): Promise<void> {
|
|
|
|
|
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);
|
2026-05-16 01:26:09 +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) {
|
|
|
|
|
const edges = Array.from(this.pendingMcuEdges.values());
|
|
|
|
|
this.pendingMcuEdges.clear();
|
|
|
|
|
for (const edge of edges) {
|
|
|
|
|
void this.handleMcuEdge(edge.boardId, edge.pinName, edge.state, edge.vcc);
|
|
|
|
|
}
|
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.
|
2026-05-16 01:26:09 +07:00
|
|
|
*/
|
|
|
|
|
async handleMcuEdge(boardId: string, pinName: string, state: boolean, vcc: number): Promise<void> {
|
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}`;
|
2026-05-16 01:26:09 +07:00
|
|
|
if (this.inFlight) {
|
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 });
|
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;
|
|
|
|
|
}
|
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);
|
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) {
|
|
|
|
|
const edges = Array.from(this.pendingMcuEdges.values());
|
|
|
|
|
this.pendingMcuEdges.clear();
|
|
|
|
|
for (const edge of edges) {
|
|
|
|
|
void this.handleMcuEdge(edge.boardId, edge.pinName, edge.state, edge.vcc);
|
|
|
|
|
}
|
2026-05-16 00:28:26 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async runSolve(): Promise<void> {
|
|
|
|
|
const state = this.simStore.getState();
|
|
|
|
|
const snap = {
|
|
|
|
|
components: state.components,
|
|
|
|
|
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 } = buildNetlist(input);
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
}
|
|
|
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
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;
|
2026-05-16 00:28:26 +07:00
|
|
|
const result = this.scheduler.getLastResult();
|
2026-05-16 01:26:09 +07:00
|
|
|
if (!ctx || !result) return;
|
2026-05-16 00:28:26 +07:00
|
|
|
|
|
|
|
|
const nodeVoltages: Record<string, number> = {};
|
|
|
|
|
const branchCurrents: Record<string, number> = {};
|
|
|
|
|
let timeWaveforms: TimeWaveforms | undefined;
|
|
|
|
|
|
2026-05-16 01:26:09 +07:00
|
|
|
for (const net of ctx.nets) {
|
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]!;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-16 01:26:09 +07:00
|
|
|
for (const vs of ctx.voltageSources) {
|
|
|
|
|
const vec = result.vectors.get(`i(${vs.toLowerCase()})`);
|
2026-05-16 00:28:26 +07:00
|
|
|
if (vec && vec.real.length > 0) {
|
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]!;
|
2026-05-16 00:28:26 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 01:26:09 +07:00
|
|
|
if (ctx.analysisKind === 'tran' && result.timeAxis.length > 0) {
|
2026-05-16 00:28:26 +07:00
|
|
|
const nodes = new Map<string, number[]>();
|
|
|
|
|
const branches = new Map<string, number[]>();
|
2026-05-16 01:26:09 +07:00
|
|
|
for (const net of ctx.nets) {
|
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));
|
|
|
|
|
}
|
2026-05-16 01:26:09 +07:00
|
|
|
for (const vs of ctx.voltageSources) {
|
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));
|
|
|
|
|
}
|
2026-05-16 01:26:09 +07:00
|
|
|
timeWaveforms = { time: Array.from(result.timeAxis), nodes, branches };
|
2026-05-16 00:28:26 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.electricalStore.publish({
|
|
|
|
|
nodeVoltages,
|
|
|
|
|
branchCurrents,
|
2026-05-16 01:26:09 +07:00
|
|
|
pinNetMap: ctx.pinNetMap,
|
|
|
|
|
analysisMode: ctx.analysisKind,
|
2026-05-16 00:28:26 +07:00
|
|
|
timeWaveforms,
|
|
|
|
|
warnings: result.warnings,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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) {
|
|
|
|
|
void this.tick();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
void this.tick();
|
|
|
|
|
return unsubscribe;
|
|
|
|
|
}
|
|
|
|
|
}
|