gokul/macro-six-usbc
USB-powered ATmega32U4 controller board with protected/switched 5 V power, six tactile keys, rotary encoder, reset/boot/ISP interfaces, power LED, and six daisy-chained SK6812 RGB LEDs driven through MOSFETs.
- Version
- 0.1.6
- License
- MIT
- Stars
- 0
scripts/fabrication.tsx
import { Circuit } from '@tscircuit/core'
import { MacroKeyboard } from '../lib/macro-keyboard'
import { runAllPlacementChecks,runAllRoutingChecks,runAllNetlistChecks,runAllSchematicChecks,dedupePcbDrcErrors } from '@tscircuit/checks'
import { convertCircuitJsonToGerberFiles } from 'circuit-json-to-gerber'
import { convertCircuitJsonToPcbSvg,convertCircuitJsonToStackedSchematicSheetsSvg,convertCircuitJsonToAssemblySvg } from 'circuit-to-svg'
import { Resvg } from '@resvg/resvg-js'
import { mkdirSync } from 'node:fs'
import { spawnSync } from 'node:child_process'
const folder='outputs/release';mkdirSync(folder+'/gerbers',{recursive:true})
let original:any[]
if(process.argv[2]) original=await Bun.file(process.argv[2]).json()
else { const circuit=new Circuit();circuit.add(<MacroKeyboard includeEnclosure={false} />);await circuit.renderUntilSettled();original=circuit.getCircuitJson() }
// Handbook function/rating notes, positioned from the completed automatic layout.
// No authored schX/schY coordinates are used.
const noteCircuit=new Circuit({platform:{routingDisabled:true}})
const noteTexts=[['U1','USB HID MCU; 5 V, 16 MHz'],['U_ESD','USB ESD protection; 5 V'],...Array.from({length:6},(_,i)=>[`U_RGB${i+1}`,'Addressable RGB; switched 5 V'])]
noteCircuit.add(<board width={10} height={10}>{noteTexts.map(([name,text])=><schematictext key={name} text={text} fontSize={0.16} anchor="top_center" />)}</board>)
await noteCircuit.renderUntilSettled()
const noteRecords=noteCircuit.getCircuitJson().filter(e=>e.type==='schematic_text')
for(let i=0;i<noteTexts.length;i++){
const source=original.find(e=>e.type==='source_component'&&e.name===noteTexts[i][0])
const chip=original.find(e=>e.type==='schematic_component'&&e.source_component_id===source?.source_component_id)
if(!chip)throw Error('Missing schematic chip for handbook note')
original.push({...noteRecords[i],schematic_text_id:'schematic_text_function_'+noteTexts[i][0],schematic_component_id:chip.schematic_component_id,schematic_sheet_id:chip.schematic_sheet_id,position:{x:chip.center.x,y:chip.center.y-chip.size.height/2-1.2}})
}
const diagnostics=dedupePcbDrcErrors((await Promise.all([runAllPlacementChecks(original),runAllRoutingChecks(original),runAllNetlistChecks(original),runAllSchematicChecks(original)])).flat())
await Bun.write(folder+'/core-checks.json',JSON.stringify(diagnostics,null,2))
if([...original,...diagnostics].some(e=>e.type.endsWith('_error')))throw Error('Circuit errors remain; export aborted')
const placedPads=new Set(original.filter(e=>e.type==='pcb_smtpad'&&!original.find(c=>c.type==='pcb_component'&&c.pcb_component_id===e.pcb_component_id)?.do_not_place).map(e=>e.pcb_smtpad_id))
const j=original.filter(e=>e.type!=='pcb_solder_paste'||placedPads.has(e.pcb_smtpad_id))
await Bun.write(folder+'/board.circuit.json',JSON.stringify(j,null,2))
const verification=spawnSync('python3',['manufacturing/verify-rotations.py',folder+'/board.circuit.json'],{encoding:'utf8'})
if(verification.status!==0)throw Error(verification.stderr||verification.stdout)
console.log(verification.stdout.trim())
const rotations=await Bun.file(folder+'/rotation-verification.json').json()
const sources=new Map(j.filter(e=>e.type==='source_component').map(e=>[e.source_component_id,e]))
const placed=j.filter(e=>e.type==='pcb_component'&&!e.do_not_place)
const quote=(v:any)=>'"'+String(v).replaceAll('"','""')+'"'
// Supplier pad-frame registration verifies each full footprint, including parts
// for which a single pin-1 classifier cannot determine a reference corner.
const cpl=placed.map(c=>{ const s=sources.get(c.source_component_id)!;const r=rotations.find((v:any)=>v.designator===s.name);if(!r)throw Error('Unchecked placement');return [s.name,c.center.x.toFixed(3),c.center.y.toFixed(3),c.layer,r.placement_rotation_deg].join(',') })
await Bun.write(folder+'/CPL.csv','Designator,Mid X,Mid Y,Layer,Rotation\r\n'+cpl.join('\r\n')+'\r\n')
const descriptions:Record<string,string>={C44854:'ATmega32U4-AU TQFP-44',C165948:'TYPE-C-31-M-12 USB connector',C202365:'EC11E18244AU rotary encoder',C5274511:'KH-SK22D07-G5 slide switch',C49234237:'CPG151101D06 brown tactile key switch',C965555:'WS2812B-2020 RGB LED',C125094:'LTST-C190KGKT green LED',C15127:'AO3401A P-channel MOSFET',C7519:'USBLC6-2SC6 ESD array',C13738:'16 MHz crystal CL9pF',C69688:'nSMD050 resettable fuse',C720477:'TS-1088-AR02016 tactile switch'}
const footprints:Record<string,string>={C44854:'TQFP-44',C165948:'TYPE-C-31-M-12',C202365:'EC11E18244AU',C5274511:'KH-SK22D07-G5',C49234237:'MX-compatible through-hole',C965555:'2020',C125094:'0603',C15127:'SOT-23',C7519:'SOT-23-6',C13738:'3225',C69688:'1206',C720477:'TS-1088-AR02016'}
descriptions.C2922787 = 'SK6812MINI-HS RGB LED'
footprints.C2922787 = 'SMD3535-4P'
const groups=new Map<string,{part:string,names:string[],comment:string}>()
for(const c of placed){ const s=sources.get(c.source_component_id)!;const part=s.supplier_part_numbers?.jlcpcb?.[0];if(!part)throw Error('Missing assembly supplier part '+s.name);const g=groups.get(part)??{part,names:[],comment:descriptions[part]??(s.resistance!==undefined?`${s.resistance} ohm 0603`:s.capacitance!==undefined?`${s.capacitance} F 0603`:part)};g.names.push(s.name);groups.set(part,g) }
await Bun.write(folder+'/BOM.csv','Comment,Designator,Footprint,LCSC Part #\n'+[...groups.values()].map(g=>[g.comment,g.names.join(','),footprints[g.part]??'0603',g.part].map(quote).join(',')).join('\n')+'\n')
for(const [name,text] of Object.entries(convertCircuitJsonToGerberFiles(j)))await Bun.write(folder+'/gerbers/'+name,text)
for(const [name,svg] of [['pcb-top',convertCircuitJsonToPcbSvg(j,{layer:'top'})],['pcb-bottom',convertCircuitJsonToPcbSvg(j,{layer:'bottom'})],['schematic-two-sheets',convertCircuitJsonToStackedSchematicSheetsSvg(j)],['assembly',convertCircuitJsonToAssemblySvg(j)]] as const){await Bun.write(folder+'/'+name+'.svg',svg);await Bun.write(folder+'/'+name+'.png',new Resvg(svg,{fitTo:{mode:'width',value:2200}}).render().asPng())}
await Bun.write(folder+'/manufacturing-processing.json',JSON.stringify({removed_non_smt_or_dnp_paste_apertures:original.length-j.length,placed_components:placed.length,bom_groups:groups.size,rotation_method:'Supplier numbered-pad registration; verified complete pad frames',routing:'Locally routed and corrected; saved routes in lib/reviewed-routes.json'},null,2))
console.log('Fabrication files exported; perform independent copper and Gerber checks before release.')