fix(spice): MCU-edge listeners detach on resubscription for QEMU boards (frozen LEDs)
connectMcuEdgesToService resubscribes its per-pin listeners whenever
pinNetMap changes. The subscription swept pin NUMBERS 0..63 and
reverse-mapped them to names ('GPIO2' on ESP32, 'GPIO17' on Pi) to match
against pinNetMap's keys — but those keys are the WIRE pin names ('2',
'4', 'A0'), so after the first solve the match failed for every pin and
the resubscription attached nothing. Any mid-run pinNetMap update then
silently killed the MCU-edge → SPICE path and the canvas froze at the
last solved state while the firmware kept toggling.
Masked until now because nothing perturbed pinNetMap mid-run on the
blink examples; pure ESP-IDF mode (#139) unmasked it — gpio_reset_pin()
leaves the internal pull-up enabled, the worker reports gpio_pull, the
handler requests an electrical resolve, pinNetMap gets a new identity,
and the ESP-IDF blink example's LED froze ON.
Fix: subscribe FROM the pinNetMap names, mapped to PinManager pins with
the same pinNameToArduinoPin the netlist collector uses (STM32 via
stm32PinNameToLinear), and hand schedulePin the netlist name so
handleMcuEdge's v_<board>_<pin> lookup hits the fast alterSource path
instead of a full rebuild per edge. The 0..63 sweep remains as the
pre-first-solve fallback. Also fixed pinNameToArduinoPin's dead 'GPIO'
branch ('GP' tested first turned 'GPIO32' into parseInt('IO32') = NaN).
This commit is contained in:
parent
734b7d0487
commit
4ff281bd40
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* Regression test for the MCU-edge listener pin-name mismatch.
|
||||
*
|
||||
* connectMcuEdgesToService subscribes its per-pin listeners from the
|
||||
* netlist's pinNetMap, whose keys are the WIRE pin names ('2', 'A0',
|
||||
* 'GP4'…). It must map those names to PinManager pin numbers with the
|
||||
* SAME function the netlist collector (collectPinStates) uses. The old
|
||||
* code reverse-mapped pin NUMBERS to names instead — producing 'GPIO2'
|
||||
* on ESP32, which never matched the wire's '2' — so any mid-run
|
||||
* resubscription (triggered e.g. by the gpio_pull that pure ESP-IDF's
|
||||
* gpio_reset_pin reports) silently detached every listener and froze
|
||||
* LEDs while the firmware kept toggling the pin.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { pinNameToArduinoPin } from '../simulation/spice/collectPinStates';
|
||||
|
||||
describe('pinNameToArduinoPin — netlist name to PinManager pin', () => {
|
||||
it('maps plain numeric ESP32 wire names (the esp32-idf-blink case)', () => {
|
||||
expect(pinNameToArduinoPin('2', 'esp32')).toBe(2);
|
||||
expect(pinNameToArduinoPin('4', 'esp32')).toBe(4);
|
||||
expect(pinNameToArduinoPin('21', 'esp32')).toBe(21);
|
||||
});
|
||||
|
||||
it('maps GPIO-prefixed names', () => {
|
||||
expect(pinNameToArduinoPin('GPIO32', 'esp32')).toBe(32);
|
||||
expect(pinNameToArduinoPin('GPIO2', 'esp32')).toBe(2);
|
||||
});
|
||||
|
||||
it('maps Pico GP names', () => {
|
||||
expect(pinNameToArduinoPin('GP4', 'raspberry-pi-pico')).toBe(4);
|
||||
});
|
||||
|
||||
it('maps Uno analog names', () => {
|
||||
expect(pinNameToArduinoPin('A0', 'arduino-uno')).toBe(14);
|
||||
expect(pinNameToArduinoPin('A5', 'arduino-uno')).toBe(19);
|
||||
});
|
||||
|
||||
it('maps ATtiny85 port names', () => {
|
||||
expect(pinNameToArduinoPin('PB3', 'attiny85')).toBe(3);
|
||||
});
|
||||
|
||||
it('rejects power pins so no listener attaches to rails', () => {
|
||||
expect(pinNameToArduinoPin('GND', 'esp32')).toBe(-1);
|
||||
expect(pinNameToArduinoPin('3V3', 'esp32')).toBe(-1);
|
||||
expect(pinNameToArduinoPin('GND', 'arduino-uno')).toBe(-1);
|
||||
expect(pinNameToArduinoPin('5V', 'arduino-uno')).toBe(-1);
|
||||
});
|
||||
});
|
||||
|
|
@ -18,18 +18,25 @@ import { BOARD_PIN_GROUPS } from './boardPinGroups';
|
|||
* Convert a board pin name (e.g. "9", "A0", "GP26", "GPIO32") to the
|
||||
* Arduino-style pin number that PinManager uses internally.
|
||||
* Returns -1 if the name doesn't map to a GPIO pin.
|
||||
*
|
||||
* Exported so connectMcuEdgesToService can subscribe its MCU-edge
|
||||
* listeners under the SAME name→pin mapping the netlist collector uses —
|
||||
* a reverse mapping that disagrees (e.g. "GPIO2" vs the wire's "2")
|
||||
* detaches every listener on resubscription and freezes LEDs mid-run.
|
||||
*/
|
||||
function pinNameToArduinoPin(pinName: string, boardKind: BoardKind): number {
|
||||
export function pinNameToArduinoPin(pinName: string, boardKind: BoardKind): number {
|
||||
const group = BOARD_PIN_GROUPS[boardKind] ?? BOARD_PIN_GROUPS.default;
|
||||
if (group.gnd.includes(pinName) || group.vcc_pins.includes(pinName)) return -1;
|
||||
if (pinName.startsWith('GP')) {
|
||||
const n = parseInt(pinName.slice(2), 10);
|
||||
return Number.isFinite(n) ? n : -1;
|
||||
}
|
||||
// 'GPIO' must be tested BEFORE 'GP': the GP branch used to shadow it,
|
||||
// turning 'GPIO32' into parseInt('IO32') = NaN → -1 (dead branch).
|
||||
if (pinName.startsWith('GPIO')) {
|
||||
const n = parseInt(pinName.slice(4), 10);
|
||||
return Number.isFinite(n) ? n : -1;
|
||||
}
|
||||
if (pinName.startsWith('GP')) {
|
||||
const n = parseInt(pinName.slice(2), 10);
|
||||
return Number.isFinite(n) ? n : -1;
|
||||
}
|
||||
if (/^A\d+$/.test(pinName)) {
|
||||
return 14 + parseInt(pinName.slice(1), 10);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,10 +27,12 @@ import {
|
|||
useSimulatorStore,
|
||||
getBoardPinManager,
|
||||
} from '../../store/useSimulatorStore';
|
||||
import { stm32LinearToPinName } from '../Stm32Bridge';
|
||||
import { stm32LinearToPinName, stm32PinNameToLinear } from '../Stm32Bridge';
|
||||
import { isStm32BoardKind, isPiBoardKind } from '../../types/board';
|
||||
import type { BoardKind } from '../../types/board';
|
||||
import { useElectricalStore } from '../../store/useElectricalStore';
|
||||
import { BOARD_PIN_GROUPS } from './boardPinGroups';
|
||||
import { pinNameToArduinoPin } from './collectPinStates';
|
||||
import type { CircuitSimulationService } from './CircuitSimulationService';
|
||||
|
||||
/** How long edges per pin coalesce. 16 ms ≈ 60 fps, well below any
|
||||
|
|
@ -143,14 +145,41 @@ export function connectMcuEdgesToService(service: CircuitSimulationService): ()
|
|||
|
||||
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;
|
||||
// Resolve which (pin number, pin name) pairs to listen on.
|
||||
//
|
||||
// When the netlist has been solved at least once, `wanted` holds the
|
||||
// EXACT pin names the wires reference ('2', 'A0', 'GP4', 'PC13', …) —
|
||||
// the same names collectPinStates keyed the V-sources on. Map each of
|
||||
// those through the SAME name→number function so the listener fires
|
||||
// on the right PinManager pin AND `handleMcuEdge` receives the name
|
||||
// whose `v_<board>_<name>` source actually exists (fast alterSource
|
||||
// path, no per-edge rebuild). The previous approach reversed pin
|
||||
// NUMBERS to names instead ('GPIO2' on ESP32) which never matched the
|
||||
// wire names, so every resubscription after a mid-run pinNetMap
|
||||
// change (e.g. a gpio_pull reported by pure ESP-IDF's gpio_reset_pin)
|
||||
// silently detached all MCU-edge listeners and froze LEDs.
|
||||
//
|
||||
// Before the first solve (`wanted` empty) fall back to the historical
|
||||
// 0..63 sweep with the reverse-mapped names.
|
||||
const listenPins: Array<{ pin: number; pinName: string }> = [];
|
||||
if (wanted.size > 0) {
|
||||
const isStm32 = isStm32BoardKind(boardKind);
|
||||
for (const pinName of wanted) {
|
||||
const pin = isStm32
|
||||
? stm32PinNameToLinear(pinName)
|
||||
: pinNameToArduinoPin(pinName, boardKind as BoardKind);
|
||||
if (pin < 0) continue;
|
||||
listenPins.push({ pin, pinName });
|
||||
}
|
||||
} else {
|
||||
for (let pin = 0; pin < 64; pin++) {
|
||||
const pinName = arduinoPinToName(pin, boardKind);
|
||||
if (!pinName) continue;
|
||||
listenPins.push({ pin, pinName });
|
||||
}
|
||||
}
|
||||
|
||||
for (const { pin, pinName } of listenPins) {
|
||||
const unsub = pm.onPinChange(pin, (_p, state) => {
|
||||
// Suppress digital edges when the pin has active PWM. The OCR-based
|
||||
// PWM duty is converted to a DC-averaged voltage in NetlistBuilder
|
||||
|
|
|
|||
Loading…
Reference in New Issue