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-A86.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())
for ref in old:
 if ref not in ['C7','C8']:assert padgeom(old[ref])==padgeom(new[ref]),ref
 else:
  assert new[ref].GetPosition()==old[ref].GetPosition()
  assert {p.GetNumber():p.GetNetname() for p in new[ref].Pads()}=={'1':'/VM','2':'/GND'}
  for p in new[ref].Pads():assert p.GetSize()==k.VECTOR2I(k.FromMM(1.1),k.FromMM(2.2))
  centers=sorted(k.ToMM(p.GetPosition().x-new[ref].GetPosition().x) for p in new[ref].Pads());assert centers==[-1.65,1.65]
  assert str(new[ref].GetFPID().GetLibItemName())=='TDK_C3225X7R1H226M250AC_1210_Land'
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-A86.kicad_pro').read_bytes()==(P/'drill_motor_a85_cutoff.kicad_pro').read_bytes()
root=E.parse(P/'netlist-A90.xml').getroot();base=E.parse(P/'baseline-netlist-A86.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-A90.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-A89.csv').open(encoding='utf-8-sig')))
for row in rows:
 if row['Ref'] in ['C7','C8']:
  row['PCBFootprint']='MotorStage:TDK_C3225X7R1H226M250AC_1210_Land';row['SelectionStatus']='A90_LOCAL_RECOMMENDED_LAND_GEOMETRY_MATCH_FACTORY_DFM_OPEN';row['Qualification']='Nominal footprint geometry corrected; factory stencil/assembly, effective capacitance, thermal/ripple, price/stock and actual motor validation open'
with (P/'motor-A90-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':'A90_NATIVE_CAP_FOOTPRINT_LOCAL_PASS_NOT_FABRICATION_RELEASE','changed_footprints':['C7','C8'],'recommended_land_mm':{'inner_gap':2.2,'pad_length':1.1,'pad_width':2.2},'identical_track_via_objects':len(list(c.GetTracks())),'unchanged_other_footprints':len(new)-2,'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-A90.xml']}}
(P/'board-verification-A90.json').write_text(json.dumps(result,indent=2)+'\n');print(json.dumps(result))
