0hmX/bluetooth-pedometer-pcb
This code defines and renders a detailed electronic circuit board layout with labeled components, footprints, 3D models, and routing, suited for PCB design and manufacturing.
- Version
- 1.0.4
- License
- unset
- Stars
- 0
scripts/export-jlc.ts
import { mkdir } from "node:fs/promises"
type CircuitElement = Record<string, any>
const inputPath = process.argv[2] ?? "dist/index/circuit.json"
const outputDir = process.argv[3] ?? "dist/manufacturing"
const circuit = (await Bun.file(inputPath).json()) as CircuitElement[]
const naturalRefSort = (a: string, b: string) =>
a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" })
const csv = (rows: Array<Array<string | number>>) =>
rows
.map((row) =>
row
.map((cell) => `"${String(cell).replaceAll('"', '""')}"`)
.join(","),
)
.join("\n") + "\n"
const packageByMpn: Record<string, string> = {
"USB4105-GF-A": "USB-C-16P-SMD",
BQ25185DLHR: "WSON-10-EP-2.2x2mm",
"S3B-PH-SM4-TB(LF)(SN)": "JST-PH-3-SMD-RA",
BQ27427YZFR: "DSBGA-9-1.62x1.58mm-P0.5mm",
TPS7A0233PDBVR: "SOT-23-5",
CC2340R52E0RGER: "VQFN-24-EP-4x4mm-P0.5mm",
"ABM11W-48.0000MHZ-7-B1U-T3": "Crystal-4P-2.0x1.6mm",
X1A0000610006: "Crystal-2P-2.0x1.2mm",
"2450AT18A100E": "Chip-Antenna-3.2x1.6mm",
BMA400: "LGA-12-2x2mm-P0.5mm",
"SM04B-SRSS-TB(LF)(SN)": "JST-SH-4-SMD-RA",
"TS-1187A-B-A-B": "Tactile-Switch-6x6mm-SMD",
AO3401A: "SOT-23",
}
const packageFor = (component: CircuitElement) => {
const mapped = packageByMpn[component.manufacturer_part_number]
if (mapped) return mapped
if (["C17", "C18", "C19", "C20", "L2"].includes(component.name)) {
return "0201 (0603 metric)"
}
if (["C1", "C2", "C10", "D1", "L1"].includes(component.name)) {
return "0603 (1608 metric)"
}
return "0402 (1005 metric)"
}
const valueFor = (component: CircuitElement) =>
component.display_resistance ??
component.display_capacitance ??
component.display_inductance ??
component.symbol_display_value ??
component.manufacturer_part_number ??
component.name
const sources = circuit.filter((element) => element.type === "source_component")
const placements = new Map(
circuit
.filter((element) => element.type === "pcb_component")
.map((element) => [element.source_component_id, element]),
)
const assembled = sources.filter(
(component) => component.supplier_part_numbers?.jlcpcb?.[0] && placements.has(component.source_component_id),
)
const bomGroups = new Map<
string,
{ comment: string; refs: string[]; footprint: string; lcsc: string; mpn: string }
>()
for (const component of assembled) {
const lcsc = `C${component.supplier_part_numbers.jlcpcb[0]}`.replace("CC", "C")
const footprint = packageFor(component)
const comment = valueFor(component)
const mpn = component.manufacturer_part_number ?? ""
const key = [lcsc, mpn, comment, footprint].join("|")
const group = bomGroups.get(key) ?? {
comment,
refs: [] as string[],
footprint,
lcsc,
mpn,
}
group.refs.push(component.name)
bomGroups.set(key, group)
}
const bomRows: Array<Array<string | number>> = [
["Comment", "Designator", "Footprint", "LCSC Part #", "Manufacturer Part Number", "Quantity"],
]
for (const group of [...bomGroups.values()].sort((a, b) => naturalRefSort(a.refs[0], b.refs[0]))) {
group.refs.sort(naturalRefSort)
bomRows.push([
group.comment,
group.refs.join(","),
group.footprint,
group.lcsc,
group.mpn,
group.refs.length,
])
}
const cplRows: Array<Array<string | number>> = [
["Designator", "Mid X", "Mid Y", "Layer", "Rotation"],
]
for (const component of assembled.sort((a, b) => naturalRefSort(a.name, b.name))) {
const placement = placements.get(component.source_component_id)!
cplRows.push([
component.name,
`${placement.center.x.toFixed(4)}mm`,
`${placement.center.y.toFixed(4)}mm`,
placement.layer === "bottom" ? "Bottom" : "Top",
((placement.rotation ?? 0) % 360 + 360) % 360,
])
}
await mkdir(outputDir, { recursive: true })
await Bun.write(`${outputDir}/jlcpcb-bom.csv`, csv(bomRows))
await Bun.write(`${outputDir}/jlcpcb-cpl.csv`, csv(cplRows))
console.log(`Wrote ${bomRows.length - 1} BOM lines and ${cplRows.length - 1} CPL placements to ${outputDir}`)