Align component groups, repair affected routing, and add preservation audits. Assign the confirmed U2 footprint and update pull-ups to 2 kΩ across design files. Document user acceptance, switch to an A3 worksheet, and refresh PCB exports.
113 lines
5.2 KiB
Python
113 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Measured U1 integration regression and exact pre-update preservation audit."""
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import tempfile
|
|
import uuid
|
|
import pcbnew as p
|
|
from shapely.geometry import Polygon
|
|
from add_device_silkscreen import add_outlines, NS as SILK_NS, xy, outline_lines
|
|
from update_u1_graphics import verify, NS as U1_NS
|
|
from remove_draft_markers import expected_tree
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
PCB = ROOT / "serial-carrier.kicad_pcb"
|
|
BASE = ROOT / "validation/pcb-before-u1-measured.kicad_pcb"
|
|
|
|
|
|
def parse(path):
|
|
tokens = iter(re.findall(r'"(?:\\.|[^"\\])*"|[()]|[^\s()]+', path.read_text()))
|
|
def group():
|
|
result = []
|
|
for token in tokens:
|
|
if token == ')':
|
|
return result
|
|
result.append(group() if token == '(' else token)
|
|
raise ValueError("Unterminated S-expression")
|
|
assert next(tokens) == '('
|
|
return group()
|
|
|
|
|
|
def ident(item):
|
|
return next((v[1].strip('"') for v in item if isinstance(v,list) and v[0]=='uuid'), None)
|
|
|
|
|
|
def canonical(item, excluded=frozenset()):
|
|
# KiCad can reorder independent board items when saving; compare their full
|
|
# parsed contents, not byte order. Pad coordinates/netnames/UUIDs remain exact.
|
|
if not isinstance(item, list):
|
|
return item
|
|
values = [canonical(v, excluded) for v in item
|
|
if not isinstance(v,list) or ident(v) not in excluded]
|
|
if item and item[0] in ("kicad_pcb", "footprint"):
|
|
return ([v for v in values if not isinstance(v,list)] +
|
|
sorted((v for v in values if isinstance(v,list)), key=repr))
|
|
# Coordinates, polygon vertex order, text and other ordered values must
|
|
# remain ordered: sorting these could conceal an X/Y swap or zone edit.
|
|
return values
|
|
|
|
|
|
def main():
|
|
assert hashlib.sha256(BASE.read_bytes()).hexdigest() == "53e36538c0669604457b9e1a9ff1b1e20422970e8f9f356088c4581868ad5045", "Pre-update snapshot changed"
|
|
assert hashlib.sha256((ROOT/"serial-carrier.kicad_pro").read_bytes()).hexdigest() == "9ad5b992785ebfe01fa7ea4e1aa391a2c7d7f1f2c3b1c13136173c301ecb6516", "Project settings changed"
|
|
before = PCB.read_bytes()
|
|
board = p.LoadBoard(str(PCB))
|
|
verify(board)
|
|
silk_ids = {str(uuid.uuid5(SILK_NS,str(i))) for i in range(4096)}
|
|
u1_ids = {str(uuid.uuid5(U1_NS,str(i))) for i in range(6)}
|
|
# The original baseline retains historical draft labels. Permit only the
|
|
# exact requested title/revision/comment edits and removal of the silk label.
|
|
# Keep proving the original integration against its original evidence, then
|
|
# audit the explicitly authorized subsequent placement/routing delta.
|
|
from validate_group_alignment import audit, BASE as GROUP_BASE
|
|
assert canonical(expected_tree(parse(BASE)), silk_ids) == canonical(parse(GROUP_BASE), silk_ids | u1_ids), "Historical U1 integration changed"
|
|
audit()
|
|
generated = [g for g in board.GetDrawings() if g.m_Uuid.AsString() in silk_ids]
|
|
assert not any(isinstance(g,p.PCB_TEXT) and g.GetText()=="HEADER GUIDES" for g in board.GetDrawings())
|
|
carrier = Polygon([(136,100),(182,100),(182,180),(100,180),(100,119),(136,119)])
|
|
mouths = []
|
|
for g in generated:
|
|
assert g.GetLayer() == p.F_SilkS
|
|
for edge in outline_lines(g):
|
|
assert carrier.covers(edge.buffer(p.ToMM(g.GetWidth())/2)), "Silk outside carrier/notch"
|
|
if g.GetWidth() == p.FromMM(0.3):
|
|
mouths.append(g)
|
|
assert len(mouths) == 2
|
|
assert sorted((xy(g.GetStart()),xy(g.GetEnd())) for g in mouths) == [
|
|
((110.795,177.705),(119.665,177.705)),
|
|
((122.195,177.705),(131.065,177.705))]
|
|
# A second refresh must reproduce all drawing geometry and UUIDs.
|
|
add_outlines(board)
|
|
with tempfile.TemporaryDirectory(prefix="u1-regression-") as temporary:
|
|
result = Path(temporary)/PCB.name
|
|
assert p.SaveBoard(str(result),board,True)
|
|
assert canonical(parse(result)) == canonical(parse(PCB)), "Silkscreen refresh not idempotent"
|
|
u1 = next(f for f in board.GetFootprints() if f.GetReference()=="U1")
|
|
shape = next(g for g in u1.GraphicalItems() if isinstance(g,p.PCB_SHAPE))
|
|
shape.SetWidth(shape.GetWidth()+1)
|
|
try:
|
|
verify(board)
|
|
except AssertionError:
|
|
pass
|
|
else:
|
|
raise AssertionError("Stale embedded geometry was not rejected")
|
|
assert PCB.read_bytes() == before
|
|
assert not (ROOT/"~serial-carrier.kicad_pcb.lck").exists()
|
|
summary = dict(measured_library_matches_embedded_u1=True,
|
|
historical_u1_integration_and_subsequent_authorized_group_delta_verified=True,
|
|
presentation_label_cleanup=True,
|
|
silk_refresh_idempotent=True, stale_geometry_negative_control=True,
|
|
silk_clipped_to_carrier=True, usb_mouth_markers=2,
|
|
generated_silk_items=len(generated),
|
|
baseline_sha256=hashlib.sha256(BASE.read_bytes()).hexdigest(),
|
|
board_sha256=hashlib.sha256(before).hexdigest(),
|
|
project_sha256=hashlib.sha256((ROOT/"serial-carrier.kicad_pro").read_bytes()).hexdigest())
|
|
(ROOT/"validation/pcb-u1-group-regression.json").write_text(json.dumps(summary,indent=2)+"\n")
|
|
print(json.dumps(summary,indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|