Files
ESP32_Serial_Swiss_Army_Knife/hardware/PCB/validation/pcb-routing-preservation.py
T
Commander1024 7d76a41540 Advance PCB carrier to A1 layout
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.
2026-09-21 12:32:01 +02:00

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))