/** * 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 { const group = BOARD_PIN_GROUPS['arduino-uno'] ?? BOARD_PIN_GROUPS.default; const vcc = group.vcc; const result: Record = {}; const pinNames = new Set(); 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): 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); }, ); });