astra-abse/t113-s3

Allwinner T113-S3 / JLCPCB C5197687: native saved fanout, LCD top, storage right, 127 perimeter exits, 30 decoupling capacitors, connected GND planes; clean DRC and shorts, all 29 vias connected.

Version
1.2.0
License
MIT
Stars
0

scripts/audit.ts

import assert from "node:assert/strict";
import type { AnyCircuitElement } from "circuit-json";
import { pins } from "../src/pins";

export function elements<T extends AnyCircuitElement["type"]>(
	json: AnyCircuitElement[],
	type: T,
) {
	return json.filter((e) => e.type === type) as Extract<
		AnyCircuitElement,
		{ type: T }
	>[];
}
const near = (actual: number, expected: number, label: string) =>
	assert.ok(
		Math.abs(actual - expected) < 0.00001,
		`${label}: ${actual} != ${expected}`,
	);

/** Audit the emitted copper and source identities, independently of the route generator. */
export function auditFanout(
	json: AnyCircuitElement[],
	offset = { x: 0, y: 0 },
	rotation = 0,
) {
	const radians = (rotation * Math.PI) / 180;
	const world = (x: number, y: number) => ({
		x: offset.x + x * Math.cos(radians) - y * Math.sin(radians),
		y: offset.y + x * Math.sin(radians) + y * Math.cos(radians),
	});
	const local = (x: number, y: number) => ({
		x: (x - offset.x) * Math.cos(radians) + (y - offset.y) * Math.sin(radians),
		y: -(x - offset.x) * Math.sin(radians) + (y - offset.y) * Math.cos(radians),
	});
	const issues = json.filter((e) => /(?:error|warning)$/.test(e.type));
	assert.deepEqual(issues, [], "Circuit JSON diagnostics must be empty");
	assert.ok(
		elements(json, "schematic_component").every((c) => c.schematic_sheet_id),
		"Components must belong to the rendered schematic sheet",
	);
	const ports = elements(json, "source_port");
	const pads = elements(json, "pcb_smtpad");
	const pcbPorts = elements(json, "pcb_port");
	const exits = elements(json, "pcb_breakout_point");
	const sourceTraces = elements(json, "source_trace");
	const copperTraces = elements(json, "pcb_trace");
	const vias = elements(json, "pcb_via");
	assert.equal(ports.length, 189);
	assert.equal(pads.length, 189);
	assert.equal(exits.length, 188);
	assert.equal(copperTraces.length, 188);
	assert.equal(vias.length, 29);
	assert.equal(
		new Set(vias.map((v) => `${v.x.toFixed(5)},${v.y.toFixed(5)}`)).size,
		29,
		"No duplicate drills",
	);
	const chip = elements(json, "source_component")[0]!;
	assert.deepEqual(chip.supplier_part_numbers?.jlcpcb, ["C5197687"]);

	let lcd = 0,
		storage = 0;
	for (const pin of pins) {
		const port = ports.find((p) => p.pin_number === pin.pin)!;
		assert.ok(port, `Missing source pin ${pin.pin}`);
		assert.equal(
			port.name,
			pin.label,
			`Imported label changed for pin ${pin.pin}`,
		);
		const pcbPort = pcbPorts.find(
			(p) => p.source_port_id === port.source_port_id,
		)!;
		const pad = pads.find((p) => p.pcb_port_id === pcbPort.pcb_port_id)!;
		assert.equal(pad.shape, "rect");
		if (pad.shape !== "rect")
			throw new Error("Expected imported rectangular pads");
		const expected = world(-pin.y, pin.x);
		near(pad.x, expected.x, `pin${pin.pin} rotated X`);
		near(pad.y, expected.y, `pin${pin.pin} rotated Y`);
		near(
			pad.width,
			rotation % 180 === 0 ? pin.height : pin.width,
			`pin${pin.pin} pad width`,
		);
		near(
			pad.height,
			rotation % 180 === 0 ? pin.width : pin.height,
			`pin${pin.pin} pad height`,
		);
		if (pin.pin === 106) {
			assert.equal(port.do_not_connect, true);
			assert.ok(!exits.some((e) => e.source_port_id === port.source_port_id));
			assert.ok(
				!sourceTraces.some((t) =>
					t.connected_source_port_ids.includes(port.source_port_id),
				),
			);
			continue;
		}
		assert.equal(port.must_be_connected, true);
		const exit = exits.find((e) => e.source_port_id === port.source_port_id)!;
		assert.ok(exit, `Missing breakout for ${pin.label}`);
		const source = sourceTraces.find((t) => t.name === `ESC_${pin.label}`)!;
		const traces = copperTraces.filter(
			(t) => t.source_trace_id === source.source_trace_id,
		);
		assert.equal(
			traces.length,
			1,
			`${pin.label} must have one complete escape trace`,
		);
		assert.ok(source.connected_source_port_ids.includes(port.source_port_id));
		assert.equal(exit.source_trace_id, source.source_trace_id);
		const trace = traces[0]!;
		const first = trace.route[0]!,
			last = trace.route.at(-1)!;
		assert.equal(first.route_type, "wire");
		assert.equal(last.route_type, "wire");
		if (first.route_type !== "wire" || last.route_type !== "wire")
			throw new Error("Expected wire endpoints");
		near(first.x, pcbPort.x, `${pin.label} copper start X`);
		near(first.y, pcbPort.y, `${pin.label} copper start Y`);
		near(last.x, exit.x, `${pin.label} copper end X`);
		near(last.y, exit.y, `${pin.label} copper end Y`);
		if (pin.pin === 91) {
			assert.equal(last.layer, "bottom", "AGND terminates in the ground plane");
			continue;
		}
		assert.equal(last.layer, "top");
		const localExit = local(exit.x, exit.y);
		const onBoundary =
			Math.abs(Math.abs(localExit.x) - 18) < 1e-5 ||
			Math.abs(Math.abs(localExit.y) - 18) < 1e-5;
		assert.ok(
			onBoundary,
			`${pin.label} must reach the 36 mm breakout boundary`,
		);
		if (/^PD(?:[0-9]|1[0-9]|2[01])$/.test(pin.label)) {
			near(localExit.y, 18, `${pin.label} LCD exits top`);
			lcd++;
		}
		if (/^PC[2-7]$|^PF[0-5]$/.test(pin.label)) {
			near(localExit.x, 18, `${pin.label} storage exits right`);
			storage++;
		}
		for (const point of trace.route)
			if (point.route_type === "wire") near(point.width, 0.1, "trace width");
	}
	assert.equal(lcd, 22);
	assert.equal(storage, 12);
	for (const via of vias) {
		near(via.outer_diameter, 0.45, "via pad diameter");
		near(via.hole_diameter, 0.2, "via drill diameter");
		assert.deepEqual([...via.layers].sort(), [
			"bottom",
			"inner1",
			"inner2",
			"top",
		]);
	}
	return {
		pads: 189,
		escapes: 127,
		internalFanoutJoins: 61,
		intentionallyUnconnectedPins: [106],
		lcdSignalsTop: lcd,
		storageSignalsRight: storage,
		traces: 188,
		vias: 29,
	};
}