Reposition the ESP32 and RS-232 reference within the notched outline, preserve the A0 snapshot, and add second-pass two-layer routing evidence. Refresh validation reports and draft previews while keeping fabrication status provisional.
100 lines
4.6 KiB
Python
100 lines
4.6 KiB
Python
#!/usr/bin/python3
|
|
"""Compare A1 against the untouched A0 snapshot, including unrelated graphics."""
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import uuid
|
|
|
|
ROOT=Path(__file__).resolve().parent.parent
|
|
OUT=ROOT/'validation'
|
|
|
|
|
|
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 child(item,key):
|
|
return next(v for v in item if isinstance(v,list) and v[0]==key)
|
|
|
|
|
|
def ident(item):
|
|
return child(item,'uuid')[1].strip('"')
|
|
|
|
|
|
def sha(path):
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
report=json.loads(OUT.joinpath('pcb-routing-reposition.json').read_text())
|
|
a0=OUT/'pcb-a0-before-reposition.kicad_pcb'
|
|
a1=ROOT/'serial-carrier.kicad_pcb'
|
|
assert sha(a0)==report['a0_snapshot_sha256'], 'A0 snapshot changed'
|
|
assert sha(OUT/'pcb-routing-input.kicad_pcb')==report['a1_unrouted_sha256'], 'A1 routing input changed'
|
|
# These two handoff documents are intentionally updated during A1 integration.
|
|
protected={name:digest for name,digest in report['protected_file_sha256'].items()
|
|
if name not in ('README.md','pcb-draft-notes.md')}
|
|
for name,digest in protected.items():
|
|
assert sha(ROOT/name)==digest, 'protected file changed: '+name
|
|
old,new=parse(a0),parse(a1)
|
|
old_fp={ident(x):x for x in old if isinstance(x,list) and x[0]=='footprint'}
|
|
new_fp={ident(x):x for x in new if isinstance(x,list) and x[0]=='footprint'}
|
|
assert set(old_fp)==set(new_fp)
|
|
for key,fp in old_fp.items():
|
|
# Footprint geometry, fields, UUIDs and connectivity must be identical;
|
|
# only the top-level placement may differ, and only for the four moves.
|
|
ref=next(v[2].strip('"') for v in fp if isinstance(v,list) and v[:2]==['property','"Reference"'])
|
|
cleaned=lambda f:[v for v in f if not isinstance(v,list) or v[0]!='at']
|
|
assert cleaned(fp)==cleaned(new_fp[key]), 'footprint content changed: '+ref
|
|
if ref not in report['moves_local_mm']:
|
|
assert child(fp,'at')==child(new_fp[key],'at'),ref
|
|
else:
|
|
expected=report['moves_local_mm'][ref][1]
|
|
assert [round(float(v)-100,6) for v in child(new_fp[key],'at')[1:3]]==expected,ref
|
|
ns=uuid.UUID('67964aa4-c3a5-45d8-9b54-250fb68e7c41')
|
|
owned={str(uuid.uuid5(ns,str(i))) for i in range(4096)}
|
|
cad_ns=uuid.UUID('ba7274a8-c35d-46bc-908d-cd86708fc6c4')
|
|
cad_owned={str(uuid.uuid5(cad_ns,str(i))) for i in range(7)}
|
|
changed_texts=set(report['changed_board_text_uuids'])
|
|
for item in old:
|
|
if isinstance(item,list) and item[0]=='gr_text':
|
|
value=json.loads(item[1])
|
|
if value=='82 x 80 mm / 2 copper layers' or value.startswith('DE-9 OUT /'):
|
|
changed_texts.add(ident(item))
|
|
|
|
def drawings(tree):
|
|
return {ident(x):x for x in tree if isinstance(x,list) and
|
|
(x[0].startswith('gr_') or x[0] in ('dimension','target','image'))}
|
|
|
|
old_gr,new_gr=drawings(old),drawings(new)
|
|
edge_ids={key for key,value in old_gr.items() if child(value,'layer')[1]=='"Edge.Cuts"'}
|
|
assert cad_owned <= set(new_gr), 'missing CAD projection graphics'
|
|
assert all(child(new_gr[key],'layer')[1]=='"Dwgs.User"' for key in cad_owned)
|
|
excluded=owned|cad_owned|changed_texts|edge_ids
|
|
unchanged_old={key:value for key,value in old_gr.items() if key not in excluded}
|
|
unchanged_new={key:value for key,value in new_gr.items() if key not in excluded}
|
|
assert unchanged_old==unchanged_new, 'unrelated board graphics changed'
|
|
assert set(old_gr)-owned==set(new_gr)-owned-cad_owned, 'non-owned board graphic UUID set changed'
|
|
assert [x for x in old if isinstance(x,list) and x[0]=='net']==[x for x in new if isinstance(x,list) and x[0]=='net']
|
|
assert not (ROOT/'~serial-carrier.kicad_pcb.lck').exists(), 'PCB reopened before handoff'
|
|
result=dict(a0_snapshot_unchanged=True,a1_unrouted_snapshot_unchanged=True,
|
|
protected_files_unchanged=list(protected),
|
|
intentional_documentation_updates=['README.md','pcb-draft-notes.md'],
|
|
cad_projection_graphics=len(cad_owned),
|
|
footprints_identical_except_authorized_placement=True,
|
|
footprint_count=len(old_fp),unrelated_board_graphics_preserved=len(unchanged_old),
|
|
non_owned_board_graphic_uuids_preserved=True,
|
|
refreshed_device_silkscreen_items=len(set(new_gr)&owned),
|
|
pcb_editor_lock_absent=True,final_board_sha256=sha(a1))
|
|
OUT.joinpath('pcb-routing-preservation.json').write_text(json.dumps(result,indent=2)+'\n')
|
|
print(json.dumps(result,indent=2))
|