velxio/frontend/src/__tests__/spice-npn-switch-integratio...

194 lines
6.5 KiB
TypeScript

/**
* End-to-end integration test for the npn-led-switch example.
*
* Runs a real AVR sketch that toggles pin 9 HIGH/LOW in a loop and exercises
* the complete solve pipeline:
* AVR port listener → PinManager → collectPinStates → NetlistBuilder →
* ngspice → branchCurrents
*
* This is the first test that can detect bugs *between* the PinManager and
* the SPICE netlist (the layer the `spice-npn-switch-diag.test.ts` skipped).
*/
import { describe, it, expect } from 'vitest';
import { AVRTestHarness, assemble, LDI, OUT, RJMP } from './helpers/avrTestHarness';
import { PinManager } from '../simulation/PinManager';
import { buildNetlist } from '../simulation/spice/NetlistBuilder';
import { runNetlist } from './helpers/testSolver';
import type { BuildNetlistInput, PinSourceState } from '../simulation/spice/types';
import { BOARD_PIN_GROUPS } from '../simulation/spice/boardPinGroups';
// ── Production collectPinStates, copied verbatim for test ─────────────────
// If this test ever diverges from subscribeToStore.ts, update both.
function collectPinStates(
pm: PinManager,
wires: Array<{
start: { componentId: string; pinName: string };
end: { componentId: string; pinName: string };
}>,
boardId = 'arduino-uno',
): Record<string, PinSourceState> {
const group = BOARD_PIN_GROUPS['arduino-uno'] ?? BOARD_PIN_GROUPS.default;
const vcc = group.vcc;
const result: Record<string, PinSourceState> = {};
const pinNames = new Set<string>();
for (const w of wires) {
if (w.start.componentId === boardId) pinNames.add(w.start.pinName);
if (w.end.componentId === boardId) pinNames.add(w.end.pinName);
}
for (const pinName of pinNames) {
if (group.gnd.includes(pinName) || group.vcc_pins.includes(pinName)) continue;
const arduinoPin = /^\d+$/.test(pinName) ? parseInt(pinName, 10) : -1;
if (arduinoPin < 0) continue;
const pwmDuty = pm.getPwmValue(arduinoPin);
if (pwmDuty > 0) {
result[pinName] = { type: 'pwm', duty: pwmDuty };
} else if (pm.getPinState(arduinoPin)) {
result[pinName] = { type: 'digital', v: vcc };
}
}
return result;
}
function npnSwitchWires() {
return [
{
id: 'w1',
start: { componentId: 'arduino-uno', pinName: '9' },
end: { componentId: 'rb', pinName: '1' },
},
{
id: 'w2',
start: { componentId: 'rb', pinName: '2' },
end: { componentId: 'q1', pinName: 'B' },
},
{
id: 'w3',
start: { componentId: 'arduino-uno', pinName: '5V' },
end: { componentId: 'rc', pinName: '1' },
},
{
id: 'w4',
start: { componentId: 'rc', pinName: '2' },
end: { componentId: 'led1', pinName: 'A' },
},
{
id: 'w5',
start: { componentId: 'led1', pinName: 'C' },
end: { componentId: 'q1', pinName: 'C' },
},
{
id: 'w6',
start: { componentId: 'q1', pinName: 'E' },
end: { componentId: 'arduino-uno', pinName: 'GND' },
},
];
}
function npnSwitchInput(pinStates: Record<string, PinSourceState>): BuildNetlistInput {
return {
components: [
{ id: 'rb', metadataId: 'resistor', properties: { value: '1000' } },
{ id: 'rc', metadataId: 'resistor', properties: { value: '220' } },
{ id: 'led1', metadataId: 'led', properties: { color: 'green' } },
{ id: 'q1', metadataId: 'bjt-2n2222', properties: {} },
],
wires: npnSwitchWires(),
boards: [
{
id: 'arduino-uno',
vcc: 5,
pins: pinStates,
groundPinNames: ['GND'],
vccPinNames: ['5V'],
},
],
analysis: { kind: 'op' },
};
}
/**
* AVR program: toggle PORTB bit 1 (pin 9) on/off in a tight loop.
* DDRB = 0x02 (pin 9 output)
* PORTB = 0x02, RJMP back; PORTB = 0x00, RJMP back
* Host calls runCycles(N) then reads PORTB to check pin 9 state.
*/
function pin9ToggleProgram(): Uint16Array {
return assemble([
LDI(16, 0x02),
OUT(0x04, 16), // DDRB = 0x02 (bit 1 = pin 9 output)
LDI(16, 0x02),
OUT(0x05, 16), // PORTB = 0x02 (pin 9 HIGH)
LDI(16, 0x00),
OUT(0x05, 16), // PORTB = 0x00 (pin 9 LOW)
RJMP(-5), // loop back — will toggle HIGH again
]);
}
describe('NPN LED switch — integration pipeline', () => {
it(
'pin 9 toggled by AVR → PinManager → SPICE produces correct LED currents',
{ timeout: 60_000 },
async () => {
const avr = new AVRTestHarness();
avr.loadProgram(pin9ToggleProgram());
const pm = new PinManager();
// Mirror AVRSimulator wiring: PORTB listener → PinManager.updatePort.
let lastPortB = 0;
avr.ports.B.addListener((value: number) => {
if (value !== lastPortB) {
pm.updatePort('PORTB', value, lastPortB);
lastPortB = value;
}
});
// Step cycle by cycle until pin 9 goes HIGH after the second OUT instruction.
let highCycle = -1;
for (let i = 0; i < 2000 && highCycle < 0; i++) {
avr.runCycles(1);
if (pm.getPinState(9)) highCycle = i;
}
console.log('pin 9 went HIGH after', highCycle, 'cycles');
expect(highCycle).toBeGreaterThanOrEqual(0);
// Solve with pin 9 HIGH and assert LED conducts.
const highPins = collectPinStates(pm, npnSwitchWires());
console.log('pinStates after HIGH write:', highPins);
expect(highPins['9']).toEqual({ type: 'digital', v: 5 });
const { netlist: nlHigh } = buildNetlist(npnSwitchInput(highPins));
const cookedHigh = await runNetlist(nlHigh);
const iHigh = Math.abs(cookedHigh.dcValue('i(v_led1_sense)'));
console.log('HIGH: LED current =', iHigh, 'A');
expect(iHigh).toBeGreaterThan(5e-3);
// Advance AVR by enough cycles for the OUT(PORTB, 0x00) to fire.
let lowCycle = -1;
for (let i = 0; i < 2000 && lowCycle < 0; i++) {
avr.runCycles(1);
if (!pm.getPinState(9)) lowCycle = i;
}
console.log('pin 9 went LOW after', lowCycle, 'additional cycles');
expect(lowCycle).toBeGreaterThanOrEqual(0);
const lowPins = collectPinStates(pm, npnSwitchWires());
console.log('pinStates after LOW write:', lowPins);
expect(lowPins['9']).toBeUndefined();
const { netlist: nlLow } = buildNetlist(npnSwitchInput(lowPins));
console.log('\n=== NETLIST (after AVR LOW) ===\n' + nlLow);
const cookedLow = await runNetlist(nlLow);
const iLow = Math.abs(cookedLow.dcValue('i(v_led1_sense)'));
console.log('LOW: LED current =', iLow, 'A');
expect(iLow).toBeLessThan(1e-6);
},
);
});