from pathlib import Path
from collections import Counter
import pcbnew as k,wx,json,hashlib,csv,xml.etree.ElementTree as E
P=Path(__file__).resolve().parent;app=wx.App(False);quiet=wx.LogNull()
b=k.LoadBoard(str(P/'baseline-A90.kicad_pcb'));c=k.LoadBoard(str(P/'drill_motor_a85_cutoff.kicad_pcb'));old={f.GetReference():f for f in b.GetFootprints()};new={f.GetReference():f for f in c.GetFootprints()};assert old.keys()==new.keys()
def geom(t):
 if isinstance(t,k.PCB_VIA):return('via',t.GetNetname(),t.GetPosition().x,t.GetPosition().y,t.GetWidth(k.F_Cu),t.GetDrillValue())
 return('track',t.GetNetname(),*sorted([(t.GetStart().x,t.GetStart().y),(t.GetEnd().x,t.GetEnd().y)]),t.GetWidth(),t.GetLayer())
assert Counter(geom(t) for t in b.GetTracks())==Counter(geom(t) for t in c.GetTracks())
def padgeom(f):return sorted((p.GetNumber(),p.GetPosition().x,p.GetPosition().y,p.GetSize().x,p.GetSize().y,int(p.GetShape()),p.GetDrillSize().x,p.GetDrillSize().y,p.GetNetname()) for p in f.Pads())
headers=['J1','J2','J3','J4','J5'];changed=0
for ref in old:
 if ref not in headers:assert padgeom(old[ref])==padgeom(new[ref]),ref
 else:
  before={p.GetNumber():p for p in old[ref].Pads()}
  for pad in new[ref].Pads():
   prior=before[pad.GetNumber()]
   assert pad.GetPosition()==prior.GetPosition() and pad.GetSize()==prior.GetSize() and pad.GetNetname()==prior.GetNetname() and pad.GetShape()==prior.GetShape()
   assert prior.GetDrillSize()==k.VECTOR2I(k.FromMM(1),k.FromMM(1))
   assert pad.GetDrillSize()==k.VECTOR2I(k.FromMM(1.2),k.FromMM(1.2));changed+=1
  assert str(new[ref].GetFPID().GetLibItemName()).startswith('WE_')
  lib=k.FootprintLoad(str(P/'MotorStage.pretty'),str(new[ref].GetFPID().GetLibItemName()));assert lib
  assert all(p.GetDrillSize()==k.VECTOR2I(k.FromMM(1.2),k.FromMM(1.2)) for p in lib.Pads())
assert changed==29
import math
pin_max_diag=math.sqrt(2)*(0.64+0.15);min_hole=1.2-0.03;max_hole=1.2+0.03
assert min_hole-pin_max_diag>0.05 and 0.95<=min_hole<=max_hole<=1.25
min_annulus=min((min(k.ToMM(p.GetSize().x),k.ToMM(p.GetSize().y))-max_hole)/2 for ref in headers for p in new[ref].Pads());assert min_annulus>=0.2
def outline(board):return sorted((int(d.GetShape()),d.GetStart().x,d.GetStart().y,d.GetEnd().x,d.GetEnd().y,d.GetWidth()) for d in board.GetDrawings() if d.GetLayer()==k.Edge_Cuts)
assert outline(b)==outline(c)
assert (P/'baseline-A90.kicad_pro').read_bytes()==(P/'drill_motor_a85_cutoff.kicad_pro').read_bytes()
root=E.parse(P/'netlist-A92.xml').getroot();base=E.parse(P/'baseline-netlist-A90.xml').getroot()
def nets(root):return {n.get('name'):{(x.get('ref'),x.get('pin')) for x in n.findall('node')} for n in root.findall('nets/net')}
assert nets(root)==nets(base)
expected={(x.get('ref'),x.get('pin')):n.get('name') for n in root.findall('nets/net') for x in n.findall('node')}
actual={(f.GetReference(),p.GetNumber()):p.GetNetname() for f in c.GetFootprints() for p in f.Pads() if f.GetReference() not in ['MH1','MH2','MH3','MH4']};assert actual==expected
lib=k.FootprintLoad(str(P/'MotorStage.pretty'),'TDK_C3225X7R1H226M250AC_1210_Land');assert lib
for p in lib.Pads():assert p.GetSize()==k.VECTOR2I(k.FromMM(1.1),k.FromMM(2.2))
r=json.loads((P/'DRC-A92.json').read_text());assert not r['violations'] and not r['unconnected_items'];assert len(r['schematic_parity'])==4 and all(x['type']=='extra_footprint' for x in r['schematic_parity'])
rows=list(csv.DictReader((P/'baseline-BOM-A91.csv').open(encoding='utf-8-sig')))
for row in rows:
 if row['Ref'] in headers:
  row['PCBFootprint']='MotorStage:'+str(new[row['Ref']].GetFPID().GetLibItemName());row['SelectionStatus']='A92_LOCAL_HOLE_DIAMETER_MARGIN_FACTORY_POSITION_MATING_HOLD';row['Qualification']='Nominal hole1.2mm; target finished1.20+/-0.03mm not factory accepted. Pin-position stack/mating/wire/polarity/current/thermal/stock/price still open'
with (P/'motor-A92-candidate-BOM.csv').open('w',encoding='utf-8-sig',newline='') as f:w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
result={'status':'A92_NATIVE_HEADER_HOLE_LOCAL_PASS_NOT_FABRICATION_RELEASE','changed_footprints':headers,'changed_holes':changed,'target_finished_hole_mm':1.2,'target_finished_hole_tolerance_mm':0.03,'target_min_diameter_margin_mm':min_hole-pin_max_diag,'target_min_annular_ring_mm':min_annulus,'factory_hole_tolerance_accepted':False,'position_tolerance_stack_closed':False,'identical_track_via_objects':len(list(c.GetTracks())),'unchanged_other_footprints':len(new)-5,'unchanged_outline':True,'unchanged_native_networks':len(nets(root)),'checked_electrical_pads':len(actual),'unchanged_project_rules':True,'DRC_violations':0,'unconnected_items':0,'preexisting_mounting_hole_parity':4,'manufacturing_release':False,'motor_run_permission':False,'physical_validation':False,'source_sha256':{x:hashlib.sha256((P/x).read_bytes()).hexdigest() for x in ['drill_motor_a85_cutoff.kicad_pcb','drill_motor_a85_cutoff.kicad_sch','netlist-A92.xml']}}
(P/'board-verification-A92.json').write_text(json.dumps(result,indent=2)+'\n');print(json.dumps(result))
