Integrate Measured ESP32 PCB Geometry
- Add measured F.Fab bounds and USB mouth markers - Add guarded preservation and regression validation - Refresh review exports and remove draft presentation labels
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
#!/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.
|
||||
assert canonical(expected_tree(parse(BASE)), silk_ids) == canonical(parse(PCB), silk_ids | u1_ids), "Change outside owned U1 additions / device silk / exact presentation labels"
|
||||
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,
|
||||
existing_uuids_pads_nets_placement_routing_settings_and_unrelated_graphics_preserved=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-update.json").write_text(json.dumps(summary,indent=2)+"\n")
|
||||
print(json.dumps(summary,indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user