feat(sim): P2 wiring ERC — missing power + dangling 2-terminal parts
First slice of the connection ("malas conexiones") checks, graph-based and run
before the solve so they report even on circuits too incomplete to solve:
- Missing power: a rated peripheral (sensor/display) wired into the circuit but
missing its VCC or GND connection -> warning. Boards are excluded (they live
in input.boards and self-power).
- Dangling 2-terminal part: a resistor / LED / capacitor / diode / inductor
connected on only one side (the other terminal floating) -> warning.
Both non-blocking. Verified zero false positives across all 69 gallery
examples. Tests: dangling resistor warns, fully-wired doesn't, module missing
GND warns.
This commit is contained in:
parent
4e20d03f4c
commit
8683c1ecf0
|
|
@ -376,6 +376,58 @@ describe('verifyCircuit — electrolytic capacitor', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('verifyCircuit — wiring ERC (bad connections)', () => {
|
||||
it(
|
||||
'warns when a 2-terminal part is connected on only one side',
|
||||
{ timeout: 30_000 },
|
||||
async () => {
|
||||
const input: BuildNetlistInput = {
|
||||
components: [pwr('src', 5), res('r1', '1k')],
|
||||
wires: [w('w1', ['src', 'SIG'], ['r1', '1'])], // r1 pin '2' left floating
|
||||
boards: [],
|
||||
analysis: { kind: 'op' },
|
||||
};
|
||||
const result = await verifyCircuit(input);
|
||||
const mc = result.warnings.find((x) => x.code === 'missing-connection' && x.componentId === 'r1');
|
||||
expect(mc, JSON.stringify(result.warnings)).toBeDefined();
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
'does NOT warn when both terminals are wired',
|
||||
{ timeout: 30_000 },
|
||||
async () => {
|
||||
const input: BuildNetlistInput = {
|
||||
components: [pwr('src', 5), res('r1', '1k')],
|
||||
wires: [
|
||||
w('w1', ['src', 'SIG'], ['r1', '1']),
|
||||
w('w2', ['r1', '2'], ['src', 'GND']),
|
||||
],
|
||||
boards: [],
|
||||
analysis: { kind: 'op' },
|
||||
};
|
||||
const result = await verifyCircuit(input);
|
||||
expect(result.warnings.map((x) => x.code)).not.toContain('missing-connection');
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
'warns when a powered module is missing its ground connection',
|
||||
{ timeout: 30_000 },
|
||||
async () => {
|
||||
const input: BuildNetlistInput = {
|
||||
components: [pwr('src', 5), { id: 'o1', metadataId: 'ssd1306', properties: {} }],
|
||||
wires: [w('w1', ['src', 'SIG'], ['o1', 'VIN'])], // VIN wired, GND not
|
||||
boards: [],
|
||||
analysis: { kind: 'op' },
|
||||
};
|
||||
const result = await verifyCircuit(input);
|
||||
const mc = result.warnings.find((x) => x.code === 'missing-connection' && x.componentId === 'o1');
|
||||
expect(mc, JSON.stringify(result.warnings)).toBeDefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// ── 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
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export type WarningCode =
|
|||
| 'led-overcurrent'
|
||||
| 'over-voltage'
|
||||
| 'reverse-polarity'
|
||||
| 'missing-connection'
|
||||
| 'resistor-overpower'
|
||||
| 'led-no-current';
|
||||
|
||||
|
|
@ -143,6 +144,73 @@ export async function verifyCircuit(
|
|||
}
|
||||
}
|
||||
|
||||
// ── Wiring ERC (graph-based, no solve) ─────────────────────────────────
|
||||
// Bad-connection checks that don't need a solve, so they run even when the
|
||||
// circuit is too incomplete to solve. Build a per-component set of wired pin
|
||||
// names from the wires first.
|
||||
const wiredPins = new Map<string, Set<string>>();
|
||||
for (const wire of input.wires) {
|
||||
for (const end of [wire.start, wire.end]) {
|
||||
let s = wiredPins.get(end.componentId);
|
||||
if (!s) {
|
||||
s = new Set();
|
||||
wiredPins.set(end.componentId, s);
|
||||
}
|
||||
s.add(end.pinName);
|
||||
}
|
||||
}
|
||||
const pinWired = (set: Set<string> | undefined, name: string): boolean => {
|
||||
if (!set) return false;
|
||||
if (set.has(name)) return true;
|
||||
// Tolerate ASCII '-' vs the Unicode minus on an electrolytic cap's "−" pin.
|
||||
if (name === '−' && set.has('-')) return true;
|
||||
if (name === '-' && set.has('−')) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
// Missing power: a rated peripheral wired into the circuit but missing its
|
||||
// supply or ground connection won't work. (Boards live in input.boards, not
|
||||
// input.components, so they're correctly excluded — a board self-powers.)
|
||||
for (const comp of input.components) {
|
||||
const rating = COMPONENT_RATINGS[comp.metadataId];
|
||||
if (!rating) continue;
|
||||
const set = wiredPins.get(comp.id);
|
||||
if (!set || set.size === 0) continue; // not connected yet — not a mistake
|
||||
if (!rating.supplyPins.some((sp) => pinWired(set, sp.name))) {
|
||||
warnings.push({
|
||||
severity: 'warning',
|
||||
code: 'missing-connection',
|
||||
componentId: comp.id,
|
||||
message: `${rating.label} ${comp.id} is wired up but its power (VCC) pin isn't connected — connect it to a supply or it won't work.`,
|
||||
});
|
||||
}
|
||||
if (!rating.gndPins.some((g) => pinWired(set, g))) {
|
||||
warnings.push({
|
||||
severity: 'warning',
|
||||
code: 'missing-connection',
|
||||
componentId: comp.id,
|
||||
message: `${rating.label} ${comp.id} is wired up but its ground (GND) pin isn't connected — connect GND or it won't work.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Dangling two-terminal part: a resistor / LED / capacitor / diode connected
|
||||
// on only one side has no current path through it.
|
||||
for (const comp of input.components) {
|
||||
const pins = twoTerminalPins(comp.metadataId);
|
||||
if (!pins) continue;
|
||||
const set = wiredPins.get(comp.id);
|
||||
if (!set) continue;
|
||||
if (pinWired(set, pins[0]) !== pinWired(set, pins[1])) {
|
||||
warnings.push({
|
||||
severity: 'warning',
|
||||
code: 'missing-connection',
|
||||
componentId: comp.id,
|
||||
message: `${comp.metadataId} ${comp.id} is connected on only one side — its other terminal is floating, so no current can flow through it.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let solve: ElectricalSolveResult | undefined;
|
||||
try {
|
||||
const cooked = await runSpice(netlist);
|
||||
|
|
@ -420,6 +488,27 @@ function isElectrolyticCap(metadataId: string): boolean {
|
|||
return metadataId === 'capacitor-electrolytic' || metadataId.startsWith('cap-elec');
|
||||
}
|
||||
|
||||
/** The two terminal pin names of a 2-terminal part, or null if not 2-terminal. */
|
||||
function twoTerminalPins(id: string): [string, string] | null {
|
||||
if (id === 'capacitor-electrolytic' || id.startsWith('cap-elec')) return ['+', '−'];
|
||||
if (
|
||||
id === 'resistor' ||
|
||||
id.startsWith('resistor-') ||
|
||||
id === 'capacitor' ||
|
||||
(id.startsWith('cap-') && !id.startsWith('cap-elec')) ||
|
||||
id === 'inductor'
|
||||
) {
|
||||
return ['1', '2'];
|
||||
}
|
||||
if (id === 'analog-resistor' || id === 'analog-capacitor' || id === 'analog-inductor') {
|
||||
return ['A', 'B'];
|
||||
}
|
||||
if (id === 'led' || id === 'diode' || id.startsWith('diode-') || id.startsWith('zener-')) {
|
||||
return ['A', 'C'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Parse a voltage rating like '25', '25V', '6.3' → volts (null if unparseable). */
|
||||
function parseVolts(raw: unknown): number | null {
|
||||
if (raw === undefined || raw === null) return null;
|
||||
|
|
|
|||
Loading…
Reference in New Issue