From 5a9bb3a70e50bae27e9b469335b7e6c5c296551d Mon Sep 17 00:00:00 2001 From: David Montero Date: Wed, 17 Jun 2026 22:40:03 +0200 Subject: [PATCH] feat(sim): P1 over-voltage warnings for parts with a rated input voltage Adds a non-blocking circuit-verifier rule: a component whose supply pin sees more than its datasheet absolute-maximum voltage warns ("X V on the VIN pin -- above its Y V maximum; not emulated accurately"). This is the "fed too much voltage" mistake the operator asked for (a 3.3-5V module wired to a 9V battery). - New componentRatings.ts: per-PIN abs-max table (SSD1306/ILI9341 displays, DHT/BMP280/HC-SR04/MPU6050 sensors, NeoPixel, servo). Per-pin thresholds so a 3V3 pin (3.6V) and a VIN pin (6V) are judged separately. Unknown parts are simply not checked; an unwired or floating supply pin is skipped. - circuitVerifier reads each rated part's supply-vs-ground voltage from the solved nets (via pinNetMap) and warns when it exceeds the rating. - VCC/VDD/3V3/5V pins ride the shared vcc_rail net (NetlistBuilder convention); VIN is a normal net. Both handled. - Tests: 9V on a module VIN warns; 5V on VIN does not; a 3.3V pin on a 5V rail warns. Boards (esp32/pico/arduino) carry ratings in the table but aren't checked yet -- BoardForSpice doesn't thread its boardKind; follow-up. --- .../src/__tests__/circuit-verifier.test.ts | 70 +++++++++++ .../src/simulation/verify/circuitVerifier.ts | 51 +++++++- .../src/simulation/verify/componentRatings.ts | 109 ++++++++++++++++++ 3 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 frontend/src/simulation/verify/componentRatings.ts diff --git a/frontend/src/__tests__/circuit-verifier.test.ts b/frontend/src/__tests__/circuit-verifier.test.ts index 681b04d6..2fd80661 100644 --- a/frontend/src/__tests__/circuit-verifier.test.ts +++ b/frontend/src/__tests__/circuit-verifier.test.ts @@ -191,6 +191,76 @@ describe('verifyCircuit — threshold overrides', () => { ); }); +describe('verifyCircuit — over-voltage on rated parts', () => { + function part(id: string, metadataId: string): BuildNetlistInput['components'][number] { + return { id, metadataId, properties: {} }; + } + + it( + 'warns when a 3.3-5V module (SSD1306 VIN) is fed 9 V', + { timeout: 30_000 }, + async () => { + const input: BuildNetlistInput = { + components: [pwr('src', 9), part('oled1', 'ssd1306')], + wires: [ + w('w1', ['src', 'SIG'], ['oled1', 'VIN']), + w('w2', ['oled1', 'GND'], ['src', 'GND']), + ], + boards: [], + analysis: { kind: 'op' }, + }; + const result = await verifyCircuit(input); + const ov = result.warnings.find((x) => x.code === 'over-voltage'); + expect(ov, JSON.stringify(result.warnings)).toBeDefined(); + expect(ov?.componentId).toBe('oled1'); + // over-voltage is non-blocking + expect(result.errors.map((e) => e.code)).not.toContain('over-voltage'); + }, + ); + + it( + 'does NOT warn when the same module is fed a safe 5 V on VIN', + { timeout: 30_000 }, + async () => { + const input: BuildNetlistInput = { + components: [pwr('src', 5), part('oled2', 'ssd1306')], + wires: [ + w('w1', ['src', 'SIG'], ['oled2', 'VIN']), + w('w2', ['oled2', 'GND'], ['src', 'GND']), + ], + boards: [], + analysis: { kind: 'op' }, + }; + const result = await verifyCircuit(input); + expect(result.warnings.map((x) => x.code)).not.toContain('over-voltage'); + }, + ); + + it( + 'warns when a strict 3.3 V pin (SSD1306 3V3) sits on a 5 V rail', + { timeout: 30_000 }, + async () => { + // VCC-like pin names (VCC/VDD/3V3/5V) canonicalise to the shared + // `vcc_rail` net, which defaults to 5 V. A 10k load gives the rail a + // real path to ground so the .op solves. The OLED's 3V3 pin (abs max + // 3.6 V) on that 5 V rail must warn. + const input: BuildNetlistInput = { + components: [part('oled3', 'ssd1306'), res('rl', '10k')], + wires: [ + w('w1', ['oled3', '3V3'], ['rl', '1']), + w('w2', ['rl', '2'], ['oled3', 'GND']), + ], + boards: [], + analysis: { kind: 'op' }, + }; + const result = await verifyCircuit(input); + const ov = result.warnings.find((x) => x.code === 'over-voltage'); + expect(ov, JSON.stringify(result.warnings)).toBeDefined(); + expect(ov?.componentId).toBe('oled3'); + }, + ); +}); + // ── Sanity: shipping examples never trigger errors ───────────────────────── // If any gallery example produces a verifier error, that's a bug in the // example itself. Loop a handful of representative ones to catch diff --git a/frontend/src/simulation/verify/circuitVerifier.ts b/frontend/src/simulation/verify/circuitVerifier.ts index 8ec2f03a..e7a6de96 100644 --- a/frontend/src/simulation/verify/circuitVerifier.ts +++ b/frontend/src/simulation/verify/circuitVerifier.ts @@ -24,6 +24,7 @@ import { buildNetlist } from '../spice/NetlistBuilder'; import { runNetlist as runSpice } from '../spice/runNetlist'; import type { BuildNetlistInput, ElectricalSolveResult } from '../spice/types'; +import { COMPONENT_RATINGS } from './componentRatings'; export type WarningSeverity = 'error' | 'warning'; export type WarningCode = @@ -32,6 +33,7 @@ export type WarningCode = | 'short-circuit' | 'source-overload' | 'led-overcurrent' + | 'over-voltage' | 'resistor-overpower' | 'led-no-current'; @@ -96,7 +98,7 @@ export async function verifyCircuit( // Run a forced .op solve so currents are scalar and deterministic. const opInput: BuildNetlistInput = { ...input, analysis: { kind: 'op' } }; - const { netlist } = buildNetlist(opInput); + const { netlist, pinNetMap } = buildNetlist(opInput); let solve: ElectricalSolveResult | undefined; try { @@ -262,6 +264,47 @@ export async function verifyCircuit( } } + // ── Rule 4: over-voltage on a rated supply pin ───────────────────────── + // Components with a rated input voltage (sensors, displays, NeoPixels, …) + // warn when their supply pin carries more than the datasheet absolute + // maximum — the "fed too much voltage" mistake (e.g. a 3.3 V module wired to + // a 9 V battery). Non-blocking: the solve is still meaningful, but the real + // part would be damaged, so we surface it and flag that this operating point + // isn't emulated accurately. (Boards aren't checked yet — BoardForSpice + // doesn't carry its boardKind; that's a follow-up.) + for (const comp of input.components) { + const rating = COMPONENT_RATINGS[comp.metadataId]; + if (!rating) continue; + // Ground reference: first wired gnd pin, else circuit ground (0 V). + let gndV = 0; + for (const g of rating.gndPins) { + const gnet = pinNetMap.get(`${comp.id}:${g}`); + if (gnet !== undefined) { + gndV = gnet === '0' ? 0 : (solve.nodeVoltages[gnet] ?? 0); + break; + } + } + for (const sp of rating.supplyPins) { + const net = pinNetMap.get(`${comp.id}:${sp.name}`); + if (net === undefined || net === '0') continue; // pin not wired / tied to GND + const sv = solve.nodeVoltages[net]; + if (sv === undefined || !Number.isFinite(sv)) continue; // net floating / unsolved + const v = Math.abs(sv - gndV); + if (v > sp.absMaxVoltage) { + warnings.push({ + severity: 'warning', + code: 'over-voltage', + componentId: comp.id, + message: `${rating.label} ${comp.id} is seeing ${formatVolts(v)} on its ${sp.name} pin — above its ${formatVolts( + sp.absMaxVoltage, + )} absolute maximum. Real hardware would likely be damaged; this voltage is not emulated accurately. Use a level shifter or the correct supply voltage.`, + metric: v, + }); + break; // one over-voltage warning per part is enough + } + } + } + return { errors, warnings, @@ -279,6 +322,12 @@ function formatAmps(a: number): string { return `${a.toExponential(2)} A`; } +function formatVolts(v: number): string { + if (v >= 1) return `${v.toFixed(1)} V`; + if (v >= 1e-3) return `${(v * 1e3).toFixed(0)} mV`; + return `${v.toExponential(2)} V`; +} + function formatPower(w: number): string { if (w >= 1) return `${w.toFixed(2)} W`; return `${(w * 1e3).toFixed(0)} mW`; diff --git a/frontend/src/simulation/verify/componentRatings.ts b/frontend/src/simulation/verify/componentRatings.ts new file mode 100644 index 00000000..7bc78997 --- /dev/null +++ b/frontend/src/simulation/verify/componentRatings.ts @@ -0,0 +1,109 @@ +/** + * Absolute-maximum supply-voltage ratings for components/boards that have a + * rated input voltage. The circuit verifier reads the solved voltage across + * each part's supply pin (vs its ground pin) and warns — non-blocking — when + * it exceeds the datasheet absolute maximum. This is the classic "fed too much + * voltage" mistake: a 3.3 V module or sensor wired straight to a 9 V battery. + * + * Per-PIN thresholds (not one per part): a board's 3V3 pin tolerates far less + * than its VIN pin, so each supply pin carries its own absMax. The first + * supply pin that is actually wired AND has a solved voltage is checked; a + * pin that isn't wired (or whose net is floating) is skipped. Ground is the + * first wired gnd pin, falling back to circuit ground (net "0"). + * + * Thresholds are ABSOLUTE maximums chosen to NOT fire on normal 3.3 V / 5 V + * use (e.g. a 3V3 pin warns above ~3.6 V, a 5 V/VIN pin above ~6 V) so a + * warning means real hardware would likely be damaged. Adding a part here is + * safe — an unknown metadataId / boardKind is simply not checked. + */ +export interface SupplyPin { + /** Exact pin name as used in wiring (case-sensitive). */ + name: string; + /** Absolute-maximum voltage on this pin (vs ground) before damage, volts. */ + absMaxVoltage: number; +} + +export interface ComponentRating { + /** Human label used in the warning message. */ + label: string; + /** Supply pins with their individual absolute-max ratings. */ + supplyPins: SupplyPin[]; + /** Ground reference pin name(s); falls back to circuit ground if none wired. */ + gndPins: string[]; +} + +// Keyed by component metadataId OR board boardKind (both are checked). +export const COMPONENT_RATINGS: Record = { + // ── Peripheral modules (loads) — the high-value cases ────────────────────── + ssd1306: { + label: 'SSD1306 OLED', + supplyPins: [ + { name: '3V3', absMaxVoltage: 3.6 }, + { name: 'VIN', absMaxVoltage: 6 }, + ], + gndPins: ['GND'], + }, + ili9341: { label: 'ILI9341 display', supplyPins: [{ name: 'VCC', absMaxVoltage: 6 }], gndPins: ['GND'] }, + dht22: { label: 'DHT22 sensor', supplyPins: [{ name: 'VCC', absMaxVoltage: 6 }], gndPins: ['GND'] }, + dht11: { label: 'DHT11 sensor', supplyPins: [{ name: 'VCC', absMaxVoltage: 5.5 }], gndPins: ['GND'] }, + bmp280: { label: 'BMP280 sensor', supplyPins: [{ name: 'VCC', absMaxVoltage: 6 }], gndPins: ['GND'] }, + 'hc-sr04': { label: 'HC-SR04 sensor', supplyPins: [{ name: 'VCC', absMaxVoltage: 6 }], gndPins: ['GND'] }, + mpu6050: { label: 'MPU6050 IMU', supplyPins: [{ name: 'VCC', absMaxVoltage: 6 }], gndPins: ['GND'] }, + servo: { label: 'servo motor', supplyPins: [{ name: 'V+', absMaxVoltage: 7.2 }], gndPins: ['GND'] }, + neopixel: { label: 'NeoPixel', supplyPins: [{ name: 'VDD', absMaxVoltage: 6 }], gndPins: ['VSS', 'GND'] }, + + // ── Boards (checked against their own supply pins) ───────────────────────── + // Each board self-drives its VCC rail to its logic voltage, so normal use + // sits below these thresholds and never warns; only genuine over-drive does. + esp32: { + label: 'ESP32', + supplyPins: [ + { name: '3V3', absMaxVoltage: 3.6 }, + { name: 'VIN', absMaxVoltage: 6 }, + ], + gndPins: ['GND', 'GND.1', 'GND.2'], + }, + 'raspberry-pi-pico': { + label: 'Raspberry Pi Pico', + supplyPins: [ + { name: '3V3', absMaxVoltage: 3.6 }, + { name: 'VBUS', absMaxVoltage: 5.5 }, + { name: 'VSYS', absMaxVoltage: 5.5 }, + ], + gndPins: ['GND.1', 'GND.2', 'GND.3', 'GND.4', 'GND'], + }, + 'pi-pico-w': { + label: 'Raspberry Pi Pico W', + supplyPins: [ + { name: '3V3', absMaxVoltage: 3.6 }, + { name: 'VBUS', absMaxVoltage: 5.5 }, + { name: 'VSYS', absMaxVoltage: 5.5 }, + ], + gndPins: ['GND.1', 'GND.2', 'GND.3', 'GND.4', 'GND'], + }, + attiny85: { label: 'ATtiny85', supplyPins: [{ name: 'VCC', absMaxVoltage: 6 }], gndPins: ['GND'] }, + 'arduino-uno': { + label: 'Arduino Uno', + supplyPins: [ + { name: '5V', absMaxVoltage: 6 }, + { name: '3.3V', absMaxVoltage: 3.6 }, + ], + gndPins: ['GND.1', 'GND.2', 'GND.3', 'GND'], + }, + 'arduino-nano': { + label: 'Arduino Nano', + supplyPins: [ + { name: '5V', absMaxVoltage: 6 }, + { name: '3V3', absMaxVoltage: 3.6 }, + ], + gndPins: ['GND.1', 'GND.2', 'GND'], + }, + 'arduino-mega': { + label: 'Arduino Mega', + supplyPins: [ + { name: '5V', absMaxVoltage: 6 }, + { name: '3.3V', absMaxVoltage: 3.6 }, + ], + gndPins: ['GND.1', 'GND.2', 'GND.3', 'GND.4', 'GND'], + }, +};