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.
86 lines
5.2 KiB
Python
86 lines
5.2 KiB
Python
#!/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))
|