shibosoftwaredev/f1c100s-linux-nema17-stepper-controller
A four-layer NEMA 17 motor-controller PCB combining an Allwinner F1C100S Linux computer, USB‑PD/buck power system, W5500 Ethernet, STM32 real-time motion control, DRV8825 stepper driver, protection/temperature/current telemetry, USB interfaces, and local LED/buzzer diagnostics.
- Version
- 0.4.56
- License
- unset
- Stars
- 0
scripts/verify.mjs
import assert from "node:assert/strict"
import crypto from "node:crypto"
import fs from "node:fs"
const circuitPath = "dist/index/circuit.json"
const circuitBytes = fs.readFileSync(circuitPath)
const circuit = JSON.parse(circuitBytes)
const of = (type) => circuit.filter((item) => item.type === type)
const parent = new Map()
const root = (id) => {
if (!parent.has(id)) parent.set(id, id)
if (parent.get(id) !== id) parent.set(id, root(parent.get(id)))
return parent.get(id)
}
const join = (a, b) => parent.set(root(a), root(b))
for (const trace of of("source_trace")) {
const ids = [...(trace.connected_source_port_ids ?? []), ...(trace.connected_source_net_ids ?? [])]
ids.slice(1).forEach((id) => join(ids[0], id))
}
const component = (name) => {
const matches = of("source_component").filter((item) => item.name === name)
assert.equal(matches.length, 1, `unique component ${name}`)
return matches[0]
}
const port = (name, pin) => {
const source = component(name)
const result = of("source_port").find((item) =>
item.source_component_id === source.source_component_id &&
(item.pin_number === pin || item.name === pin || item.port_hints?.includes(pin)))
assert.ok(result, `${name}.${pin}`)
return result.source_port_id
}
const net = (name) => {
const result = of("source_net").find((item) => item.name === name)
assert.ok(result, `net ${name}`)
return result.source_net_id
}
const expectNet = (name, pin, netName) => {
assert.equal(root(port(name, pin)), root(net(netName)), `${name}.${pin} -> ${netName}`)
}
const expectPair = (aName, aPin, bName, bPin) => {
assert.equal(root(port(aName, aPin)), root(port(bName, bPin)), `${aName}.${aPin} -> ${bName}.${bPin}`)
}
const expectPart = (name, mpn, lcsc) => {
assert.equal(component(name).manufacturer_part_number, mpn, `${name} MPN`)
assert.deepEqual(component(name).supplier_part_numbers.jlcpcb, [lcsc], `${name} JLCPCB part`)
}
const placedComponent = (name) => of("pcb_component").find((item) =>
item.source_component_id === component(name).source_component_id)
const schematicComponent = (name) => {
const result = of("schematic_component").find((item) =>
item.source_component_id === component(name).source_component_id)
assert.ok(result, `schematic component ${name}`)
return result
}
// Keep each LDO's input bypass on its input (left) side and output bypass on
// its output (right) side. The cloud style analyzer checks this relationship,
// while older local CLI versions do not yet report it.
for (const regulator of ["U_2V5", "U_1V2", "U_2V8"]) {
const regulatorX = schematicComponent(regulator).center.x
assert.ok(schematicComponent(`C_${regulator}_IN`).center.x < regulatorX,
`${regulator} input capacitor is left of the regulator`)
assert.ok(schematicComponent(`C_${regulator}_OUT`).center.x > regulatorX,
`${regulator} output capacitor is right of the regulator`)
}
// Fabrication geometry and DRC invariants.
const errors = circuit.filter((item) => item.type.endsWith("_error"))
assert.deepEqual(errors, [], "circuit JSON contains no DRC errors")
const board = of("pcb_board")[0]
const BOARD_LEFT = -21.15
const BOARD_RIGHT = 21.15
const BOARD_BOTTOM = -21.15
const BOARD_TOP = 21.15
const FAB_EDGE_CLEARANCE = 0.2
const CONNECTOR_HOLE_EDGE_CLEARANCE = 0.25
const MOUNT_HOLE_DIAMETER = 3.5
const MOUNT_HARDWARE_RADIUS = 2.75
const MOUNT_TOP_COPPER_CLEARANCE = 0.1
const MOTOR_SIDE_STANDOFF_RADIUS = 2.5
const MOUNT_CENTERS = [
{ x: -15.5, y: -15.5 },
{ x: -15.5, y: 15.5 },
{ x: 15.5, y: -15.5 },
{ x: 15.5, y: 15.5 },
]
const EXPECTED_OUTLINE = [
{ x: BOARD_LEFT, y: BOARD_TOP }, { x: BOARD_RIGHT, y: BOARD_TOP },
{ x: BOARD_RIGHT, y: BOARD_BOTTOM }, { x: BOARD_LEFT, y: BOARD_BOTTOM },
]
assert.equal(board.num_layers, 4)
assert.equal(board.thickness,1.6,"explicit standard 1.6 mm four-layer fabrication thickness")
assert.deepEqual(board.outline, EXPECTED_OUTLINE,
"strict 42.3 x 42.3 mm NEMA17 square with no connector-support tabs")
assert.equal(board.min_via_hole_diameter, 0.3)
assert.equal(board.min_via_pad_diameter, 0.45)
const vias = of("pcb_via")
assert.ok(vias.length > 0)
assert.ok(vias.every((via) => via.hole_diameter === 0.3 && via.outer_diameter === 0.45),
"every via is exactly 0.30 mm drill / 0.45 mm pad")
assert.ok(vias.every((via) =>
via.layers?.length === 4 &&
["top", "inner1", "inner2", "bottom"].every((layer) => via.layers.includes(layer))),
"every via is a full-stack L1-L4 through-via")
const highCurrentSourceTraceIds = new Set([
"USB_VBUS_A", "USB_VBUS_B", "BUCK_IN_CAP", "BUCK_SW", "VM_INPUT", "VM_FUSED",
"VM_PROTECTED", "BUCK_INPUT_BYPASS", "MOTOR_A_SENSED_PLUS", "MOTOR_B_SENSED_PLUS",
].map(name => {
const source = of("source_trace").find(trace => trace.name === name)
assert.ok(source, `required high-current source ${name}`)
return source.source_trace_id
}))
const highCurrentPcbTraces = of("pcb_trace")
.filter((trace) => highCurrentSourceTraceIds.has(trace.source_trace_id))
assert.equal(new Set(highCurrentPcbTraces.map(trace => trace.source_trace_id)).size,
highCurrentSourceTraceIds.size, "every high-current trunk has routed copper")
assert.ok(highCurrentPcbTraces.every((trace) => trace.route
.filter((point) => point.route_type === "wire")
.every((point) => point.layer === "top" || point.layer === "bottom")),
"USB-PD, buck and motor-supply high-current trunks use outer-layer copper only")
for(const name of ["DRV_ISEN_A","DRV_ISEN_B","MOTOR_A_PLUS","MOTOR_A_MINUS","MOTOR_B_PLUS","MOTOR_B_MINUS"]){
const source=of("source_trace").find(t=>t.name===name)
assert.ok(source,`${name} source exists`)
const routes=of("pcb_trace").filter(t=>t.source_trace_id===source.source_trace_id)
assert.ok(routes.length>0,`${name} has actual current-carrying copper`)
assert.ok(routes.every(t=>t.route.filter(p=>p.route_type==="wire").every(p=>["top","bottom"].includes(p.layer)&&p.width>=.3-1e-9)),`${name} including regulation-shunt return stays >=0.30 mm on outer layers`)
}
assert.equal(new Set(vias.map((via) => `${via.x.toFixed(6)},${via.y.toFixed(6)}`)).size, vias.length,
"no coincident vias")
for (const placed of of("pcb_component")) {
if (placed.is_allowed_to_be_off_board) continue
const x0 = placed.center.x - placed.width / 2
const x1 = placed.center.x + placed.width / 2
const y0 = placed.center.y - placed.height / 2
const y1 = placed.center.y + placed.height / 2
assert.ok(x0 >= -21.15 - 1e-6 && x1 <= 21.15 + 1e-6 && y0 >= -21.15 - 1e-6 && y1 <= 21.15 + 1e-6,
`${placed.pcb_component_id} is within the board outline`)
}
// `allowOffBoard` is appropriate for an edge connector's plastic housing,
// never for its drilled features or copper. Check primitives independently so
// a component-level exemption cannot hide clipped holes or annular rings.
const geometrySourceById = new Map(of("source_component")
.map((item) => [item.source_component_id, item]))
const geometryPcbById = new Map(of("pcb_component")
.map((item) => [item.pcb_component_id, item]))
for (const cad of of("cad_component")) {
const pcb = geometryPcbById.get(cad.pcb_component_id)
if (!pcb) continue
assert.ok(pcb.layer === "bottom" ? cad.position.z < 0 : cad.position.z > 0,
`${geometrySourceById.get(pcb.source_component_id)?.name} CAD model matches its assembly face`)
}
const pdCad = of("cad_component").find((entry) =>
entry.source_component_id === component("U_PD").source_component_id)
assert.ok(pdCad?.model_obj_url?.includes("C24833806"), "U_PD retains its supplier 3D model")
assert.equal(placedComponent("U_PD").layer, "top", "U_PD occupies the top-side layout pocket")
const primitiveComponentName = (primitive) => {
const placed = geometryPcbById.get(primitive.pcb_component_id)
return geometrySourceById.get(placed?.source_component_id)?.name
}
const primitiveBounds = (primitive) => {
if (primitive.points?.length) {
const xs = primitive.points.map((point) => point.x)
const ys = primitive.points.map((point) => point.y)
return {
left: Math.min(...xs), right: Math.max(...xs),
bottom: Math.min(...ys), top: Math.max(...ys),
}
}
const width = primitive.outer_width ?? primitive.outer_diameter ??
primitive.width ?? primitive.hole_diameter ?? primitive.radius * 2
const height = primitive.outer_height ?? primitive.outer_diameter ??
primitive.height ?? primitive.hole_diameter ?? primitive.radius * 2
assert.ok(Number.isFinite(width) && Number.isFinite(height),
`${primitive.type} ${primitive.pcb_component_id ?? primitive.pcb_via_id} has measurable bounds`)
return {
left: primitive.x - width / 2,
right: primitive.x + width / 2,
bottom: primitive.y - height / 2,
top: primitive.y + height / 2,
}
}
const edgeClearance = (primitive) => {
const bounds = primitiveBounds(primitive)
return Math.min(
bounds.left - BOARD_LEFT,
BOARD_RIGHT - bounds.right,
bounds.bottom - BOARD_BOTTOM,
BOARD_TOP - bounds.top,
)
}
const distancePointToSegment = (point, a, b) => {
const dx = b.x - a.x
const dy = b.y - a.y
const lengthSquared = dx * dx + dy * dy
const t = lengthSquared === 0 ? 0 : Math.max(0, Math.min(1,
((point.x - a.x) * dx + (point.y - a.y) * dy) / lengthSquared))
return Math.hypot(point.x - (a.x + t * dx), point.y - (a.y + t * dy))
}
const pointInOutline = (point) => {
let inside = false
for (let i = 0, j = EXPECTED_OUTLINE.length - 1; i < EXPECTED_OUTLINE.length; j = i++) {
const a = EXPECTED_OUTLINE[i]
const b = EXPECTED_OUTLINE[j]
if ((a.y > point.y) !== (b.y > point.y) &&
point.x < ((b.x - a.x) * (point.y - a.y)) / (b.y - a.y) + a.x)
inside = !inside
}
return inside
}
const connectorHoleEdgeClearance = (primitive) => {
const bounds = primitiveBounds(primitive)
const radius = Math.max(bounds.right - bounds.left, bounds.top - bounds.bottom) / 2
const center = { x: primitive.x, y: primitive.y }
if (!pointInOutline(center)) return -Infinity
return Math.min(...EXPECTED_OUTLINE.map((point, index) =>
distancePointToSegment(center, point, EXPECTED_OUTLINE[(index + 1) % EXPECTED_OUTLINE.length]))) - radius
}
const fabricationPrimitives = [
...of("pcb_plated_hole"),
...of("pcb_hole"),
...of("pcb_smtpad"),
...of("pcb_via"),
]
for (const primitive of fabricationPrimitives) {
const name = primitiveComponentName(primitive)
const clearance = (name === "J_ETH" || name === "J_MOTOR") &&
(primitive.type === "pcb_plated_hole" || primitive.type === "pcb_hole")
? connectorHoleEdgeClearance(primitive)
: edgeClearance(primitive)
assert.ok(clearance >= FAB_EDGE_CLEARANCE - 1e-6,
`${primitiveComponentName(primitive) ?? primitive.type} ${primitive.type} has at least ${FAB_EDGE_CLEARANCE} mm edge clearance`)
}
for (const primitive of [...of("pcb_plated_hole"), ...of("pcb_hole")]) {
const name = primitiveComponentName(primitive)
if (name !== "J_ETH" && name !== "J_MOTOR") continue
assert.ok(connectorHoleEdgeClearance(primitive) >= CONNECTOR_HOLE_EDGE_CLEARANCE - 1e-6,
`${name} ${primitive.type} has at least ${CONNECTOR_HOLE_EDGE_CLEARANCE} mm edge clearance`)
}
const distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y)
const assertNear = (actual, expected, tolerance, message) =>
assert.ok(Math.abs(actual - expected) <= tolerance,
`${message}: expected ${expected} +/- ${tolerance}, got ${actual}`)
const mountHoles = MOUNT_CENTERS.map((center, index) => {
const placed = placedComponent(`MH${index + 1}`)
assert.ok(placed, `MH${index + 1} placed component`)
assertNear(placed.center.x, center.x, 1e-6, `MH${index + 1} x`)
assertNear(placed.center.y, center.y, 1e-6, `MH${index + 1} y`)
const holes = of("pcb_hole").filter((hole) =>
hole.pcb_component_id === placed.pcb_component_id &&
hole.hole_diameter === MOUNT_HOLE_DIAMETER)
assert.equal(holes.length, 1, `MH${index + 1} has one 3.5 mm NPTH`)
assertNear(holes[0].x, center.x, 1e-6, `MH${index + 1} drill x`)
assertNear(holes[0].y, center.y, 1e-6, `MH${index + 1} drill y`)
return center
})
const mountKeepouts = of("pcb_keepout")
assert.equal(mountKeepouts.length, 4,
"four standard-head mounting keepouts reach fabrication output")
for (const center of MOUNT_CENTERS) {
const keepout = mountKeepouts.find((entry) =>
distance(entry.center, center) <= 1e-6)
assert.ok(keepout, `mounting keepout exists at ${center.x},${center.y}`)
assert.equal(keepout.shape, "circle")
assert.equal(keepout.radius, MOUNT_HARDWARE_RADIUS,
"mounting keepout reserves the 5.5 mm ISO 4762 M3 head envelope")
assert.deepEqual(keepout.layers, ["top"],
"mounting keepout marks the screw-head side")
assert.equal(keepout.allow_placements, true,
"courtyard-only overlaps are allowed; physical bodies are checked below")
assert.equal(keepout.allow_traces, true,
"masked signal copper may pass below the mechanical head envelope")
assert.equal(keepout.warning_only, true,
"through-via overlap is advisory because every affected via is tented")
}
const mountSilkscreenCircles = of("pcb_silkscreen_circle").filter((entry) =>
entry.layer === "top" &&
entry.radius === MOUNT_HARDWARE_RADIUS &&
MOUNT_CENTERS.some((center) => distance(entry.center, center) <= 1e-6))
assert.equal(mountSilkscreenCircles.length, 4,
"four visible top-silkscreen rings mark the M3 head keepouts")
// The circular records are mechanical screw-head envelopes. Copper pours are
// clipped by them; masked signal traces are permitted below the head. Keep all
// exposed pads and plated-hole copper out, and require every through-via whose
// annulus falls under a head to be tented on the assembled board.
for (const center of MOUNT_CENTERS) {
for (const via of vias) {
const overlapsHead = distance(center, via) < MOUNT_HARDWARE_RADIUS +
via.outer_diameter / 2 + MOUNT_TOP_COPPER_CLEARANCE - 1e-6
if (overlapsHead)
assert.equal(via.tented_on_top, true,
`${via.pcb_via_id} below the M3 head is solder-mask tented`)
}
for (const pad of of("pcb_smtpad").filter((entry) => entry.layer === "top")) {
const bounds = primitiveBounds(pad)
const dx = Math.max(bounds.left - center.x, center.x - bounds.right, 0)
const dy = Math.max(bounds.bottom - center.y, center.y - bounds.top, 0)
assert.ok(Math.hypot(dx, dy) >= MOUNT_HARDWARE_RADIUS - 1e-6,
`${primitiveComponentName(pad)} top pad clears the standard M3 head keepout`)
}
for (const hole of of("pcb_plated_hole")) {
const copperRadius = Math.max(hole.outer_diameter ?? 0,
hole.outer_width ?? 0, hole.outer_height ?? 0) / 2
assert.ok(distance(center, hole) >= MOUNT_HARDWARE_RADIUS + copperRadius - 1e-6,
`${primitiveComponentName(hole)} plated copper clears the standard M3 head keepout`)
}
}
for (const [a, b] of [[0, 1], [0, 2], [1, 3], [2, 3]])
assertNear(distance(mountHoles[a], mountHoles[b]), 31, 2e-6,
`MH${a + 1}-MH${b + 1} NEMA17 pitch`)
for (const [a, b] of [[0, 3], [1, 2]])
assertNear(distance(mountHoles[a], mountHoles[b]), 31 * Math.SQRT2, 2e-6,
`MH${a + 1}-MH${b + 1} NEMA17 diagonal`)
const mountPatternRotationDeg = Math.atan2(
mountHoles[2].y - mountHoles[0].y,
mountHoles[2].x - mountHoles[0].x,
) * 180 / Math.PI
assertNear(mountPatternRotationDeg, 0, 2e-6,
"carrier is square to the motor")
const projectedCarrierOverhangMm =
(42.3 * (Math.abs(Math.cos(mountPatternRotationDeg * Math.PI / 180)) +
Math.abs(Math.sin(mountPatternRotationDeg * Math.PI / 180))) - 42) / 2
assertNear(projectedCarrierOverhangMm, 0.15, 2e-6,
"aligned 42.3 mm carrier projects 0.15 mm beyond each side of a 42 mm motor")
// BOM and assembly invariants.
const dnpSourceIds = new Set(of("pcb_component")
.filter((item) => item.do_not_place)
.map((item) => item.source_component_id))
const placedSourceIds = new Set(of("pcb_component")
.map((item) => item.source_component_id)
.filter(Boolean))
const populated = of("source_component").filter((item) =>
placedSourceIds.has(item.source_component_id) &&
item.ftype !== "simple_test_point" &&
!dnpSourceIds.has(item.source_component_id))
const addedCurrentParts = ["R_INLINE_A","R_INLINE_B","U_CURRENT_A","U_CURRENT_B",
"C_CURRENT_A_SUPPLY","C_CURRENT_B_SUPPLY","R_CURRENT_A_FILTER","R_CURRENT_B_FILTER",
"C_CURRENT_A_FILTER","C_CURRENT_B_FILTER"]
const addedUsbParts = ["U_USB_GATE","Q_USB_ATTACH","R_USB_BASE","R_USB_BASE_PD","R_USB_OE_PU","C_USB_GATE"]
const addedSoftwareCurrentParts = ["U_CURRENT_DAC","C_CURRENT_DAC"]
for (const name of [...addedCurrentParts,...addedUsbParts,...addedSoftwareCurrentParts])
assert.ok(populated.some(part=>part.name===name),`${name} is populated`)
assert.equal(populated.length, 183 + addedCurrentParts.length + addedUsbParts.length + addedSoftwareCurrentParts.length)
for (const item of populated) {
assert.ok(item.supplier_part_numbers?.jlcpcb?.some((code) => /^C\d+$/.test(code)),
`${item.name} has a JLCPCB part number`)
}
const sourceById = new Map(of("source_component").map((item) => [item.source_component_id, item]))
const assembled = of("pcb_component").filter((placed) => {
const source = sourceById.get(placed.source_component_id)
return source && source.ftype !== "simple_test_point" && !placed.do_not_place
})
assert.equal(assembled.length, 183 + addedCurrentParts.length + addedUsbParts.length + addedSoftwareCurrentParts.length)
assert.ok(assembled.some((placed) => placed.layer === "top"), "top-side assembly exists")
assert.ok(assembled.some((placed) => placed.layer === "bottom"), "bottom-side assembly exists")
assert.ok(of("pcb_smtpad").some((pad) => pad.layer === "bottom"), "bottom-side SMT pads exist")
const motorSource = of("source_component").find((item) => item.name === "MOTOR")
assert.ok(motorSource, "NEMA 17 motor exists in the generated assembly")
const motorCad = of("cad_component").find((item) =>
item.source_component_id === motorSource.source_component_id)
assert.ok(motorCad, "NEMA 17 motor has a generated CAD model")
assert.match(motorCad.model_glb_url ?? "",
/nema17_bodylength34mm_shaftlength20mm_round_jstph4\.glb$/)
assert.doesNotMatch(motorCad.model_glb_url ?? "", /backfaceholes|plainbackface/,
"motor retains the modelprinter default rear M3 screws")
assertNear(motorCad.position.z, -34-6-board.thickness/2, 1e-9, "motor assembly Z follows board thickness, 6 mm gap and 34 mm body")
assertNear(motorCad.rotation.x, 180, 1e-9, "motor assembly shaft-out rotation")
assertNear(motorCad.rotation.y, 0, 1e-9, "motor assembly has no Y-axis tilt")
assertNear(motorCad.rotation.z, 90, 1e-9,
"motor body stays square with the PCB while its wire side faces J_MOTOR")
const motorConnector = component("J_MOTOR")
assert.equal(motorConnector.ftype, "simple_connector",
"J_MOTOR is the board-side harness connector")
assert.equal(motorConnector.standard, "jst_ph")
assert.equal(motorConnector.pin_count, 4)
const motorCables = of("cad_cable").filter((item) => item.name === "MOTOR_CABLE")
assert.equal(motorCables.length, 1, "one documented motor cable is rendered")
const motorCable = motorCables[0]
assert.equal(motorCable.from_source_component_id, motorSource.source_component_id,
"motor cable starts at MOTOR.wireside")
assert.equal(motorCable.to_source_component_id, motorConnector.source_component_id,
"motor cable ends at the real board-side J_MOTOR connector")
assert.equal(motorCable.cableprinter_string, "jst_ph_pins4")
assert.ok(motorCable.path.length >= 2, "motor cable has a rendered route")
const cableExit = {
x: motorCable.path[1].x - motorCable.path[0].x,
y: motorCable.path[1].y - motorCable.path[0].y,
}
const connectorPlacement = placedComponent("J_MOTOR")
const bottomEdgeDot = -cableExit.y / Math.hypot(cableExit.x, cableExit.y)
assert.ok(bottomEdgeDot > 0.999,
"MOTOR.wireside exits squarely toward the PCB bottom edge")
assert.ok(connectorPlacement.center.y < board.center.y,
"J_MOTOR is on the same bottom edge as MOTOR.wireside")
assertNear(
Math.abs(motorCable.path.at(-1).x - motorCable.path[0].x),
3,
1e-6,
"motor cable bends across J_MOTOR's small along-edge offset",
)
expectPart("J_USB_DATA", "TYPE-C-31-M-12", "C165948")
expectPart("J_USB_PWR", "TYPE-C-31-M-12", "C165948")
const sameEdgeData = placedComponent("J_USB_DATA")
const sameEdgePower = placedComponent("J_USB_PWR")
assert.ok(sameEdgeData.center.y > 15 && sameEdgePower.center.y > 15,
"both unchanged USB-C connectors are recessed at the north edge")
assert.ok(Math.abs(sameEdgeData.center.y-sameEdgePower.center.y) < 1e-6)
assert.equal(sameEdgeData.rotation, 180)
assert.equal(sameEdgePower.rotation, 180)
assert.equal(sameEdgeData.layer, "top")
assert.equal(sameEdgePower.layer, "top", "power and data USB-C both occupy the top assembly face")
// C165948 OBJ bounds: x [-4.47,4.47], y [-5.3,2.6], referenced to
// model_origin_position.y=-2.7500289. Checking the PAD bounds alone misses
// the shell's 5.3500289 mm forward extension from the placement anchor.
for(const usb of [sameEdgeData,sameEdgePower]) {
assert.ok(usb.center.y+5.3500289<=BOARD_TOP-.1,
"complete USB-C metal housing remains inside the board with 0.1 mm margin")
assert.ok(usb.center.x-4.47>=BOARD_LEFT && usb.center.x+4.47<=BOARD_RIGHT)
}
expectPart("R_USB_DATA_CC1", "0402WGF5101TCE", "C25905")
expectPart("R_USB_DATA_CC2", "0402WGF5101TCE", "C25905")
expectPart("U_PD", "HUSB238A-BB001-QN16R", "C24833806")
expectPart("Q_VBUS", "AO4407A", "C3011194")
expectPart("Q_PD_ENABLE", "BSS84_R1_00001", "C391434")
expectPart("U_BUCK", "TPS54302DDCR", "C311983")
// Check actual emitted metal as well as noConnect. A schematic-only fix must
// never leave the former 15 V branch touching EN's 7 V maximum-rated pin.
const buckEnablePort = port("U_BUCK", "EN")
assert.equal(of("source_port").find(p=>p.source_port_id===buckEnablePort).do_not_connect,true)
assert.ok(!of("source_trace").some(t=>t.connected_source_port_ids?.includes(buckEnablePort)),"EN remains externally open")
const buckEnableAudit = JSON.parse(fs.readFileSync("reports/buck-enable-audit.json","utf8"))
assert.equal(buckEnableAudit.circuitSha256,crypto.createHash("sha256").update(circuitBytes).digest("hex"),"fresh emitted EN isolation audit")
assert.equal(buckEnableAudit.logicalOpen,true)
assert.equal(buckEnableAudit.explicitNoConnect,true)
assert.equal(buckEnableAudit.emittedCopperIncludingPours,true)
assert.ok(buckEnableAudit.minimumEmittedCopperClearanceMm>=.1-2e-4,"EN is clear of actual tracks, drills and pours")
expectPair("Q_VBUS", "pin8", "U_BUCK", "VIN")
expectPart("U_3V3", "AP2112K-3.3TRG1", "C51118")
expectPart("U_ETH", "W5500", "C32843")
expectPart("J_ETH", "HR911105A", "C12074")
expectPart("U_MOTION", "STM32G030F6P6", "C724040")
expectPart("U_MOTOR", "DRV8825PWPR", "C81582")
expectPart("U_TEMP", "TMP102AIDRLR", "C99269")
expectPart("D_STATUS", "WS2812B-2020-V6", "C52917434")
expectPart("U_STATUS_BUFFER", "SN74AHCT1G125DBVR", "C7484")
expectPart("BZ1", "KLJ-4020", "C556936")
expectPart("Q_BUZZER", "AO3400A", "C20917")
expectPart("D_BUZZER", "1N5819WT", "C5185918")
expectPart("C_VM_BULK", "CA55-D050M226TE100", "C41417029")
assert.equal(component("C_VM_BULK").max_voltage_rating, 50)
expectPart("SW_RESET", "B3U-1000P", "C231329")
expectPart("R_ISET_210", "RC0402FR-071K8L", "C163481")
expectPart("R_PD_12V", "0402WGF1332TCE", "C122547")
// Service option resistors are DNP in the safe production default: their
// documented values remain JLCPCB-orderable but produce no paste or 3D body.
for (const name of ["R_ISET_210", "R_PD_12V"]) {
const source = component(name)
const placed = placedComponent(name)
assert.equal(placed.do_not_place, true, `${name} is DNP`)
assert.ok(source.manufacturer_part_number, `${name} has an MPN for assembly variants`)
assert.ok(source.supplier_part_numbers?.jlcpcb?.some((code) => /^C\d+$/.test(code)),
`${name} has a JLCPCB part for assembly variants`)
assert.equal(of("pcb_solder_paste").filter((paste) =>
paste.pcb_component_id === placed.pcb_component_id).length, 0,
`${name} is excluded from the assembly stencil`)
assert.equal(of("cad_component").filter((cad) =>
cad.source_component_id === source.source_component_id).length, 0,
`${name} has no assembled 3D body`)
}
// JP_BOOT is fabricated as a normally closed cuttable copper link. It must
// never reappear as a fitted header, paste aperture or phantom 3D body.
const bootSource = component("JP_BOOT")
const bootPlaced = placedComponent("JP_BOOT")
assert.equal(bootPlaced.do_not_place, true)
assert.equal(bootSource.manufacturer_part_number, undefined)
assert.ok(!bootSource.supplier_part_numbers?.jlcpcb?.length)
expectNet("JP_BOOT", "SOC_CS", "SOC_FLASH_CS")
expectNet("JP_BOOT", "FLASH_CS", "FLASH_CS")
const bootPads = of("pcb_smtpad")
.filter((pad) => pad.pcb_component_id === bootPlaced.pcb_component_id)
.sort((a, b) => a.x - b.x)
assert.deepEqual(bootPads.map((pad) => ({
x: Number((pad.x - bootPlaced.center.x).toFixed(6)),
y: Number((pad.y - bootPlaced.center.y).toFixed(6)),
width: pad.width,
height: pad.height,
})), [
{ x: -0.5, y: 0, width: 0.7, height: 0.8 },
{ x: 0.5, y: 0, width: 0.7, height: 0.8 },
], "JP_BOOT uses the compact 1.0 mm-pitch cuttable land pattern")
assert.equal(of("pcb_plated_hole").filter((hole) =>
hole.pcb_component_id === bootPlaced.pcb_component_id).length, 0,
"JP_BOOT has no through-hole features")
assert.equal(of("pcb_solder_paste").filter((paste) =>
paste.pcb_component_id === bootPlaced.pcb_component_id).length, 0,
"JP_BOOT is excluded from the assembly stencil")
assert.equal(of("cad_component").filter((cad) =>
cad.source_component_id === bootSource.source_component_id).length, 0,
"JP_BOOT has no assembled 3D body")
const bootBridge = of("pcb_trace").filter((trace) =>
trace.pcb_component_id === bootPlaced.pcb_component_id)
assert.equal(bootBridge.length, 1, "JP_BOOT retains one fabricated copper bridge")
assert.ok(bootBridge[0].route.every((point) =>
point.route_type !== "wire" || (point.layer === "top" && point.width === 0.2)),
"JP_BOOT bridge is a 0.20 mm cuttable top-copper neck")
// SW_RESET previously paired a small two-pad footprint with a larger four-pad
// TL3342 assembly model. Lock the electrical land pattern, BOM identity, CAD
// model, and mechanical clearances together so that mismatch cannot recur.
const resetPlaced = placedComponent("SW_RESET")
const resetPads = of("pcb_smtpad")
.filter((pad) => pad.pcb_component_id === resetPlaced.pcb_component_id)
.sort((a, b) => a.x - b.x)
assert.deepEqual(resetPads.map((pad) => ({
x: Number((pad.x - resetPlaced.center.x).toFixed(6)),
y: Number((pad.y - resetPlaced.center.y).toFixed(6)),
width: pad.width,
height: pad.height,
})), [
{ x: -1.7, y: 0, width: 0.8, height: 1.7 },
{ x: 1.7, y: 0, width: 0.8, height: 1.7 },
], "SW_RESET uses the Omron B3U-1000P recommended 4.2 x 1.7 mm land pattern")
const resetCad = of("cad_component").find((item) =>
item.source_component_id === component("SW_RESET").source_component_id)
assert.equal(resetCad.model_obj_url,
"https://modelcdn.tscircuit.com/easyeda_models/assets/C231329.obj")
assert.equal(resetCad.model_step_url,
"https://modelcdn.tscircuit.com/easyeda_models/assets/C231329.step")
const boundsAround = (placed, width = placed.width, height = placed.height) => ({
left: placed.center.x - width / 2,
right: placed.center.x + width / 2,
bottom: placed.center.y - height / 2,
top: placed.center.y + height / 2,
})
const rectangularClearance = (a, b) => Math.hypot(
Math.max(a.left - b.right, b.left - a.right, 0),
Math.max(a.bottom - b.top, b.bottom - a.top, 0),
)
const pointToRectClearance = (point, rect) => Math.hypot(
Math.max(rect.left - point.x, point.x - rect.right, 0),
Math.max(rect.bottom - point.y, point.y - rect.top, 0),
)
const assemblyBodyBounds = (placed, name) => {
if (name !== "J_ETH") return boundsAround(placed)
// HR911105A is rotated 270 degrees. The imported courtyard is 18.585 mm
// wide because it encloses the two shield tabs; it is not the plastic body.
// Use the more conservative 16.1 x 21.6 mm imported body/silkscreen outline
// (HanRun specifies a 16.0 mm body) for the physical screw-head check.
return {
left: placed.center.x - 12.8650111,
right: placed.center.x + 8.7349711,
bottom: placed.center.y - 8.0503522,
top: placed.center.y + 8.0503522,
}
}
// Standard ISO 4762 socket heads are 5.5 mm diameter. Reserve that exact
// top-side assembly envelope for the normal NEMA 17 screw without a washer.
// The motor-facing side uses a small nonconductive spacer.
const mountPlaced = MOUNT_CENTERS.map((center, index) => {
const placed = placedComponent(`MH${index + 1}`)
assert.ok(placed, `MH${index + 1} placed component`)
const courtyard = of("pcb_courtyard_circle")
.find((entry) => entry.pcb_component_id === placed.pcb_component_id)
assert.ok(courtyard, `MH${index + 1} hardware courtyard exists`)
assert.equal(courtyard.radius, MOUNT_HARDWARE_RADIUS,
`MH${index + 1} courtyard reserves the standard M3 screw-head envelope`)
assert.deepEqual(courtyard.center, center,
`MH${index + 1} hardware courtyard is concentric with its drill`)
return { center, placed }
})
for (const placed of assembled) {
const name = sourceById.get(placed.source_component_id)?.name
const body = assemblyBodyBounds(placed, name)
const requiredRadius = placed.layer === "top"
? MOUNT_HARDWARE_RADIUS
: MOTOR_SIDE_STANDOFF_RADIUS
for (const { center } of mountPlaced)
assert.ok(pointToRectClearance(center, body) >= requiredRadius - 1e-6,
placed.layer === "top"
? `${name} body clears the 5.5 mm ISO 4762 M3 no-washer head envelope`
: `${name} body clears the 5.0 mm motor-side spacer envelope`)
}
const ethernetPlaced = placedComponent("J_ETH")
const ethernetBodyClearance = pointToRectClearance(
MOUNT_CENTERS[0], assemblyBodyBounds(ethernetPlaced, "J_ETH"),
) - MOUNT_HARDWARE_RADIUS
assert.ok(ethernetBodyClearance >= 0.5,
"HR911105A body has at least 0.5 mm lower-left ISO 4762 head clearance")
const ethernetFeatures = [...of("pcb_plated_hole"), ...of("pcb_hole")]
.filter((item) => item.pcb_component_id === ethernetPlaced.pcb_component_id)
const ethernetFeatureClearance = Math.min(...ethernetFeatures.map((item) =>
Math.hypot(item.x - MOUNT_CENTERS[0].x, item.y - MOUNT_CENTERS[0].y) -
Math.max(item.outer_diameter ?? 0, item.hole_diameter ?? 0) / 2 -
MOUNT_HARDWARE_RADIUS))
assert.ok(ethernetFeatureClearance >= 0.5,
"HR911105A drilled/copper features have at least 0.5 mm lower-left screw-head clearance")
const bootCopper = boundsAround(bootPlaced, 1.7, 0.8)
const bootMountCenter = MOUNT_CENTERS[1]
const nearestBootPoint = {
x: Math.max(bootCopper.left, Math.min(bootMountCenter.x, bootCopper.right)),
y: Math.max(bootCopper.bottom, Math.min(bootMountCenter.y, bootCopper.top)),
}
assert.ok(Math.hypot(nearestBootPoint.x - bootMountCenter.x,
nearestBootPoint.y - bootMountCenter.y) - MOUNT_HOLE_DIAMETER / 2 >= 0.75,
"JP_BOOT copper has at least 0.75 mm clearance from the NEMA17 mounting hole")
const usbDataShell = boundsAround(placedComponent("J_USB_DATA"), 8.94, 7.9)
assert.ok(rectangularClearance(bootCopper, usbDataShell) >= 1.5,
"JP_BOOT copper has at least 1.5 mm clearance from the DATA USB-C shell")
const resetBody = boundsAround(resetPlaced, 4.0, 2.5)
for (const name of ["U_USB_ESD", "C_ESD"])
assert.ok(rectangularClearance(resetBody, boundsAround(placedComponent(name))) >= 0.5,
`SW_RESET body has at least 0.5 mm clearance from ${name}`)
const resetMountCenter = MOUNT_CENTERS[1]
const nearestResetPoint = {
x: Math.max(resetBody.left, Math.min(resetMountCenter.x, resetBody.right)),
y: Math.max(resetBody.bottom, Math.min(resetMountCenter.y, resetBody.top)),
}
assert.ok(Math.hypot(nearestResetPoint.x - resetMountCenter.x,
nearestResetPoint.y - resetMountCenter.y) - MOUNT_HOLE_DIAMETER / 2 >= 0.5,
"SW_RESET body has at least 0.5 mm clearance from the NEMA17 mounting hole")
// Linux talks to the real-time MCU; it does not generate step pulses.
expectNet("U1", "UART0_TX", "MOTION_RX")
expectNet("U1", "UART0_RX", "MOTION_TX")
expectNet("U1", "PE2", "MOTION_BOOT")
expectNet("U1", "PE3", "MOTION_NRST")
expectNet("U_MOTION", "PA3", "MOTION_RX")
expectNet("U_MOTION", "PA2", "MOTION_TX")
expectNet("U_MOTION", "PA6", "MOTOR_DIR")
expectNet("U_MOTION", "PA7", "MOTOR_STEP")
expectNet("U_MOTION", "PA4", "MOTOR_ENABLE_RAW")
expectNet("U_MOTION", "PB7_PB8", "MOTOR_FAULT")
expectNet("U_MOTION", "PA0", "MOTION_TEMP_ALERT")
expectNet("U_MOTOR", "STEP", "MOTOR_STEP")
expectNet("U_MOTOR", "DIR", "MOTOR_DIR")
expectNet("U_MOTOR", "nSLEEP", "MOTOR_ENABLE")
expectNet("U_MOTOR", "nRESET", "MOTOR_ENABLE")
expectNet("U_MOTOR", "nFAULT", "MOTOR_FAULT")
expectNet("Q_PD_ENABLE", "S", "MOTOR_ENABLE_RAW")
expectNet("Q_PD_ENABLE", "D", "MOTOR_ENABLE")
expectPair("Q_PD_ENABLE", "G", "U_PD", "GATE")
expectPair("R_PD_GOOD_PU", 1, "U_PD", "GATE")
expectPair("R_PD_GOOD_PU", 2, "Q_VBUS", "S")
expectNet("R_ENABLE_PD", 1, "MOTOR_ENABLE")
expectNet("R_ENABLE_PD", 2, "BOARD_GND")
// Raw PD voltage and gate-valid are independently observed by the MCU.
// No raw-VBUS or GATE net may be directly connected to an MCU input.
expectPart("U_1V2", "TLV75512PDBVR", "C2877864")
expectPair("R_PD_ADC_TOP", 1, "U_PD", "VBUS")
expectPair("R_PD_ADC_TOP", 2, "R_PD_ADC_BOTTOM", 1)
expectNet("R_PD_ADC_BOTTOM", 1, "PD_ADC")
expectNet("R_PD_ADC_BOTTOM", 2, "BOARD_GND")
expectNet("C_PD_ADC", 1, "PD_ADC")
expectNet("C_PD_ADC", 2, "BOARD_GND")
expectNet("U_MOTION", "PA1", "PD_ADC")
expectPair("R_PD_STATUS_TOP", 1, "U_PD", "GATE")
expectPair("R_PD_STATUS_TOP", 2, "Q_PD_STATUS", "G")
expectPair("R_PD_STATUS_BOTTOM_A", 1, "Q_PD_STATUS", "G")
expectPair("R_PD_STATUS_BOTTOM_A", 2, "R_PD_STATUS_BOTTOM_B", 1)
expectNet("R_PD_STATUS_BOTTOM_B", 2, "BOARD_GND")
expectNet("Q_PD_STATUS", "S", "BOARD_GND")
expectNet("Q_PD_STATUS", "D", "PD_POWER_GOOD")
expectNet("U_MOTION", 20, "PD_POWER_GOOD")
expectNet("R_PD_STATUS_PULLUP", 1, "PD_POWER_GOOD")
expectPair("R_PD_STATUS_PULLUP", 2, "U_MOTION", 4)
expectPart("Q_PD_STATUS", "BSS138", "C52895")
for(const name of ["R_PD_ADC_TOP","R_PD_STATUS_TOP","R_PD_STATUS_BOTTOM_A","R_PD_STATUS_BOTTOM_B"]) {
expectPart(name,"0402WGF1004TCE","C26083")
assert.equal(component(name).resistance,1_000_000)
}
for(const name of ["R_PD_ADC_BOTTOM","R_PD_STATUS_PULLUP"]) {
expectPart(name,"0402WGF1003TCE","C25741")
assert.equal(component(name).resistance,100_000)
}
assert.equal(component("C_PD_ADC").capacitance,100e-9)
for(const [name,pin] of [["U_PD","VBUS"],["U_PD","GATE"]])
for(const adcPin of [8,20]) assert.notEqual(root(port(name,pin)),root(port("U_MOTION",adcPin)),
"high-voltage PD signals remain isolated from MCU inputs")
// Fixed 1/16 microstepping, software-set nominal 0–396 mA peak per phase.
expectPart("U_CURRENT_DAC", "MCP4725A0T-E/CH", "C144198")
expectPart("C_CURRENT_DAC", "CL03A104KP3NNNC", "C49062")
expectPair("U_CURRENT_DAC", "VOUT", "R_REF_TOP", 1)
expectPair("U_CURRENT_DAC", "VDD", "C_U_2V5_IN", 1)
expectPair("U_CURRENT_DAC", "SCL", "U_MOTION", 16)
expectPair("U_CURRENT_DAC", "SDA", "U_MOTION", 17)
expectNet("U_CURRENT_DAC", "A0", "BOARD_GND")
expectNet("U_CURRENT_DAC", "VSS", "BOARD_GND")
expectNet("U_MOTOR", "NENBL", "BOARD_GND")
expectNet("U_MOTOR", "MODE0", "BOARD_GND")
expectNet("U_MOTOR", "MODE1", "BOARD_GND")
expectPair("U_MOTOR", "MODE2", "R_V3V3_MOTOR", 2)
assert.equal(component("R_REF_TOP").resistance, 9100)
assert.equal(component("R_REF_BOTTOM").resistance, 2000)
assert.equal(component("R_ISEN_A").resistance, 0.3)
assert.equal(component("R_ISEN_B").resistance, 0.3)
const traceWidths = (name) => {
const source = of("source_trace").find((item) => item.name === name)
assert.ok(source, `source trace ${name}`)
return of("pcb_trace").filter((item) => item.source_trace_id === source.source_trace_id)
.flatMap((item) => item.route.filter((point) => point.route_type === "wire").map((point) => point.width))
}
const traceLayers = (name) => {
const source = of("source_trace").find((item) => item.name === name)
assert.ok(source, `source trace ${name}`)
return of("pcb_trace").filter((item) => item.source_trace_id === source.source_trace_id)
.flatMap((item) => item.route.filter((point) => point.route_type === "wire").map((point) => point.layer))
}
for (const name of ["MOTOR_A_PLUS", "MOTOR_A_MINUS", "MOTOR_B_PLUS", "MOTOR_B_MINUS",
"MOTOR_A_SENSED_PLUS", "MOTOR_B_SENSED_PLUS"]) {
const widths = traceWidths(name)
assert.ok(widths.length > 0 && Math.min(...widths) >= 0.275 && Math.max(...widths) === 0.3,
`${name} is routed at approximately 0.30 mm`)
assert.ok(traceLayers(name).every((layer) => layer === "top" || layer === "bottom"),
`${name} stays on outer copper`)
}
for (const name of ["VM_INPUT", "VM_FUSED"]) {
const widths = traceWidths(name)
assert.ok(widths.length > 0 && Math.min(...widths) >= 0.15,
`${name} maintains at least 0.15 mm copper for the fixed 0.40 A/phase target`)
assert.ok(traceLayers(name).every((layer) => layer === "top" || layer === "bottom"),
`${name} stays on outer copper`)
}
// The DNP parallel option can only reduce the default DRV8825 current limit.
expectPair("R_ISET_210", 1, "C_REF", 1)
expectNet("R_ISET_210", 2, "BOARD_GND")
assert.equal(component("R_ISET_210").resistance, 1800)
const parallel = (...values) => 1 / values.reduce((sum, value) => sum + 1 / value, 0)
const motorCurrent = (bottomResistance) =>
(3.3 * bottomResistance / (9100 + bottomResistance)) / (5 * 0.3)
assert.ok(Math.abs(motorCurrent(2000) - 0.3964) < 0.0001)
assert.ok(Math.abs(motorCurrent(parallel(2000, 1800)) - 0.2074) < 0.0001)
assert.ok(motorCurrent(parallel(2000, 1800)) < motorCurrent(2000),
"closing the current selector remains fail-safe")
// The power-only USB-C input negotiates PD; the self-powered DATA connector
// carries only USB2/FEL and has independent Type-C device pull-downs.
expectPair("J_USB_PWR", 15, "J_USB_PWR", 16)
expectPair("J_USB_PWR", 15, "Q_VBUS", "S")
expectPair("J_USB_PWR", 15, "U_PD", "VBUS")
assert.notEqual(root(port("J_USB_PWR", 15)), root(port("Q_VBUS", "D")),
"raw receptacle VBUS remains isolated from switched VBUS_PD")
// CC1/CC2 are symmetric HUSB238A orientation-detector inputs. They are
// intentionally crossed here to avoid a board-level copper crossing; the
// unused ORIENT indication would simply have inverted polarity.
expectPair("J_USB_PWR", 6, "U_PD", "CC2")
expectPair("J_USB_PWR", 12, "U_PD", "CC1")
expectNet("J_USB_PWR", 13, "BOARD_GND")
expectNet("J_USB_PWR", 14, "BOARD_GND")
for (const pin of [7, 9]) assert.notEqual(root(port("J_USB_PWR", pin)), root(net("USB_N")))
for (const pin of [8, 10]) assert.notEqual(root(port("J_USB_PWR", pin)), root(net("USB_P")))
expectNet("J_USB_DATA", 7, "USB_N_PORT")
expectNet("J_USB_DATA", 9, "USB_N_PORT")
expectNet("J_USB_DATA", 8, "USB_P_PORT")
expectNet("J_USB_DATA", 10, "USB_P_PORT")
expectNet("U_USB_ESD", 1, "USB_N_PORT")
expectNet("U_USB_ESD", 3, "USB_P_PORT")
// USBLC6-2SC6 is SOT23-6L, not the substantially smaller SOT-666 P6.
expectPart("U_USB_ESD", "USBLC6-2SC6", "C7519")
const esdLands = of("pcb_smtpad").filter(p => p.pcb_component_id === placedComponent("U_USB_ESD").pcb_component_id)
assert.equal(esdLands.length, 6)
assert.ok(esdLands.every(p => Math.abs(p.width - 1.2) < 1e-6 && Math.abs(p.height - .6) < 1e-6))
assert.ok(Math.abs(Math.max(...esdLands.map(p => p.x)) - Math.min(...esdLands.map(p => p.x)) - 2.3) < 1e-6)
assert.ok(Math.abs(Math.max(...esdLands.map(p => p.y)) - Math.min(...esdLands.map(p => p.y)) - 1.9) < 1e-6)
expectPart("U_USB_GATE", "TS3USB31ERSER", "C967436")
expectPart("Q_USB_ATTACH", "MMBT3904(RANGE:100-300)", "C20526")
expectPair("U_USB_ESD", 4, "U_USB_GATE", "D_N")
expectPair("U_USB_ESD", 6, "U_USB_GATE", "D_P")
expectNet("U_USB_GATE", "HSD_N", "USB_P")
expectNet("U_USB_GATE", "HSD_P", "USB_N")
for (const [input, output] of [["D_P", "HSD_P"], ["D_N", "HSD_N"]])
assert.notEqual(root(port("U_USB_GATE", input)), root(port("U_USB_GATE", output)),
"no PCB copper bypasses either hardware disconnect channel")
expectPair("J_USB_DATA", 15, "J_USB_DATA", 16)
expectPair("J_USB_DATA", 16, "U_USB_ESD", 5)
expectPair("U_USB_ESD", 5, "C_ESD", 1)
expectPair("J_USB_DATA", 16, "R_USB_BASE", 1)
expectPair("R_USB_BASE", 2, "Q_USB_ATTACH", "B")
expectPair("R_USB_BASE_PD", 1, "Q_USB_ATTACH", "B")
expectNet("R_USB_BASE_PD", 2, "BOARD_GND")
expectNet("Q_USB_ATTACH", "E", "BOARD_GND")
expectPair("Q_USB_ATTACH", "C", "U_USB_GATE", "N_OE")
expectPair("R_USB_OE_PU", 1, "U_USB_GATE", "N_OE")
expectPair("R_USB_OE_PU", 2, "U_USB_GATE", "VCC")
expectPair("C_USB_GATE", 1, "U_USB_GATE", "VCC")
expectPair("U_USB_GATE", "VCC", "VCC_TV", 1)
expectNet("U_USB_GATE", "GND", "BOARD_GND")
expectNet("C_USB_GATE", 2, "BOARD_GND")
assert.equal(component("R_USB_BASE").resistance, 47000)
assert.equal(component("R_USB_BASE_PD").resistance, 100000)
assert.equal(component("R_USB_OE_PU").resistance, 10000)
assert.equal(component("C_USB_GATE").capacitance, 100e-9)
assert.equal(of("source_trace").find(trace=>trace.name==="USB_GATE_VCC")?.max_length,5,
"the local USB-switch bypass keeps its 5 mm decoupling constraint")
assert.notEqual(root(port("J_USB_DATA", 16)), root(port("U_USB_GATE", "VCC")),
"host VBUS only biases Q through 47k and never supplies the system rail")
expectPair("J_USB_DATA", 6, "R_USB_DATA_CC1", 1)
expectNet("R_USB_DATA_CC1", 2, "BOARD_GND")
expectPair("J_USB_DATA", 12, "R_USB_DATA_CC2", 2)
expectNet("R_USB_DATA_CC2", 1, "BOARD_GND")
assert.equal(component("R_USB_DATA_CC1").resistance, 5100)
assert.equal(component("R_USB_DATA_CC2").resistance, 5100)
for (const pin of [15, 16]) {
assert.notEqual(root(port("J_USB_DATA", pin)), root(port("Q_VBUS", "D")))
assert.notEqual(root(port("J_USB_DATA", pin)), root(port("L_BUCK", 2)))
}
// USB-PD power path and protected 15 V motor rail. HUSB238A can cold-start
// from its raw VBUS input while VDD is unavailable, then drives the P-FET.
expectPair("U_PD", "VDD", "R_V3V3_RIGHT", 2)
expectNet("U_PD", "EN_N", "BOARD_GND")
expectPair("U_PD", "SNK_VSET", "R_PD_VSET", 1)
expectPair("U_PD", "SNK_ISET", "R_PD_ISET", 1)
expectPair("U_PD", "DBG_N", "R_PD_DEBUG", 1)
assert.equal(component("R_PD_VSET").resistance, 11000)
assert.equal(component("R_PD_ISET").resistance, 21000)
assert.equal(component("R_PD_DEBUG").resistance, 910000)
expectPair("R_PD_12V", 1, "R_PD_VSET", 1)
expectNet("R_PD_12V", 2, "BOARD_GND")
assert.equal(component("R_PD_12V").resistance, 13300)
assert.ok(Math.abs(parallel(11000, 13300) - 6020.576) < 0.01,
"PD voltage selector produces the nominal 12 V VSET resistance")
expectPair("Q_VBUS", "G", "U_PD", "GATE")
expectPair("Q_VBUS", "D", "F_VM", 1)
for (const pin of [2, 3]) expectPair("Q_VBUS", "S", "Q_VBUS", pin)
for (const pin of [6, 7, 8]) expectPair("Q_VBUS", "D", "Q_VBUS", pin)
for (const name of ["VBUS_SOURCE_1", "VBUS_SOURCE_2",
"VBUS_DRAIN_5", "VBUS_DRAIN_7"]) {
const widths = traceWidths(name)
assert.ok(widths.length > 0 && Math.min(...widths) >= 0.6,
`${name} maintains at least 0.60 mm copper outside package pads`)
}
// The Type-C VBUS contacts require short 0.25 mm pad escapes. Both routes widen
// to 0.80 mm immediately after their dogbone vias and remain wide through the
// internal-layer trunks.
const usbVbusAWidths = traceWidths("USB_VBUS_A")
assert.ok(usbVbusAWidths.length > 0 && Math.min(...usbVbusAWidths) >= 0.25 &&
Math.max(...usbVbusAWidths) >= 0.8,
"USB_VBUS_A uses 0.25 mm connector escapes and an 0.80 mm trunk")
const usbVbusBWidths = traceWidths("USB_VBUS_B")
assert.ok(usbVbusBWidths.length > 0 && Math.min(...usbVbusBWidths) >= 0.25 &&
Math.max(...usbVbusBWidths) >= 0.8,
"USB_VBUS_B uses a 0.25 mm connector escape and widens to a 0.80 mm trunk")
expectPair("F_VM", 1, "Q_VBUS", "D")
expectPair("F_VM", 2, "D_VM_BLOCK", "anode")
expectNet("D_VM_BLOCK", "cathode", "VMOTOR")
expectNet("U_MOTOR", "VMA", "VMOTOR")
expectNet("U_MOTOR", "VMB", "VMOTOR")
assert.notEqual(root(net("VMOTOR")), root(port("L_BUCK", 2)))
assert.notEqual(root(net("VMOTOR")), root(port("R_V3V3_RIGHT", 2)))
// WIZnet reference MDI network: 49.9 ohm 1% termination, RX AC coupling,
// 10 ohm TX center-tap feed and the prescribed center-tap decoupling values.
for (const name of ["R_ETH_TXN", "R_ETH_TXP", "R_ETH_RXN", "R_ETH_RXP"])
assert.equal(component(name).resistance, 49.9)
assert.equal(component("C_ETH_RXN_AC").capacitance, 6.8e-9)
assert.equal(component("C_ETH_RXP_AC").capacitance, 6.8e-9)
assert.equal(component("R_ETH_TCT").resistance, 10)
assert.equal(component("C_ETH_TCT").capacitance, 22e-9)
assert.equal(component("C_ETH_RCT").capacitance, 10e-9)
expectPair("U_ETH", "TXP", "J_ETH", 1)
expectPair("U_ETH", "TXN", "J_ETH", 2)
expectPair("U_ETH", "RXP", "C_ETH_RXP_AC", 1)
expectPair("C_ETH_RXP_AC", 2, "J_ETH", 3)
expectPair("U_ETH", "RXN", "C_ETH_RXN_AC", 1)
expectPair("C_ETH_RXN_AC", 2, "J_ETH", 6)
// W5500 is the dedicated internal SPI1 peripheral; there are no expansion headers.
for (const [socPin, ethNet] of [["PE7", "ETH_CS"], ["PE8", "ETH_MOSI"],
["PE10", "ETH_MISO"], ["PE5", "ETH_RST"], ["PE6", "ETH_INT"]]) {
expectNet("U1", socPin, ethNet)
}
expectNet("U1", "PE9", "ETH_SCLK_SOC")
expectNet("U_ETH", "SCSn", "ETH_CS")
expectNet("U_ETH", "MOSI", "ETH_MOSI")
expectPair("U_ETH", "SCLK", "R_ETH_SCLK", 1)
expectPart("R_ETH_SCLK","RC0201JR-070RL","C106228")
expectNet("R_ETH_SCLK", 2, "ETH_SCLK_SOC")
expectNet("U_ETH", "MISO", "ETH_MISO")
expectNet("U_ETH", "RSTn", "ETH_RST")
expectNet("U_ETH", "INTn", "ETH_INT")
assert.equal(of("source_component").filter((item) => ["J_SPI", "J_I2C"].includes(item.name)).length, 0)
// Connector mapping and motion-MCU-owned temperature telemetry.
for (const [driverPin, connectorPin] of [["AOUT2", 3], ["BOUT2", 2]]) {
expectPair("U_MOTOR", driverPin, "J_MOTOR", connectorPin)
}
for (const [phase, driverPin, connectorPin, adcPin] of [["A","AOUT1",4,12],["B","BOUT1",1,15]]) {
const shunt=`R_INLINE_${phase}`, amplifier=`U_CURRENT_${phase}`
const filter=`R_CURRENT_${phase}_FILTER`, capacitor=`C_CURRENT_${phase}_FILTER`
expectPair("U_MOTOR",driverPin,shunt,1)
expectPair(shunt,2,"J_MOTOR",connectorPin)
assert.notEqual(root(port(shunt,1)),root(port(shunt,2)),`${phase} shunt must not be bypassed`)
assert.equal(component(shunt).resistance,.3,`${phase} telemetry shunt is 0.30 ohm`)
expectPart(`C_CURRENT_${phase}_SUPPLY`,"TCC0201X7R104K100ZT","C7393837")
assert.equal(component(`C_CURRENT_${phase}_SUPPLY`).max_voltage_rating,10,
"current-sense 3.3 V bypass declares its actual 10 V catalog rating")
expectPair(amplifier,"IN_POS",shunt,1)
expectPair(amplifier,"IN_NEG",shunt,2)
expectPair(amplifier,"VS","U_MOTION",4)
expectPair(amplifier,"REF1","U_MOTION",4)
for (const pin of ["GND","REF2","NC"]) expectNet(amplifier,pin,"BOARD_GND")
expectPair(amplifier,"OUT",filter,1)
expectPair(filter,2,capacitor,1)
expectPair(filter,2,"U_MOTION",adcPin)
expectNet(capacitor,2,"BOARD_GND")
assert.equal(component(filter).resistance,1000,`${phase} ADC isolation resistor is 1k`)
assert.equal(component(capacitor).capacitance,1e-7,`${phase} ADC filter is 100nF`)
}
expectNet("U_TEMP", "SDA", "MOTION_I2C_SDA")
expectNet("U_TEMP", "SCL", "MOTION_I2C_SCL")
expectNet("U_TEMP", "ADD0", "BOARD_GND")
expectNet("U_TEMP", "ALERT", "MOTION_TEMP_ALERT")
// The motion MCU owns all local diagnostics so fault indication survives a
// stalled Linux scheduler. PA13 feeds the 5 V logic buffer/LED, while PB9's
// timer PWM drives a protected low-side magnetic-buzzer stage.
expectPair("U_MOTION", "PA13", "R_STATUS_PD", 1)
expectPair("R_STATUS_PD", 1, "U_STATUS_BUFFER", "A")
expectNet("R_STATUS_PD", 2, "BOARD_GND")
expectNet("U_STATUS_BUFFER", "N_OE", "BOARD_GND")
expectNet("U_STATUS_BUFFER", "GND", "BOARD_GND")
expectPair("U_STATUS_BUFFER", "Y", "R_STATUS_DATA", 1)
expectPair("R_STATUS_DATA", 2, "D_STATUS", "DI")
expectPair("D_STATUS", "VDD", "U_STATUS_BUFFER", "VCC")
expectNet("D_STATUS", "GND", "BOARD_GND")
expectPair("U_MOTION", "PB9_PC14_OSC32_IN", "R_BUZZER_GATE", 1)
expectPair("R_BUZZER_GATE", 2, "Q_BUZZER", "G")
expectPair("R_BUZZER_PD", 1, "Q_BUZZER", "G")
expectNet("R_BUZZER_PD", 2, "BOARD_GND")
expectNet("Q_BUZZER", "S", "BOARD_GND")
expectPair("BZ1", "NEG", "Q_BUZZER", "D")
expectPair("D_BUZZER", "anode", "BZ1", "NEG")
expectPair("D_BUZZER", "cathode", "BZ1", "POS")
expectPair("BZ1", "POS", "C_MOTION_BULK", 1)
const motorPlaced = of("pcb_component").find((item) =>
item.source_component_id === component("U_MOTOR").source_component_id)
const motorEpGround = of("source_trace").find((item) => item.name === "DRV_EP_GND")
const thermalVias = vias.filter((via) => via.pcb_via_id.startsWith("driver_thermal_"))
assert.equal(thermalVias.length, 12)
assert.ok(thermalVias.some(via=>via.x===-.85 && via.y===-12.9),
"the DAC ground stitch reaches the existing driver ground pad")
assert.ok(thermalVias.every((via) => via.source_trace_id === motorEpGround.source_trace_id),
"all native thermal drills belong to the motor PowerPAD ground")
assert.ok(thermalVias.every((via) =>
Math.abs(via.x - motorPlaced.center.x) <= 2.8 &&
Math.abs(via.y - motorPlaced.center.y) <= 1.55),
"all twelve thermal vias drill through the DRV8825 exposed pad")
// Stock report must correspond to this exact generated circuit and contain no failures.
const stock = JSON.parse(fs.readFileSync("reports/jlcpcb-stock-audit.json", "utf8"))
const circuitSha256 = crypto.createHash("sha256").update(circuitBytes).digest("hex")
const continuity=JSON.parse(fs.readFileSync("reports/connectivity-audit.json","utf8"))
assert.equal(continuity.circuitSha256,circuitSha256,"continuity audit matches built copper")
assert.equal(continuity.sourceFailures,0)
assert.equal(continuity.disconnectedTerminalIslands,0)
assert.equal(continuity.emittedCopperOnly,true)
const preflightPath="reports/coil-all-copper-preflight.json"
const preflight=JSON.parse(fs.readFileSync(preflightPath,"utf8"))
const preflightMeta=JSON.parse(fs.readFileSync("reports/coil-all-copper-preflight-meta.json","utf8"))
const fileSha256=path=>crypto.createHash("sha256").update(fs.readFileSync(path)).digest("hex")
assert.equal(preflightMeta.circuitSha256,circuitSha256,"independent copper preflight matches this emitted circuit")
assert.equal(preflightMeta.physicalCopperSha256,fileSha256("routing/physical-copper.json"))
assert.equal(preflightMeta.findingsSha256,fileSha256(preflightPath))
assert.equal(preflightMeta.failureCount,0,"independent copper clearance must pass, not only native DRC")
assert.deepEqual(preflight,[],"no independent copper-clearance regressions")
assert.equal(stock.circuitSha256, circuitSha256, "stock report matches current circuit JSON")
assert.equal(stock.populatedComponentCount, populated.length)
assert.equal(stock.uniquePartCount, new Set(populated.map(part=>part.supplier_part_numbers.jlcpcb[0])).size)
assert.deepEqual(stock.missingCodes, [])
assert.ok(stock.boardQuantity>=5,"stock audit covers at least five assembled boards")
assert.ok(stock.parts.every((part) => part.status === "in-stock" && part.orderableStock >= part.requiredForBatch),
"all JLCPCB parts cover board quantity, assembly minimums and attrition")
const drillRegression = JSON.parse(fs.readFileSync("reports/drill-clearance-regression.json", "utf8"))
assert.equal(drillRegression.circuitSha256, circuitSha256, "drill comparison matches emitted copper")
assert.equal(drillRegression.regressionCount, 0, "no added drill/foreign-copper clearance violations")
const moduleRegression = JSON.parse(fs.readFileSync("reports/module-test-regression.json", "utf8"))
assert.equal(moduleRegression.addedFailures, 0, "no added stored-module test failures")
assert.ok(moduleRegression.currentFailed <= moduleRegression.baselineFailed,"module failures may be fixed, not introduced")
assert.equal(moduleRegression.currentPassed+moduleRegression.currentFailed,32,"all original module tests still execute")
const bomValidation=JSON.parse(fs.readFileSync("reports/bom-validation.json","utf8"))
assert.equal(bomValidation.circuitSha256,circuitSha256,"catalog electrical checks match emitted circuit")
assert.deepEqual(bomValidation.findings,[],"no wrong-value/rating/declared-MPN selections")
const additionalDfm=JSON.parse(fs.readFileSync("reports/additional-dfm-review.json","utf8"))
assert.equal(additionalDfm.circuitSha256,circuitSha256)
assert.deepEqual(additionalDfm.findings,[],"no additional pad/PTH DFM findings in the stated scope")
const solderProcess=JSON.parse(fs.readFileSync("reports/solder-process-audit.json","utf8"))
assert.equal(solderProcess.circuitSha256,circuitSha256,"solder-process review matches emitted circuit")
assert.equal(solderProcess.ordinaryPadDrillIntersections.length,0,"no via drill intersects any populated ordinary SMT solder land")
const stencilArtwork=JSON.parse(fs.readFileSync("reports/stencil-artwork-audit.json","utf8"))
assert.equal(stencilArtwork.circuitSha256,circuitSha256,"stencil artwork audit matches emitted circuit")
assert.deepEqual(stencilArtwork.failures,[],"every populated SMT land needs same-face paste artwork, no bare test-pad paste")
assert.equal(stencilArtwork.missingLandCount,0)
const report = {
circuitSha256,
fabricationReady: false,
fabricationBlockers: {
independentDrillClearanceFindings: drillRegression.currentFindings,
silkscreenBelowPublished1mmHeight: additionalDfm.silkscreenBelowPublished1mmHeight,
ordinaryPadDrillIntersections: solderProcess.ordinaryPadDrillIntersections.length,
thermalSolderProcessQualified: solderProcess.thermalStencilProcessQualified,
filledCappedViaProcessSpecified: solderProcess.specifiedFilledCappedProcess,
stencilArtworkCoveredLands: stencilArtwork.populatedExposedSmtLands,
stencilArtworkIsNotProcessApproval: true,
usbDataVbusAttachDetection: "hardware TS3USB31E isolation implemented, including ROM FEL; powered/unpowered host attach and USB signal integrity still require bench qualification",
inheritedModuleTestFailures: moduleRegression.currentFailed,
ethernetAndAnalogSignalIntegrity: "requires further layout/stackup review",
currentFabricationHandoff: "not exported; unresolved solder-process gate blocks assembly handoff",
hardwareBringupAndCurrentCalibration: "not performed",
actualRearMotorFastening: "requires physical confirmation",
},
board: {
motorFacingBodyMm: { width: 42.3, height: 42.3 },
pcbOutlineMm: { width: board.width, height: board.height },
layers: board.num_layers,
connectorHoleMinEdgeClearanceMm: { ethernet: 0.355, motor: 0.35 },
mounting: {
patternMm: 31,
carrierRotationDeg: Number(mountPatternRotationDeg.toFixed(3)),
projectedCarrierOverhangMm: Number(projectedCarrierOverhangMm.toFixed(3)),
holeDiameterMm: MOUNT_HOLE_DIAMETER,
fittedBodyEnvelopeDiameterMm: 2 * MOUNT_HARDWARE_RADIUS,
motorSideSpacerEnvelopeDiameterMm: 2 * MOTOR_SIDE_STANDOFF_RADIUS,
hardware: "Standard ISO 4762 M3 socket-head screw, 5.5 mm nominal head; no washer",
motorSideInterface: "5.0 mm OD nonconductive spacer; verify the rear endbell interface and measure replacement M3 through-bolt length on the actual motor",
ethernetBodyClearanceMm: Number(ethernetBodyClearance.toFixed(3)),
ethernetFeatureClearanceMm: Number(ethernetFeatureClearance.toFixed(3)),
},
},
drcErrors: errors.length,
nativeWarnings: circuit.filter(item => item.type.endsWith("_warning")).length,
addedDrillClearanceFindings: drillRegression.regressionCount,
storedModuleTests: { status: moduleRegression.status, passed: moduleRegression.currentPassed,
failed: moduleRegression.currentFailed, addedFailures: moduleRegression.addedFailures },
pcbTraces: of("pcb_trace").length,
pcbVias: vias.length,
viaHoleDiameterMm: 0.3,
viaPadDiameterMm: 0.45,
populatedComponents: populated.length,
bottomAssemblyComponents: assembled.filter((placed) => placed.layer === "bottom").length,
jlcpcbUniqueParts: stock.uniquePartCount,
jlcpcbStockIssues: stock.parts.filter((part) => part.status !== "in-stock").length,
assemblyOrderCaveats: stock.parts.filter(part =>
part.orderableStock < (part.minimumAssemblyQuantity ?? part.quantityPerBoard))
.map(({code,designators,orderableStock,minimumAssemblyQuantity,assemblyLossQuantity}) =>
({code,designators,orderableStock,minimumAssemblyQuantity,assemblyLossQuantity})),
targetMotor: "StepperOnline 17HS13-0404S1 NEMA 17, 0.40 A/phase, 30 ohm, 37 mH +/-20%",
motorStage: {
driver: "DRV8825PWPR",
supply: "15 V USB-PD default; 12 V selectable",
nominalSoftwarePeakCurrentRangeMa: [0, 396],
currentSetting: "MCP4725; disarmed-only; verified readback; legacy R_ISET_210 remains DNP",
motorPhaseCopper: "outer layers",
fusedSupplyCopper: "0.50 mm fuse feed; 0.80 mm VM trunk and restored motor-area bridge, with retained local pad breakouts; outer layers",
microstep: "1/16",
coilTelemetry: "two inline 0.300 ohm shunts, INA241B1 gain 10, 1k/100nF filters; nominal signed mA, not RMS or protection",
},
control: { linux: "F1C100S", realtimeMotion: "STM32G030F6P6", link: "UART0" },
ethernet: "W5500 + HR911105A",
thermalVias: thermalVias.length,
}
fs.writeFileSync("verification.json", `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2))