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),
	);
}