feat(sim): P1 over-voltage for boards + electrolytic capacitors

Extends the over-voltage rule to the two cases the previous slice deferred:

- Boards (ESP32 / Pico / Arduino / ...): a board's supply pins all collapse to
  the self-driven vcc_rail net, so an external source on them makes the .op
  singular rather than readable. Added a graph-based check (runs before the
  solve): if a power source is wired to a board supply pin and its nominal
  voltage exceeds that pin's rating, warn. Threaded boardKind into
  BoardForSpice so the verifier can look up the board rating.
- Electrolytic capacitors: new `voltage` rating property (select, default 25V,
  on capacitor-electrolytic + cap-elec-* presets, via component-overrides +
  regenerated metadata). The verifier reads the DC voltage across the +/- pins
  and warns on over-voltage (vent/burst) and on reverse polarity (a polarized
  cap wired backwards). Defaults to 25V when the property is unset.

Tests: 9V battery -> ESP32 VIN warns, 1.5V doesn't; 24V across a 16V cap warns,
5V across a 25V cap doesn't; reverse-biased cap warns. All real-ngspice.
This commit is contained in:
David Montero 2026-06-18 01:19:13 +02:00
parent 12ed306efb
commit 4e20d03f4c
6 changed files with 813 additions and 59 deletions

View File

@ -3589,10 +3589,29 @@
"type": "string",
"defaultValue": "1u",
"control": "text"
},
{
"name": "voltage",
"type": "string",
"control": "select",
"options": [
"6.3",
"10",
"16",
"25",
"35",
"50",
"63",
"100",
"200",
"400"
],
"defaultValue": "25"
}
],
"defaultValues": {
"value": "1u"
"value": "1u",
"voltage": "25"
},
"pinCount": 0,
"id": "cap-elec-1u",
@ -3615,10 +3634,29 @@
"type": "string",
"defaultValue": "10u",
"control": "text"
},
{
"name": "voltage",
"type": "string",
"control": "select",
"options": [
"6.3",
"10",
"16",
"25",
"35",
"50",
"63",
"100",
"200",
"400"
],
"defaultValue": "25"
}
],
"defaultValues": {
"value": "10u"
"value": "10u",
"voltage": "25"
},
"pinCount": 0,
"id": "cap-elec-10u",
@ -3641,10 +3679,29 @@
"type": "string",
"defaultValue": "100u",
"control": "text"
},
{
"name": "voltage",
"type": "string",
"control": "select",
"options": [
"6.3",
"10",
"16",
"25",
"35",
"50",
"63",
"100",
"200",
"400"
],
"defaultValue": "25"
}
],
"defaultValues": {
"value": "100u"
"value": "100u",
"voltage": "25"
},
"pinCount": 0,
"id": "cap-elec-100u",
@ -3667,10 +3724,29 @@
"type": "string",
"defaultValue": "1000u",
"control": "text"
},
{
"name": "voltage",
"type": "string",
"control": "select",
"options": [
"6.3",
"10",
"16",
"25",
"35",
"50",
"63",
"100",
"200",
"400"
],
"defaultValue": "25"
}
],
"defaultValues": {
"value": "1000u"
"value": "1000u",
"voltage": "25"
},
"pinCount": 0,
"id": "cap-elec-1000u",
@ -3693,10 +3769,29 @@
"type": "string",
"defaultValue": "47u",
"control": "text"
},
{
"name": "voltage",
"type": "string",
"control": "select",
"options": [
"6.3",
"10",
"16",
"25",
"35",
"50",
"63",
"100",
"200",
"400"
],
"defaultValue": "25"
}
],
"defaultValues": {
"value": "47u"
"value": "47u",
"voltage": "25"
},
"pinCount": 0,
"id": "cap-elec-47u",
@ -3719,10 +3814,29 @@
"type": "string",
"defaultValue": "470u",
"control": "text"
},
{
"name": "voltage",
"type": "string",
"control": "select",
"options": [
"6.3",
"10",
"16",
"25",
"35",
"50",
"63",
"100",
"200",
"400"
],
"defaultValue": "25"
}
],
"defaultValues": {
"value": "470u"
"value": "470u",
"voltage": "25"
},
"pinCount": 0,
"id": "cap-elec-470u",
@ -3744,10 +3858,29 @@
"type": "string",
"defaultValue": "10u",
"control": "text"
},
{
"name": "voltage",
"type": "string",
"control": "select",
"options": [
"6.3",
"10",
"16",
"25",
"35",
"50",
"63",
"100",
"200",
"400"
],
"defaultValue": "25"
}
],
"defaultValues": {
"value": "10u"
"value": "10u",
"voltage": "25"
},
"pinCount": 0,
"id": "capacitor-electrolytic",

View File

