gokul/cm4-media-carrier

A tscircuit package created by gokul.

Version
0.1.0
License
unset
Stars
0

tools/native-audit.py

"""Run with KiCad's bundled Python; audits board geometry before library normalization."""
import json, math, sys
from pathlib import Path
import pcbnew
root=Path(sys.argv[1]).resolve()
j=json.loads((root/'circuit.json').read_text())
b=pcbnew.LoadBoard(str(root/'carrier.kicad_pcb'))
mm=pcbnew.ToMM
source={e['source_component_id']:e for e in j if e['type']=='source_component'}
components={e['pcb_component_id']:source[e['source_component_id']]['name'] for e in j if e['type']=='pcb_component' and e['source_component_id'] in source}
ports={e['pcb_port_id']:e for e in j if e['type']=='pcb_port'}
sports={e['source_port_id']:e for e in j if e['type']=='source_port'}
nets={e.get('subcircuit_connectivity_map_key'):e['name'] for e in j if e['type']=='source_net'}
actual=[]
for fp in b.GetFootprints():
 for p in fp.Pads():
  bb=p.GetBoundingBox(); c=bb.GetCenter()
  actual.append(dict(ref=fp.GetReference(),pin=p.GetNumber(),x=mm(c.x)-100,y=100-mm(c.y),width=mm(bb.GetWidth()),height=mm(bb.GetHeight()),drill_x=mm(p.GetDrillSize().x),drill_y=mm(p.GetDrillSize().y),angle=p.GetOrientationDegrees(),net=p.GetNetname()))
errors=[]; checked=0
for e in j:
 if e['type'] not in ['pcb_smtpad','pcb_plated_hole']: continue
 ref=components[e['pcb_component_id']]; sp=sports[ports[e['pcb_port_id']]['source_port_id']]
 if e['shape']=='polygon':
  xs=[p['x'] for p in e['points']];ys=[p['y'] for p in e['points']]
  x=(max(xs)+min(xs))/2;y=(max(ys)+min(ys))/2;w=max(xs)-min(xs);h=max(ys)-min(ys)
 else:
  x=e['x'];y=e['y'];w=e.get('width',e.get('rect_pad_width',e.get('outer_width',e.get('outer_diameter',e.get('radius',0)*2))))
  h=e.get('height',e.get('rect_pad_height',e.get('outer_height',e.get('outer_diameter',e.get('radius',0)*2))))
  angle=math.radians(e.get('ccw_rotation',e.get('rect_ccw_rotation',0)))
  w,h=abs(w*math.cos(angle))+abs(h*math.sin(angle)),abs(w*math.sin(angle))+abs(h*math.cos(angle))
 matches=[p for p in actual if p['ref']==ref and abs(p['x']-x)<.00001 and abs(p['y']-y)<.00001]
 if len(matches)!=1: errors.append(dict(ref=ref,reason='missing/ambiguous pad',expected=[x,y,w,h],matches=matches));continue
 p=matches[0];checked+=1
 if max(abs(p['width']-w),abs(p['height']-h))>.00001: errors.append(dict(ref=ref,reason='pad dimensions',expected=[w,h],actual=p))
 expected_pin=str(sp.get('pin_number',sp['name']))
 if p['pin']!=expected_pin: errors.append(dict(ref=ref,reason='pin number',expected=expected_pin,actual=p))
 expected_net=nets.get(sp.get('subcircuit_connectivity_map_key'))
 if expected_net is not None and p['net']!=expected_net: errors.append(dict(ref=ref,reason='named net',expected=expected_net,actual=p))
 if e['type']=='pcb_plated_hole':
  expected=sorted([e.get('hole_width',e.get('hole_diameter')),e.get('hole_height',e.get('hole_diameter'))])
  if max(abs(a-c) for a,c in zip(expected,sorted([p['drill_x'],p['drill_y']])))>.00001: errors.append(dict(ref=ref,reason='drill dimensions',expected=expected,actual=p))
# Normalize rotated library footprints from the independently checked native board.
# Board file is not resaved or altered. This corrects exporter library-local rotation only.
writer=pcbnew.PCB_IO_KICAD_SEXPR(); normalized=[]
for fp in b.GetFootprints():
 if abs(fp.GetOrientationDegrees())>.000001:
  normalized.append(fp.GetReference());fp.SetOrientationDegrees(0);fp.SetPosition(pcbnew.VECTOR2I(0,0))
  writer.FootprintSave(str(root/'tscircuit.pretty'),fp)
(root/'native-pad-geometry.json').write_text(json.dumps(actual,indent=2))
report=dict(checked=checked,expected=285,mismatches=errors,normalized_library_references=normalized,board_file_unchanged=True,passes=checked==285 and not errors)
(root/'native-pad-audit.json').write_text(json.dumps(report,indent=2)); print(json.dumps(report,indent=2))
assert report['passes']