astra/iphone-encoder-counter
A CR2450-powered MSP430 FRAM counter combines an always-on dual-channel TMR rotation sensor, pushbutton input, and ST25DV NFC tag with an integrated tuned printed antenna and I²C/GPIO interface.
- Version
- 1.0.3
- License
- unset
- Stars
- 0
scripts/check_exports.py
"""Check native Gerber/CPL geometry and catalog-matched assembly BOM."""
from pathlib import Path
import json,zipfile,re,csv,io,collections,math
root=Path(__file__).resolve().parents[1]
d=json.loads((root/'dist/index/circuit.json').read_text())
catalog=json.loads((root/'jlc-catalog-check.json').read_text())
z=zipfile.ZipFile(root/'fabrication/iphone-encoder-counter-jlc.zip')
def segment(a,b,w):
pts=sorted([(round(a[0],6),round(a[1],6)),(round(b[0],6),round(b[1],6))])
return tuple(pts)+ (round(w,6),)
expected={'top':set(),'bottom':set()}
for e in d:
if e['type']!='pcb_trace':continue
for a,b in zip(e['route'],e['route'][1:]):
if a['route_type']==b['route_type']=='wire' and a['layer']==b['layer'] and (a['x'],a['y'])!=(b['x'],b['y']):expected[a['layer']].add(segment((a['x'],a['y']),(b['x'],b['y']),a['width']))
actual={}
for layer,f in [('top','F_Cu.gbr'),('bottom','B_Cu.gbr')]:
s=z.read(f).decode();aps={m[0]:(m[1],m[2]) for m in re.findall(r'%ADD(\d+)([A-Z]+),([^*]+)\*%',s)};x=y=0;ap=None;paths=set()
for l in s.splitlines():
m=re.fullmatch(r'D(\d+)\*',l)
if m:ap=m[1];continue
if not l.startswith(('X','Y')):continue
xm=re.search(r'X(-?\d+)',l);ym=re.search(r'Y(-?\d+)',l);dm=re.search(r'D0([123])\*',l);assert dm,l
nx=int(xm[1])/1e6 if xm else x;ny=int(ym[1])/1e6 if ym else y
if dm[1]=='1' and (x,y)!=(nx,ny):
assert aps[ap][0]=='C';paths.add(segment((x,y),(nx,ny),float(aps[ap][1])))
x,y=nx,ny
actual[layer]=paths
assert paths==expected[layer], (layer,'missing',list(expected[layer]-paths)[:4],'extra',list(paths-expected[layer])[:4])
# Drill positions in the same signed-mm coordinate system.
s=z.read('drill-L1-L2.drl').decode()
hits=collections.Counter((round(float(a),4),round(float(b),4)) for a,b in re.findall(r'^X(-?[0-9.]+)Y(-?[0-9.]+)$',s,re.M))
want=collections.Counter((round(e['x'],4),round(e['y'],4)) for e in d if e['type'] in ['pcb_via','pcb_plated_hole'])
assert hits==want,(hits-want,want-hits)
bom=list(csv.DictReader(io.StringIO(z.read('bom.csv').decode())))
cpl=list(csv.DictReader(io.StringIO(z.read('pick_and_place.csv').decode())))
assert {x['Designator'] for x in bom}=={x['Designator'] for x in cpl}
assert len(bom)==23 and not ({'L1','J2'} & {x['Designator'] for x in bom})
source={e['name']:e for e in d if e['type']=='source_component'}
code=(root/'index.circuit.tsx').read_text()
for b in bom:
c=b['JLCPCB Part #']
assert c and c in source[b['Designator']]['supplier_part_numbers']['jlcpcb']
if c=='C17208043':
listing=json.loads((root/'validation/u2-jlc-listing.json').read_text())
assert listing['jlcpcb_part_number']==c and listing['manufacturer_part_number']==source[b['Designator']]['manufacturer_part_number']
else:
r=catalog['parts'][c]['results'][0]
assert r['stock']>0 and r['mfr']==source[b['Designator']]['manufacturer_part_number']
for p in cpl:
line=next(l for l in code.splitlines() if 'name="'+p['Designator']+'"' in l)
x=float(re.search(r'pcbX=\{(-?[\d.]+)\}',line)[1]);y=float(re.search(r'pcbY=\{(-?[\d.]+)\}',line)[1])
assert abs(float(p['Mid X'])-x)<.001 and abs(float(p['Mid Y'])-y)<.001
rotation=re.search(r'pcbRotation=\{(-?[\d.]+)\}',line)
assert p['Layer']=='top' and float(p['Rotation'])==(float(rotation[1]) if rotation else 0)
assert not any('error' in e['type'] for e in d)
report={'result':'PASS','copper_segments_by_layer':{k:len(v) for k,v in actual.items()},'copper_matches_circuit_json':True,'drill_hits':sum(hits.values()),'jlc_assembled_parts_per_board':len(bom),'bom_cpl_reference_sets_match':True,'all_cpl_centers_match_source':True,'assembly_side':'top','all_assembly_parts_have_jlcpcb_ids':True,'sourcing_blockers':['U2 C17208043: zero JLC stock; confirm sourcing and assembly acceptance','U3 C5267456: live stock 3, insufficient for assumed 5-board batch'],'catalog_checked_at_utc':catalog['checked_at_utc']}
(root/'validation/export-audit.json').write_text(json.dumps(report,indent=2)+'\n');print(json.dumps(report,indent=2))