#10 — ESP32 ADC clipping warning: `pushEsp32Waveforms` now counts how many samples land outside the 0-3.3 V ADC range. If > 10% of a pin's waveform clips, console.warn once per pin with the observed range. Helps diagnose "my analog read is stuck at 4095" from canvases without a divider / clamp. #11 — PinManager subscriptions scoped to circuit pins. Previously `connectMcuEdgesToService.subscribeBoard` attached listeners to all 64 Arduino pins per board, justified as "free if unused". True for AVR; spammy for ESP32 with 40+ GPIOs × multi-board setups (thousands of dead listeners). Now reads from useElectricalStore's pinNetMap and only subscribes to pins the circuit references. Re-subscribes when pinNetMap changes (new wire added/removed). #16 — `__spiceDebug()` window helper. Restored after the legacy subscribeToStore deletion in Phase 1c. Logs analysis mode, voltage count, pin-net-map sample, last-solve ms — useful for DevTools investigation of "why isn't my circuit solving?" reports. 1461 tests pass. #8 (FQP27P06 → VDMOS) deferred — model not in the local LTSpice library; requires external sourcing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6c6dea3326
commit
37f35488c7
|
|
@ -109,6 +109,9 @@ const ADC_PIN_TO_GPIO: Partial<Record<BoardKind, (pinName: string, channel: numb
|
|||
export function connectAnalogInputsToMcu(): () => void {
|
||||
const patchedAdcs = new WeakSet<object>();
|
||||
const qemuWaveformChannels = new Set<string>();
|
||||
// Phase 1d #10 — warn-once set so ADC-clip messages don't spam the
|
||||
// console. Key is `${boardId}:${pinName}`.
|
||||
const clipWarned = new Set<string>();
|
||||
let replayStartMs = 0;
|
||||
let replayEpochLatched = false;
|
||||
|
||||
|
|
@ -171,10 +174,29 @@ export function connectAnalogInputsToMcu(): () => void {
|
|||
const samples = net ? timeWaveforms.nodes.get(net) : undefined;
|
||||
if (!samples || samples.length === 0) continue;
|
||||
const u12 = new Uint16Array(samples.length);
|
||||
// Phase 1d #10 — count clip events to surface ESP32 ADC
|
||||
// range violations once per pin. If more than 10% of
|
||||
// samples land outside [0, 3.3] V, warn the user; a
|
||||
// rectifier without a clamp is the canonical case.
|
||||
let clipped = 0;
|
||||
let observedMin = Infinity;
|
||||
let observedMax = -Infinity;
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
const v = Math.max(0, Math.min(3.3, samples[i]));
|
||||
const s = samples[i];
|
||||
if (s < observedMin) observedMin = s;
|
||||
if (s > observedMax) observedMax = s;
|
||||
if (s < 0 || s > 3.3) clipped++;
|
||||
const v = Math.max(0, Math.min(3.3, s));
|
||||
u12[i] = Math.round((v / 3.3) * 4095);
|
||||
}
|
||||
const clipKey = `${boardId}:${pinName}`;
|
||||
if (clipped > samples.length / 10 && !clipWarned.has(clipKey)) {
|
||||
clipWarned.add(clipKey);
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`[adc-clip] ${clipKey}: ${clipped}/${samples.length} samples outside [0, 3.3] V (range ${observedMin.toFixed(2)}…${observedMax.toFixed(2)} V). ESP32 ADC reads will saturate at the rails. Add a divider or clamp if you need the full swing.`,
|
||||
);
|
||||
}
|
||||
const gpioPin = gpioFn(pinName, channel);
|
||||
if (gpioPin < 0) continue;
|
||||
shim.setAdcWaveform(gpioPin, u12, periodNs);
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import {
|
|||
useSimulatorStore,
|
||||
getBoardPinManager,
|
||||
} from '../../store/useSimulatorStore';
|
||||
import { useElectricalStore } from '../../store/useElectricalStore';
|
||||
import { BOARD_PIN_GROUPS } from './boardPinGroups';
|
||||
import type { CircuitSimulationService } from './CircuitSimulationService';
|
||||
|
||||
|
|
@ -86,6 +87,27 @@ export function connectMcuEdgesToService(service: CircuitSimulationService): ()
|
|||
return String(arduinoPin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up which pin names this board actually wires into the SPICE
|
||||
* netlist. Reads from `pinNetMap` (populated after each solve) so
|
||||
* we subscribe to ~3-8 pins per board instead of all 64.
|
||||
*
|
||||
* Phase 1d #11: previously we subscribed to every Arduino pin 0..63
|
||||
* "since unused listeners are free" — true for AVR (8 pins) but
|
||||
* spammy for ESP32 (40+ GPIOs × multiple boards = thousands of
|
||||
* dead listeners). Now scoped to pins the circuit references.
|
||||
*/
|
||||
function pinsInCircuit(boardId: string): Set<string> {
|
||||
const { pinNetMap } = useElectricalStore.getState();
|
||||
const pins = new Set<string>();
|
||||
for (const key of pinNetMap.keys()) {
|
||||
const idx = key.indexOf(':');
|
||||
if (idx < 0) continue;
|
||||
if (key.slice(0, idx) === boardId) pins.add(key.slice(idx + 1));
|
||||
}
|
||||
return pins;
|
||||
}
|
||||
|
||||
function subscribeBoard(boardId: string, boardKind: string): void {
|
||||
const pm = getBoardPinManager(boardId);
|
||||
if (!pm) return;
|
||||
|
|
@ -95,13 +117,16 @@ export function connectMcuEdgesToService(service: CircuitSimulationService): ()
|
|||
const pinSubs = new Map<number, () => void>();
|
||||
boardSubs.set(boardId, pinSubs);
|
||||
|
||||
// Subscribe to every Arduino pin 0..63 — PinManager only fires
|
||||
// listeners that match real port events, so unused pins are
|
||||
// free. We cover digital + analog + RP2040/ESP32 GPIO ranges in
|
||||
// one sweep.
|
||||
const wanted = pinsInCircuit(boardId);
|
||||
|
||||
// Sweep 0..63 but only attach a listener when the pin name maps
|
||||
// to one of the wires in the current netlist. Re-subscription
|
||||
// when the canvas changes happens via `syncBoardSubscriptions` on
|
||||
// store-level board diffs and on `pinNetMap` updates below.
|
||||
for (let pin = 0; pin < 64; pin++) {
|
||||
const pinName = arduinoPinToName(pin, boardKind);
|
||||
if (!pinName) continue;
|
||||
if (wanted.size > 0 && !wanted.has(pinName)) continue;
|
||||
const unsub = pm.onPinChange(pin, (_p, state) => {
|
||||
schedulePin(boardId, pinName, state, vcc);
|
||||
});
|
||||
|
|
@ -133,8 +158,20 @@ export function connectMcuEdgesToService(service: CircuitSimulationService): ()
|
|||
if (state.boards !== prev.boards) syncBoardSubscriptions();
|
||||
});
|
||||
|
||||
// Re-subscribe when the pinNetMap changes — a new wire / removed
|
||||
// wire might add or drop pins that need listeners. Drop ALL subs
|
||||
// and re-create from the new pinNetMap (cheap: a Map clear and
|
||||
// ~10 pm.onPinChange calls).
|
||||
const unsubElectrical = useElectricalStore.subscribe((state, prev) => {
|
||||
if (state.pinNetMap === prev.pinNetMap) return;
|
||||
const boards = useSimulatorStore.getState().boards;
|
||||
for (const id of Array.from(boardSubs.keys())) unsubscribeBoard(id);
|
||||
for (const b of boards) subscribeBoard(b.id, b.boardKind);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubBoards();
|
||||
unsubElectrical();
|
||||
for (const pinSubs of boardSubs.values()) {
|
||||
for (const unsub of pinSubs.values()) unsub();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,29 @@ export function startSimulation(): () => void {
|
|||
const unsubService = service.start();
|
||||
const unsubAdc = connectAnalogInputsToMcu();
|
||||
const unsubEdges = connectMcuEdgesToService(service);
|
||||
|
||||
// Phase 1d #16 — debug helper. Call `__spiceDebug()` from DevTools
|
||||
// to get a snapshot of the simulation state (analysis mode, voltage
|
||||
// count, pin map, last solve time, etc.). Useful for diagnosing
|
||||
// "why is my circuit not solving?" reports from users.
|
||||
(window as unknown as { __spiceDebug?: () => void }).__spiceDebug = () => {
|
||||
const electrical = useElectricalStore.getState();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[__spiceDebug]', {
|
||||
analysisMode: electrical.analysisMode,
|
||||
converged: electrical.converged,
|
||||
error: electrical.error,
|
||||
lastSolveMs: electrical.lastSolveMs,
|
||||
nodeVoltageCount: Object.keys(electrical.nodeVoltages).length,
|
||||
branchCurrentCount: Object.keys(electrical.branchCurrents).length,
|
||||
pinNetMapSize: electrical.pinNetMap.size,
|
||||
hasTimeWaveforms: !!electrical.timeWaveforms,
|
||||
paused: electrical.paused,
|
||||
sampleVoltages: Object.entries(electrical.nodeVoltages).slice(0, 8),
|
||||
pinNetSample: [...electrical.pinNetMap.entries()].slice(0, 8),
|
||||
});
|
||||
};
|
||||
|
||||
return () => {
|
||||
unsubService();
|
||||
unsubAdc();
|
||||
|
|
|
|||
Loading…
Reference in New Issue