mohan-bee/rc-link-breakaway-panel

A four-layer RC link panel combining an STM32/E28 2.4‑GHz transmitter with USB-RP2040 socket and an STM32/E28 receiver providing six buffered PWM servo outputs, regulated 3.3‑V power, headers, controls, and protection/decoupling.

Version
0.1.5
License
unset
Stars
0

tools/package-panel.py

import csv, json, zipfile, re
from pathlib import Path

root=Path(__file__).resolve().parents[1]
release=root/'manufacturing'
files=['F_Cu.gbr','In1_Cu.gbr','In2_Cu.gbr','B_Cu.gbr','F_Mask.gbr','B_Mask.gbr',
       'F_SilkScreen.gbr','B_SilkScreen.gbr','F_Paste.gbr','B_Paste.gbr',
       'Edge_Cuts.gbr','drill-L1-L4.drl','drill_npth.drl']
with zipfile.ZipFile(release/'raw-gerbers.zip') as source, zipfile.ZipFile(release/'RC-LINK-PANEL-Gerbers.zip','w',zipfile.ZIP_DEFLATED) as output:
    for name in files: output.writestr(name,source.read(name))
circuit=json.loads((root/'dist/index/circuit.json').read_text())
components={e['name']:e for e in circuit if e['type']=='source_component'}
poses=[]
for board,dx,dy in [('transmitter',-23.5,0),('receiver',36.5,-3)]:
    audit=json.loads((root/'boards'/f'{board}.assembly-poses.json').read_text())
    assert audit['status']=='pass'
    prefix='TX' if board=='transmitter' else 'RX'
    original=json.loads((root/'boards'/f'{board}.circuit.json').read_text())
    old_components={e['name']:e for e in original if e['type']=='source_component'}
    for part in audit['parts']:
        p={**part,'refdes':re.sub(r'^([A-Z]+)_?', lambda m: m[1]+'_'+prefix+'_', part['refdes']),'x_mm':part['x_mm']+dx,'y_mm':part['y_mm']+dy}
        assert p['refdes'] in components,p
        current=components[p['refdes']]
        before=old_components[part['refdes']]
        placed=next(e for e in circuit if e['type']=='pcb_component' and e['source_component_id']==current['source_component_id'])
        old_placed=next(e for e in original if e['type']=='pcb_component' and e['source_component_id']==before['source_component_id'])
        assert abs(placed['center']['x']-old_placed['center']['x']-dx)<1e-6,p
        assert abs(placed['center']['y']-old_placed['center']['y']-dy)<1e-6,p
        assert (placed['rotation']-old_placed['rotation'])%360==0,p
        assert p['supplier'] in current['supplier_part_numbers']['jlcpcb'],p
        poses.append(p)
assert len(poses)==75 and len({p['refdes'] for p in poses})==75
required={e['name'] for e in circuit if e['type']=='source_component' and e.get('supplier_part_numbers',{}).get('jlcpcb') and not next(p for p in circuit if p['type']=='pcb_component' and p['source_component_id']==e['source_component_id']).get('do_not_place')}
assert required=={p['refdes'] for p in poses},required^{p['refdes'] for p in poses}
with (release/'BOM.csv').open('w') as f:
    w=csv.writer(f);w.writerow(['Comment','Designator','Footprint','JLCPCB Part #','Manufacturer Part Number'])
    for p in poses:w.writerow([p.get('value',p['mpn']),p['refdes'],p['package'],p['supplier'],p['mpn']])
with (release/'CPL.csv').open('w') as f:
    w=csv.writer(f);w.writerow(['Designator','Mid X','Mid Y','Layer','Rotation'])
    for p in poses:w.writerow([p['refdes'],f'{p["x_mm"]:.6f}mm',f'{p["y_mm"]:.6f}mm','top',p['rotation_degrees']])
(release/'assembly-poses.json').write_text(json.dumps(dict(status='pass',parts=poses),indent=2)+'\n')
(root/'validation/assembly-export-audit.json').write_text(json.dumps(dict(status='pass',placements=len(poses),unique_designators=True,all_current_parts_included=True,supplier_ids_verified=True,source_positions_and_rotations_preserved=True),indent=2)+'\n')
print('Packaged one panel: 33 TX + 42 RX = 75 top SMT placements; original supplier poses translated, not re-inferred.')