AnasSarkiz/usb-c-pd-brushed-motor
USB-C PD-powered motor controller with protected input, selectable 5/9/12 V buck supply, MCU qualification, PWM H-bridge drive, overvoltage sensing, regenerative energy-dump resistors, and safety interlocks.
- Version
- 0.0.1-a48-cloud-pipeline9-unrouted-c41d79a-305b1e7a
- License
- unset
- Stars
- 0
scripts/audit-product-placement.ts
import { createHash } from "node:crypto"
import { readFile, writeFile } from "node:fs/promises"
import {
applyToPoint,
compose,
rotateDEG,
translate,
} from "transformation-matrix"
import { any_circuit_element } from "circuit-json"
import { z } from "zod"
import { productPlacement } from "../circuit/product-placement"
import stackup from "../docs/STACKUP-A31.json"
const routingPhase = z
.enum(["unrouted", "routed"])
.parse(process.argv[2] ?? "unrouted")
const artifact = await readFile("dist/index/circuit.json", "utf8")
const elements = z.array(any_circuit_element).parse(JSON.parse(artifact))
const board = elements.find((element) => element.type === "pcb_board")
const copperLayers = z
.array(z.enum(["top", "inner1", "inner2", "bottom"]))
.parse(stackup.copperLayers.map((layer) => layer.name))
if (board?.num_layers !== copperLayers.length)
throw new Error("Native board layer count differs from manufacturing stackup")
const manifest = z
.array(
z.object({
ref: z.string(),
code: z.string(),
pins: z.record(z.string(), z.string()),
nc: z.array(z.string()),
}),
)
.parse(JSON.parse(await readFile("docs/design-manifest.json", "utf8")))
const sources = elements.filter((e) => e.type === "source_component")
const issues: string[] = []
const inspected = []
let expectedVias = 0
const close = (a: number, b: number) => Math.abs(a - b) < 1e-6
if (
sources.length !== manifest.length ||
Object.keys(productPlacement).length !== manifest.length
)
issues.push("Incomplete source/placement inventory")
for (const part of manifest) {
const placement = Object.entries(productPlacement).find(
([reference]) => reference === part.ref,
)?.[1]
const source = sources.find((e) => e.name === part.ref)
const pcb = elements.find(
(e) =>
e.type === "pcb_component" &&
e.source_component_id === source?.source_component_id,
)
if (!placement || !source || pcb?.type !== "pcb_component") {
issues.push(`${part.ref}: missing placement/PCB/source`)
continue
}
const probe = z
.array(any_circuit_element)
.parse(
JSON.parse(
await readFile(
`dist/tests/supplier-audit/${part.code}/circuit.json`,
"utf8",
),
),
)
const originSource = probe.find((e) => e.type === "source_component")
const origin = probe.find(
(e) =>
e.type === "pcb_component" &&
e.source_component_id === originSource?.source_component_id,
)
if (origin?.type !== "pcb_component") {
issues.push(`${part.ref}: missing validated supplier PCB`)
continue
}
const footprintToBoard = compose(
translate(placement.x, placement.y),
rotateDEG(placement.ccwRotationDegrees),
)
const expectedCenter = applyToPoint(footprintToBoard, origin.center)
const rotation = ((placement.ccwRotationDegrees % 360) + 360) % 360
if (
!close(pcb.center.x, expectedCenter.x) ||
!close(pcb.center.y, expectedCenter.y) ||
!close(pcb.rotation ?? 0, rotation)
)
issues.push(`${part.ref}: wrong component transform`)
const sideways = rotation === 90 || rotation === 270
if (
!close(pcb.width, sideways ? origin.height : origin.width) ||
!close(pcb.height, sideways ? origin.width : origin.height)
)
issues.push(`${part.ref}: wrong footprint bounds`)
for (const alias of [...Object.keys(part.pins), ...part.nc]) {
const findSourcePort = (collection: typeof elements, componentId: string) =>
collection.find(
(e) =>
e.type === "source_port" &&
e.source_component_id === componentId &&
(e.name === alias || e.port_hints?.includes(alias)),
)
const targetSourcePort = findSourcePort(
elements,
source.source_component_id,
)
const originSourcePort = findSourcePort(probe, origin.source_component_id)
if (
targetSourcePort?.type !== "source_port" ||
originSourcePort?.type !== "source_port"
) {
issues.push(`${part.ref}.${alias}: missing source port`)
continue
}
const targetPort = elements.find(
(e) =>
e.type === "pcb_port" &&
e.source_port_id === targetSourcePort?.source_port_id,
)
const originPort = probe.find(
(e) =>
e.type === "pcb_port" &&
e.source_port_id === originSourcePort?.source_port_id,
)
if (targetPort?.type !== "pcb_port" || originPort?.type !== "pcb_port") {
issues.push(`${part.ref}.${alias}: missing electrical PCB port`)
continue
}
const expected = applyToPoint(footprintToBoard, originPort)
if (!close(targetPort.x, expected.x) || !close(targetPort.y, expected.y))
issues.push(`${part.ref}.${alias}: wrong port transform`)
const targetPads = elements.filter(
(e) =>
(e.type === "pcb_smtpad" || e.type === "pcb_plated_hole") &&
e.pcb_port_id === targetPort.pcb_port_id,
)
const originPads = probe.filter(
(e) =>
(e.type === "pcb_smtpad" || e.type === "pcb_plated_hole") &&
e.pcb_port_id === originPort.pcb_port_id,
)
if (targetPads.length !== originPads.length || !targetPads.length)
issues.push(`${part.ref}.${alias}: pad/port association mismatch`)
for (const [index, pad] of targetPads.entries()) {
const original = originPads[index]
if (!original || original.type !== pad.type) {
issues.push(`${part.ref}.${alias}: pad type mismatch`)
continue
}
if (
pad.type === "pcb_smtpad" &&
original.type === "pcb_smtpad" &&
pad.shape === "polygon" &&
original.shape === "polygon"
) {
if (pad.points.length !== original.points.length)
issues.push(`${part.ref}.${alias}: polygon size mismatch`)
for (const [pointIndex, point] of pad.points.entries()) {
const originalPoint = original.points[pointIndex]
if (!originalPoint) continue
const expectedPoint = applyToPoint(footprintToBoard, originalPoint)
if (
!close(point.x, expectedPoint.x) ||
!close(point.y, expectedPoint.y)
)
issues.push(`${part.ref}.${alias}: polygon transform mismatch`)
}
} else if (
"x" in pad &&
"x" in original &&
"y" in pad &&
"y" in original
) {
const expectedPad = applyToPoint(footprintToBoard, {
x: original.x,
y: original.y,
})
if (!close(pad.x, expectedPad.x) || !close(pad.y, expectedPad.y))
issues.push(`${part.ref}.${alias}: pad transform mismatch`)
} else issues.push(`${part.ref}.${alias}: unsupported pad geometry`)
}
}
const originalVias = probe.filter((e) => e.type === "pcb_via")
expectedVias += originalVias.length
for (const via of originalVias) {
const layerIndices = via.layers.map((layer) =>
copperLayers.findIndex((candidate) => candidate === layer),
)
if (layerIndices.some((index) => index < 0))
throw new Error(`${part.ref}: unsupported imported via layer`)
const physicalLayers = copperLayers.slice(
Math.min(...layerIndices),
Math.max(...layerIndices) + 1,
)
const expected = applyToPoint(footprintToBoard, via)
const actual = elements.find(
(e) =>
e.type === "pcb_via" &&
close(e.x, expected.x) &&
close(e.y, expected.y),
)
if (
actual?.type !== "pcb_via" ||
!close(actual.hole_diameter, via.hole_diameter) ||
!close(actual.outer_diameter, via.outer_diameter) ||
JSON.stringify(actual.layers) !== JSON.stringify(physicalLayers)
)
issues.push(`${part.ref}: missing/changed original thermal via`)
}
inspected.push({
ref: part.ref,
code: part.code,
footprintAnchorMm: placement,
actualCenterMm: pcb.center,
electricalPins: Object.keys(part.pins).length + part.nc.length,
originalVias: originalVias.length,
})
}
const vias = elements.filter((e) => e.type === "pcb_via")
if (
expectedVias !== 12 ||
vias.length < expectedVias ||
(routingPhase === "unrouted" && vias.length !== expectedVias)
)
issues.push("Original EP via inventory mismatch or unexpected new via")
const routedTraceCount = elements.filter((e) => e.type === "pcb_trace").length
if (routingPhase === "routed" && routedTraceCount === 0)
issues.push("Routed audit requires actual copper traces")
if (routingPhase === "unrouted" && routedTraceCount > 0)
issues.push("Unexpected routed copper before gate")
for (const x of [-35, 35])
for (const y of [-27.5, 27.5])
if (
!elements.some(
(e) =>
e.type === "pcb_hole" &&
close(e.x, x) &&
close(e.y, y) &&
e.hole_shape === "circle" &&
close(e.hole_diameter, 3.2),
)
)
issues.push(`Missing 3.2mm mounting hole ${x},${y}`)
const paste = elements.filter((e) => e.type === "pcb_solder_paste")
if (paste.some((e) => "pcb_plated_hole_id" in e && e.pcb_plated_hole_id))
issues.push("THT stencil aperture generated")
if (!paste.length) issues.push("SMT paste unexpectedly absent")
const report = {
revision: "A22",
routingPhase,
routedTraceCount,
artifactSha256: createHash("sha256").update(artifact).digest("hex"),
purchasedComponentCount: manifest.length,
inspectedCount: inspected.length,
importedThermalVias: expectedVias,
thtPasteCount: paste.filter(
(e) => "pcb_plated_hole_id" in e && e.pcb_plated_hole_id,
).length,
inspected,
issues,
}
await writeFile(
`evidence/product-placement-audit-${routingPhase}-A22.json`,
JSON.stringify(report, null, 2) + "\n",
)
console.log(
JSON.stringify(
{
purchased: manifest.length,
inspected: inspected.length,
thermalVias: expectedVias,
issues,
},
null,
2,
),
)
if (issues.length) process.exitCode = 1