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.
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/python3
|
||||
"""Read-only draft audit; needs pcbnew, numpy, shapely and kicad-cli.
|
||||
Writes fresh DRC and audit JSON reports, never modifies the board.
|
||||
|
||||
Independent continuous centreline/radius checks complement KiCad DRC. Restricted
|
||||
here to the existing circle/axis-aligned rectangular THT pads and straight tracks.
|
||||
"""
|
||||
from collections import Counter
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import pcbnew as p
|
||||
from shapely.geometry import Point, LineString, Polygon, box
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
spec = importlib.util.spec_from_file_location('router', ROOT / 'route_pcb_draft.py')
|
||||
r = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(r)
|
||||
board = p.LoadBoard(str(r.PCB))
|
||||
original = p.LoadBoard(str(ROOT / 'validation/pcb-routing-input.kicad_pcb'))
|
||||
assert r.identity(board) == r.identity(original), 'immutable source geometry/linkage changed'
|
||||
items, holes = [], []
|
||||
for f in board.GetFootprints():
|
||||
for pad in f.Pads():
|
||||
xy = r.pos(pad)
|
||||
assert pad.GetOrientationDegrees() % 90 == 0
|
||||
assert pad.GetShape() in (p.PAD_SHAPE_CIRCLE, p.PAD_SHAPE_RECT)
|
||||
if pad.GetShape() == p.PAD_SHAPE_CIRCLE:
|
||||
assert pad.GetSize().x == pad.GetSize().y
|
||||
geom, radius = Point(xy), p.ToMM(pad.GetSize().x) / 2
|
||||
else:
|
||||
bb = pad.GetBoundingBox()
|
||||
geom = box(p.ToMM(bb.GetX()), p.ToMM(bb.GetY()),
|
||||
p.ToMM(bb.GetRight()), p.ToMM(bb.GetBottom()))
|
||||
radius = 0
|
||||
label = f.GetReference() + '.' + pad.GetNumber() + ':' + pad.m_Uuid.AsString()
|
||||
layers = {l for l in r.LAYERS if pad.IsOnLayer(l)}
|
||||
items.append((label, pad.GetNetCode(), layers, geom, radius))
|
||||
if pad.GetDrillSize().x:
|
||||
assert pad.GetDrillSize().x == pad.GetDrillSize().y
|
||||
holes.append((label, Point(xy), p.ToMM(pad.GetDrillSize().x) / 2, False))
|
||||
track_lengths = Counter()
|
||||
widths = Counter()
|
||||
for t in board.GetTracks():
|
||||
is_via = isinstance(t, p.PCB_VIA)
|
||||
if is_via:
|
||||
geom, radius, layers = Point(r.pos(t)), p.ToMM(t.GetWidth(p.F_Cu)) / 2, set(r.LAYERS)
|
||||
assert abs(radius - 0.35) < 1e-8 and t.GetDrillValue() == r.mm(0.3)
|
||||
holes.append((t.m_Uuid.AsString(), geom, 0.15, True))
|
||||
else:
|
||||
geom = LineString([(p.ToMM(t.GetStart().x), p.ToMM(t.GetStart().y)),
|
||||
(p.ToMM(t.GetEnd().x), p.ToMM(t.GetEnd().y))])
|
||||
radius, layers = p.ToMM(t.GetWidth()) / 2, {t.GetLayer()}
|
||||
expected = 0.5 if t.GetNetname() in ('/+3V3', '/GND') else 0.25
|
||||
assert abs(2 * radius - expected) < 1e-8
|
||||
widths[str(2 * radius)] += 1
|
||||
track_lengths[t.GetNetname()] += geom.length
|
||||
items.append((t.m_Uuid.AsString(), t.GetNetCode(), layers, geom, radius))
|
||||
|
||||
min_clearance = float('inf')
|
||||
closest = None
|
||||
for i, a in enumerate(items):
|
||||
for b in items[i + 1:]:
|
||||
if not a[2].intersection(b[2]) or (a[1] and a[1] == b[1]):
|
||||
continue
|
||||
distance = a[3].distance(b[3]) - a[4] - b[4]
|
||||
if distance < min_clearance:
|
||||
min_clearance, closest = distance, (a[0], b[0])
|
||||
assert min_clearance >= 0.25 - 1e-6, (min_clearance, closest)
|
||||
outline = Polygon([(100,100),(182,100),(182,180),(136,180),(136,161),(100,161)])
|
||||
rf = box(110, 100, 131, 113)
|
||||
min_edge, min_rf = float('inf'), float('inf')
|
||||
for label, net, layers, geom, radius in items:
|
||||
assert outline.covers(geom), label
|
||||
edge_gap = outline.boundary.distance(geom) - radius
|
||||
rf_gap = rf.distance(geom) - radius
|
||||
min_edge, min_rf = min(min_edge, edge_gap), min(min_rf, rf_gap)
|
||||
assert edge_gap >= 0.5 - 1e-6, (label, edge_gap)
|
||||
assert rf_gap > 0, (label, rf_gap)
|
||||
min_via_hole_gap = float('inf')
|
||||
for i, a in enumerate(holes):
|
||||
for b in holes[i + 1:]:
|
||||
if not (a[3] or b[3]):
|
||||
continue
|
||||
gap = a[1].distance(b[1]) - a[2] - b[2]
|
||||
min_via_hole_gap = min(min_via_hole_gap, gap)
|
||||
assert gap >= 0.25 - 1e-6, (a[0], b[0], gap)
|
||||
|
||||
# Refusal paths must leave the routed board byte-for-byte unchanged.
|
||||
before = r.PCB.read_bytes()
|
||||
guards = []
|
||||
for args in (['--route'], ['--route', '--overwrite'], []):
|
||||
result = subprocess.run(['/usr/bin/python3', '-B', str(ROOT/'route_pcb_draft.py'), *args],
|
||||
capture_output=True, text=True, timeout=20)
|
||||
assert result.returncode == 2, (args, result.stdout, result.stderr)
|
||||
assert r.PCB.read_bytes() == before
|
||||
guards.append({'args': args, 'exit_code': result.returncode,
|
||||
'reason': result.stderr.splitlines()[-1]})
|
||||
subprocess.run(['kicad-cli', 'pcb', 'drc', '--format', 'json', '--all-track-errors',
|
||||
'--severity-all', '--exit-code-violations', '-o',
|
||||
str(ROOT/'validation/pcb-routing-drc.json'), str(r.PCB)],
|
||||
check=True, timeout=60)
|
||||
assert r.PCB.read_bytes() == before, 'board changed during audit'
|
||||
drc = json.loads(ROOT.joinpath('validation/pcb-routing-drc.json').read_text())
|
||||
assert not drc['violations'] and not drc['unconnected_items']
|
||||
report = dict(immutable_geometry_nets_pads_linkage_preserved=True,
|
||||
board_sha256=hashlib.sha256(before).hexdigest(),
|
||||
independent_min_different_net_or_npth_clearance_mm=min_clearance,
|
||||
closest_items=closest, min_copper_or_npth_edge_gap_mm=min_edge,
|
||||
min_rf_gap_mm=min_rf, min_via_to_other_drilled_hole_gap_mm=min_via_hole_gap,
|
||||
track_width_counts=widths, routed_length_mm_by_net=track_lengths,
|
||||
tracks=sum(not isinstance(t,p.PCB_VIA) for t in board.GetTracks()),
|
||||
vias=sum(isinstance(t,p.PCB_VIA) for t in board.GetTracks()),
|
||||
copper_zones=sum(not z.GetIsRuleArea() for z in board.Zones()),
|
||||
drc_freshly_run=True,
|
||||
drc_violations=len(drc['violations']), unconnected_items=len(drc['unconnected_items']),
|
||||
drc_ignored_checks=drc['ignored_checks'], overwrite_guard_tests=guards)
|
||||
ROOT.joinpath('validation/pcb-routing-check.json').write_text(json.dumps(report, indent=2)+'\n')
|
||||
print(json.dumps(report, indent=2))
|
||||
Reference in New Issue
Block a user