seveibar/am3352-dev-board-4layer-dogbone
Defines PCB footprints and pin mappings for a TI AM3352ZCZ100 processor, JST GH connectors, and assembly-part lookup helpers for capacitors, resistors, and headers.
- Version
- 0.1.5
- License
- unset
- Stars
- 0
scripts/export-assembly.mjs
import fs from 'node:fs'
import crypto from 'node:crypto'
import assert from 'node:assert/strict'
import {convertCircuitJsonToPickAndPlaceRows} from 'circuit-json-to-pnp-csv'
import Papa from 'papaparse'
const raw=fs.readFileSync('dist/index/circuit.json'),cj=JSON.parse(raw),orient=JSON.parse(fs.readFileSync('design/assembly-orientations.json'))
const source=new Map(cj.filter(e=>e.type==='source_component').map(e=>[e.source_component_id,e]))
assert.equal(cj.filter(e=>e.type.endsWith('_error')).length,0,'Resolve board errors before assembly export')
const packages=new Map()
for(const file of fs.readdirSync('design/assembly-sources')) {
if(!file.endsWith('.json'))continue
const data=JSON.parse(fs.readFileSync('design/assembly-sources/'+file))
for(const p of data.response?.components??[]) packages.set('C'+p.lcsc,p.package)
}
const smt=[],manual=[],orientationEvidence=[]
const prepared=structuredClone(cj)
for(const comp of prepared.filter(e=>e.type==='pcb_component')){
const part=source.get(comp.source_component_id)
if(part.ftype==='simple_test_point'){comp.do_not_place=true;continue}
assert(part.manufacturer_part_number,`Missing MPN: ${part.name}`)
if(part.ftype==='simple_pin_header') {comp.do_not_place=true;manual.push({Designator:part.name,ManufacturerPartNumber:part.manufacturer_part_number,Quantity:1,Process:'Through-hole insertion after SMT'});continue}
const code=part.supplier_part_numbers?.jlcpcb?.[0];assert(code,`Missing JLCPCB code: ${part.name}`)
assert(orient[code],`Missing supplier orientation: ${part.name} ${code}`)
assert(comp.pin1_location,`Missing authored orientation: ${part.name}`)
comp.supplier_pin1_location_map={jlcpcb:orient[code].pin1Location}
smt.push({Comment:part.manufacturer_part_number,Designator:part.name,Footprint:packages.get(code)||part.manufacturer_part_number,'LCSC Part #':code})
orientationEvidence.push({designator:part.name,part:code,authoredPin1:comp.pin1_location,supplierPin1:orient[code].pin1Location,source:orient[code].source})
}
const rows=convertCircuitJsonToPickAndPlaceRows(prepared,{supplier:'jlcpcb',requireSupplierRotation:true})
assert.equal(rows.length,smt.length)
assert.deepEqual(new Set(rows.map(r=>r.designator)),new Set(smt.map(r=>r.Designator)))
assert.equal(smt.length+manual.length+1,source.size,'Every component must be accounted for')
for(const r of rows)assert(Number.isFinite(r.rotation)&&Number.isFinite(r.mid_x)&&Number.isFinite(r.mid_y),'Invalid placement')
const output='assembly';fs.mkdirSync(output,{recursive:true})
const bomGroups=new Map()
for(const r of smt){const k=r['LCSC Part #'];if(bomGroups.has(k))bomGroups.get(k).Designator+=','+r.Designator;else bomGroups.set(k,{...r})}
fs.writeFileSync(output+'/jlcpcb-bom.csv',Papa.unparse([...bomGroups.values()])+'\n')
fs.writeFileSync(output+'/jlcpcb-cpl.csv',Papa.unparse(rows.map(r=>({Designator:r.designator,'Mid X':Number(r.mid_x.toFixed(6))+'mm','Mid Y':Number(r.mid_y.toFixed(6))+'mm',Layer:r.layer,Rotation:r.rotation})))+'\n')
fs.writeFileSync(output+'/manual-assembly.csv',Papa.unparse([...manual,{Designator:'BOOT shunts (accessories)',ManufacturerPartNumber:'M50-1900005',Quantity:16,Process:'Fit boot configuration during bring-up; not an SMT placement'}])+'\n')
fs.writeFileSync(output+'/orientation-audit.json',JSON.stringify({circuitSha256:crypto.createHash('sha256').update(raw).digest('hex'),smtComponents:smt.length,manualComponents:manual.length,unresolvedRotations:0,orientations:orientationEvidence,placements:rows},null,2)+'\n')
console.log('Assembly export:',smt.length,'SMT,',manual.length,'manual headers, one bare test pad; zero unresolved rotations')