mohan-bee/gimbal-one-budget-pcb
A 4-layer STM32-based gimbal controller PCB with an onboard LSM6DS3TR-C IMU, three DRV8313 3-phase motor drivers, protected 3S battery input, 3.3 V buck regulation, and SWD/UART control interfaces.
- Version
- 0.1.0
- License
- unset
- Stars
- 0
fabrication-files.ts
PCB
Schematic
import {readFileSync,writeFileSync,mkdirSync} from "node:fs"
import assert from "node:assert/strict"
import {convertCircuitJsonToAssemblySvg,convertCircuitJsonToPcbSvg} from "circuit-to-svg"
import {convertCircuitJsonToPickAndPlaceRows} from "circuit-json-to-pnp-csv"
const circuit=JSON.parse(readFileSync("assembly-reviewed.circuit.json","utf8"))
const rows=convertCircuitJsonToPickAndPlaceRows(circuit,{supplier:"jlcpcb",requireSupplierRotation:true})
assert.equal(rows.length,65)
const csv=(rows:unknown[][])=>rows.map(r=>r.map(x=>`"${String(x??"").replaceAll('"','""')}"`).join(",")).join("\r\n")+"\r\n"
mkdirSync("fabrication",{recursive:true})
// Gerbers/drills use the board lower-left datum. Authored board coordinates are centred.
writeFileSync("fabrication/jlcpcb-cpl.csv",csv([["Designator","Mid X","Mid Y","Layer","Rotation"],...rows.map(r=>[r.designator,(r.mid_x+42).toFixed(3),(r.mid_y+35).toFixed(3),r.layer,r.rotation])]))
const smt=new Set(rows.map(r=>r.designator));const bom:any[][]=[];const manual:any[][]=[]
for(const s of circuit.filter((x:any)=>x.type==="source_component"&&x.manufacturer_part_number)){
const p=circuit.find((x:any)=>x.type==="pcb_component"&&x.source_component_id===s.source_component_id)
const code=s.supplier_part_numbers?.jlcpcb?.[0]??""
if(smt.has(s.name)){
assert.ok(code,`Missing supplier code ${s.name}`)
bom.push([s.manufacturer_part_number,s.name,s.name.startsWith("R")||(/^C\d+$/.test(s.name)&&s.name!=="C1")?(/^(CL21)/.test(s.manufacturer_part_number)?"0805":"0603"):s.manufacturer_part_number,code])
}else if(s.name.startsWith("J_")||s.name==="L1")manual.push([s.name,s.manufacturer_part_number,code,p.center.x+42,p.center.y+35,p.rotation,s.name==="L1"?"Non-polar; horizontal terminal axis, fit after SMT reflow":"Fit from top; pin numbering/housing orientation per assembly drawing"])
}
assert.equal(bom.length,65);assert.equal(manual.length,9)
writeFileSync("fabrication/jlcpcb-bom.csv",csv([["Comment","Designator","Footprint","LCSC Part #"],...bom]))
writeFileSync("fabrication/manual-assembly.csv",csv([["Designator","MPN","Supplier code","X mm from lower-left","Y mm from lower-left","Authored rotation deg","Instructions"],...manual]))
const original=JSON.parse(readFileSync("dist/controller/circuit.json","utf8"))
writeFileSync("fabrication/assembly-top.svg",convertCircuitJsonToAssemblySvg(original,{width:2000,height:1700,includeVersion:false}))
const critical=["D_GS","D_TVS","C1","Q1","U_BUCK","U_MCU","U_IMU","U_PITCH","U_ROLL","U_YAW"]
const escape=(v:string)=>v.replaceAll("&","&").replaceAll("<","<")
const cells=critical.map((name,i)=>{
const s=original.find((x:any)=>x.type==="source_component"&&x.name===name)
const p=original.find((x:any)=>x.type==="pcb_component"&&x.source_component_id===s.source_component_id)
const pads=original.filter((x:any)=>x.type==="pcb_smtpad"&&x.pcb_component_id===p.pcb_component_id)
const markedPin=name.startsWith("D_")?2:1
const ports=new Map(original.filter((x:any)=>x.type==="source_port"&&x.source_component_id===s.source_component_id).map((x:any)=>[x.source_port_id,x]))
const pcbports=new Map(original.filter((x:any)=>x.type==="pcb_port"&&x.pcb_component_id===p.pcb_component_id).map((x:any)=>[x.pcb_port_id,x]))
const scale=Math.min(190/p.width,140/p.height)
const marks=pads.map((pad:any)=>{
const port:any=ports.get((pcbports.get(pad.pcb_port_id) as any)?.source_port_id)
const num=port?.pin_number;const x=175+(pad.x-p.center.x)*scale,y=125-(pad.y-p.center.y)*scale
return `<rect x="${x-pad.width*scale/2}" y="${y-pad.height*scale/2}" width="${pad.width*scale}" height="${pad.height*scale}" fill="${num===markedPin?"#d92d20":"#d0d5dd"}" stroke="#344054" stroke-width="0.5"/><text x="${x}" y="${y+2.5}" text-anchor="middle" font-size="7" fill="${num===markedPin?"white":"black"}">${num}</text>`
}).join("")
const note=name.startsWith("D_")?"RED = cathode / supplier pin 1 / authored pin 2 → VM":name==="C1"?"RED = positive pin 1 → VM; negative pin 2 → GND":"RED = pin 1; top view in installed board orientation"
return `<g transform="translate(${(i%3)*360},${Math.floor(i/3)*250+55})"><rect x="5" y="5" width="345" height="235" rx="6" fill="white" stroke="#98a2b3"/><text x="18" y="29" font-size="18" font-weight="bold">${name}</text><text x="18" y="47" font-size="11">${escape(s.manufacturer_part_number)}</text>${marks}<text x="18" y="211" font-size="9">${note}</text><text x="18" y="228" font-size="10">PCB centre (${p.center.x}, ${p.center.y}) mm, relative to board centre</text></g>`
}).join("")
writeFileSync("fabrication/assembly-polarity.svg",`<svg xmlns="http://www.w3.org/2000/svg" width="2160" height="2150" viewBox="0 0 1080 1075"><rect width="1080" height="1075" fill="#f2f4f7"/><g font-family="Arial,sans-serif"><text x="20" y="29" font-size="23" font-weight="bold">Gimbal One budget LSM6DS3TR-C — polarity and pin 1, TOP VIEW</text><text x="20" y="47" font-size="12">Physical pad locations govern. Supplier diode numbering differs from the authored symbol numbering.</text>${cells}</g></svg>`)
for(const layer of ["top","inner1","inner2","bottom"] as const)writeFileSync(`fabrication/copper-${layer}.svg`,convertCircuitJsonToPcbSvg(original,{layer,width:1600,height:1400,showSolderMask:false,showFabricationNotes:false,includeVersion:false}))
console.log("Created matching-origin SMT BOM/CPL, 9-part manual list, assembly drawing and four copper previews")