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-gerbers.mjs
import { readFileSync } from "node:fs"
import { execFileSync } from "node:child_process"
const zipPath = process.argv[2] ?? "outputs/MSPM0G3507_USB_C_JLCPCB_Gerbers.zip"
const circuitPath = process.argv[3] ?? "dist/index/circuit.json"
const circuit = JSON.parse(readFileSync(circuitPath, "utf8"))
const failures = []
const assert = (condition, message) => {
if (!condition) failures.push(message)
}
const zipText = (name) => execFileSync("unzip", ["-p", zipPath, name], { encoding: "utf8" })
const fileNames = execFileSync("unzip", ["-Z1", zipPath], { encoding: "utf8" }).trim().split("\n")
const expectedFiles = [
"F_Cu.gbr",
"B_Cu.gbr",
"F_Mask.gbr",
"B_Mask.gbr",
"F_Paste.gbr",
"B_Paste.gbr",
"F_SilkScreen.gbr",
"B_SilkScreen.gbr",
"Edge_Cuts.gbr",
"F_Fab.gbr",
"drill-L1-L2.drl",
"drill_npth.drl",
"bom.csv",
"pick_and_place.csv",
]
for (const fileName of expectedFiles) assert(fileNames.includes(fileName), `ZIP is missing ${fileName}`)
assert(
!fileNames.some((name) => /inner|In\d_Cu|drill-L(?!1-L2)/i.test(name)),
"ZIP contains an inner-layer or non-through drill file",
)
const countFlashes = (gerber) => (gerber.match(/D03\*/g) ?? []).length
const edgeCuts = zipText("Edge_Cuts.gbr")
const outlineCoordinates = [...edgeCuts.matchAll(/X(-?\d+)Y(-?\d+)/g)].map((match) => ({
x: Number(match[1]) / 1_000_000,
y: Number(match[2]) / 1_000_000,
}))
const outlineBounds = {
minX: Math.min(...outlineCoordinates.map(({ x }) => x)),
maxX: Math.max(...outlineCoordinates.map(({ x }) => x)),
minY: Math.min(...outlineCoordinates.map(({ y }) => y)),
maxY: Math.max(...outlineCoordinates.map(({ y }) => y)),
}
const gerberBoardSize = {
width: outlineBounds.maxX - outlineBounds.minX,
height: outlineBounds.maxY - outlineBounds.minY,
}
assert(
Math.abs(gerberBoardSize.width - 90) < 0.001 && Math.abs(gerberBoardSize.height - 21) < 0.001,
`Gerber outline bounds are ${gerberBoardSize.width} mm by ${gerberBoardSize.height} mm instead of 90 mm by 21 mm`,
)
const hasOutlinePoint = (x, y) => outlineCoordinates.some(
(point) => Math.abs(point.x - x) < 0.001 && Math.abs(point.y - y) < 0.001,
)
for (const [x, y] of [
[-45, 10.5], [45, 10.5], [45, -10.5], [-45, -10.5],
]) {
assert(hasOutlinePoint(x, y), `Gerber outline is missing rectangular vertex (${x}, ${y}) mm`)
}
const topPasteFlashes = countFlashes(zipText("F_Paste.gbr"))
const bottomPasteFlashes = countFlashes(zipText("B_Paste.gbr"))
const circuitPasteCount = circuit.filter(
(element) => element.type === "pcb_solder_paste" && element.layer === "top",
).length
assert(
topPasteFlashes === circuitPasteCount,
`Top paste has ${topPasteFlashes} flashes; Circuit JSON has ${circuitPasteCount}`,
)
assert(bottomPasteFlashes === 0, `Bottom paste contains ${bottomPasteFlashes} flashes`)
const parseExcellon = (text) => {
const tools = new Map()
let activeTool = null
let pendingSlotStart = null
for (const line of text.split(/\r?\n/)) {
const definition = line.match(/^T(\d+)C([\d.]+)/)
if (definition) {
tools.set(definition[1], { diameter: Number(definition[2]), hits: 0, slots: [] })
continue
}
const selection = line.match(/^T(\d+)$/)
if (selection) {
activeTool = tools.get(selection[1]) ?? null
pendingSlotStart = null
continue
}
if (!activeTool) continue
const point = line.match(/^X(-?[\d.]+)Y(-?[\d.]+)$/)
if (point) {
pendingSlotStart = { x: Number(point[1]), y: Number(point[2]) }
activeTool.hits += 1
continue
}
const slotEnd = line.match(/^G85X(-?[\d.]+)Y(-?[\d.]+)$/)
if (slotEnd && pendingSlotStart) {
activeTool.slots.push({
start: pendingSlotStart,
end: { x: Number(slotEnd[1]), y: Number(slotEnd[2]) },
})
pendingSlotStart = null
}
}
return [...tools.values()]
}
const platedTools = parseExcellon(zipText("drill-L1-L2.drl"))
const nonPlatedTools = parseExcellon(zipText("drill_npth.drl"))
const topCopper = zipText("F_Cu.gbr")
const bottomCopper = zipText("B_Cu.gbr")
const findTool = (tools, diameter) =>
tools.find((tool) => Math.abs(tool.diameter - diameter) < 0.00001)
const shieldTool = findTool(platedTools, 0.799998)
const groveTool = findTool(platedTools, 0.849986)
const headerTool = findTool(platedTools, 1.05)
const viaTool = findTool(platedTools, 0.3)
const usbLocatingTool = findTool(nonPlatedTools, 0.599999)
const mountingTool = findTool(nonPlatedTools, 2.7)
const circuitViaCount = circuit.filter((element) => element.type === "pcb_via").length
assert(viaTool?.hits === circuitViaCount, `Through-drill file has ${viaTool?.hits ?? 0} via hits; Circuit JSON has ${circuitViaCount}`)
assert(/^%ADD\d+C,0\.450000\*%$/m.test(topCopper), "Top copper lacks the 0.45 mm via-pad aperture")
assert(/^%ADD\d+C,0\.450000\*%$/m.test(bottomCopper), "Bottom copper lacks the 0.45 mm via-pad aperture")
assert(headerTool?.hits === 40, `Expected 40 header drills, found ${headerTool?.hits ?? 0}`)
assert(groveTool?.hits === 4, `Expected four Grove drills, found ${groveTool?.hits ?? 0}`)
assert(shieldTool?.slots.length === 4, `Expected four USB-C shield slots, found ${shieldTool?.slots.length ?? 0}`)
assert(usbLocatingTool?.hits === 2, `Expected two USB-C locating holes, found ${usbLocatingTool?.hits ?? 0}`)
assert(mountingTool?.hits === 4, `Expected four M2.5 mounting holes, found ${mountingTool?.hits ?? 0}`)
for (const [index, slot] of (shieldTool?.slots ?? []).entries()) {
const dx = Math.abs(slot.end.x - slot.start.x)
const dy = Math.abs(slot.end.y - slot.start.y)
assert(dx >= 0.599 && dy < 0.001, `USB-C shield slot ${index + 1} is not horizontally oriented after board rotation`)
}
const bom = zipText("bom.csv").trim().split(/\r?\n/)
const cpl = zipText("pick_and_place.csv").trim().split(/\r?\n/)
assert(bom[0]?.includes("Manufacturer Part Number"), "Embedded BOM lacks the manufacturer-part-number column")
assert(bom[0]?.includes("JLCPCB Part #"), "Embedded BOM lacks the JLCPCB part-number column")
assert(bom.length === 39, `Embedded BOM has ${bom.length - 1} placements instead of 38`)
assert(cpl.length === 39, `Embedded CPL has ${cpl.length - 1} placements instead of 38`)
for (const [designator, jlc, mpn] of [
["U_MCU", "C22362630", "MSPM0G3507SPTR"],
["U_USB_UART", "C84681", "CH340C"],
["D_USER_RGB", "C3029072", "SZYY1615RGB-A 4pin"],
["R_RGB_RED", "C25104", "0402WGF3300TCE"],
["J_GROVE_I2C", "C7429580", "ZX-HY2.0-4PZZ"],
["J_I2C_JST_SH", "C160404", "SM04B-SRSS-TB(LF)(SN)"],
["J_SPI_DISPLAY", "C160407", "SM08B-SRSS-TB(LF)(SN)"],
["J_TOP", "C42431804", "PH2.54-1X20P-H25"],
["J_BOTTOM", "C42431804", "PH2.54-1X20P-H25"],
]) {
assert(
bom.some((line) => line.startsWith(`${designator},`) && line.includes(`,${jlc},${mpn}`)),
`Embedded BOM is missing ${designator} / ${jlc} / ${mpn}`,
)
}
assert(!bom.some((line) => line.includes("MSP430F5503")), "Embedded BOM contains stale MSP430 data")
assert(cpl.some((line) => /^U_MCU,-3\.500,0\.000,top,0$/.test(line)), "Embedded CPL has the wrong MCU placement")
assert(cpl.some((line) => /^U_USB_UART,-25\.900,0\.000,top,270$/.test(line)), "Embedded CPL has the wrong CH340C placement")
assert(cpl.some((line) => /^J_TOP,0\.000,8\.890,top,0$/.test(line)), "Embedded CPL has the wrong top-header placement")
assert(cpl.some((line) => /^J_BOTTOM,0\.000,-8\.890,top,0$/.test(line)), "Embedded CPL has the wrong bottom-header placement")
assert(cpl.some((line) => /^J_SPI_DISPLAY,41\.850,4\.500,top,90$/.test(line)), "Embedded CPL has the wrong SPI connector placement")
assert(cpl.some((line) => /^J_I2C_JST_SH,42\.000,-5\.100,top,90$/.test(line)), "Embedded CPL has the wrong JST-SH connector placement")
assert(cpl.some((line) => /^J_GROVE_I2C,33\.500,-7\.200,top,0$/.test(line)), "Embedded CPL has the wrong Grove connector placement")
console.log(JSON.stringify({
files: fileNames.length,
boardMm: [gerberBoardSize.width, gerberBoardSize.height],
copperLayers: ["top", "bottom"],
drill: {
throughVias: viaTool?.hits,
viaHoleDiameterMm: 0.3,
viaPadDiameterMm: 0.45,
headerHoles: headerTool?.hits,
groveHoles: groveTool?.hits,
usbShieldSlots: shieldTool?.slots.length,
usbShieldSlotsOrientation: "horizontal, matching rotated PCB footprint",
usbLocatingHoles: usbLocatingTool?.hits,
mountingHoles: mountingTool?.hits,
},
pasteFlashes: { top: topPasteFlashes, bottom: bottomPasteFlashes },
assemblyPlacements: { bom: bom.length - 1, cpl: cpl.length - 1 },
failures,
}, null, 2))
if (failures.length > 0) process.exitCode = 1