Record Accepted PCB Placement And Fit

Align component groups, repair affected routing, and add preservation
audits.
Assign the confirmed U2 footprint and update pull-ups to 2 kΩ across
design files.
Document user acceptance, switch to an A3 worksheet, and refresh PCB
exports.
This commit is contained in:
2026-09-22 10:28:08 +02:00
parent 901afbe754
commit c257158ada
42 changed files with 29500 additions and 17205 deletions
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/python3
"""Exact authorized placement/value delta, retained copper and body geometry audit."""
import copy
import hashlib
import json
import uuid
import pcbnew as p
from validate_u1_update import parse, canonical, ident
from add_device_silkscreen import NS
from refine_group_placement import ROOT, BASE, MOVES
METADATA = {
**{f'R{i}': ('Rating', 'Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified') for i in range(1,4)},
**{f'SW{i}': ('Procurement', 'User reports purchased') for i in range(1,4)},
'U2': ('Status', 'User confirmed male pinout/header/footprint and pin-by-pin connections'),
}
def expected_identity(board):
"""Apply only the seven authorized placements and three resistor values."""
fps={f.GetReference():f for f in board.GetFootprints()}
for ref,(old,new) in MOVES.items():
fps[ref].SetPosition(p.VECTOR2I(p.FromMM(new[0]),p.FromMM(new[1])))
if ref.startswith('R'): fps[ref].SetValue('2k 1%')
return board
def audit():
assert hashlib.sha256(BASE.read_bytes()).hexdigest()=='b9e020939fff42b5c7a3161ed89af9dc3c1b9c016268311b3db7ab407ed67192'
old=parse(BASE)
new=parse(ROOT/'serial-carrier.kicad_pcb')
from update_presentation import expected_tree
wanted=expected_tree(old)
for f in wanted:
if not isinstance(f,list) or f[0]!='footprint': continue
ref=next(v[2].strip('"') for v in f if isinstance(v,list) and v[:2]==['property','"Reference"'])
if ref in METADATA:
key,value=METADATA[ref]
next(v for v in f if isinstance(v,list) and v[:2]==['property',json.dumps(key)])[2]=json.dumps(value)
if ref in MOVES:
at=next(v for v in f if isinstance(v,list) and v[0]=='at')
at[1:3]=[format(x,'.10g') for x in MOVES[ref][1]]
if ref.startswith('R'):
next(v for v in f if isinstance(v,list) and v[:2]==['property','"Value"'])[2]='"2k 1%"'
silk={str(uuid.uuid5(NS,str(i))) for i in range(4096)}
def without_copper(tree):
return [v for v in tree if not isinstance(v,list) or v[0] not in ('segment','via')]
assert canonical(without_copper(wanted),silk)==canonical(without_copper(new),silk), 'Unexpected non-copper edit'
before={ident(v):v for v in old if isinstance(v,list) and v[0] in ('segment','via')}
after={ident(v):v for v in new if isinstance(v,list) and v[0] in ('segment','via')}
migration=json.loads((ROOT/'validation/pcb-group-migration.json').read_text())
removed=set(before)-set(after)
assert removed==set(before)&set(migration['removed_track_uuids'])
assert all(before[k]==after[k] for k in before.keys()&after.keys()), 'Retained copper changed'
assert len(before.keys()&after.keys())==476
for name in ('serial-carrier.kicad_pro','serial-carrier.kicad_prl'):
assert hashlib.sha256((ROOT/name).read_bytes()).hexdigest()==migration['protected_sha256'][name]
board=p.LoadBoard(str(ROOT/'serial-carrier.kicad_pcb'))
fps={f.GetReference():f for f in board.GetFootprints()}
def xy(v): return p.ToMM(v.x),p.ToMM(v.y)
# Published U2 F.Fab outline includes the bare PCB beyond the header row.
u2pts=[xy(v) for g in fps['U2'].GraphicalItems() if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab for v in (g.GetStart(),g.GetEnd())]
axis=(min(x for x,y in u2pts)+max(x for x,y in u2pts))/2
bottom=max(y for x,y in u2pts)
oled=next(g for g in fps['DS1'].GraphicalItems() if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab and g.GetStart()==fps['DS1'].GetPosition())
assert abs((xy(oled.GetStart())[0]+xy(oled.GetEnd())[0])/2-axis)<1e-6
resistor_boxes=[g for i in range(1,4) for g in fps[f'R{i}'].GraphicalItems() if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab and g.GetShape()==p.SHAPE_T_RECT]
assert len(resistor_boxes)==3
left=min(xy(g.GetStart())[0] for g in resistor_boxes)
right=max(xy(g.GetEnd())[0] for g in resistor_boxes)
assert abs((left+right)/2-axis)<1e-6
topgap=min(xy(g.GetStart())[1] for g in resistor_boxes)-bottom
bottomgap=xy(oled.GetStart())[1]-max(xy(g.GetEnd())[1] for g in resistor_boxes)
assert abs(topgap-2.0815)<1e-6 and abs(bottomgap-topgap)<1e-6
switchpts=[xy(v) for i in range(1,4) for g in fps[f'SW{i}'].GraphicalItems() if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab for v in (g.GetStart(),g.GetEnd())]
assert abs((min(x for x,y in switchpts)+max(x for x,y in switchpts))/2-axis)<1e-6
switchgap=min(y for x,y in switchpts)-xy(oled.GetEnd())[1]
assert abs(switchgap-5.25)<1e-6
result=dict(common_axis_absolute_x_mm=axis,resistor_to_rs232_body_gap_mm=round(topgap,6),resistor_to_oled_body_gap_mm=round(bottomgap,6),oled_to_button_body_gap_mm=switchgap,
original_copper_items=len(before),retained_copper_items=476,removed_original_copper_items=len(removed),new_copper_items=len(after.keys()-before.keys()),
exact_unrelated_content_preserved=True,project_settings_preserved=True,baseline_sha256=migration['baseline_sha256'],board_sha256=hashlib.sha256((ROOT/'serial-carrier.kicad_pcb').read_bytes()).hexdigest())
(ROOT/'validation/pcb-group-alignment.json').write_text(json.dumps(result,indent=2)+'\n')
return result
if __name__=='__main__': print(json.dumps(audit(),indent=2))