diff --git a/frontend/src/__tests__/pro-board-power-rails.test.ts b/frontend/src/__tests__/pro-board-power-rails.test.ts new file mode 100644 index 00000000..68601820 --- /dev/null +++ b/frontend/src/__tests__/pro-board-power-rails.test.ts @@ -0,0 +1,53 @@ +/** + * A pro board's supply rail. + * + * BOARD_PIN_GROUPS is keyed by the BoardKind union, so an overlay board could + * never appear in it and fell through to `default`: 5 V, with supply pads named + * "5V"/"VCC". Every RP2350 and ESP32-family board the overlay registers is a + * 3.3 V part whose pad is called "3V3", so the whole netlist solved at the wrong + * rail — silently. It surfaces as soon as anything analog is wired: a divider + * off the 3V3 pad of a Pimoroni Pico Plus 2 W solved at 5 V, and a potentiometer + * at half travel read 2.50 V where the hardware gives 1.65 V. + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { registerProBoards, type ProBoardDef } from '../lib/proBoardRegistry'; +import { boardPinGroupFor, BOARD_PIN_GROUPS } from '../simulation/spice/boardPinGroups'; + +const KIND = 'test-overlay-3v3-board'; + +describe('boardPinGroupFor', () => { + beforeEach(() => { + registerProBoards([ + { + kind: KIND, + label: '3V3 overlay board', + fqbn: null, + description: 'test', + tag: 'velxio-test-3v3', + size: { w: 10, h: 10 }, + power: { + vcc: 3.3, + gnd: ['GND', 'GND.1'], + vcc_pins: ['3V3'], + aux: { volts: 5, pins: ['VBUS', 'VSYS'] }, + }, + } as ProBoardDef, + ]); + }); + + it('uses the rail a pro board declares, not the 5 V default', () => { + const g = boardPinGroupFor(KIND); + expect(g.vcc).toBe(3.3); + expect(g.vcc_pins).toContain('3V3'); + expect(g.aux?.volts).toBe(5); + }); + + it('still answers for the boards the OSS table names', () => { + expect(boardPinGroupFor('raspberry-pi-pico').vcc).toBe(3.3); + expect(boardPinGroupFor('arduino-uno').vcc).toBe(5); + }); + + it('falls back to the default for a board nobody has described', () => { + expect(boardPinGroupFor('some-unregistered-kind')).toBe(BOARD_PIN_GROUPS.default); + }); +}); diff --git a/frontend/src/lib/proBoardRegistry.ts b/frontend/src/lib/proBoardRegistry.ts index e76d97a7..090392d2 100644 --- a/frontend/src/lib/proBoardRegistry.ts +++ b/frontend/src/lib/proBoardRegistry.ts @@ -99,6 +99,25 @@ export interface ProBoardDef { * capable GP40/41/42 through 1k) is declared under the name a wire uses. */ adcPins?: Array<{ pinName: string; channel: number }>; + /** + * Supply pads and logic voltage, for the SPICE netlist — the overlay's + * version of BOARD_PIN_GROUPS, which is keyed by the BoardKind union and so + * can never name a pro board. + * + * Without it a board falls back to that table's `default`: 5 V, ground pads + * called GND/GND.1/GND.2, supply pads called 5V/VCC. For a 3.3 V board with a + * "3V3" pad that is wrong twice over, and silently — the rail is simply + * solved at 5 V, so every divider, pull-up and LED current on the board is + * off by 1.5x. + * + * Shape matches BoardPinGroup: { vcc, gnd[], vcc_pins[], aux? }. + */ + power?: { + vcc: number; + gnd: string[]; + vcc_pins: string[]; + aux?: { volts: number; pins: string[] }; + }; /** 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/boardPinGroups.ts b/frontend/src/simulation/spice/boardPinGroups.ts index 8433e473..09278207 100644 --- a/frontend/src/simulation/spice/boardPinGroups.ts +++ b/frontend/src/simulation/spice/boardPinGroups.ts @@ -6,6 +6,7 @@ * Extend this table as new boards are added. */ import type { BoardKind } from '../../types/board'; +import { getProBoard } from '../../lib/proBoardRegistry'; export interface BoardPinGroup { /** Supply voltage (V). */ @@ -155,3 +156,24 @@ export const BOARD_PIN_GROUPS: Record = { aux: { volts: 5, pins: ['5V'] }, }, }; + +/** + * The pin classification for a board, including boards this table cannot name. + * + * BOARD_PIN_GROUPS is keyed by the BoardKind union, so an overlay board — whose + * kind is a runtime string — could never appear in it and fell through to + * `default`: a 5 V part whose only supply pad is called "5V". Every RP2350 and + * ESP32-family board registered by the overlay was therefore solved at the + * wrong rail. It shows up the moment anything analog is wired: a divider off + * the 3V3 pad of a Pimoroni Pico Plus 2 W solved at 5 V, so a potentiometer at + * half travel read 2.50 V instead of 1.65 V. + * + * A pro board declares its own supply pads through ProBoardDef.power. + */ +export function boardPinGroupFor(kind: string): BoardPinGroup { + const known = BOARD_PIN_GROUPS[kind as AllBoardKinds]; + if (known) return known; + const pro = getProBoard(kind)?.power; + if (pro) return pro; + return BOARD_PIN_GROUPS.default; +} diff --git a/frontend/src/simulation/spice/connectMcuEdgesToService.ts b/frontend/src/simulation/spice/connectMcuEdgesToService.ts index 9ec9ac6e..39323941 100644 --- a/frontend/src/simulation/spice/connectMcuEdgesToService.ts +++ b/frontend/src/simulation/spice/connectMcuEdgesToService.ts @@ -31,7 +31,7 @@ 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 { boardPinGroupFor } from './boardPinGroups'; import { pinNameToArduinoPin } from './collectPinStates'; import type { CircuitSimulationService } from './CircuitSimulationService'; @@ -137,7 +137,7 @@ export function connectMcuEdgesToService(service: CircuitSimulationService): () function subscribeBoard(boardId: string, boardKind: string): void { const pm = getBoardPinManager(boardId); if (!pm) return; - const group = BOARD_PIN_GROUPS[boardKind as keyof typeof BOARD_PIN_GROUPS] ?? BOARD_PIN_GROUPS.default; + const group = boardPinGroupFor(boardKind); const vcc = group.vcc; const pinSubs = new Map void>(); diff --git a/frontend/src/simulation/spice/storeAdapter.ts b/frontend/src/simulation/spice/storeAdapter.ts index a0a96ebe..c2d666bf 100644 --- a/frontend/src/simulation/spice/storeAdapter.ts +++ b/frontend/src/simulation/spice/storeAdapter.ts @@ -16,7 +16,7 @@ import type { } from './types'; import type { Wire } from '../../types/wire'; import type { BoardKind } from '../../types/board'; -import { BOARD_PIN_GROUPS } from './boardPinGroups'; +import { boardPinGroupFor } from './boardPinGroups'; import { parseValueWithUnits } from './valueParser'; import { PASSIVE_PRESETS } from './componentToSpice'; @@ -182,7 +182,7 @@ export function buildInputFromStore(snap: StoreSnapshot): BuildNetlistInput { })); const boards: BoardForSpice[] = snap.boards.map((b) => { - const group = BOARD_PIN_GROUPS[b.boardKind] ?? BOARD_PIN_GROUPS.default; + const group = boardPinGroupFor(b.boardKind); return { id: b.id, boardKind: b.boardKind, diff --git a/frontend/src/simulation/verify/verifyFromStore.ts b/frontend/src/simulation/verify/verifyFromStore.ts index e5958db9..a7cec3b8 100644 --- a/frontend/src/simulation/verify/verifyFromStore.ts +++ b/frontend/src/simulation/verify/verifyFromStore.ts @@ -10,7 +10,7 @@ import { useSimulatorStore } from '../../store/useSimulatorStore'; import { buildInputFromStore } from '../spice/storeAdapter'; -import { BOARD_PIN_GROUPS } from '../spice/boardPinGroups'; +import { boardPinGroupFor } from '../spice/boardPinGroups'; import type { PinSourceState } from '../spice/types'; import { verifyCircuit, type VerificationResult } from './circuitVerifier'; @@ -47,7 +47,7 @@ export async function verifyCircuitFromStore(): Promise = {}; - const group = BOARD_PIN_GROUPS[b.boardKind] ?? BOARD_PIN_GROUPS.default; + const group = boardPinGroupFor(b.boardKind); const wiredPinNames = new Set(); for (const w of sim.wires) { if (w.start.componentId === b.id) wiredPinNames.add(w.start.pinName);