ShiboSoftwareDev/mspm0g3507-gameboy-handheld
An AO3400A N-channel MOSFET for power switching and an AP2112K-3.3TRG1 LDO regulator providing a regulated 3.3 V output, both in compact SMD packages.
- Version
- 1.2.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_GAMEBOY_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",
"In1_Cu.gbr",
"In2_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-L4.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(?:0|[3-9]\d*)_Cu|drill-L(?!1-L4)/i.test(name)),
"ZIP contains an unexpected copper or plated-drill layer",
)
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 - 72) < 0.001 && Math.abs(gerberBoardSize.height - 112) < 0.001,
`Gerber outline bounds are ${gerberBoardSize.width} mm by ${gerberBoardSize.height} mm instead of 72 mm by 112 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 [
[-36, 56], [36, 56], [36, -56], [-36, -56],
]) {
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-L4.drl"))
const nonPlatedTools = parseExcellon(zipText("drill_npth.drl"))
const topCopper = zipText("F_Cu.gbr")
const inner1Copper = zipText("In1_Cu.gbr")
const inner2Copper = zipText("In2_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 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(inner1Copper), "Inner 1 copper lacks the 0.45 mm via-pad aperture")
assert(/^%ADD\d+C,0\.450000\*%$/m.test(inner2Copper), "Inner 2 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(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(dy >= 0.599 && dx < 0.001, `USB-C shield slot ${index + 1} is not vertically oriented at the lower board edge`)
}
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 === 70, `Embedded BOM has ${bom.length - 1} placements instead of 69`)
assert(cpl.length === 70, `Embedded CPL has ${cpl.length - 1} placements instead of 69`)
for (const [designator, jlc, mpn] of [
["U_MCU", "C22362630", "MSPM0G3507SPTR"],
["U_USB_UART", "C84681", "CH340C"],
["J_BATTERY", "C160353", "B3B-PH-SM4-TB(LF)(SN)"],
["U_BATTERY_CHARGER", "C15464", "BQ24075RGTR"],
["R_CHARGE_CURRENT", "C25890", "0402WGF3301TCE"],
["SW_POWER", "C431540", "MSK12C02"],
["U_3V3_BUCK_BOOST", "C15516", "TPS63031DSKR"],
["U_BACKLIGHT_BOOST", "C59567", "TPS61240DRVR"],
["D_USER_RGB", "C3029072", "SZYY1615RGB-A 4pin"],
["R_RGB_RED", "C25104", "0402WGF3300TCE"],
["J_AUDIO_MODULE", "C160404", "SM04B-SRSS-TB(LF)(SN)"],
["J_DISPLAY_FPC", "C2911826", "0.5K-AS-50PWBG"],
["U_BACKLIGHT_SWITCH", "C20917", "AO3400A"],
["R_BACKLIGHT_K1", "C25076", "0402WGF1000TCE"],
["SW_UP", "C10852", "4*4*1.7 贴片"],
["SW_A", "C10852", "4*4*1.7 贴片"],
["SW_START", "C10852", "4*4*1.7 贴片"],
["SW_SELECT", "C10852", "4*4*1.7 贴片"],
]) {
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,0\.000,5\.000,top,0$/.test(line)), "Embedded CPL has the wrong MCU placement")
assert(cpl.some((line) => /^U_USB_UART,0\.000,-36\.000,top,0$/.test(line)), "Embedded CPL has the wrong CH340C placement")
assert(cpl.some((line) => /^J_DISPLAY_FPC,32\.000,34\.000,top,90$/.test(line)), "Embedded CPL has the wrong display FPC connector placement")
assert(cpl.some((line) => /^J_AUDIO_MODULE,28\.000,4\.500,top,90$/.test(line)), "Embedded CPL has the wrong audio connector placement")
assert(cpl.some((line) => /^SW_UP,-22\.000,-8\.000,top,90$/.test(line)), "Embedded CPL has the wrong D-pad Up placement")
assert(cpl.some((line) => /^SW_A,27\.200,-11\.000,top,0$/.test(line)), "Embedded CPL has the wrong A-button placement")
assert(cpl.some((line) => /^SW_START,8\.000,-25\.500,top,0$/.test(line)), "Embedded CPL has the wrong Start-button placement")
assert(cpl.some((line) => /^SW_SELECT,-8\.000,-25\.000,top,0$/.test(line)), "Embedded CPL has the wrong Select-button placement")
console.log(JSON.stringify({
files: fileNames.length,
boardMm: [gerberBoardSize.width, gerberBoardSize.height],
copperLayers: ["top", "inner1", "inner2", "bottom"],
drill: {
throughVias: viaTool?.hits,
viaHoleDiameterMm: 0.3,
viaPadDiameterMm: 0.45,
usbShieldSlots: shieldTool?.slots.length,
usbShieldSlotsOrientation: "vertical at the lower board edge",
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