Files
ESP32_Serial_Swiss_Army_Knife/hardware/PCB/validate_pcb_draft.py
T
Commander1024 60c1e279d6 Add KiCad module carrier PCB draft
Introduce an editable KiCad 10 schematic and routed two-layer 82 × 80 mm
layout with local provisional footprints, validation tooling, and
component
research. Keep the RS-232 male-module mapping and mechanical clearances
explicitly provisional pending hardware verification.
2026-09-20 23:14:01 +02:00

95 lines
4.4 KiB
Python

#!/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 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())}
assert points=={(0,0),(82,0),(82,80),(36,80),(36,61),(0,61)}
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],
"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 provisional female-reference footprint assigned on PCB only; schematic footprint intentionally blank",
"ignored_DRC_checks":report["ignored_checks"],
"not_validated":["male module pin map/fit", "assembly envelope and 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.")