abse/museair-usb
USB-C–powered ESP32-C3 controller board with AP63203 buck conversion, AP2112K 3.3V regulation, I²C SEN62 sensor interface, status LED, buttons, UART/I²C headers, and extensive decoupling.
- Version
- 0.5.7
- License
- unset
- Stars
- 0
scripts/repair-routing.mjs
// Guarded post-routing clearance fixes. Keep pad/net geometry unchanged and
// recompute every design check before accepting the edited copper.
import fs from 'node:fs'
import {runAllChecks} from '@tscircuit/checks'
import {repairSpeakerRouting} from './apply-speaker-routing.mjs'
import {repairDisplayAudio} from './apply-display-audio-routing-repairs.mjs'
import {repairLightPipeRouting} from './apply-lightpipe-routing.mjs'
const file=new URL('../dist/index/circuit.json',import.meta.url)
const records=JSON.parse(fs.readFileSync(file))
const inputErrors=records.filter(r=>r.type.includes('error'))
const moved=repairSpeakerRouting(records)??repairDisplayAudio(records)
moved.push(...repairLightPipeRouting(records))
function moveVia(id,x,y){
const via=records.find(r=>r.type==='pcb_via'&&r.pcb_via_id===id)
if(!via)throw Error('Missing expected via '+id)
const expected={pcb_via_11:[4.232,2.631],pcb_via_2:[-14.757549121379453,6.53149449910151],pcb_via_10:[10.051,21.502],pcb_via_6:[-16.786999,7.625]}[id]
if(!expected||Math.hypot(via.x-expected[0],via.y-expected[1])>1e-5)throw Error('Routing input changed; review repair for '+id)
const old={x:via.x,y:via.y}
let points=0
for(const tr of records.filter(r=>r.type==='pcb_trace'))for(const p of tr.route)if(Math.abs(p.x-old.x)<1e-6&&Math.abs(p.y-old.y)<1e-6){p.x=x;p.y=y;points++}
if(!points)throw Error('Via had no attached trace points '+id)
via.x=x;via.y=y;moved.push({id,old,new:{x,y},attached_points:points})
}
const revision=JSON.parse(fs.readFileSync(new URL('../package.json',import.meta.url))).version
if(revision==='0.5.6'){
const trace=records.find(r=>r.type==='pcb_trace'&&r.pcb_trace_id==='source_net_2_mst5_0')
if(inputErrors.some(r=>r.pcb_trace_id===trace?.pcb_trace_id)&&trace){
for(const p of trace.route)if(p.route_type==='wire'&&p.y>23.45&&p.y<24.9){p.x=-13.5;p.width=.15}
moved.push({trace:trace.pcb_trace_id,repair:'Center 0.15 mm V3V3 trace in the existing C7 pad gap'})
}
for(const [id,dx,dy] of [['pcb_via_2',.15,0],['pcb_via_10',.25,0],['pcb_via_6',-.55,0]]){
const via=records.find(r=>r.type==='pcb_via'&&r.pcb_via_id===id)
if(!via)continue
const relevant=inputErrors.some(r=>r.pcb_pad_ids?.includes(id)||(id==='pcb_via_6'&&r.type==='pcb_placement_error'&&r.message.includes('(-16.79mm, 7.63mm)')))
if(relevant){
if(id==='pcb_via_6'){
const shared=records.find(r=>r.type==='pcb_via'&&r.pcb_via_id==='pcb_via_2')
if(shared.subcircuit_connectivity_map_key!==via.subcircuit_connectivity_map_key)throw Error('Cannot share vias across different nets')
moveVia(id,shared.x,shared.y);records.splice(records.indexOf(via),1);moved.push({removed_via:id,shared_via:shared.pcb_via_id,net_preserved:true})
}else moveVia(id,via.x+dx,via.y+dy)
}
}
const nativeGapVia=records.find(r=>r.type==='pcb_via'&&r.pcb_via_id==='pcb_via_11')
if(nativeGapVia&&Math.hypot(nativeGapVia.x-4.232,nativeGapVia.y-2.631)<1e-5)moveVia('pcb_via_11',4.262,2.631)
}
if(revision==='0.5.7'){
const coordinateMoves=new Map()
const key=p=>p.x.toFixed(6)+','+p.y.toFixed(6)
for(const traceId of ['source_net_3_mst25_0','source_net_3_mst29_0','source_net_3_mst32_0']){
if(!inputErrors.some(e=>e.pcb_trace_id===traceId&&e.message?.includes('board edge')))continue
const tr=records.find(r=>r.type==='pcb_trace'&&r.pcb_trace_id===traceId)
if(!tr||!tr.source_trace_id)throw Error('Expected GND edge trace changed')
// This auxiliary GND link also has a continuous refilled ground plane.
// Use a 0.15 mm edge segment to retain clearance to the adjacent I2C vias.
for(const p of tr.route)if(p.route_type==='wire'&&p.y < -34)p.width=.15
for(const p of tr.route)if(p.y < -34.4){
if(p.start_pcb_port_id||p.end_pcb_port_id)throw Error('Cannot move a component terminal')
coordinateMoves.set(key(p),{x:p.x,y:-34.4})
}
}
const cornerId='source_net_3_mst16_0'
if(inputErrors.some(e=>e.pcb_trace_id===cornerId&&e.message?.includes('pcb_keepout'))){
const tr=records.find(r=>r.type==='pcb_trace'&&r.pcb_trace_id===cornerId)
for(const p of tr.route)if(p.x>-17.7&&p.x<-16&&p.y>-27.5&&p.y<-27.25){
if(p.start_pcb_port_id||p.end_pcb_port_id)throw Error('Cannot move a component terminal')
coordinateMoves.set(key(p),{x:p.x,y:-27.5})
}
}
for(const tr of records.filter(r=>r.type==='pcb_trace'))for(const p of tr.route){const n=coordinateMoves.get(key(p));if(n){p.x=n.x;p.y=n.y}}
for(const via of records.filter(r=>r.type==='pcb_via')){const n=coordinateMoves.get(key(via));if(n){via.x=n.x;via.y=n.y}}
if(coordinateMoves.size)moved.push({repair:'Move shared GND route vertices inward from USB board edge and carrier keepout',vertices:coordinateMoves.size})
}
if(revision==='0.5.7'){
// Move reference/board artwork clear of the new buck and output capacitors.
const labelPositions={'U2':[3,-26.8],'L1':[3,-18.5],'M U S E A I R':[-5,-14.1],'USB-C 5V':[14,-26]}
for(const text of records.filter(r=>r.type==='pcb_silkscreen_text')){
const p=labelPositions[text.text];if(!p)continue
text.anchor_position={x:p[0],y:p[1]};text.ccw_rotation=0
}
}
// Refill ground planes against the repaired geometry; old pours have stale voids.
const {CopperPourPipelineSolver,convertCircuitJsonToInputProblem,initializeManifoldGeometry}=await import('@tscircuit/copper-pour-solver')
const clean=records.filter(r=>!r.type.includes('error')&&r.type!=='pcb_copper_pour')
for(const tr of clean.filter(r=>r.type==='pcb_trace'))for(const p of tr.route){delete p.is_inside_copper_pour;delete p.copper_pour_id}
const board=clean.find(r=>r.type==='pcb_board')
const oldPour=records.find(r=>r.type==='pcb_copper_pour')
if(!board||!oldPour)throw Error('Build did not produce the required PCB board and ground-pour metadata')
const outline=board.outline?.length?board.outline:[{x:board.center.x-board.width/2,y:board.center.y-board.height/2},{x:board.center.x+board.width/2,y:board.center.y-board.height/2},{x:board.center.x+board.width/2,y:board.center.y+board.height/2},{x:board.center.x-board.width/2,y:board.center.y+board.height/2}]
const regions=['top','bottom'].map(layer=>({layer,source_net_id:oldPour.source_net_id,subcircuit_id:oldPour.subcircuit_id,pad_margin:.2,trace_margin:.2,pour_margin:.2,board_edge_margin:.4,cutout_margin:.2,outline}))
await initializeManifoldGeometry()
const {brep_shapes_by_region}=new CopperPourPipelineSolver(convertCircuitJsonToInputProblem(clean,regions)).getOutput()
let pourIndex=0
for(const [i,shapes] of brep_shapes_by_region.entries())for(const brep_shape of shapes)clean.push({type:'pcb_copper_pour',pcb_copper_pour_id:'pcb_copper_pour_'+pourIndex++,shape:'brep',layer:regions[i].layer,brep_shape,source_net_id:oldPour.source_net_id,subcircuit_id:oldPour.subcircuit_id,covered_with_solder_mask:oldPour.covered_with_solder_mask})
const findings=await runAllChecks(clean)
const errors=findings.filter(r=>r.type.includes('error'))
fs.writeFileSync(new URL('../validation/esp-buck-routing-repairs.json',import.meta.url),JSON.stringify({input_error_count:inputErrors.length,repairs:moved,findings,status:errors.length?'fail':'pass'},null,2)+'\n')
console.log('Routing repairs:',moved.length,'remaining errors:',errors.length)
if(errors.length){console.log(JSON.stringify(errors,null,2));process.exitCode=1}else{
fs.writeFileSync(file,JSON.stringify([...clean,...findings],null,2)+'\n')
const {convertCircuitJsonToPcbSvg}=await import('circuit-to-svg')
fs.writeFileSync(new URL('../dist/index/pcb.svg',import.meta.url),convertCircuitJsonToPcbSvg(clean))
}