ShiboSoftwareDev/mspm0g3507-usb-c-dev-board
This circuit features a USB Type-C connector with integrated resistors and passives, a USB ESD protection device, a CH340 USB-to-serial interface chip, a 3.3V regulator (AP2112K), multiple decoupling and bulk capacitors, and a Microcontroller (MSPM0G3507) with associated I2C and SPI connections, providing USB communication, power regulation, and RGB LED control.
- Version
- 1.4.1
- License
- unset
- Stars
- 0
scripts/audit-fabrication.mjs
import { readFileSync } from "node:fs"
import {
runAllNetlistChecks,
runAllPinSpecificationChecks,
runAllPlacementChecks,
runAllRoutingChecks,
} from "@tscircuit/checks"
const circuitPath = process.argv[2] ?? "dist/index/circuit.json"
const circuit = JSON.parse(readFileSync(circuitPath, "utf8"))
const source = readFileSync("index.circuit.tsx", "utf8")
const failures = []
const elements = (type) => circuit.filter((element) => element.type === type)
const assert = (condition, message) => {
if (!condition) failures.push(message)
}
const rounded = (value) => Number(value.toFixed(3))
const sourceComponents = elements("source_component")
const sourcePorts = elements("source_port")
const sourceNets = elements("source_net")
const sourceTraces = elements("source_trace")
const pcbComponents = elements("pcb_component")
const pcbBoards = elements("pcb_board")
const pcbPorts = elements("pcb_port")
const pcbTraces = elements("pcb_trace")
const vias = elements("pcb_via")
const schematicSheets = elements("schematic_sheet")
const schematicComponents = elements("schematic_component")
const schematicTraces = elements("schematic_trace")
const courtyards = [
...elements("pcb_courtyard_outline"),
...elements("pcb_courtyard_rect"),
]
const sourceComponentById = new Map(
sourceComponents.map((component) => [component.source_component_id, component]),
)
const componentByName = new Map(
sourceComponents.map((component) => [component.name, component]),
)
const sourceNetByName = new Map(sourceNets.map((net) => [net.name, net]))
const portsByComponentId = new Map()
for (const port of sourcePorts) {
const ports = portsByComponentId.get(port.source_component_id) ?? []
ports.push(port)
portsByComponentId.set(port.source_component_id, ports)
}
const pcbComponentBySourceId = new Map(
pcbComponents.map((component) => [component.source_component_id, component]),
)
const approximately = (actual, expected, tolerance = 0.001) =>
Math.abs(actual - expected) <= tolerance
const pcbPortBySourceId = new Map(
pcbPorts.map((port) => [port.source_port_id, port]),
)
const pcbPortById = new Map(pcbPorts.map((port) => [port.pcb_port_id, port]))
const pcbTracesBySourceId = new Map()
for (const trace of pcbTraces) {
const traces = pcbTracesBySourceId.get(trace.source_trace_id) ?? []
traces.push(trace)
pcbTracesBySourceId.set(trace.source_trace_id, traces)
}
const viaCountByPcbTraceId = new Map()
for (const via of vias) {
viaCountByPcbTraceId.set(
via.pcb_trace_id,
(viaCountByPcbTraceId.get(via.pcb_trace_id) ?? 0) + 1,
)
}
const sourcePort = (componentName, portName) => {
const component = componentByName.get(componentName)
return (portsByComponentId.get(component?.source_component_id) ?? []).find(
(port) => port.name === portName || port.port_hints?.includes(portName),
)
}
const assertPortNet = (componentName, portName, netName) => {
const port = sourcePort(componentName, portName)
const net = sourceNetByName.get(netName)
assert(Boolean(port), `${componentName}.${portName} is missing`)
assert(Boolean(net), `net.${netName} is missing`)
assert(
port?.subcircuit_connectivity_map_key === net?.subcircuit_connectivity_map_key,
`${componentName}.${portName} is not on net.${netName}`,
)
}
const traceLength = (trace) => {
let length = 0
for (let index = 1; index < trace.route.length; index += 1) {
const previous = trace.route[index - 1]
const current = trace.route[index]
length += Math.hypot(current.x - previous.x, current.y - previous.y)
}
return length
}
const traceStats = (name) => {
const sourceTrace = sourceTraces.find((trace) => trace.name === name)
assert(sourceTrace, `Missing named source trace ${name}`)
if (!sourceTrace) return { length: Number.NaN, vias: Number.NaN, layers: [] }
const traces = pcbTracesBySourceId.get(sourceTrace.source_trace_id) ?? []
assert(traces.length > 0, `${name} has no routed PCB trace`)
return {
sourceTrace,
traces,
length: traces.reduce((sum, trace) => sum + traceLength(trace), 0),
vias: traces.reduce(
(sum, trace) => sum + (viaCountByPcbTraceId.get(trace.pcb_trace_id) ?? 0),
0,
),
layers: [
...new Set(
traces.flatMap((trace) =>
trace.route.map((point) => point.layer).filter(Boolean),
),
),
],
}
}
// Three-terminal copper trees can be represented as autorouter MST segments.
// Prefer a routed endpoint-to-endpoint segment; otherwise use the same
// straight-line fallback as `tsci check trace-length`.
const endpointTraceLength = (name) => {
const stats = traceStats(name)
const sourceIds = new Set(stats.sourceTrace.connected_source_port_ids)
const exact = stats.traces.find((trace) => {
const pcbSourceIds = (trace.connectsTo ?? [])
.map((pcbPortId) => pcbPortById.get(pcbPortId)?.source_port_id)
.filter(Boolean)
return pcbSourceIds.length === sourceIds.size && pcbSourceIds.every((id) => sourceIds.has(id))
})
if (exact) return traceLength(exact)
const endpoints = stats.sourceTrace.connected_source_port_ids
.map((id) => pcbPortBySourceId.get(id))
.filter(Boolean)
assert(endpoints.length === 2, `${name} does not have two positioned endpoints`)
return Math.hypot(endpoints[1].x - endpoints[0].x, endpoints[1].y - endpoints[0].y)
}
const checkResults = {
netlist: await runAllNetlistChecks(circuit),
pinSpecification: await runAllPinSpecificationChecks(circuit),
placement: await runAllPlacementChecks(circuit),
routing: await runAllRoutingChecks(circuit),
}
for (const [name, results] of Object.entries(checkResults)) {
assert(
results.length === 0,
`${name} checks returned ${results.length}: ${results.map((result) => result.message).join(" | ")}`,
)
}
assert(sourceComponents.length === 38, `Expected 38 source components, found ${sourceComponents.length}`)
assert(pcbComponents.length === 38, `Expected 38 PCB components, found ${pcbComponents.length}`)
assert(pcbBoards.length === 1, `Expected one PCB outline, found ${pcbBoards.length}`)
const pcbBoard = pcbBoards[0]
assert(
approximately(pcbBoard?.width, 90) && approximately(pcbBoard?.height, 21),
`PCB bounds are ${pcbBoard?.width ?? "missing"} mm by ${pcbBoard?.height ?? "missing"} mm instead of 90 mm by 21 mm`,
)
const expectedBoardOutline = [
[-45, 10.5], [45, 10.5], [45, -10.5], [-45, -10.5],
]
assert(
pcbBoard?.outline?.length === expectedBoardOutline.length &&
expectedBoardOutline.every(([x, y], index) =>
approximately(pcbBoard.outline[index]?.x, x) &&
approximately(pcbBoard.outline[index]?.y, y),
),
"PCB outline is not the expected uniform 90 mm by 21 mm Pico-width rectangle",
)
const topHeader = pcbComponentBySourceId.get(componentByName.get("J_TOP")?.source_component_id)
const bottomHeader = pcbComponentBySourceId.get(componentByName.get("J_BOTTOM")?.source_component_id)
const headerRowSpacing = Math.abs(topHeader?.center.y - bottomHeader?.center.y)
const boardHalfHeight = 10.5
const boardMinX = -45
const boardMaxX = 45
const edgeOverhang = boardHalfHeight - Math.abs(topHeader?.center.y)
const breadboardPitch = 2.54
const nominalJumperHousingHalfWidth = breadboardPitch / 2
const adjacentHoleCenterClearance =
Math.abs(topHeader?.center.y) + breadboardPitch - boardHalfHeight
const adjacentJumperBodyClearance =
adjacentHoleCenterClearance - nominalJumperHousingHalfWidth
const headerPadEdgeClearance =
boardHalfHeight - (Math.abs(topHeader?.center.y) + topHeader?.height / 2)
assert(
approximately(topHeader?.center.x, 0) && approximately(topHeader?.center.y, 8.89),
"J_TOP is not centered on the +8.89 mm breadboard row",
)
assert(
approximately(bottomHeader?.center.x, 0) && approximately(bottomHeader?.center.y, -8.89),
"J_BOTTOM is not centered on the -8.89 mm breadboard row",
)
assert(approximately(headerRowSpacing, 17.78), `Header row spacing is ${headerRowSpacing} mm instead of 17.78 mm`)
assert(approximately(edgeOverhang, 1.61), `Header-to-board-edge overhang is ${edgeOverhang} mm instead of 1.61 mm`)
assert(
adjacentHoleCenterClearance >= 0.8,
`Immediately adjacent breadboard holes are obstructed: only ${adjacentHoleCenterClearance.toFixed(3)} mm from center to PCB edge`,
)
assert(
headerPadEdgeClearance >= 0.3,
`Header copper-to-board-edge clearance is only ${headerPadEdgeClearance.toFixed(3)} mm`,
)
for (const header of [topHeader, bottomHeader]) {
assert(
approximately(header?.width, 49.86) && approximately(header?.height, 1.6),
"Header copper footprint is not 49.86 mm by 1.6 mm",
)
}
let lastHeaderHousingClearance = Number.POSITIVE_INFINITY
for (const headerName of ["J_TOP", "J_BOTTOM"]) {
const header = componentByName.get(headerName)
const headerPcbComponent = pcbComponentBySourceId.get(header?.source_component_id)
const xs = pcbPorts
.filter((port) => port.pcb_component_id === headerPcbComponent?.pcb_component_id)
.map((port) => port.x)
.sort((a, b) => a - b)
assert(xs.length === 20, `${headerName} has ${xs.length} PCB pins instead of 20`)
assert(
xs.slice(1).every((x, index) => approximately(x - xs[index], 2.54)),
`${headerName} is not on a 2.54 mm breadboard grid`,
)
const headerHoles = elements("pcb_plated_hole").filter(
(hole) => hole.pcb_component_id === headerPcbComponent?.pcb_component_id,
)
assert(headerHoles.length === 20, `${headerName} has ${headerHoles.length} plated holes instead of 20`)
assert(
headerHoles.every((hole) =>
hole.shape === "circular_hole_with_rect_pad" &&
approximately(hole.hole_diameter, 1.05) &&
approximately(hole.rect_pad_width, 1.6) &&
approximately(hole.rect_pad_height, 1.6) &&
approximately(hole.rect_border_radius, 0.8) &&
(Math.min(hole.rect_pad_width, hole.rect_pad_height) - hole.hole_diameter) / 2 >= 0.275 - 0.001,
),
`${headerName} does not retain the supplier's 1.05 mm drill and 0.275 mm annular ring`,
)
const housingEnvelope = Math.max(...xs.map(Math.abs)) + nominalJumperHousingHalfWidth
lastHeaderHousingClearance = Math.min(
lastHeaderHousingClearance,
Math.min(boardMaxX - housingEnvelope, -boardMinX - housingEnvelope),
)
}
assert(
lastHeaderHousingClearance >= 0.5,
`Board ends too early: only ${lastHeaderHousingClearance.toFixed(3)} mm beyond the end-pin jumper housing`,
)
assert(schematicSheets.length === 1, `Expected one schematic sheet, found ${schematicSheets.length}`)
const mainSchematicSheet = schematicSheets[0]
assert(mainSchematicSheet?.name === "MAIN", "The main schematic sheet is missing or misnamed")
assert(mainSchematicSheet?.sheet_size === "ansi_b", "The main schematic sheet is not ANSI B")
assert(
mainSchematicSheet?.sheet_width === 431.8 && mainSchematicSheet?.sheet_height === 279.4,
"The main schematic sheet dimensions are not 431.8 mm by 279.4 mm",
)
assert(
schematicComponents.length === sourceComponents.length &&
schematicComponents.every(
(component) => component.schematic_sheet_id === mainSchematicSheet?.schematic_sheet_id,
),
"Not every source component is represented on the main schematic sheet",
)
assert(
schematicTraces.every(
(trace) => trace.schematic_sheet_id === mainSchematicSheet?.schematic_sheet_id,
),
"Not every schematic trace is assigned to the main schematic sheet",
)
const schematicText = new Set(elements("schematic_text").map((text) => text.text))
for (const sectionTitle of [
"USB-C receptacle & CC",
"USB protection & UART bridge",
"5 V to 3.3 V power",
"MCU, reset & boot",
"RGB debug LED",
"I2C connectors",
"SPI display connector",
"GPIO & debug headers",
]) {
assert(schematicText.has(sectionTitle), `Schematic section title is missing: ${sectionTitle}`)
}
for (const component of sourceComponents) {
assert(Boolean(component.manufacturer_part_number), `${component.name} has no manufacturer part number`)
assert(
Array.isArray(component.supplier_part_numbers?.jlcpcb) &&
component.supplier_part_numbers.jlcpcb.length > 0,
`${component.name} has no JLCPCB part number`,
)
}
const courtyardComponentIds = new Set(courtyards.map((courtyard) => courtyard.pcb_component_id))
for (const component of pcbComponents) {
const sourceComponent = sourceComponentById.get(component.source_component_id)
assert(
courtyardComponentIds.has(component.pcb_component_id),
`${sourceComponent?.name ?? component.pcb_component_id} has no courtyard`,
)
assert(component.layer === "top", `${sourceComponent?.name ?? component.pcb_component_id} is not on the top layer`)
}
for (const via of vias) {
assert(
via.layers.length === 2 && via.layers.includes("top") && via.layers.includes("bottom"),
`${via.pcb_via_id} is not a top-to-bottom through-via`,
)
assert(
approximately(via.hole_diameter, 0.3),
`${via.pcb_via_id} drill is ${via.hole_diameter} mm instead of 0.30 mm`,
)
assert(
approximately(via.outer_diameter, 0.45),
`${via.pcb_via_id} pad is ${via.outer_diameter} mm instead of 0.45 mm`,
)
}
const usbTraceNames = [
"USB_DP_FLIP_A",
"USB_DP_FLIP_B",
"USB_DM_FLIP_A",
"USB_DM_FLIP_B",
"USB_DM_TO_ESD",
"USB_DP_PROTECTED",
"USB_DM_PROTECTED",
]
const criticalStats = Object.fromEntries(
usbTraceNames.map((name) => [name, traceStats(name)]),
)
for (const [name, stats] of Object.entries(criticalStats)) {
assert(stats.vias === 0, `${name} contains ${stats.vias} via(s)`)
assert(
stats.layers.length === 1 && stats.layers[0] === "top",
`${name} is not entirely top-layer`,
)
}
const protectedLengths = {
DP: endpointTraceLength("USB_DP_PROTECTED"),
DM: endpointTraceLength("USB_DM_PROTECTED"),
}
const protectedSkew = Math.abs(protectedLengths.DP - protectedLengths.DM)
assert(protectedSkew <= 0.2, `Protected USB pair skew is ${protectedSkew.toFixed(3)} mm`)
const joinPin1 = pcbPortBySourceId.get(sourcePort("R_USB_DM_JOIN", "pin1")?.source_port_id)
const joinPin2 = pcbPortBySourceId.get(sourcePort("R_USB_DM_JOIN", "pin2")?.source_port_id)
const joinSpan = Math.hypot(joinPin2.x - joinPin1.x, joinPin2.y - joinPin1.y)
const usbPaths = {
A_DP: endpointTraceLength("USB_DP_FLIP_A") + protectedLengths.DP,
A_DM: endpointTraceLength("USB_DM_FLIP_A") + endpointTraceLength("USB_DM_TO_ESD") + protectedLengths.DM,
B_DP: endpointTraceLength("USB_DP_FLIP_B") + protectedLengths.DP,
B_DM: endpointTraceLength("USB_DM_FLIP_B") + joinSpan + endpointTraceLength("USB_DM_TO_ESD") + protectedLengths.DM,
}
const usbSkew = {
A: Math.abs(usbPaths.A_DP - usbPaths.A_DM),
B: Math.abs(usbPaths.B_DP - usbPaths.B_DM),
}
assert(
Math.max(usbSkew.A, usbSkew.B) <= 1,
`USB end-to-end skew exceeds 1 mm: ${JSON.stringify(usbSkew)}`,
)
const vcoreStats = traceStats("VCORE_DECOUPLE")
assert(vcoreStats.vias === 0, `VCORE_DECOUPLE contains ${vcoreStats.vias} via(s)`)
assert(
vcoreStats.layers.length === 1 && vcoreStats.layers[0] === "top",
"VCORE_DECOUPLE is not entirely top-layer",
)
assert(vcoreStats.length <= 4, `VCORE_DECOUPLE is ${vcoreStats.length.toFixed(3)} mm, over 4 mm`)
for (const switchName of ["SW_BOOT", "SW_RESET"]) {
const component = componentByName.get(switchName)
const ports = portsByComponentId.get(component?.source_component_id) ?? []
const pcbComponent = pcbComponentBySourceId.get(component?.source_component_id)
const physicalPorts = pcbPorts.filter((port) => port.pcb_component_id === pcbComponent?.pcb_component_id)
assert(component?.ftype === "simple_push_button", `${switchName} is not a semantic pushbutton`)
assert(ports.length === 2 && physicalPorts.length === 2, `${switchName} is not a true two-terminal switch`)
assert(
ports[0]?.subcircuit_connectivity_map_key !== ports[1]?.subcircuit_connectivity_map_key,
`${switchName} terminals are shorted together`,
)
}
assertPortNet("SW_BOOT", "pin1", "PA18_BSL_INVOKE")
assertPortNet("SW_BOOT", "pin2", "VDD_3V3")
assertPortNet("SW_RESET", "pin1", "MCU_RESET_N")
assertPortNet("SW_RESET", "pin2", "GND")
const rgbLed = componentByName.get("D_USER_RGB")
const rgbPorts = portsByComponentId.get(rgbLed?.source_component_id) ?? []
assert(rgbLed?.ftype === "simple_chip", "D_USER_RGB is not a four-channel semantic component")
assert(rgbLed?.manufacturer_part_number === "SZYY1615RGB-A 4pin", "D_USER_RGB has the wrong MPN")
assert(rgbLed?.supplier_part_numbers?.jlcpcb?.[0] === "C3029072", "D_USER_RGB has the wrong JLCPCB part number")
assert(rgbPorts.length === 4, `D_USER_RGB has ${rgbPorts.length} ports instead of four`)
assertPortNet("D_USER_RGB", "pin4", "GND")
assert(sourcePort("D_USER_RGB", "pin4")?.requires_ground === true, "D_USER_RGB common cathode is not marked requiresGround")
const rgbAnodeNetKeys = new Set()
for (const [color, resistor, gpioNet, ledPin] of [
["red", "R_RGB_RED", "PB2_RGB_RED", "pin1"],
["green", "R_RGB_GREEN", "PB3_RGB_GREEN", "pin2"],
["blue", "R_RGB_BLUE", "PB14_RGB_BLUE", "pin3"],
]) {
const resistorComponent = componentByName.get(resistor)
assert(resistorComponent?.resistance === 330, `${resistor} is not 330 ohms`)
assert(resistorComponent?.manufacturer_part_number === "0402WGF3300TCE", `${resistor} has the wrong MPN`)
assert(resistorComponent?.supplier_part_numbers?.jlcpcb?.[0] === "C25104", `${resistor} has the wrong JLCPCB part number`)
assertPortNet(resistor, "pin1", gpioNet)
const ledPort = sourcePort("D_USER_RGB", ledPin)
assert(ledPort?.requires_power === true, `D_USER_RGB ${color} anode is not marked requiresPower`)
rgbAnodeNetKeys.add(ledPort?.subcircuit_connectivity_map_key)
assert(
ledPort?.subcircuit_connectivity_map_key ===
sourcePort(resistor, "pin2")?.subcircuit_connectivity_map_key,
`D_USER_RGB ${color} anode is not connected through ${resistor}`,
)
}
assert(rgbAnodeNetKeys.size === 3, "D_USER_RGB color channels are shorted together")
assertPortNet("U_MCU", "PA7", "PA7")
assertPortNet("J_BOTTOM", "PA7", "PA7")
const expectedConnectorNets = {
J_I2C_JST_SH: { pin1: "GND", pin2: "VDD_3V3", pin3: "PA0_I2C_SDA", pin4: "PA1_I2C_SCL" },
J_GROVE_I2C: { pin1: "PA1_I2C_SCL", pin2: "PA0_I2C_SDA", pin3: "VDD_3V3", pin4: "GND" },
J_SPI_DISPLAY: {
pin1: "GND", pin2: "VDD_3V3", pin3: "PB9_SPI_SCK", pin4: "PB8_SPI_MOSI",
pin5: "PB7_SPI_MISO", pin6: "PB6_SPI_CS", pin7: "PA8_SPI_DC", pin8: "PA9_SPI_RST",
},
}
for (const [componentName, pins] of Object.entries(expectedConnectorNets)) {
assert(componentByName.get(componentName)?.ftype === "simple_connector", `${componentName} is not a connector`)
for (const [pin, net] of Object.entries(pins)) assertPortNet(componentName, pin, net)
}
for (const contact of ["A6", "B6", "A7", "B7"]) {
assert(Boolean(sourcePort("J_USB_C", contact)), `J_USB_C is missing ${contact}`)
}
assertPortNet("U_USB_UART", "VCC", "VDD_3V3")
assertPortNet("U_USB_UART", "V3", "VDD_3V3")
assertPortNet("R_UART_TX", "pin2", "PA11_UART_RX")
assertPortNet("R_UART_RX", "pin1", "PA10_UART_TX")
const mountingHoles = elements("pcb_hole").filter(
(hole) => hole.pcb_component_id === null && Math.abs(hole.hole_diameter - 2.7) < 0.001,
)
const fiducials = elements("pcb_smtpad").filter(
(pad) => pad.pcb_component_id === null && pad.shape === "circle" && pad.radius === 0.5,
)
assert(mountingHoles.length === 4, `Expected four M2.5 mounting holes, found ${mountingHoles.length}`)
assert(fiducials.length === 3, `Expected three SMT fiducials, found ${fiducials.length}`)
for (const [x, y] of [[-42, 8.3], [-42, -8.3], [35.5, 8.3], [26.75, -8.3]]) {
assert(
mountingHoles.some((hole) => approximately(hole.x, x) && approximately(hole.y, y)),
`Missing M2.5 mounting hole at (${x}, ${y}) mm`,
)
}
for (const hole of mountingHoles) {
const edgeClearance = Math.min(
pcbBoard.width / 2 - Math.abs(hole.x),
pcbBoard.height / 2 - Math.abs(hole.y),
) - hole.hole_diameter / 2
assert(edgeClearance >= 0.8, `${hole.pcb_hole_id} has only ${edgeClearance.toFixed(3)} mm edge clearance`)
}
const silk = elements("pcb_silkscreen_text").map((text) => text.text)
for (const requiredText of ["19 3V3", "3 3V3", "JST P1:G 2:3V3 3:SDA 4:SCL", "GROVE P1:SCL 2:SDA 3:3V3 4:G", "RGB R:PB2 G:PB3 B:PB14"]) {
assert(silk.includes(requiredText), `Required silkscreen label is missing: ${requiredText}`)
}
assert(silk.some((text) => text.startsWith("SPI P1:G 2:3V3")), "SPI pinout silkscreen is missing")
assert(!source.includes("PA7_USER_LED"), "Stale PA7 white-LED net name remains")
assert(!source.includes("D_USER_WHITE"), "Stale white-LED component remains")
assert(!sourceComponents.some((component) => component.ftype === "simple_crystal"), "A crystal is present; CH340C should use its internal oscillator")
assert(!/<pcbtrace\b/i.test(source), "Manual <pcbtrace> routing is present")
assert(!/<via\b/i.test(source), "Manual <via> placement is present")
assert(!/<tracehint\b/i.test(source), "Manual <tracehint> routing is present")
assert(!/\bpcbPath\s*=/.test(source), "Manual pcbPath routing is present")
assert((source.match(/<differentialpair\b/g) ?? []).length === 1, "Expected one explicit protected USB differential pair")
const pasteLayers = [...new Set(elements("pcb_solder_paste").map((paste) => paste.layer))]
assert(pasteLayers.length === 1 && pasteLayers[0] === "top", `Unexpected solder-paste layers: ${pasteLayers.join(", ")}`)
assert(!circuit.some((element) => ["inner1", "inner2", "inner3", "inner4"].includes(element.layer)), "Inner-layer PCB elements are present")
const embeddedErrors = circuit.filter((element) => element.type.endsWith("_error"))
assert(embeddedErrors.length === 0, `Circuit JSON contains ${embeddedErrors.length} embedded errors`)
assert(
elements("schematic_element_outside_sheet_warning").length === 0,
"One or more schematic elements extend outside the sheet",
)
assert(
!elements("schematic_component_styling_warning").some(
(warning) => warning.styling_issue_type === "missing_schematic_sheet",
),
"The circuit reports a missing schematic sheet",
)
const allowedWarnings = new Set([
"schematic_component_styling_warning",
"supplier_footprint_mismatch_warning",
"source_part_not_found_warning",
])
const unexpectedWarnings = circuit.filter(
(element) => element.type.endsWith("_warning") && !allowedWarnings.has(element.type),
)
assert(unexpectedWarnings.length === 0, `Circuit JSON contains unexpected warnings: ${unexpectedWarnings.map((warning) => warning.type).join(", ")}`)
const footprintWarnings = elements("supplier_footprint_mismatch_warning")
const ch340FootprintWarnings = footprintWarnings.filter((warning) => warning.message.includes("jlcpcb:C84681"))
assert(
footprintWarnings.length === 1 && ch340FootprintWarnings.length === 1,
"Unexpected supplier-footprint warning set",
)
const supplierFetchWarnings = elements("source_part_not_found_warning")
console.log(JSON.stringify({
checks: Object.fromEntries(Object.entries(checkResults).map(([name, results]) => [name, results.length])),
components: sourceComponents.length,
courtyards: courtyardComponentIds.size,
pcbTraces: pcbTraces.length,
throughVias: vias.length,
mountingHoles: mountingHoles.length,
fiducials: fiducials.length,
mechanical: {
overallBoardBoundsMm: [pcbBoard?.width, pcbBoard?.height],
uniformPicoWidthMm: boardHalfHeight * 2,
headerRowSpacingMm: rounded(headerRowSpacing),
headerPitchMm: 2.54,
edgeOverhangMm: rounded(edgeOverhang),
adjacentHoleCenterToBoardEdgeMm: rounded(adjacentHoleCenterClearance),
nominalJumperBodyPlanOverlapMm: rounded(Math.max(0, -adjacentJumperBodyClearance)),
boardBeyondEndPinJumperHousingMm: rounded(lastHeaderHousingClearance),
headerPadToBoardEdgeMm: rounded(headerPadEdgeClearance),
immediatelyAdjacentBreadboardHoleCentersOutsideBoard: true,
},
pasteLayers,
schematic: {
sheet: mainSchematicSheet?.display_name,
size: mainSchematicSheet?.sheet_size,
components: schematicComponents.length,
traces: schematicTraces.length,
sections: 8,
},
usb: {
pathsMm: Object.fromEntries(Object.entries(usbPaths).map(([name, value]) => [name, rounded(value)])),
skewMm: { A: rounded(usbSkew.A), B: rounded(usbSkew.B), protected: rounded(protectedSkew) },
allTopLayerAndViaFree: usbTraceNames.every((name) => criticalStats[name].vias === 0),
},
vcore: { lengthMm: rounded(vcoreStats.length), vias: vcoreStats.vias },
switches: "two terminal, distinct nets, active-high BOOT and active-low RESET",
rgbDebugLed: "common cathode; PB2 red, PB3 green, PB14 blue; active-high through 330Ω",
warningsReviewed: {
exactFootprintComparison: ch340FootprintWarnings.map((warning) => warning.message),
transientSupplierFetchWarnings: supplierFetchWarnings.length,
},
failures,
}, null, 2))
if (failures.length > 0) process.exitCode = 1