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.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user