hrithik18k/punck-tscircuit-recreation

A four-layer Eurorack drum/sampler controller PCB built around an STM32H743, dual QSPI flash, PCM5100 stereo DAC, TL074 analog audio circuitry, input clamps, 12V-to-3.3V power regulation, MIDI/USB/SWD connectors, controls, LEDs, and trigger/audio I/O.

Version
1.0.3
License
unset
Stars
0

scripts/verify.ts

import fs from 'node:fs'
import {parse,children,child,atom} from './sexpr'
import {any_circuit_element} from 'circuit-json'
const report:any[]=[]
const round=(n:number)=>Math.round(n*1e5)/1e5
const pt=(x:number,y:number)=>`${round(x)},${round(y)}`
const edge=(a:string,b:string,w:number,layer:string,net:string)=>`${[a,b].sort().join('|')}|${round(w)}|${layer}|${net}`
const layer=(s:string)=>s==='F.Cu'?'top':s==='B.Cu'?'bottom':s.replace(/In(\d+)\.Cu/,'inner$1')
const netName=(s:string)=>{const n=s.trim().replace(/\+/g,'_P').replace(/-/g,'_').replace(/[^A-Za-z0-9_]/g,'_').replace(/_+/g,'_').replace(/^_+|_+$/g,'');return /^\d/.test(n)?`net_${n}`:n}
for(const name of ['components','controls','panel']) {
 const title=name[0].toUpperCase()+name.slice(1)
 const src=parse(fs.readFileSync(`source/Punck_${title}.kicad_pcb`,'utf8'))
 const json=JSON.parse(fs.readFileSync(`prepared/${name}.circuit.json`,'utf8'))
 const source=json.filter((e:any)=>e.type==='source_component')
 const pcs=json.filter((e:any)=>e.type==='pcb_component')
 const first=children(src,'footprint')[0]
 const ref=atom(children(first,'property').find(p=>atom(p[1])==='Reference')?.[2])
 const sc=source[0], pc=pcs.find((p:any)=>p.source_component_id===sc.source_component_id)
 const at=child(first,'at')!
 const cx=Number(at[1])-pc.center.x,cy=Number(at[2])+pc.center.y
 const nets=Object.fromEntries(children(src,'net').map(n=>[n[1],netName(atom(n[2]))]))
 const original=children(src,'segment').map(s=>{const a=child(s,'start')!,b=child(s,'end')!;return edge(pt(Number(a[1])-cx,cy-Number(a[2])),pt(Number(b[1])-cx,cy-Number(b[2])),Number(child(s,'width')![1]),layer(atom(child(s,'layer')![1])),nets[child(s,'net')![1] as string])})
 const converted:string[]=[]
 for(const t of json.filter((e:any)=>e.type==='pcb_trace')) {
 const st=json.find((e:any)=>e.type==='source_trace'&&e.source_trace_id===t.source_trace_id)
 const net=json.find((e:any)=>e.type==='source_net'&&e.source_net_id===st?.connected_source_net_ids?.[0])?.name??''
 for(let i=1;i<t.route.length;i++){const a=t.route[i-1],b=t.route[i];if(a.route_type==='wire'&&b.route_type==='wire'&&a.layer===b.layer) converted.push(edge(pt(a.x,a.y),pt(b.x,b.y),a.width,a.layer,net))}
 }
 const counts=(a:string[])=>{const m=new Map<string,number>();for(const x of a)m.set(x,(m.get(x)??0)+1);return m}
 const a=counts(original),b=counts(converted)
 const mismatch=[...new Set([...a.keys(),...b.keys()])].filter(k=>a.get(k)!==b.get(k))
 const positions=children(src,'footprint').filter((f,i)=>{const s=source[i],p=pcs.find((e:any)=>e.source_component_id===s?.source_component_id),a=child(f,'at')!;return !p||Math.abs(p.center.x-(Number(a[1])-cx))>1e-5||Math.abs(p.center.y-(cy-Number(a[2])))>1e-5})
 const invalid=json.flatMap((e:any,i:number)=>{const p=any_circuit_element.safeParse(e);return p.success?[]:[{i,type:e.type}]})
 report.push({name,footprints:pcs.length,componentPositionMismatches:positions.length,sourceSegments:original.length,convertedSegments:converted.length,segmentGeometryWidthLayerNetMismatches:mismatch.length,mismatchExamples:mismatch.slice(0,3),schemaInvalid:invalid})
}
fs.writeFileSync('evidence/verification.json',JSON.stringify(report,null,2))
console.log(report)
if(report.some(r=>r.componentPositionMismatches||r.segmentGeometryWidthLayerNetMismatches||r.schemaInvalid.length)) process.exitCode=1