mohan-bee/fitness-band
A 4-layer BLE wearable mainboard with Li-ion charging/protection, 3.3/1.8 V power regulation, motion and battery sensing, plus a folded 2-layer flex sensor board carrying MAX30102 optical and TMP117 temperature sensors.
- Version
- 0.0.2
- License
- unset
- Stars
- 0
scripts/export-release.py
"""Run with KiCad's Python; export the frozen native boards, never rebuild TSX."""
from pathlib import Path
import json,csv,subprocess,hashlib,re
import pcbnew as k
root=Path(__file__).resolve().parents[1];cli='/Applications/KiCad.app/Contents/MacOS/kicad-cli'
release=root/'manufacturing/rev-b'
manifest={}
for kind,entry in [('main','index'),('flex','sensor-flex')]:
folder=release/kind;pcb=folder/f'{kind}.kicad_pcb';gerbers=folder/'gerbers';gerbers.mkdir(exist_ok=True)
report=root/f'validation/{kind}-native-drc.json'
subprocess.run([cli,'pcb','drc',str(pcb),'--refill-zones','--save-board','--format','json','-o',str(report)],check=True)
drc=json.loads(report.read_text());assert not drc['violations'] and not drc['unconnected_items'],f'{kind}: DRC is not clean'
layers='F.Cu,In1.Cu,In2.Cu,B.Cu,F.Mask,B.Mask,F.Paste,B.Paste,F.SilkS,B.SilkS,Edge.Cuts' if kind=='main' else 'F.Cu,B.Cu,B.Mask,F.Paste,B.Paste,Edge.Cuts,F.Adhesive,B.Adhesive,Eco1.User'
subprocess.run([cli,'pcb','export','gerbers',str(pcb),'-l',layers,'-o',str(gerbers)+'/'],check=True)
if kind=='flex':
for pattern,name in [('*F_Adhesive*','TOP_COVERLAY_OPENINGS.gbr'),('*B_Adhesive*','BOTTOM_FR4_STIFFENER_0p4mm.gbr'),('*User_Eco1*','BOTTOM_PI_STIFFENER_0p15mm.gbr'),('*B_Mask*','BOTTOM_COVERLAY_NO_OPENINGS.gbr')]:
found=list(gerbers.glob(pattern));assert len(found)==1,(pattern,found);found[0].rename(gerbers/name)
for filename,role in [('TOP_COVERLAY_OPENINGS.gbr','TopCoverlayOpenings'),('BOTTOM_FR4_STIFFENER_0p4mm.gbr','BottomFR4Stiffener'),('BOTTOM_PI_STIFFENER_0p15mm.gbr','BottomPIStiffener')]:
p=gerbers/filename;s=p.read_text();s=re.sub(r'%TF.FileFunction,[^*]+\*%',f'%TF.FileFunction,Other,{role}*%',s);p.write_text(s)
# Fabrication notes specify the stack-up; omit generic job-file material
# defaults and paths invalidated by descriptive flex artwork filenames.
for p in gerbers.glob('*.gbrjob'):p.unlink()
subprocess.run([cli,'pcb','export','drill',str(pcb),'-o',str(gerbers)+'/','--format','excellon','--excellon-units','mm','--excellon-separate-th','--generate-map','--map-format','svg','--generate-report'],check=True)
subprocess.run([cli,'pcb','export','pos',str(pcb),'-o',str(folder/'positions-native.csv'),'--format','csv','--units','mm','--exclude-dnp'],check=True)
snapshot=release/f'source-snapshots/{kind}.circuit.json'
elements=json.loads(snapshot.read_text());parts={e['name']:e for e in elements if e['type']=='source_component'}
b=k.LoadBoard(str(pcb));pads=[];bom=[];assembly=[]
for fp in b.GetFootprints():
ref=fp.GetReference();p=fp.GetPosition();fp_pads=list(fp.Pads())
for pad in fp_pads:
pos=pad.GetPosition();pads.append({'ref':ref,'pin':pad.GetNumber(),'net':pad.GetNetname(),'x':pos.x/1e6,'y':pos.y/1e6})
if ref in parts and not parts[ref].get('do_not_place') and not (fp.GetAttributes()&k.FP_DNP):
source=parts[ref];mpn=source.get('manufacturer_part_number');assert mpn,ref
bom.append({'Reference':ref,'Quantity':1,'MPN':mpn,'Value':fp.GetValue(),'Assembly':'SMT TOP','Substitution':'NONE without engineering review'})
pin1=next((q for q in fp_pads if q.GetNumber()=='1'),None)
assembly.append({'Reference':ref,'MPN':mpn,'Center X mm':p.x/1e6,'Center Y mm':p.y/1e6,'Native rotation deg':fp.GetOrientationDegrees(),'Pin1 X mm':pin1.GetPosition().x/1e6 if pin1 else '', 'Pin1 Y mm':pin1.GetPosition().y/1e6 if pin1 else ''})
for filename,rows in [('BOM.csv',bom),('assembly-orientation.csv',assembly)]:
rows.sort(key=lambda r:r['Reference'])
with (folder/filename).open('w',newline='') as f:
w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
(root/f'validation/{kind}-native-pads.json').write_text(json.dumps(pads,indent=2)+'\n')
manifest[kind]={'native_sha256':hashlib.sha256(pcb.read_bytes()).hexdigest(),'circuit_json_path':str(snapshot.relative_to(root)),'circuit_json_sha256':hashlib.sha256(snapshot.read_bytes()).hexdigest(),'drc_violations':0,'unconnected_items':0,'smt_parts':len(bom)}
(release/'release-manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')