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.
This commit is contained in:
parent
b7d1e6469d
commit
5a9bb3a70e
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -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<string, ComponentRating> = {
|
||||
// ── 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'],
|
||||
},
|
||||
};
|
||||
Loading…
Reference in New Issue