diff --git a/frontend/src/__tests__/pro-board-adc-pins.test.ts b/frontend/src/__tests__/pro-board-adc-pins.test.ts new file mode 100644 index 00000000..b5357c43 --- /dev/null +++ b/frontend/src/__tests__/pro-board-adc-pins.test.ts @@ -0,0 +1,65 @@ +/** + * The SPICE analog path and overlay boards. + * + * ADC_PIN_MAP and ADC_PIN_TO_GPIO in connectAnalogInputsToMcu are keyed by the + * BoardKind union. An overlay board's kind is a runtime string that can never + * join that union, so every pro board was skipped before a single volt was + * injected — a divider or an LDR wired to one solved correctly in SPICE and + * then had nowhere to go. Parts that inject directly (a potentiometer, through + * partUtils) were unaffected, which is what made the gap easy to miss. + * + * A pro board now declares its own pads via ProBoardDef.adcPins. + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { registerProBoards, getProBoard, type ProBoardDef } from '../lib/proBoardRegistry'; + +const KIND = 'test-overlay-adc-board'; + +describe('ProBoardDef.adcPins', () => { + beforeEach(() => { + registerProBoards([ + { + kind: KIND, + label: 'Overlay ADC board', + fqbn: null, + description: 'test', + tag: 'velxio-test-adc', + size: { w: 10, h: 10 }, + // The shape the Pimoroni Pico Plus 2 W uses: pads silked with a GP + // number that are wired to a different, ADC-capable GPIO. + pinToNumber: (name: string) => (name === 'GP26' ? 26 : null), + adcPins: [{ pinName: 'GP26', channel: 0 }], + } as ProBoardDef, + ]); + }); + + it('is readable from the registry, which is what the SPICE path consults', () => { + const def = getProBoard(KIND); + expect(def?.adcPins).toEqual([{ pinName: 'GP26', channel: 0 }]); + }); + + it('carries the pad name a wire uses, not the GPIO behind it', () => { + // connectAnalogInputsToMcu looks the net up by `${boardId}:${pinName}`, so + // the name has to match pinInfo. Resolving it to a GPIO is pinToNumber's + // job, and the two can disagree — that is the whole point on a board whose + // pads are tied to a second GPIO. + const def = getProBoard(KIND)!; + const pin = def.adcPins![0]; + expect(pin.pinName).toBe('GP26'); + expect(def.pinToNumber!(pin.pinName)).toBe(26); + }); + + it('leaves a board that declares nothing without an analog path', () => { + registerProBoards([ + { + kind: 'test-overlay-no-adc', + label: 'No ADC', + fqbn: null, + description: 'test', + tag: 'velxio-test-noadc', + size: { w: 10, h: 10 }, + } as ProBoardDef, + ]); + expect(getProBoard('test-overlay-no-adc')?.adcPins).toBeUndefined(); + }); +}); diff --git a/frontend/src/lib/proBoardRegistry.ts b/frontend/src/lib/proBoardRegistry.ts index db7df46d..e76d97a7 100644 --- a/frontend/src/lib/proBoardRegistry.ts +++ b/frontend/src/lib/proBoardRegistry.ts @@ -84,6 +84,21 @@ export interface ProBoardDef { /** pinInfo name -> GPIO number (power/ground pins -> -1). Falls back to the * generic numeric parse when omitted. Return null for "not mine". */ pinToNumber?: (pinName: string) => number | null; + /** + * Which pads reach the ADC, and on which channel — the overlay's own version + * of the OSS ADC_PIN_MAP. + * + * That map is keyed by the BoardKind union, which no overlay kind can join, + * so the SPICE analog path skipped every pro board outright: a divider or an + * LDR wired to one of these boards solved correctly and then had nowhere to + * go. Parts that inject directly (a potentiometer through partUtils) were + * unaffected, which is why this looked like it worked. + * + * Names are pad names as they appear in pinInfo. A pad that is analog under a + * different silk (the Pimoroni Pico Plus 2 W ties GP26/27/28 to the ADC- + * capable GP40/41/42 through 1k) is declared under the name a wire uses. + */ + adcPins?: Array<{ pinName: string; channel: number }>; /** In-browser simulator factory (e.g. the RP2350/Hazard3 emulator). The pm * argument is the store's PinManager instance. */ createSimulator?: (pm: unknown) => ProBoardSimulator; diff --git a/frontend/src/simulation/spice/connectAnalogInputsToMcu.ts b/frontend/src/simulation/spice/connectAnalogInputsToMcu.ts index 79b7925a..9a487a83 100644 --- a/frontend/src/simulation/spice/connectAnalogInputsToMcu.ts +++ b/frontend/src/simulation/spice/connectAnalogInputsToMcu.ts @@ -31,6 +31,7 @@ import { setAdcVoltage } from '../parts/partUtils'; import type { BoardKind } from '../../types/board'; import { interpolateAt } from './waveformStats'; import { boardPinToNumber } from '../../utils/boardPinMapping'; +import { getProBoard } from '../../lib/proBoardRegistry'; // Which Arduino-style pin name maps to which ADC channel, per board. function adcRange(prefix: string, start: number, count: number) { @@ -119,6 +120,18 @@ const ADC_PIN_TO_GPIO: Partial | undefined { + return ADC_PIN_MAP[kind] ?? getProBoard(kind)?.adcPins; +} + /** Silkscreen name -> GPIO for the ADC injection. ESP32-family boards go * through the canonical boardPinToNumber table (nano-esp32 'A0' -> GPIO1, * xiao 'D1' -> GPIO2, bare '34' -> 34, ...); AVR/ATtiny/Pico keep their @@ -131,6 +144,13 @@ function gpioForBoardPin(kind: BoardKind, pinName: string, channel: number): num const n = boardPinToNumber(kind, pinName); if (n != null && n >= 0) return n; } + // An overlay board owns its pad->GPIO mapping (ProBoardDef.pinToNumber); + // ADC_PIN_TO_GPIO cannot hold it for the same union reason as above. + const proDef = getProBoard(kind); + if (proDef?.pinToNumber) { + const n = proDef.pinToNumber(pinName); + if (n != null && n >= 0) return n; + } const legacy = ADC_PIN_TO_GPIO[kind]; return legacy ? legacy(pinName, channel) : -1; } @@ -148,7 +168,7 @@ export function connectAnalogInputsToMcu(): () => void { const { nodeVoltages, pinNetMap, sourcedNets } = useElectricalStore.getState(); const { boards } = useSimulatorStore.getState(); for (const board of boards) { - const adcPins = ADC_PIN_MAP[board.boardKind]; + const adcPins = adcPinsFor(board.boardKind); if (!adcPins) continue; const sim = getBoardSimulator(board.id); if (!sim) continue; @@ -187,7 +207,7 @@ export function connectAnalogInputsToMcu(): () => void { const { boards } = useSimulatorStore.getState(); const { pinNetMap, timeWaveforms } = useElectricalStore.getState(); for (const board of boards) { - const adcPins = ADC_PIN_MAP[board.boardKind]; + const adcPins = adcPinsFor(board.boardKind); if (!adcPins) continue; const sim = getBoardSimulator(board.id); if (!sim) continue; @@ -257,7 +277,7 @@ export function connectAnalogInputsToMcu(): () => void { pushEsp32Waveforms(); const { boards } = useSimulatorStore.getState(); for (const board of boards) { - const adcPins = ADC_PIN_MAP[board.boardKind]; + const adcPins = adcPinsFor(board.boardKind); if (!adcPins) continue; const sim = getBoardSimulator(board.id); if (!sim) continue;