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