#!/usr/bin/python3 """One-shot A0 -> A1 placement migration of the existing board, never regeneration. Requires --apply and the untouched pcb-a0-before-reposition.kicad_pcb snapshot. Never replaces the snapshot or accepts an already edited/migrated input. """ import argparse import hashlib import json from pathlib import Path import sys import pcbnew as p ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT)) from route_pcb_draft import identity PCB = ROOT / 'serial-carrier.kicad_pcb' BACKUP = ROOT / 'validation/pcb-a0-before-reposition.kicad_pcb' LOCK = ROOT / '~serial-carrier.kicad_pcb.lck' OLD = [(0,0),(82,0),(82,80),(36,80),(36,61),(0,61)] NEW = [(36,0),(82,0),(82,80),(0,80),(0,19),(36,19)] MOVES = {'U1': ((8,4),(8,23)), 'U2': ((43,30),(43,26.670)), 'H1': ((3.5,3.5),(3.5,22.5)), 'H2': ((3.5,56),(3.5,75))} TEXT_MOVES = {'RF RESERVE':19, 'VERIFY ANTENNA':19, 'ESP32 HW678':19, 'USB ACCESS - VERIFY':19, 'DE-9 OUT / VERIFY MALE':-3.330, 'MAX3243 REF':-3.330} def vec(x,y): return p.VECTOR2I(p.FromMM(x),p.FromMM(y)) def local(v): return (round(p.ToMM(v.x)-100,6),round(p.ToMM(v.y)-100,6)) def sha(path): return hashlib.sha256(path.read_bytes()).hexdigest() def normalized_footprints(board): # Normalize placement only, retaining UUIDs, local pad geometry and linkage. entries = identity(board)[1] normalized = [] for entry in entries: fields = list(entry) x,y = fields[5] fields[5] = (0,0) pads = [] for pad in fields[-1]: values = list(pad) values[3] = (round(values[3][0]-x,6),round(values[3][1]-y,6)) pads.append(tuple(values)) fields[-1] = pads normalized.append(tuple(fields)) return normalized def main(): parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('--apply',action='store_true') args=parser.parse_args() if not args.apply: parser.error('explicit --apply required; only accepts the original A0 snapshot') assert not LOCK.exists(), 'PCB Editor lock exists' assert BACKUP.exists(), 'make the one-time A0 snapshot first' before=PCB.read_bytes() assert before==BACKUP.read_bytes(), 'current board differs from A0 snapshot; refusing migration' protected=[ROOT/name for name in ('serial-carrier.kicad_pro','serial-carrier.kicad_sch', 'serial-carrier.kicad_prl','README.md','pcb-draft-notes.md','fp-lib-table', 'sym-lib-table','Carrier.kicad_sym')] protected+=list((ROOT/'Carrier.pretty').glob('*')) hashes={str(path.relative_to(ROOT)):sha(path) for path in protected if path.is_file()} board=p.LoadBoard(str(PCB)) immutable=normalized_footprints(board) net_table=identity(board)[-1] footprints={f.GetReference():f for f in board.GetFootprints()} for ref,(old,new) in MOVES.items(): fp=footprints[ref] assert local(fp.GetPosition())==old,(ref,local(fp.GetPosition())) fp.SetPosition(vec(new[0]+100,new[1]+100)) for track in list(board.GetTracks()): board.Delete(track) edges=[d for d in board.GetDrawings() if d.GetLayer()==p.Edge_Cuts] assert len(edges)==6 for i,(a,b) in enumerate(zip(OLD,OLD[1:]+OLD[:1])): edge=next(e for e in edges if frozenset((local(e.GetStart()),local(e.GetEnd())))==frozenset((a,b))) aa,bb=NEW[i],NEW[(i+1)%6] edge.SetStart(vec(aa[0]+100,aa[1]+100)) edge.SetEnd(vec(bb[0]+100,bb[1]+100)) assert len(board.Zones())==1 and board.Zones()[0].GetIsRuleArea() zone=board.Zones()[0] contour=zone.Outline().COutline(0) assert {local(contour.CPoint(i)) for i in range(contour.PointCount())}=={(10,0),(31,0),(31,13),(10,13)} zone.Move(vec(0,19)) changed_texts=[] for text,dy in TEXT_MOVES.items(): found=[d for d in board.GetDrawings() if isinstance(d,p.PCB_TEXT) and d.GetText()==text] assert len(found)==1,(text,len(found)) drawing=found[0] drawing.Move(vec(0,dy)) if text=='DE-9 OUT / VERIFY MALE': drawing.SetText('DE-9 OUT / NOMINAL MALE') changed_texts.append(drawing.m_Uuid.AsString()) for d in board.GetDrawings(): if isinstance(d,p.PCB_TEXT) and d.GetText()=='Carrier envelope only; module overhang and connector clearance not qualified': d.SetText('Nominal RS-232 metal verified; ESP32 USB / antenna / assembly clearance pending') changed_texts.append(d.m_Uuid.AsString()) title=board.GetTitleBlock() title.SetRevision('A1 DRAFT') title.SetComment(0,'NOT FOR FABRICATION - USB / antenna geometry pending; verify module pinout / assembly') assert normalized_footprints(board)==immutable assert identity(board)[-1]==net_table candidate=ROOT/'validation/pcb-routing-reposition-candidate.kicad_pcb' assert p.SaveBoard(str(candidate),board,True) saved=p.LoadBoard(str(candidate)) assert normalized_footprints(saved)==immutable assert identity(saved)==identity(board) assert not LOCK.exists() and PCB.read_bytes()==before assert all(sha(ROOT/name)==digest for name,digest in hashes.items()), 'protected input changed' candidate.replace(PCB) report=dict(revision='A1',a0_snapshot_sha256=sha(BACKUP),a1_unrouted_sha256=sha(PCB), outline_local_mm=NEW,moves_local_mm=MOVES,changed_board_text_uuids=changed_texts, normalized_footprints_preserved=True,nets_preserved=True, protected_file_sha256=hashes,remaining_tracks=len(saved.GetTracks())) ROOT.joinpath('validation/pcb-routing-reposition.json').write_text(json.dumps(report,indent=2)+'\n') print(json.dumps(report,indent=2)) if __name__=='__main__': main()