#!/usr/bin/env python3 """Read-only PCB checks and fresh KiCad reports; does not regenerate or save PCB. Requires KiCad's pcbnew binding in system Python. The known U2 footprint parity warning is reported explicitly, not hidden or added to KiCad's exclusions. """ import hashlib import json from pathlib import Path import subprocess import xml.etree.ElementTree as ET import pcbnew as p ROOT=Path(__file__).resolve().parent PCB=ROOT/"serial-carrier.kicad_pcb" OUT=ROOT/"validation" def run(*args): subprocess.run(args,check=True,cwd=ROOT) run("kicad-cli","sch","export","netlist","--format","kicadxml","-o",str(OUT/"pcb-source-netlist.xml"),str(ROOT/"serial-carrier.kicad_sch")) run("kicad-cli","pcb","drc","--format","json","--all-track-errors","--severity-all","--schematic-parity","-o",str(OUT/"pcb-parity-drc.json"),str(PCB)) board=p.LoadBoard(str(PCB)) source=ET.parse(OUT/"pcb-source-netlist.xml").getroot() comps={c.get("ref"):c for c in source.findall("components/comp")} footprints={f.GetReference():f for f in board.GetFootprints()} assert set(footprints)==set(comps)|{"H1","H2","H3","H4"} assert board.GetCopperLayerCount()==2 assert p.ToMM(board.GetDesignSettings().GetBoardThickness())==1.6 outline=[d for d in board.GetDrawings() if d.GetLayer()==p.Edge_Cuts] assert len(outline)==6 points={(round(p.ToMM(v.x)-100,5),round(p.ToMM(v.y)-100,5)) for d in outline for v in (d.GetStart(),d.GetEnd())} expected_outline=[(36,0),(82,0),(82,80),(0,80),(0,19),(36,19)] assert points==set(expected_outline) expected_edges={frozenset((a,b)) for a,b in zip(expected_outline,expected_outline[1:]+expected_outline[:1])} actual_edges={frozenset((round(p.ToMM(v.x)-100,5),round(p.ToMM(v.y)-100,5)) for v in (d.GetStart(),d.GetEnd())) for d in outline} assert actual_edges==expected_edges placements={"U1":(8,23),"U2":(43,26.670),"DS1":(43,36), "R1":(43,34),"R2":(54,34),"R3":(65,34), "SW1":(43,68),"SW2":(55,68),"SW3":(67,68), "H1":(3.5,22.5),"H2":(3.5,75),"H3":(78,4),"H4":(78,76)} for ref,(x,y) in placements.items(): position=footprints[ref].GetPosition() assert position.x==p.FromMM(x+100) and position.y==p.FromMM(y+100),ref assert footprints[ref].GetOrientationDegrees()==0,ref assert len(board.Zones())==1 reserve=board.Zones()[0] assert reserve.GetIsRuleArea() and reserve.IsOnLayer(p.F_Cu) and reserve.IsOnLayer(p.B_Cu) assert reserve.GetDoNotAllowTracks() and reserve.GetDoNotAllowVias() and reserve.GetDoNotAllowZoneFills() contour=reserve.Outline().COutline(0) assert {(p.ToMM(contour.CPoint(i).x),p.ToMM(contour.CPoint(i).y)) for i in range(contour.PointCount())}=={(110,119),(131,119),(131,132),(110,132)} # Reference bare-PCB front is 26.670 mm above the U2 pad-row origin. u2_front=p.ToMM(footprints['U2'].GetPosition().y)-26.670 assert abs(u2_front-100)<1e-6 assert any(isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab and g.GetShape()==p.SHAPE_T_SEGMENT and g.GetStart().y==p.FromMM(100) and g.GetEnd().y==p.FromMM(100) for g in footprints['U2'].GraphicalItems()), 'U2 bare-PCB front not flush' assert max(x for x,y in points)-min(x for x,y in points)==82 assert max(y for x,y in points)-min(y for x,y in points)==80 padnets={} for net in source.findall("nets/net"): for node in net.findall("node"): padnets[(node.get("ref"),node.get("pin"))]=net.get("name") seen=set() for ref,comp in comps.items(): fp=footprints[ref] assert fp.GetValue()==comp.findtext("value"),ref assert fp.GetPath().AsString().endswith("/"+comp.findtext("tstamps")),ref wanted=comp.findtext("footprint") if ref=="U2": assert not wanted assert fp.GetFPIDAsString()=="Carrier:MAX3243_Reference_Provisional" else: assert fp.GetFPIDAsString()==wanted,ref for pad in fp.Pads(): if not pad.GetNumber(): continue key=ref,pad.GetNumber() assert pad.GetNetname()==padnets[key],(key,pad.GetNetname(),padnets[key]) seen.add(key) assert seen==set(padnets),"Missing or extra electrical pads" for ref in ("H1","H2","H3","H4"): assert footprints[ref].IsBoardOnly() pads=list(footprints[ref].Pads()) assert len(pads)==1 and pads[0].GetDrillSize().x==p.FromMM(3.2) assert pads[0].GetAttribute()==p.PAD_ATTRIB_NPTH tracks=list(board.GetTracks()) assert tracks,"Draft unexpectedly unrouted" for track in tracks: if isinstance(track,p.PCB_VIA): assert track.GetDrillValue()==p.FromMM(0.3) assert track.GetWidth(p.F_Cu)==p.FromMM(0.7) else: assert track.GetLayer() in (p.F_Cu,p.B_Cu) expected=0.5 if track.GetNetname() in ("/+3V3","/GND") else 0.25 assert track.GetWidth()==p.FromMM(expected) report=json.loads((OUT/"pcb-parity-drc.json").read_text()) assert not report["violations"],report["violations"] assert not report["unconnected_items"],report["unconnected_items"] parity=report["schematic_parity"] assert len(parity)==1 and parity[0]["type"]=="footprint_symbol_mismatch",parity assert len(parity[0]["items"])==1 and parity[0]["items"][0]["uuid"]==footprints["U2"].m_Uuid.AsString() summary={ "carrier_bounds_mm":[82,80], "outline_local_mm":expected_outline, "placements_local_mm":placements, "rf_reserve_absolute_mm":[110,119,131,132], "u2_bare_pcb_front_local_y_mm":round(u2_front-100,6), "nominal_user_confirmed_rs232_metal_mm":{"shell_overhang":3.870,"hex_mouth_overhang":2.870,"flange_front_inset":1.930}, "tracks":sum(not isinstance(t,p.PCB_VIA) for t in tracks), "vias":sum(isinstance(t,p.PCB_VIA) for t in tracks), "copper_layers":2, "draft_thickness_mm":1.6, "electrical_footprints":len(comps), "proposed_M3_mounts":4, "physical_DRC_violations":0, "unconnected_items":0, "schematic_pad_nets_match":True, "known_parity_warning":"U2 reference footprint assigned on PCB only; schematic footprint intentionally blank; nominal male external metal dimensions user-confirmed", "project_settings_sha256":hashlib.sha256((ROOT/'serial-carrier.kicad_pro').read_bytes()).hexdigest(), "ignored_DRC_checks":report["ignored_checks"], "not_validated":["male module pin map/header fit", "ESP32 geometry and complete assembly envelope/heights", "header/socket fit", "RF performance and antenna clearance", "power/return-path integrity", "USB power path", "OLED pull-ups", "fabrication process", "hardware operation"], } (OUT/"pcb-validation-summary.json").write_text(json.dumps(summary,indent=2)+"\n") print("PCB draft verified: 82 x 80 mm; 2 copper layers; 0 physical DRC violations; 0 unrouted.") print("All schematic pad nets match. One expected U2 footprint parity warning remains visible.") print("Not fabrication-ready; see pcb-draft-notes.md and ignored checks in the DRC report.")