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
exports.ts
PCB
Schematic
import { mkdirSync, readFileSync, writeFileSync } from "node:fs"
import { convertCircuitJsonToSchematicSvg } from "circuit-to-svg"
import { convertCircuitJsonToPickAndPlaceCsv, type PickAndPlaceRotationWarning } from "circuit-json-to-pnp-csv"
import type { AnyCircuitElement } from "circuit-json"
import { getRotationBetweenPcbPin1Locations } from "circuit-json"
import { analyzePcbPin1Location } from "@tscircuit/circuit-json-util"
import assert from "node:assert/strict"
const circuit = JSON.parse(readFileSync("dist/controller/circuit.json","utf8")) as AnyCircuitElement[]
const codes=JSON.parse(readFileSync("supplier-codes.json","utf8")) as Record<string,string>
const reviewed=structuredClone(circuit)
const orientationReview:Record<string,unknown>[]=[]
const manualNames=new Set(["J_BAT","J_PITCH","J_ROLL","J_YAW","J_BOOT","J_SWD","J_UART","J_STOP","L1"])
for(const s of reviewed.filter(x=>x.type==="source_component")){
if(s.type!=="source_component")continue
const p=reviewed.find(x=>x.type==="pcb_component"&&x.source_component_id===s.source_component_id)
if(!p||p.type!=="pcb_component"||p.do_not_place||s.ftype==="simple_test_point")continue
if(manualNames.has(s.name)){
p.do_not_place=true
orientationReview.push({designator:s.name,method:"post-reflow manual assembly",reason:s.name==="L1"?"Supplier library unavailable; use Bourns land pattern and horizontal terminal axis. Non-polar part.":"Through-hole connector: pin numbering and housing direction on assembly drawing govern."})
continue
}
const mpn=s.manufacturer_part_number!
const supplier=JSON.parse(readFileSync(`supplier-library/${mpn.replace(/[^a-zA-Z0-9_-]/g,"_")}.json`,"utf8"))
assert.equal(supplier.find((x:any)=>x.type==="source_component")?.manufacturer_part_number?.toLowerCase(),mpn.toLowerCase(),`Supplier MPN mismatch ${s.name}`)
const frame=analyzePcbPin1Location(supplier)
assert.ok(frame,`No supplier orientation for ${s.name}`)
assert.ok(p.pin1_location,`No authored orientation for ${s.name}`)
// Builtin diode numbers pin 1 as anode; supplier SOD/SMB numbers pin 1 as cathode.
if(s.name==="D_GS"||s.name==="D_TVS"){
assert.equal(p.pin1_location,"topside_left")
p.pin1_location="bottomside_right"
}
p.supplier_pin1_location_map={jlcpcb:frame}
const delta=getRotationBetweenPcbPin1Locations(frame,p.pin1_location)
assert.notEqual(delta,null,`Incompatible terminal axes ${s.name}`)
if(codes[mpn])s.supplier_part_numbers={jlcpcb:[codes[mpn]]}
orientationReview.push({designator:s.name,mpn,supplierCode:s.supplier_part_numbers?.jlcpcb?.[0],method:"exact supplier MPN and terminal frame",supplierFrame:frame,authoredFrame:p.pin1_location,rotationDelta:delta,placementRotation:(p.rotation+delta!+360)%360,polarityNote:s.name==="D_GS"||s.name==="D_TVS"?"Supplier pin 1 = cathode; mapped to authored pin 2. Cathode connects to VM.":undefined})
}
assert.equal(orientationReview.length,74)
writeFileSync("assembly-orientation-review.json",JSON.stringify(orientationReview,null,2))
writeFileSync("assembly-reviewed.circuit.json",JSON.stringify(reviewed))
const components=circuit.filter(x=>x.type==="source_component")
const inverse=Object.fromEntries(Object.entries(JSON.parse(readFileSync("refdes.json","utf8"))).map(([k,v])=>[String(v),k]))
const rows:unknown[][]=[["Designator","Function","Manufacturer part number","JLCPCB candidate codes (verify)","Footprint/placement","Voltage rating V","Assembly status"]]
for(const component of components){
const pcb=circuit.find(x=>x.type==="pcb_component"&&x.source_component_id===component.source_component_id)
if(!pcb||pcb.type!=="pcb_component"||pcb.do_not_place)continue
rows.push([component.name,inverse[component.name]??component.name,component.manufacturer_part_number??"",codes[component.manufacturer_part_number??""]??component.supplier_part_numbers?.jlcpcb?.join(" / ")??"",`${pcb.layer}; ${pcb.rotation} deg`,"max_voltage_rating" in component?component.max_voltage_rating:"",manualNames.has(component.name)?"POST-REFLOW MANUAL ASSEMBLY":"Supplier terminal frame reviewed; approve supplier placement preview"])
}
const csv=rows.map(row=>row.map(v=>'"'+String(v??"").replaceAll('"','""')+'"').join(",")).join("\r\n")
writeFileSync("parts-review.csv",csv)
const warnings:PickAndPlaceRotationWarning[]=[]
writeFileSync("placement-review.csv",convertCircuitJsonToPickAndPlaceCsv(reviewed,{supplier:"jlcpcb",requireSupplierRotation:true,onRotationWarning:w=>warnings.push(w)}))
writeFileSync("placement-rotation-warnings.json",JSON.stringify(warnings,null,2))
console.log(`Exported ${rows.length-1} populated part lines; 65 supplier-frame-checked SMT placements, 9 explicit manual placements; ${warnings.length} unresolved supplier rotation warnings.`)
mkdirSync("dist/index/sheets",{recursive:true})
for(const sheet of circuit.filter(x=>x.type==="schematic_sheet")){
if(sheet.type!=="schematic_sheet")continue
writeFileSync(`dist/index/sheets/${sheet.schematic_sheet_id}.svg`,convertCircuitJsonToSchematicSvg(circuit,{schematicSheetId:sheet.schematic_sheet_id,width:1600,height:1000}))
}