@ -261,6 +261,121 @@ describe('verifyCircuit — over-voltage on rated parts', () => {
);
});
describe('verifyCircuit — board over-voltage (graph-based)', () => {
function board(id: string, boardKind: string): BuildNetlistInput['boards'][number] {
return {
id,
boardKind,
vcc: 3.3,
pins: {},
groundPinNames: ['GND', 'GND.1', 'GND.2'],
vccPinNames: ['3V3', 'VIN', '5V'],
};
}
it(
'warns when a 9 V battery is wired to an ESP32 VIN pin',
{ timeout: 30_000 },
async () => {
const input: BuildNetlistInput = {
components: [{ id: 'bat', metadataId: 'battery-9v', properties: {} }],
wires: [
{ id: 'w1', start: { componentId: 'bat', pinName: '+' }, end: { componentId: 'esp32', pinName: 'VIN' } },
{ id: 'w2', start: { componentId: 'esp32', pinName: 'GND' }, end: { componentId: 'bat', pinName: '' } },
],
boards: [board('esp32', 'esp32')],
analysis: { kind: 'op' },
};
const result = await verifyCircuit(input);
const ov = result.warnings.find((w) => w.code === 'over-voltage' && w.componentId === 'esp32');
expect(ov, JSON.stringify(result.warnings)).toBeDefined();
},
);
it(
'does NOT warn when an AA battery (1.5 V) powers the VIN pin',
{ timeout: 30_000 },
async () => {
const input: BuildNetlistInput = {
components: [{ id: 'bat', metadataId: 'battery-aa', properties: {} }],
wires: [
{ id: 'w1', start: { componentId: 'bat', pinName: '+' }, end: { componentId: 'esp32', pinName: 'VIN' } },
{ id: 'w2', start: { componentId: 'esp32', pinName: 'GND' }, end: { componentId: 'bat', pinName: '' } },
],
boards: [board('esp32', 'esp32')],
analysis: { kind: 'op' },
};
const result = await verifyCircuit(input);
expect(result.warnings.map((w) => w.code)).not.toContain('over-voltage');
},
);
});
describe('verifyCircuit — electrolytic capacitor', () => {
function cap(id: string, voltage: string): BuildNetlistInput['components'][number] {
return { id, metadataId: 'capacitor-electrolytic', properties: { value: '10u', voltage } };
}
it(
'warns when the voltage across it exceeds its rating',
{ timeout: 30_000 },
async () => {
// 24 V across a 16 V cap.
const input: BuildNetlistInput = {
components: [pwr('src', 24), cap('c1', '16')],
wires: [
w('w1', ['src', 'SIG'], ['c1', '+']),
w('w2', ['c1', ''], ['src', 'GND']),
],
boards: [],
analysis: { kind: 'op' },
};
const result = await verifyCircuit(input);
const ov = result.warnings.find((x) => x.code === 'over-voltage' && x.componentId === 'c1');
expect(ov, JSON.stringify(result.warnings)).toBeDefined();
},
);
it(
'does NOT warn when within its rating',
{ timeout: 30_000 },
async () => {
const input: BuildNetlistInput = {
components: [pwr('src', 5), cap('c2', '25')],
wires: [
w('w1', ['src', 'SIG'], ['c2', '+']),
w('w2', ['c2', ''], ['src', 'GND']),
],
boards: [],
analysis: { kind: 'op' },
};
const result = await verifyCircuit(input);
expect(result.warnings.map((x) => x.code)).not.toContain('over-voltage');
expect(result.warnings.map((x) => x.code)).not.toContain('reverse-polarity');
},
);
it(
'warns on reverse polarity',
{ timeout: 30_000 },
async () => {
// +5 V on the minus pin, plus pin to ground → reverse-biased.
const input: BuildNetlistInput = {
components: [pwr('src', 5), cap('c3', '25')],
wires: [
w('w1', ['src', 'SIG'], ['c3', '']),
w('w2', ['c3', '+'], ['src', 'GND']),
],
boards: [],
analysis: { kind: 'op' },
};
const result = await verifyCircuit(input);
const rp = result.warnings.find((x) => x.code === 'reverse-polarity' && x.componentId === 'c3');
expect(rp, 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

View File

@ -185,6 +185,7 @@ export function buildInputFromStore(snap: StoreSnapshot): BuildNetlistInput {
const group = BOARD_PIN_GROUPS[b.boardKind] ?? BOARD_PIN_GROUPS.default;
return {
id: b.id,
boardKind: b.boardKind,
vcc: group.vcc,
pins: b.pinStates,
groundPinNames: group.gnd,

View File

@ -32,6 +32,9 @@ export interface WireForSpice {
export interface BoardForSpice {
/** Stable unique board id (used as prefix in net names). */
id: string;
/** Board kind (e.g. 'arduino-uno', 'esp32') used to look up over-voltage
* ratings in the verifier. Optional: older callers may omit it. */
boardKind?: string;
/** Supply voltage of this board, V. */
vcc: number;
/**

View File

@ -34,6 +34,7 @@ export type WarningCode =
| 'source-overload'
| 'led-overcurrent'
| 'over-voltage'
| 'reverse-polarity'
| 'resistor-overpower'
| 'led-no-current';
@ -100,6 +101,48 @@ export async function verifyCircuit(
const opInput: BuildNetlistInput = { ...input, analysis: { kind: 'op' } };
const { netlist, pinNetMap } = buildNetlist(opInput);
// ── Board over-voltage (graph-based, no solve) ─────────────────────────
// Runs BEFORE the solve so it still reports even when an external source on
// a board's vcc rail makes the .op singular. A board's supply pins (3V3 /
// 5V / VIN / VBUS / VSYS) all collapse to the single self-driven `vcc_rail`
// net, so an over-voltage can't be read from node voltages — instead check
// the wiring directly: a power source wired to a board supply pin whose
// nominal voltage exceeds that pin's rating. Non-blocking.
const boardWarned = new Set<string>();
for (const board of input.boards) {
if (!board.boardKind) continue;
const rating = COMPONENT_RATINGS[board.boardKind];
if (!rating) continue;
for (const wire of input.wires) {
if (boardWarned.has(board.id)) break;
for (const [boardEnd, srcEnd] of [
[wire.start, wire.end],
[wire.end, wire.start],
] as const) {
if (boardEnd.componentId !== board.id) continue;
const sp = rating.supplyPins.find((p) => p.name === boardEnd.pinName);
if (!sp) continue;
const src = input.components.find((c) => c.id === srcEnd.componentId);
if (!src) continue;
const info = sourceInfo(src);
if (!info || !info.posPins.includes(srcEnd.pinName)) continue;
if (info.volts > sp.absMaxVoltage) {
boardWarned.add(board.id);
warnings.push({
severity: 'warning',
code: 'over-voltage',
componentId: board.id,
message: `${rating.label} ${board.id} has ${formatVolts(info.volts)} wired to its ${sp.name} pin — above its ${formatVolts(
sp.absMaxVoltage,
)} maximum. Real hardware would likely be damaged. Use the correct supply voltage (a regulator or level shifter).`,
metric: info.volts,
});
break;
}
}
}
}
let solve: ElectricalSolveResult | undefined;
try {
const cooked = await runSpice(netlist);
@ -305,6 +348,45 @@ export async function verifyCircuit(
}
}
// ── Rule 6: electrolytic capacitor over-voltage / reverse polarity ─────
// Electrolytic caps have a voltage rating (over it they vent / burst) and a
// polarity (reverse-biasing destroys them). Their +/- pins are normal nets,
// so the DC voltage across them is read straight from the solve.
for (const c of input.components) {
if (!isElectrolyticCap(c.metadataId)) continue;
const posNet = pinNetMap.get(`${c.id}:+`);
const negNet = pinNetMap.get(`${c.id}:`) ?? pinNetMap.get(`${c.id}:-`);
if (posNet === undefined || negNet === undefined) continue; // not fully wired
const vPos = posNet === '0' ? 0 : solve.nodeVoltages[posNet];
const vNeg = negNet === '0' ? 0 : solve.nodeVoltages[negNet];
if (vPos === undefined || vNeg === undefined || !Number.isFinite(vPos) || !Number.isFinite(vNeg)) {
continue; // a terminal net is floating / unsolved
}
const v = vPos - vNeg;
const rating = parseVolts(c.properties.voltage) ?? 25;
if (v > rating) {
warnings.push({
severity: 'warning',
code: 'over-voltage',
componentId: c.id,
message: `Electrolytic capacitor ${c.id} has ${formatVolts(v)} across it — above its ${formatVolts(
rating,
)} rating. A real capacitor would vent or burst; use a higher-voltage part.`,
metric: v,
});
} else if (v < -0.5) {
warnings.push({
severity: 'warning',
code: 'reverse-polarity',
componentId: c.id,
message: `Electrolytic capacitor ${c.id} is reverse-biased (${formatVolts(
Math.abs(v),
)} backwards). Polarized capacitors are destroyed when connected backwards swap its + and - terminals.`,
metric: v,
});
}
}
return {
errors,
warnings,
@ -313,6 +395,38 @@ export async function verifyCircuit(
};
}
/** Nominal positive-output voltage + positive pin names of a power source. */
function sourceInfo(
comp: BuildNetlistInput['components'][number],
): { posPins: string[]; volts: number } | null {
const id = comp.metadataId;
if (id === 'battery-9v') return { posPins: ['+'], volts: 9 };
if (id === 'battery-aa') return { posPins: ['+'], volts: 1.5 };
if (id === 'battery-coin-cell') return { posPins: ['+'], volts: 3 };
if (id === 'power-supply') {
return { posPins: ['+', 'SIG', 'VCC'], volts: Math.abs(Number(comp.properties.voltage ?? 5)) };
}
if (id === 'signal-generator') {
const off = Number(comp.properties.offset ?? 0);
const amp = String(comp.properties.waveform ?? 'sine').toLowerCase() === 'dc'
? 0
: Number(comp.properties.amplitude ?? 0);
return { posPins: ['SIG', '+'], volts: Math.abs(off) + Math.abs(amp) };
}
return null;
}
function isElectrolyticCap(metadataId: string): boolean {
return metadataId === 'capacitor-electrolytic' || metadataId.startsWith('cap-elec');
}
/** 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;
const m = /^\s*([0-9]*\.?[0-9]+)/.exec(String(raw));
return m ? parseFloat(m[1]!) : null;
}
// ── Formatting helpers ────────────────────────────────────────────────────
function formatAmps(a: number): string {

View File

@ -10,7 +10,16 @@
"properties": [],
"defaultValues": {},
"pinCount": 16,
"tags": ["a4988", "stepper", "driver", "motor", "step", "dir", "pololu", "motors"],
"tags": [
"a4988",
"stepper",
"driver",
"motor",
"step",
"dir",
"pololu",
"motors"
],
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect width=\"64\" height=\"64\" fill=\"#1a2332\" rx=\"4\"/>\n <rect x=\"16\" y=\"7\" width=\"32\" height=\"50\" rx=\"3\" fill=\"#0b3d2e\" stroke=\"#2d8f5a\" stroke-width=\"0.8\"/>\n <rect x=\"25\" y=\"25\" width=\"14\" height=\"14\" rx=\"1\" fill=\"#15151a\" stroke=\"#333\"/>\n <text x=\"32\" y=\"19\" text-anchor=\"middle\" font-family=\"sans-serif\" font-size=\"6\" fill=\"#cfe8d8\" font-weight=\"bold\">A4988</text>\n <g fill=\"#d4af37\"><rect x=\"11\" y=\"14\" width=\"5\" height=\"2\"/><rect x=\"11\" y=\"22\" width=\"5\" height=\"2\"/><rect x=\"11\" y=\"30\" width=\"5\" height=\"2\"/><rect x=\"11\" y=\"38\" width=\"5\" height=\"2\"/><rect x=\"48\" y=\"14\" width=\"5\" height=\"2\"/><rect x=\"48\" y=\"22\" width=\"5\" height=\"2\"/><rect x=\"48\" y=\"30\" width=\"5\" height=\"2\"/><rect x=\"48\" y=\"38\" width=\"5\" height=\"2\"/></g>\n <text x=\"32\" y=\"51\" text-anchor=\"middle\" font-family=\"sans-serif\" font-size=\"5\" fill=\"#8fbfa6\">STEP/DIR</text></svg>"
},
{
@ -1670,10 +1679,29 @@
"type": "string",
"defaultValue": "10u",
"control": "text"
},
{
"name": "voltage",
"type": "string",
"control": "select",
"options": [
"6.3",
"10",
"16",
"25",
"35",
"50",
"63",
"100",
"200",
"400"
],
"defaultValue": "25"
}
],
"defaultValues": {
"value": "10u"
"value": "10u",
"voltage": "25"
},
"pinCount": 0,
"tags": [
@ -1695,10 +1723,29 @@
"type": "string",
"defaultValue": "1u",
"control": "text"
},
{
"name": "voltage",
"type": "string",
"control": "select",
"options": [
"6.3",
"10",
"16",
"25",
"35",
"50",
"63",
"100",
"200",
"400"
],
"defaultValue": "25"
}
],
"defaultValues": {
"value": "1u"
"value": "1u",
"voltage": "25"
},
"pinCount": 0,
"tags": [
@ -1721,10 +1768,29 @@
"type": "string",
"defaultValue": "10u",
"control": "text"
},
{
"name": "voltage",
"type": "string",
"control": "select",
"options": [
"6.3",
"10",
"16",
"25",
"35",
"50",
"63",
"100",
"200",
"400"
],
"defaultValue": "25"
}
],
"defaultValues": {
"value": "10u"
"value": "10u",
"voltage": "25"
},
"pinCount": 0,
"tags": [
@ -1747,10 +1813,29 @@
"type": "string",
"defaultValue": "47u",
"control": "text"
},
{
"name": "voltage",
"type": "string",
"control": "select",
"options": [
"6.3",
"10",
"16",
"25",
"35",
"50",
"63",
"100",
"200",
"400"
],
"defaultValue": "25"
}
],
"defaultValues": {
"value": "47u"
"value": "47u",
"voltage": "25"
},
"pinCount": 0,
"tags": [
@ -1773,10 +1858,29 @@
"type": "string",
"defaultValue": "100u",
"control": "text"
},
{
"name": "voltage",
"type": "string",
"control": "select",
"options": [
"6.3",
"10",
"16",
"25",
"35",
"50",
"63",
"100",
"200",
"400"
],
"defaultValue": "25"
}
],
"defaultValues": {
"value": "100u"
"value": "100u",
"voltage": "25"
},
"pinCount": 0,
"tags": [
@ -1799,10 +1903,29 @@
"type": "string",
"defaultValue": "470u",
"control": "text"
},
{
"name": "voltage",
"type": "string",
"control": "select",
"options": [
"6.3",
"10",
"16",
"25",
"35",
"50",
"63",
"100",
"200",
"400"
],
"defaultValue": "25"
}
],
"defaultValues": {
"value": "470u"
"value": "470u",
"voltage": "25"
},
"pinCount": 0,
"tags": [
@ -1825,10 +1948,29 @@
"type": "string",
"defaultValue": "1000u",
"control": "text"
},
{
"name": "voltage",
"type": "string",
"control": "select",
"options": [
"6.3",
"10",
"16",
"25",
"35",
"50",
"63",
"100",
"200",
"400"
],
"defaultValue": "25"
}
],
"defaultValues": {
"value": "1000u"
"value": "1000u",
"voltage": "25"
},
"pinCount": 0,
"tags": [
@ -1919,12 +2061,34 @@
"category": "displays",
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect x=\"4\" y=\"6\" width=\"56\" height=\"48\" rx=\"3\" fill=\"#e8e2d4\" stroke=\"#b8aa90\"/><rect x=\"10\" y=\"12\" width=\"44\" height=\"36\" fill=\"#f4f1e8\" stroke=\"#a89a80\"/><text x=\"32\" y=\"34\" text-anchor=\"middle\" font-size=\"7\" fill=\"#5a5040\" font-family=\"monospace\">e-Paper</text><rect x=\"22\" y=\"54\" width=\"20\" height=\"6\" fill=\"#d49a3c\"/></svg>",
"properties": [
{ "name": "panelKind", "type": "string", "defaultValue": "epaper-1in54-bw", "control": "text" },
{ "name": "refreshMs", "type": "number", "defaultValue": 50, "control": "number" }
{
"name": "panelKind",
"type": "string",
"defaultValue": "epaper-1in54-bw",
"control": "text"
},
{
"name": "refreshMs",
"type": "number",
"defaultValue": 50,
"control": "number"
}
],
"defaultValues": { "panelKind": "epaper-1in54-bw", "refreshMs": 50 },
"defaultValues": {
"panelKind": "epaper-1in54-bw",
"refreshMs": 50
},
"pinCount": 8,
"tags": ["epaper", "e-paper", "e-ink", "display", "ssd1681", "1.54", "gxepd2", "spi"]
"tags": [
"epaper",
"e-paper",
"e-ink",
"display",
"ssd1681",
"1.54",
"gxepd2",
"spi"
]
},
{
"id": "epaper-2in13-bw",
@ -1933,12 +2097,35 @@
"category": "displays",
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect x=\"2\" y=\"18\" width=\"60\" height=\"30\" rx=\"3\" fill=\"#e8e2d4\" stroke=\"#b8aa90\"/><rect x=\"6\" y=\"22\" width=\"52\" height=\"22\" fill=\"#f4f1e8\" stroke=\"#a89a80\"/><text x=\"32\" y=\"36\" text-anchor=\"middle\" font-size=\"6\" fill=\"#5a5040\" font-family=\"monospace\">2.13\"</text><rect x=\"24\" y=\"48\" width=\"16\" height=\"5\" fill=\"#d49a3c\"/></svg>",
"properties": [
{ "name": "panelKind", "type": "string", "defaultValue": "epaper-2in13-bw", "control": "text" },
{ "name": "refreshMs", "type": "number", "defaultValue": 50, "control": "number" }
{
"name": "panelKind",
"type": "string",
"defaultValue": "epaper-2in13-bw",
"control": "text"
},
{
"name": "refreshMs",
"type": "number",
"defaultValue": 50,
"control": "number"
}
],
"defaultValues": { "panelKind": "epaper-2in13-bw", "refreshMs": 50 },
"defaultValues": {
"panelKind": "epaper-2in13-bw",
"refreshMs": 50
},
"pinCount": 8,
"tags": ["epaper", "e-paper", "e-ink", "display", "ssd1675", "il3897", "2.13", "gxepd2", "spi"]
"tags": [
"epaper",
"e-paper",
"e-ink",
"display",
"ssd1675",
"il3897",
"2.13",
"gxepd2",
"spi"
]
},
{
"id": "epaper-2in13-bwr",
@ -1947,12 +2134,36 @@
"category": "displays",
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect x=\"2\" y=\"18\" width=\"60\" height=\"30\" rx=\"3\" fill=\"#e8e2d4\" stroke=\"#b8aa90\"/><rect x=\"6\" y=\"22\" width=\"52\" height=\"22\" fill=\"#f4f1e8\" stroke=\"#a89a80\"/><circle cx=\"15\" cy=\"32\" r=\"3\" fill=\"#c01818\"/><text x=\"38\" y=\"36\" text-anchor=\"middle\" font-size=\"6\" fill=\"#5a5040\" font-family=\"monospace\">B/W/R</text><rect x=\"24\" y=\"48\" width=\"16\" height=\"5\" fill=\"#d49a3c\"/></svg>",
"properties": [
{ "name": "panelKind", "type": "string", "defaultValue": "epaper-2in13-bwr", "control": "text" },
{ "name": "refreshMs", "type": "number", "defaultValue": 80, "control": "number" }
{
"name": "panelKind",
"type": "string",
"defaultValue": "epaper-2in13-bwr",
"control": "text"
},
{
"name": "refreshMs",
"type": "number",
"defaultValue": 80,
"control": "number"
}
],
"defaultValues": { "panelKind": "epaper-2in13-bwr", "refreshMs": 80 },
"defaultValues": {
"panelKind": "epaper-2in13-bwr",
"refreshMs": 80
},
"pinCount": 8,
"tags": ["epaper", "e-paper", "e-ink", "display", "ssd1680", "tri-color", "red", "2.13", "gxepd2", "spi"]
"tags": [
"epaper",
"e-paper",
"e-ink",
"display",
"ssd1680",
"tri-color",
"red",
"2.13",
"gxepd2",
"spi"
]
},
{
"id": "epaper-2in9-bw",
@ -1961,12 +2172,34 @@
"category": "displays",
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect x=\"2\" y=\"16\" width=\"60\" height=\"34\" rx=\"3\" fill=\"#e8e2d4\" stroke=\"#b8aa90\"/><rect x=\"6\" y=\"20\" width=\"52\" height=\"26\" fill=\"#f4f1e8\" stroke=\"#a89a80\"/><text x=\"32\" y=\"36\" text-anchor=\"middle\" font-size=\"6\" fill=\"#5a5040\" font-family=\"monospace\">2.9\"</text><rect x=\"24\" y=\"50\" width=\"16\" height=\"5\" fill=\"#d49a3c\"/></svg>",
"properties": [
{ "name": "panelKind", "type": "string", "defaultValue": "epaper-2in9-bw", "control": "text" },
{ "name": "refreshMs", "type": "number", "defaultValue": 50, "control": "number" }
{
"name": "panelKind",
"type": "string",
"defaultValue": "epaper-2in9-bw",
"control": "text"
},
{
"name": "refreshMs",
"type": "number",
"defaultValue": 50,
"control": "number"
}
],
"defaultValues": { "panelKind": "epaper-2in9-bw", "refreshMs": 50 },
"defaultValues": {
"panelKind": "epaper-2in9-bw",
"refreshMs": 50
},
"pinCount": 8,
"tags": ["epaper", "e-paper", "e-ink", "display", "ssd1680", "2.9", "gxepd2", "spi"]
"tags": [
"epaper",
"e-paper",
"e-ink",
"display",
"ssd1680",
"2.9",
"gxepd2",
"spi"
]
},
{
"id": "epaper-2in9-bwr",
@ -1975,12 +2208,36 @@
"category": "displays",
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect x=\"2\" y=\"16\" width=\"60\" height=\"34\" rx=\"3\" fill=\"#e8e2d4\" stroke=\"#b8aa90\"/><rect x=\"6\" y=\"20\" width=\"52\" height=\"26\" fill=\"#f4f1e8\" stroke=\"#a89a80\"/><circle cx=\"15\" cy=\"32\" r=\"3\" fill=\"#c01818\"/><text x=\"38\" y=\"36\" text-anchor=\"middle\" font-size=\"6\" fill=\"#5a5040\" font-family=\"monospace\">B/W/R</text><rect x=\"24\" y=\"50\" width=\"16\" height=\"5\" fill=\"#d49a3c\"/></svg>",
"properties": [
{ "name": "panelKind", "type": "string", "defaultValue": "epaper-2in9-bwr", "control": "text" },
{ "name": "refreshMs", "type": "number", "defaultValue": 80, "control": "number" }
{
"name": "panelKind",
"type": "string",
"defaultValue": "epaper-2in9-bwr",
"control": "text"
},
{
"name": "refreshMs",
"type": "number",
"defaultValue": 80,
"control": "number"
}
],
"defaultValues": { "panelKind": "epaper-2in9-bwr", "refreshMs": 80 },
"defaultValues": {
"panelKind": "epaper-2in9-bwr",
"refreshMs": 80
},
"pinCount": 8,
"tags": ["epaper", "e-paper", "e-ink", "display", "ssd1680", "tri-color", "red", "2.9", "gxepd2", "spi"]
"tags": [
"epaper",
"e-paper",
"e-ink",
"display",
"ssd1680",
"tri-color",
"red",
"2.9",
"gxepd2",
"spi"
]
},
{
"id": "epaper-4in2-bw",
@ -1989,12 +2246,35 @@
"category": "displays",
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect x=\"2\" y=\"6\" width=\"60\" height=\"50\" rx=\"3\" fill=\"#e8e2d4\" stroke=\"#b8aa90\"/><rect x=\"6\" y=\"10\" width=\"52\" height=\"38\" fill=\"#f4f1e8\" stroke=\"#a89a80\"/><text x=\"32\" y=\"32\" text-anchor=\"middle\" font-size=\"7\" fill=\"#5a5040\" font-family=\"monospace\">4.2\"</text><rect x=\"24\" y=\"56\" width=\"16\" height=\"5\" fill=\"#d49a3c\"/></svg>",
"properties": [
{ "name": "panelKind", "type": "string", "defaultValue": "epaper-4in2-bw", "control": "text" },
{ "name": "refreshMs", "type": "number", "defaultValue": 80, "control": "number" }
{
"name": "panelKind",
"type": "string",
"defaultValue": "epaper-4in2-bw",
"control": "text"
},
{
"name": "refreshMs",
"type": "number",
"defaultValue": 80,
"control": "number"
}
],
"defaultValues": { "panelKind": "epaper-4in2-bw", "refreshMs": 80 },
"defaultValues": {
"panelKind": "epaper-4in2-bw",
"refreshMs": 80
},
"pinCount": 8,
"tags": ["epaper", "e-paper", "e-ink", "display", "ssd1683", "uc8176", "4.2", "gxepd2", "spi"]
"tags": [
"epaper",
"e-paper",
"e-ink",
"display",
"ssd1683",
"uc8176",
"4.2",
"gxepd2",
"spi"
]
},
{
"id": "epaper-7in5-bw",
@ -2003,12 +2283,35 @@
"category": "displays",
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect x=\"1\" y=\"4\" width=\"62\" height=\"54\" rx=\"3\" fill=\"#e8e2d4\" stroke=\"#b8aa90\"/><rect x=\"5\" y=\"8\" width=\"54\" height=\"42\" fill=\"#f4f1e8\" stroke=\"#a89a80\"/><text x=\"32\" y=\"32\" text-anchor=\"middle\" font-size=\"8\" fill=\"#5a5040\" font-family=\"monospace\">7.5\"</text><rect x=\"24\" y=\"58\" width=\"16\" height=\"5\" fill=\"#d49a3c\"/></svg>",
"properties": [
{ "name": "panelKind", "type": "string", "defaultValue": "epaper-7in5-bw", "control": "text" },
{ "name": "refreshMs", "type": "number", "defaultValue": 100, "control": "number" }
{
"name": "panelKind",
"type": "string",
"defaultValue": "epaper-7in5-bw",
"control": "text"
},
{
"name": "refreshMs",
"type": "number",
"defaultValue": 100,
"control": "number"
}
],
"defaultValues": { "panelKind": "epaper-7in5-bw", "refreshMs": 100 },
"defaultValues": {
"panelKind": "epaper-7in5-bw",
"refreshMs": 100
},
"pinCount": 8,
"tags": ["epaper", "e-paper", "e-ink", "display", "uc8179", "gd7965", "7.5", "gxepd2", "spi"]
"tags": [
"epaper",
"e-paper",
"e-ink",
"display",
"uc8179",
"gd7965",
"7.5",
"gxepd2",
"spi"
]
},
{
"id": "epaper-5in65-7c",
@ -2017,12 +2320,36 @@
"category": "displays",
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect x=\"2\" y=\"6\" width=\"60\" height=\"50\" rx=\"3\" fill=\"#e8e2d4\" stroke=\"#b8aa90\"/><rect x=\"6\" y=\"10\" width=\"52\" height=\"38\" fill=\"#f4f1e8\" stroke=\"#a89a80\"/><circle cx=\"14\" cy=\"19\" r=\"3\" fill=\"#202020\"/><circle cx=\"22\" cy=\"19\" r=\"3\" fill=\"#c01010\"/><circle cx=\"30\" cy=\"19\" r=\"3\" fill=\"#20a040\"/><circle cx=\"38\" cy=\"19\" r=\"3\" fill=\"#3060c0\"/><circle cx=\"46\" cy=\"19\" r=\"3\" fill=\"#e0c830\"/><circle cx=\"54\" cy=\"19\" r=\"3\" fill=\"#e08020\"/><text x=\"32\" y=\"38\" text-anchor=\"middle\" font-size=\"6\" fill=\"#5a5040\" font-family=\"monospace\">ACeP 7</text><rect x=\"24\" y=\"56\" width=\"16\" height=\"5\" fill=\"#d49a3c\"/></svg>",
"properties": [
{ "name": "panelKind", "type": "string", "defaultValue": "epaper-5in65-7c", "control": "text" },
{ "name": "refreshMs", "type": "number", "defaultValue": 150, "control": "number" }
{
"name": "panelKind",
"type": "string",
"defaultValue": "epaper-5in65-7c",
"control": "text"
},
{
"name": "refreshMs",
"type": "number",
"defaultValue": 150,
"control": "number"
}
],
"defaultValues": { "panelKind": "epaper-5in65-7c", "refreshMs": 150 },
"defaultValues": {
"panelKind": "epaper-5in65-7c",
"refreshMs": 150
},
"pinCount": 8,
"tags": ["epaper", "e-paper", "e-ink", "display", "uc8159c", "acep", "7-color", "5.65", "gxepd2", "spi"]
"tags": [
"epaper",
"e-paper",
"e-ink",
"display",
"uc8159c",
"acep",
"7-color",
"5.65",
"gxepd2",
"spi"
]
},
{
"id": "bmp280",
@ -2033,7 +2360,15 @@
"properties": [],
"defaultValues": {},
"pinCount": 4,
"tags": ["sensor", "i2c", "pressure", "temperature", "bmp280", "weather", "barometer"]
"tags": [
"sensor",
"i2c",
"pressure",
"temperature",
"bmp280",
"weather",
"barometer"
]
},
{
"$comment": "I2C-backpack variant of LCD1602: reuses the wokwi-lcd1602 web component but renders the 4-pin I2C header (GND/VCC/SDA/SCL) instead of the parallel 16-pin header. ProtocolParts registers the matching HD44780 + PCF8574 decoder so Arduino sketches using LiquidCrystal_I2C work out of the box.",
@ -2055,7 +2390,10 @@
"type": "string",
"defaultValue": "green",
"control": "select",
"options": ["green", "blue"],
"options": [
"green",
"blue"
],
"description": "LCD panel colour"
},
{
@ -2074,7 +2412,15 @@
"backlight": true
},
"pinCount": 4,
"tags": ["lcd", "i2c", "display", "hd44780", "pcf8574", "1602", "liquidcrystal"]
"tags": [
"lcd",
"i2c",
"display",
"hd44780",
"pcf8574",
"1602",
"liquidcrystal"
]
},
{
"$comment": "I2C-backpack variant of LCD2004. Same PCF8574+HD44780 protocol, but 20x4 geometry and 2004 row offsets (0x00, 0x40, 0x14, 0x54).",
@ -2100,7 +2446,15 @@
"backlight": true
},
"pinCount": 4,
"tags": ["lcd", "i2c", "display", "hd44780", "pcf8574", "2004", "liquidcrystal"]
"tags": [
"lcd",
"i2c",
"display",
"hd44780",
"pcf8574",
"2004",
"liquidcrystal"
]
},
{
"$comment": "Picker shortcut for SSD1306 in I2C mode — same wokwi-ssd1306 element, but the user finds it by name without having to discover the protocol selector on the generic ssd1306 entry. Issue #101.",
@ -2110,11 +2464,23 @@
"category": "displays",
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect width=\"64\" height=\"64\" fill=\"#0b1226\" rx=\"4\"/><rect x=\"8\" y=\"14\" width=\"48\" height=\"32\" rx=\"2\" fill=\"#000\"/><text x=\"32\" y=\"30\" text-anchor=\"middle\" font-size=\"7\" font-family=\"monospace\" fill=\"#9be1ff\" font-weight=\"bold\">SSD1306</text><text x=\"32\" y=\"40\" text-anchor=\"middle\" font-size=\"6\" font-family=\"monospace\" fill=\"#9be1ff\">I2C 0x3C</text><text x=\"32\" y=\"60\" text-anchor=\"middle\" font-size=\"7\" font-family=\"sans-serif\" fill=\"#e5e7eb\" font-weight=\"bold\">SDA SCL</text></svg>",
"properties": [
{ "name": "imageData", "type": "string", "control": "text" }
{
"name": "imageData",
"type": "string",
"control": "text"
}
],
"defaultValues": { "protocol": "i2c" },
"defaultValues": {
"protocol": "i2c"
},
"pinCount": 4,
"tags": ["ssd1306", "oled", "display", "i2c", "0x3c"]
"tags": [
"ssd1306",
"oled",
"display",
"i2c",
"0x3c"
]
},
{
"$comment": "Picker shortcut for SSD1306 in SPI mode — counterpart to ssd1306-i2c.",
@ -2124,11 +2490,22 @@
"category": "displays",
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect width=\"64\" height=\"64\" fill=\"#0b1226\" rx=\"4\"/><rect x=\"8\" y=\"14\" width=\"48\" height=\"32\" rx=\"2\" fill=\"#000\"/><text x=\"32\" y=\"30\" text-anchor=\"middle\" font-size=\"7\" font-family=\"monospace\" fill=\"#9be1ff\" font-weight=\"bold\">SSD1306</text><text x=\"32\" y=\"40\" text-anchor=\"middle\" font-size=\"6\" font-family=\"monospace\" fill=\"#9be1ff\">SPI mode</text><text x=\"32\" y=\"60\" text-anchor=\"middle\" font-size=\"6\" font-family=\"sans-serif\" fill=\"#e5e7eb\" font-weight=\"bold\">MOSI/SCK/DC</text></svg>",
"properties": [
{ "name": "imageData", "type": "string", "control": "text" }
{
"name": "imageData",
"type": "string",
"control": "text"
}
],
"defaultValues": { "protocol": "spi" },
"defaultValues": {
"protocol": "spi"
},
"pinCount": 7,
"tags": ["ssd1306", "oled", "display", "spi"]
"tags": [
"ssd1306",
"oled",
"display",
"spi"
]
}
],
"led": {
@ -2181,7 +2558,18 @@
"name": "KY-040 Rotary Encoder",
"category": "input",
"description": "KY-040 incremental rotary encoder with push-button. Turning the knob outputs quadrature pulses on CLK and DT (direction = which leads); the shaft press drives an active-low SW. Reads from any board's GPIO.",
"tags": ["rotary", "encoder", "rotary encoder", "rotary-encoder", "ky-040", "ky040", "knob", "quadrature", "dial", "input"],
"tags": [
"rotary",
"encoder",
"rotary encoder",
"rotary-encoder",
"ky-040",
"ky040",
"knob",
"quadrature",
"dial",
"input"
],
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect width=\"64\" height=\"64\" fill=\"#1a2332\" rx=\"4\"/>\n <rect x=\"12\" y=\"38\" width=\"40\" height=\"18\" rx=\"2\" fill=\"#1f6f43\" stroke=\"#2d8f5a\" stroke-width=\"0.6\"/>\n <rect x=\"17\" y=\"54\" width=\"2.5\" height=\"6\" fill=\"#d8b24a\"/>\n <rect x=\"24\" y=\"54\" width=\"2.5\" height=\"6\" fill=\"#d8b24a\"/>\n <rect x=\"31\" y=\"54\" width=\"2.5\" height=\"6\" fill=\"#d8b24a\"/>\n <rect x=\"38\" y=\"54\" width=\"2.5\" height=\"6\" fill=\"#d8b24a\"/>\n <rect x=\"45\" y=\"54\" width=\"2.5\" height=\"6\" fill=\"#d8b24a\"/>\n <circle cx=\"32\" cy=\"24\" r=\"15\" fill=\"#9aa3ad\" stroke=\"#6b7280\" stroke-width=\"1\"/>\n <circle cx=\"32\" cy=\"24\" r=\"10\" fill=\"#c7cdd4\"/>\n <rect x=\"31\" y=\"11\" width=\"2\" height=\"8\" rx=\"1\" fill=\"#475569\"/>\n <path d=\"M 45 14 A 16 16 0 0 1 49 27\" fill=\"none\" stroke=\"#4ade80\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n <path d=\"M 49 27 l -4 -1 l 2 -4 z\" fill=\"#4ade80\"/></svg>"
}
}
}