From 4e20d03f4c42b4b5e06e40519d7963906bb330d2 Mon Sep 17 00:00:00 2001 From: David Montero Date: Thu, 18 Jun 2026 01:19:13 +0200 Subject: [PATCH] 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. --- frontend/public/components-metadata.json | 147 +++++- .../src/__tests__/circuit-verifier.test.ts | 115 ++++ frontend/src/simulation/spice/storeAdapter.ts | 1 + frontend/src/simulation/spice/types.ts | 3 + .../src/simulation/verify/circuitVerifier.ts | 114 ++++ scripts/component-overrides.json | 492 ++++++++++++++++-- 6 files changed, 813 insertions(+), 59 deletions(-) diff --git a/frontend/public/components-metadata.json b/frontend/public/components-metadata.json index 3a295ed5..64c75a13 100644 --- a/frontend/public/components-metadata.json +++ b/frontend/public/components-metadata.json @@ -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", diff --git a/frontend/src/__tests__/circuit-verifier.test.ts b/frontend/src/__tests__/circuit-verifier.test.ts index 2fd80661..bbe65423 100644 --- a/frontend/src/__tests__/circuit-verifier.test.ts +++ b/frontend/src/__tests__/circuit-verifier.test.ts @@ -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 diff --git a/frontend/src/simulation/spice/storeAdapter.ts b/frontend/src/simulation/spice/storeAdapter.ts index fe0ff07d..6dc25dcb 100644 --- a/frontend/src/simulation/spice/storeAdapter.ts +++ b/frontend/src/simulation/spice/storeAdapter.ts @@ -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, diff --git a/frontend/src/simulation/spice/types.ts b/frontend/src/simulation/spice/types.ts index ce34c775..34fd357d 100644 --- a/frontend/src/simulation/spice/types.ts +++ b/frontend/src/simulation/spice/types.ts @@ -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; /** diff --git a/frontend/src/simulation/verify/circuitVerifier.ts b/frontend/src/simulation/verify/circuitVerifier.ts index e7a6de96..7fcccaae 100644 --- a/frontend/src/simulation/verify/circuitVerifier.ts +++ b/frontend/src/simulation/verify/circuitVerifier.ts @@ -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(); + 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 { diff --git a/scripts/component-overrides.json b/scripts/component-overrides.json index ec0cc4a0..f5202828 100644 --- a/scripts/component-overrides.json +++ b/scripts/component-overrides.json @@ -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": "\n \n \n A4988\n \n STEP/DIR" }, { @@ -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": "e-Paper", "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": "2.13\"", "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": "B/W/R", "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": "2.9\"", "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": "B/W/R", "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": "4.2\"", "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": "7.5\"", "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": "ACeP 7", "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": "SSD1306I2C 0x3CSDA SCL", "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": "SSD1306SPI modeMOSI/SCK/DC", "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": "\n \n \n \n \n \n \n \n \n \n \n " } -} +} \ No newline at end of file