AnasSarkiz/magnetic-shutter-remote-r8
A USB‑C rechargeable battery-powered ESP32‑C3 wireless controller with Li‑ion charging, protected 3.3‑V buck‑boost regulation, thermal/safety monitoring, status LEDs, buttons, programming UART, crystal clock, and 2.4‑GHz antenna matching network.
- Version
- 0.4.0-prototype
- License
- unset
- Stars
- 0
scripts/audit-r9-circuit.ts
import { any_circuit_element } from "circuit-json";
import { z } from "zod";
// Independent schematic contract: Espressif ESP32-C3 datasheet v2.4 and TI/JST sources.
// This audit does not qualify footprints, RF performance or routed copper.
export function auditBareChip(input: unknown) {
const circuit = any_circuit_element.array().parse(input);
const components = circuit.filter((e) => e.type === "source_component");
const ports = circuit.filter((e) => e.type === "source_port");
const nets = circuit.filter((e) => e.type === "source_net");
// Repeated supplier lands use canonical explicit internal connections. Never
// infer a shared net from a component name or matching terminal label alone.
const memberships = ports.map((port) => ({
sourcePortId: port.source_port_id,
connectivityKey: port.subcircuit_connectivity_map_key,
}));
const internalConnections = circuit.filter(
(e) => e.type === "source_component_internal_connection",
);
let changed = true;
while (changed) {
changed = false;
for (const connection of internalConnections) {
const group = memberships.filter((m) =>
connection.source_port_ids.includes(m.sourcePortId),
);
const keys = new Set(
group.flatMap((m) => (m.connectivityKey ? [m.connectivityKey] : [])),
);
if (keys.size > 1)
throw new Error("Conflicting internally connected terminal nets");
const key = [...keys][0];
if (!key) continue;
for (const membership of group) {
if (!membership.connectivityKey) {
membership.connectivityKey = key;
changed = true;
}
}
}
}
const contracts = [
{
ref: "J1",
mpn: "TYPE-C-31-M-12",
part: "C165948",
pins: {
1: "GND",
2: "GND",
3: "GND",
4: "GND",
6: "CC1",
12: "CC2",
13: "GND",
14: "GND",
15: "USB5V",
16: "USB5V",
},
},
{
ref: "C21",
mpn: "GJM1555C1H1R5BB01D",
part: "C76900",
pins: { 1: "RF_CHIP", 2: "GND" },
},
{
ref: "C22",
mpn: "GJM1555C1H1R5BB01D",
part: "C76900",
pins: { 1: "RF_50", 2: "GND" },
},
{
ref: "SW2",
mpn: "TS24CA",
part: "C393942",
pins: { 1: "SHUTTER", 2: "GND", 3: "GND", 4: "GND" },
},
{
ref: "Q1",
mpn: "2N7002",
part: "C8545",
pins: { 1: "USB5V", 2: "GND", 3: "REG_EN" },
},
{
ref: "Q2",
mpn: "2N7002",
part: "C8545",
pins: { 1: "TEMP_OK", 2: "GND", 3: "CHARGE_DISABLE" },
},
{
ref: "SW1",
mpn: "MSK12C02",
part: "C431540",
pins: { 1: "BATTERY_OK", 2: "POWER_SWITCH", 3: "GND", 4: "GND" },
},
{
ref: "U4",
mpn: "TPS3839G33DBZR",
part: "C485802",
pins: { 1: "GND", 2: "BATTERY_OK", 3: "VBAT" },
},
{
ref: "U1",
mpn: "ESP32-C3FH4",
part: "C2858491",
pins: {
1: "RF_CHIP",
2: "RADIO_3V3",
3: "RADIO_3V3",
6: "STRAP_GPIO2",
7: "EN",
9: "SHUTTER",
10: "PAIR",
11: "V3",
12: "STATUS_LED",
14: "STRAP_GPIO8",
15: "BOOT",
17: "V3",
18: "FLASH_VDD",
27: "UART_RX",
28: "UART_TX",
29: "XTAL_N",
30: "XTAL_P",
31: "V3",
32: "V3",
33: "GND",
},
},
{
ref: "Y1",
mpn: "TXM40M0004252HBCEO00T",
part: "C284176",
pins: { 1: "XTAL_LOAD_P", 2: "GND", 3: "XTAL_N", 4: "GND" },
},
{
ref: "L2",
mpn: "LQG15HN2N0B02D",
part: "C6980360",
pins: { 1: "V3", 2: "RADIO_3V3" },
},
{
ref: "L3",
mpn: "AHW2012C-24NJTF",
part: "C6808034",
pins: { 1: "XTAL_P", 2: "XTAL_LOAD_P" },
},
{
ref: "L4",
mpn: "LQG15HN2N0B02D",
part: "C6980360",
pins: { 1: "RF_50", 2: "RF_CHIP" },
},
{
ref: "ANT1",
mpn: "ANT3216LL00R2400A",
part: "C22423581",
pins: { 1: "RF_50" },
},
{
ref: "R14",
mpn: "0603WAF1002T5E",
part: "C25804",
pins: { 1: "V3", 2: "STRAP_GPIO2" },
},
{
ref: "R15",
mpn: "0603WAF1002T5E",
part: "C25804",
pins: { 1: "V3", 2: "STRAP_GPIO8" },
},
{
ref: "R16",
mpn: "0603WAF1002T5E",
part: "C25804",
pins: { 1: "V3", 2: "BOOT" },
},
{
ref: "R17",
mpn: "0603WAF1002T5E",
part: "C25804",
pins: { 1: "V3", 2: "EN" },
},
{
ref: "C16",
mpn: "CL10A105KB8NNNC",
part: "C15849",
pins: { 1: "FLASH_VDD", 2: "GND" },
},
{
ref: "C23",
mpn: "CL10A105KB8NNNC",
part: "C15849",
pins: { 1: "EN", 2: "GND" },
},
{
ref: "U3",
mpn: "TPS63031DSKR",
part: "C15516",
pins: {
1: "V3",
2: "INDUCTOR_L2",
3: "GND",
4: "INDUCTOR_L1",
5: "VBAT",
6: "REG_EN",
7: "GND",
8: "VBAT",
9: "GND",
10: "V3",
11: "GND",
},
},
{
ref: "J2",
mpn: "SM02B-SRSS-TB(LF)(SN)",
part: "C160402",
pins: { 1: "GND", 2: "VBAT", 3: "GND", 4: "GND" },
},
{
ref: "J3",
mpn: "BM03B-SRSS-TB(LF)(SN)",
part: "C160389",
pins: { 1: "UART_RX", 2: "GND", 3: "UART_TX", 4: "GND", 5: "GND" },
},
{
ref: "L1",
mpn: "SWPA3015S1R5NT",
part: "C56594",
pins: { 1: "INDUCTOR_L2", 2: "INDUCTOR_L1" },
},
];
for (const contract of contracts) {
const component = components.find((e) => e.name === contract.ref);
if (
!component ||
component.manufacturer_part_number !== contract.mpn ||
!component.supplier_part_numbers?.jlcpcb?.includes(contract.part)
) {
throw new Error(`${contract.ref}: exact manufacturer identity mismatch`);
}
for (const [pin, netName] of Object.entries(contract.pins)) {
const port = ports.find(
(e) =>
e.source_component_id === component.source_component_id &&
e.pin_number === Number(pin),
);
const net = nets.find((e) => e.name === netName);
if (
!port ||
!net?.subcircuit_connectivity_map_key ||
memberships.find((m) => m.sourcePortId === port.source_port_id)
?.connectivityKey !== net.subcircuit_connectivity_map_key
) {
throw new Error(`${contract.ref}.pin${pin}: expected ${netName}`);
}
}
}
const r3 = components.find((e) => e.name === "R3");
if (
r3?.ftype !== "simple_resistor" ||
r3.resistance !== 1000 ||
r3.supplier_part_numbers?.jlcpcb?.[0] !== "C21190"
) {
throw new Error("Charge setting must be the qualified exact 1k resistor");
}
const radio = components.find((e) => e.name === "U1");
const radioPorts = ports.filter(
(e) => e.source_component_id === radio?.source_component_id,
);
for (const pin of [2, 3, 11, 17, 31, 32]) {
if (!radioPorts.find((e) => e.pin_number === pin)?.requires_power)
throw new Error(`U1.pin${pin}: power metadata missing`);
}
for (const pin of [4, 5, 8, 13, 16, 19, 20, 21, 22, 23, 24, 25, 26]) {
if (!radioPorts.find((e) => e.pin_number === pin)?.do_not_connect)
throw new Error(
`U1.pin${pin}: reserved/unused pin must remain unconnected`,
);
}
const flash = radioPorts.find((e) => e.pin_number === 18);
if (!flash?.provides_power || flash.requires_power)
throw new Error(
"VDD_SPI is an internal flash supply output, not a 3V3 input",
);
const antenna = components.find((e) => e.name === "ANT1");
if (
!ports.find(
(e) =>
e.source_component_id === antenna?.source_component_id &&
e.pin_number === 2,
)?.do_not_connect
)
throw new Error("ANT1.pin2 must remain unconnected");
if (components.some((e) => e.manufacturer_part_number === "ESPC3-12-N4"))
throw new Error("Former module remains fitted");
const pcbChip = circuit
.filter((e) => e.type === "pcb_component")
.find((e) => e.source_component_id === radio?.source_component_id);
const pads = circuit.filter(
(e) =>
e.type === "pcb_smtpad" &&
e.pcb_component_id === pcbChip?.pcb_component_id,
);
if (pads.length !== 33)
throw new Error(
"Bare QFN must retain all32 perimeter pads and exposed ground pad",
);
const board = circuit.find((e) => e.type === "pcb_board");
const usbSource = components.find((e) => e.name === "J1");
const usbBoard = circuit
.filter((e) => e.type === "pcb_component")
.find((e) => e.source_component_id === usbSource?.source_component_id);
const shellSlots = circuit.filter(
(e) =>
e.type === "pcb_plated_hole" &&
e.pcb_component_id === usbBoard?.pcb_component_id,
);
if (shellSlots.length !== 4)
throw new Error("USB requires four grounded shell slots");
for (const slot of shellSlots) {
if (
slot.type !== "pcb_plated_hole" ||
slot.shape !== "pill" ||
slot.hole_width !== 0.6 ||
![1.4, 1.7].includes(slot.hole_height)
)
throw new Error(
"USB slot differs from reviewed HRO0.6×1.4/1.7mm drawing",
);
if (
(slot.outer_height - slot.hole_height) / 2 < 0.18 ||
slot.hole_height < 2 * slot.hole_width
)
throw new Error("USB shell-slot annulus/aspect is not manufacturable");
}
if (board?.width !== 44 || board.height !== 56 || board.num_layers !== 2) {
throw new Error(
"Board envelope differs from battery/antenna qualification",
);
}
return {
stage: "schematic-only",
contracts: contracts.length,
fittedReferences: components.length,
chipPads: pads.length,
physicalQualification: "pending",
};
}
if (import.meta.main) {
const file = z
.string()
.min(1)
.parse(Bun.argv[2] ?? "dist/index/circuit.json");
console.log(
JSON.stringify(auditBareChip(await Bun.file(file).json()), null, 2),
);
}