abse/boostxl-uln2003
This code defines a React component that models a through-hole or surface-mount LED with specified pin labels, footprint, silkscreen, and 3D CAD model for PCB layout.
- Version
- 1.0.5
- License
- unset
- Stars
- 0
scripts/verify-release.mjs
import { existsSync, readFileSync } from "node:fs"
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
const root = join(dirname(fileURLToPath(import.meta.url)), "..")
const sourcePath = join(root, "index.circuit.tsx")
const circuitPath = join(root, "dist/index/circuit.json")
const guidePath = join(root, "reference/ti/BOOSTXL-ULN2003-user-guide-SLCU002.pdf")
const failures = []
const requireCheck = (condition, message) => {
if (!condition) failures.push(message)
}
requireCheck(existsSync(circuitPath), "dist/index/circuit.json is missing; run bun run build")
requireCheck(existsSync(guidePath), "official TI SLCU002 reference PDF is missing")
if (!existsSync(circuitPath)) {
console.error(failures.join("\n"))
process.exit(1)
}
const source = readFileSync(sourcePath, "utf8")
const circuit = JSON.parse(readFileSync(circuitPath, "utf8"))
const elements = (type) => circuit.filter((element) => element.type === type)
const components = elements("source_component")
const componentByName = new Map(components.map((component) => [component.name, component]))
const pcbBySourceId = new Map(elements("pcb_component").map((component) => [component.source_component_id, component]))
const sourcePorts = elements("source_port")
const sourceNets = elements("source_net")
const sourceTraces = elements("source_trace")
const portFor = (ref, portName) => {
const component = componentByName.get(ref)
return sourcePorts.find((port) =>
port.source_component_id === component?.source_component_id
&& (port.name === portName || String(port.pin_number) === portName)
)
}
const requirePortOnNet = (ref, portName, netName) => {
const port = portFor(ref, portName)
const net = sourceNets.find((candidate) => candidate.name === netName)
const trace = sourceTraces.find((candidate) =>
candidate.connected_source_port_ids.includes(port?.source_port_id)
&& candidate.connected_source_net_ids.includes(net?.source_net_id)
)
requireCheck(Boolean(trace), `${ref} pin ${portName} must connect to ${netName}`)
}
const expectedRefs = [
"J0", "J1J3", "J2J4", "J5", "J6", "J7", "J8", "J9",
"S1", "S2", "U1", "U2", "U3", "Q1",
"C1", "C2", "C3",
"D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8",
"R1", "R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R11", "R12", "R13", "R14", "R15",
"TP1", "TP2", "TP3", "TP4",
]
for (const ref of expectedRefs) requireCheck(componentByName.has(ref), `missing reference ${ref}`)
const names = components.map((component) => component.name)
requireCheck(new Set(names).size === names.length, "duplicate source-component reference designator")
const expectedHeaderConnections = {
J1J3: { 1: "V3V3", 2: "V5", 3: "GP2", 4: "DGND", 11: "GP6", 13: "GP8", 15: "GP9", 19: "GP10" },
J0: { 1: "GP6", 3: "GP8", 5: "GP9", 8: "GP10" },
J2J4: { 1: "DGND", 13: "GP13", 15: "GP12", 17: "GP11" },
J6: { 1: "V3V3" },
}
for (const [ref, pinMap] of Object.entries(expectedHeaderConnections)) {
for (const [pin, net] of Object.entries(pinMap)) requirePortOnNet(ref, pin, net)
}
requireCheck(sourcePorts.filter((port) => port.source_component_id === componentByName.get("J0")?.source_component_id).length === 8, "J0 must be the eight-pin DNP development header from TI Figure 26")
const board = elements("pcb_board")[0]
requireCheck(board?.num_layers === 4, "board must have four copper layers")
requireCheck(board?.width === 56 && board?.height === 60, "provisional board outline changed unexpectedly")
requireCheck(board?.min_via_hole_diameter === 0.3, "minimum via hole must be 0.3 mm")
requireCheck(board?.min_via_pad_diameter === 0.45, "minimum via pad must be 0.45 mm")
requireCheck(elements("pcb_copper_pour").some((pour) => pour.layer === "bottom"), "bottom AGND copper pour is missing")
requireCheck(elements("pcb_keepout").length === 4, "four switch-slot routing keepouts are required")
const errors = circuit.filter((element) => element.type.endsWith("_error"))
requireCheck(errors.length === 0, `generated circuit contains ${errors.length} DRC/error objects`)
requireCheck(elements("pcb_trace").length >= 80, "too few routed PCB traces for the reviewed netlist")
requireCheck(elements("pcb_via").length > 0, "routed board unexpectedly contains no vias")
requireCheck(elements("pcb_jumper").length === 0, "autorouter inserted jumpers")
const expectedSheets = [
"LaunchPad Interface and Mode Selection",
"Serial and Parallel Control",
"ULN2003 Motor Driver and Outputs",
"Motor Supply and Protection",
]
const schematicSheets = elements("schematic_sheet").sort((a, b) => a.sheet_index - b.sheet_index)
requireCheck(schematicSheets.length === expectedSheets.length, `expected ${expectedSheets.length} schematic sheets`)
for (const [index, name] of expectedSheets.entries()) {
requireCheck(schematicSheets[index]?.name === name, `schematic sheet ${index + 1} must be ${name}`)
requireCheck(schematicSheets[index]?.sheet_index === index + 1, `${name} has the wrong sheet index`)
}
const sheetIdByName = new Map(schematicSheets.map((sheet) => [sheet.name, sheet.schematic_sheet_id]))
const schematicComponentBySourceId = new Map(elements("schematic_component").map((component) => [component.source_component_id, component]))
const expectedSheetComponents = {
"LaunchPad Interface and Mode Selection": ["J0", "J1J3", "J2J4", "J5", "J6", "S1", "S2", "R11", "R12", "R13"],
"Serial and Parallel Control": ["U1", "C2", "R2", "R6", "R8", "R9", "R10", "D1", "D2", "D3", "D4", "R1", "R3", "R4", "R5", "R7"],
"ULN2003 Motor Driver and Outputs": ["U2", "U3", "Q1", "TP1", "TP2", "TP3", "TP4", "D5", "D6", "R14", "C3", "J7", "J8"],
"Motor Supply and Protection": ["J9", "D7", "D8", "C1", "R15"],
}
for (const [sheetName, refs] of Object.entries(expectedSheetComponents)) {
const sheetId = sheetIdByName.get(sheetName)
for (const ref of refs) {
const sourceComponent = componentByName.get(ref)
const schematicComponent = sourceComponent && schematicComponentBySourceId.get(sourceComponent.source_component_id)
requireCheck(schematicComponent?.schematic_sheet_id === sheetId, `${ref} must remain on ${sheetName}`)
}
}
const motorTraceNames = [
"M1_CH1", "M1_CH2", "M1_CH3", "M1_CH4",
"M2_CH5", "M2_CH6", "M2_CH7", "M2_CH8",
]
for (const name of motorTraceNames) {
const sourceTrace = sourceTraces.find((trace) => trace.name === name)
const pcbTraces = elements("pcb_trace").filter((trace) => trace.source_trace_id === sourceTrace?.source_trace_id)
const wireWidths = pcbTraces.flatMap((trace) => trace.route ?? []).filter((point) => point.route_type === "wire").map((point) => point.width)
requireCheck(Boolean(sourceTrace), `missing named motor output trace ${name}`)
requireCheck(pcbTraces.length > 0, `${name} has no routed PCB trace`)
requireCheck(Math.min(...wireWidths) >= 0.3, `${name} tapers below 0.30 mm`)
}
const motorNetEndpoints = [
{ net: "M1_CH1", fitted: ["Q1", "1"], connector: ["J7", "1"], alternative: ["U3", "1C"] },
{ net: "M1_CH2", fitted: ["U2", "1C"], connector: ["J7", "2"], alternative: ["U3", "2C"] },
{ net: "M1_CH3", fitted: ["U2", "2C"], connector: ["J7", "3"], alternative: ["U3", "3C"] },
{ net: "M1_CH4", fitted: ["U2", "3C"], connector: ["J7", "4"], alternative: ["U3", "4C"] },
{ net: "M2_CH5", fitted: ["U2", "4C"], connector: ["J8", "1"], alternative: ["U3", "5C"] },
{ net: "M2_CH6", fitted: ["U2", "5C"], connector: ["J8", "2"], alternative: ["U3", "6C"] },
{ net: "M2_CH7", fitted: ["U2", "6C"], connector: ["J8", "3"], alternative: ["U3", "7C"] },
{ net: "M2_CH8", fitted: ["U2", "7C"], connector: ["J8", "4"], alternative: ["U3", "8C"] },
]
for (const { net, fitted, connector, alternative } of motorNetEndpoints) {
const sourceNet = sourceNets.find((candidate) => candidate.name === net)
const constrainedTrace = sourceTraces.find((trace) => trace.name === net)
const fittedPort = portFor(...fitted)
const connectorPort = portFor(...connector)
const alternativePort = portFor(...alternative)
const alternativeTrace = sourceTraces.find((trace) =>
trace.connected_source_port_ids.includes(alternativePort?.source_port_id)
&& trace.connected_source_net_ids.includes(sourceNet?.source_net_id)
)
requireCheck(Boolean(sourceNet), `missing TI output net ${net}`)
requireCheck(constrainedTrace?.connected_source_net_ids.includes(sourceNet?.source_net_id), `${net} constrained route is not joined to its named net`)
requireCheck(constrainedTrace?.connected_source_port_ids.includes(fittedPort?.source_port_id), `${net} is missing its fitted driver endpoint`)
requireCheck(constrainedTrace?.connected_source_port_ids.includes(connectorPort?.source_port_id), `${net} is missing its motor connector endpoint`)
requireCheck(Boolean(alternativeTrace), `${net} is not shared with the DNP ULN2803 alternative`)
}
requireCheck(!source.includes("<netlabel"), "explicit <netlabel> is forbidden")
requireCheck(source.includes('minViaHoleDiameter="0.3mm"'), "source via-hole rule is missing")
requireCheck(source.includes('minViaPadDiameter="0.45mm"'), "source via-pad rule is missing")
for (const sheetName of expectedSheets) {
requireCheck(source.includes(`= "${sheetName}"`), `source sheet title is missing: ${sheetName}`)
}
const dnpRefs = ["J0", "J5", "J6", "R2", "R6", "R9", "R10", "U3"]
for (const ref of dnpRefs) {
const component = componentByName.get(ref)
const pcb = component && pcbBySourceId.get(component.source_component_id)
requireCheck(pcb?.do_not_place === true, `${ref} must remain DNP`)
}
for (const component of components) {
if (/^[CRDSUJQ]/.test(component.name)) {
requireCheck(component.supplier_part_numbers?.jlcpcb?.length > 0, `${component.name} lacks JLCPCB/LCSC sourcing`)
}
}
const expectedMpn = {
U1: "SN74HC595DR",
U2: "ULN2003ADR",
U3: "ULN2803ADWR",
Q1: "CSD17571Q2",
S1: "KH_SK22D07_G5",
S2: "KH_SK22D07_G5",
D5: "MMSZ5242B",
D6: "SS24FL",
D7: "SS24FL",
D8: "SS24FL",
J0: "DNP-1X8-2.54MM",
}
for (const [ref, mpn] of Object.entries(expectedMpn)) {
requireCheck(componentByName.get(ref)?.manufacturer_part_number === mpn, `${ref} must use ${mpn}`)
}
const expectedReferenceDisplayNames = {
J1J3: "J1/J3",
J2J4: "J2/J4",
D5: "D5 12V",
D6: "D6 DB2W40200L",
D7: "D7 DB2W40200L",
D8: "D8 DB2W40200L",
}
for (const [ref, displayName] of Object.entries(expectedReferenceDisplayNames)) {
requireCheck(componentByName.get(ref)?.display_name === displayName, `${ref} schematic label must match TI Figure 26`)
}
const expectedResistance = {
R1: 1000, R2: 560, R3: 1000, R4: 1000, R5: 0,
R6: 560, R7: 1000, R8: 560, R9: 560, R10: 560,
R11: 560, R12: 560, R13: 560, R14: 0, R15: 0,
}
for (const [ref, resistance] of Object.entries(expectedResistance)) {
requireCheck(componentByName.get(ref)?.resistance === resistance, `${ref} resistance does not match TI Figure 26`)
}
requireCheck(componentByName.get("C1")?.capacitance === 1e-6, "C1 must be 1 uF")
requireCheck(componentByName.get("C2")?.capacitance === 1e-7, "C2 must be 0.1 uF")
requireCheck(componentByName.get("C3")?.capacitance === 1e-7, "C3 must be 0.1 uF")
const switchNetChecks = [
"connections={{ pin1: N.GP11, pin2: N.SER, pin4: N.GP12, pin5: N.RCLK }}",
"connections={{ pin1: N.GP13, pin2: N.SRCLK, pin4: N.V3V3, pin5: N.OE, pin6: N.DGND }}",
"connections={{ pin1: N.IN1, pin2: N.GP11 }}",
"connections={{ pin1: N.IN2, pin2: N.GP12 }}",
"connections={{ pin1: N.IN3, pin2: N.GP13 }}",
]
for (const expected of switchNetChecks) requireCheck(source.includes(expected), `TI switch mapping changed: ${expected}`)
if (failures.length) {
console.error(`Release audit failed (${failures.length}):`)
for (const failure of failures) console.error(`- ${failure}`)
process.exit(1)
}
console.log("Electrical release audit passed with eight constrained motor-output routes; first-article mechanical and bench validation remain")