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/finish_fab.py
"""Finish native tscircuit Gerbers: 0.15 mm legend, clear of mask openings.
Copper, mask, drill, BOM and placement data are retained byte-for-byte.
Requires shapely 2.x. Run after tsci export -f gerbers.
"""
from pathlib import Path
import zipfile,re,json,hashlib
from shapely.geometry import Point,LineString,box
from shapely.ops import unary_union
root=Path(__file__).resolve().parents[1]
p=root/'fabrication/iphone-encoder-counter-gerbers.zip'
with zipfile.ZipFile(p) as z:data={n:z.read(n) for n in z.namelist()}
def read(s):
assert '%FSLAX46Y46*%' in s and '%MOMM*%' in s
apertures={m[0]:(m[1],[float(x) for x in m[2].split('X')]) for m in re.findall(r'%ADD(\d+)([CR]),([0-9.X]+)\*%',s)}
x=y=0;ap=None;segments=[];flashes=[]
for line in s.splitlines():
m=re.fullmatch(r'D(\d+)\*',line)
if m:ap=m[1];continue
if not line.startswith(('X','Y')):continue
xm=re.search(r'X(-?\d+)',line);ym=re.search(r'Y(-?\d+)',line);dm=re.search(r'D0([123])\*',line)
assert dm,line
nx=float(xm[1])/1e6 if xm else x;ny=float(ym[1])/1e6 if ym else y
if dm[1]=='1':segments.append(((x,y),(nx,ny),apertures[ap]))
if dm[1]=='3':flashes.append(((nx,ny),apertures[ap]))
x,y=nx,ny
return segments,flashes
mask_segments,flashes=read(data['F_Mask.gbr'].decode());assert not mask_segments
masks=[]
for (x,y),(typ,dims) in flashes:
if typ=='C':g=Point(x,y).buffer(dims[0]/2,quad_segs=64)
else:g=box(x-dims[0]/2,y-dims[1]/2,x+dims[0]/2,y+dims[1]/2)
masks.append(g)
exclusion=unary_union(masks).buffer(.2751) # half 0.15 mm stroke + 0.20 mm gap
segments,fs=read(data['F_SilkScreen.gbr'].decode());assert not fs
lines=[]
for a,b,ap in segments:
if a==b:continue
g=LineString([a,b]).difference(exclusion)
pieces=list(g.geoms) if hasattr(g,'geoms') else [g]
for part in pieces:
if part.is_empty or part.geom_type!='LineString' or part.length<.001:continue
pts=list(part.coords)
for a,b in zip(pts,pts[1:]):lines.append((a,b))
def xy(p):return 'X'+str(round(p[0]*1e6))+'Y'+str(round(p[1]*1e6))
header='''G04 Native tscircuit legend finished for 0.15mm minimum stroke and 0.20mm mask clearance*
%TF.FileFunction,Legend,Top*%
%TF.FilePolarity,Positive*%
%FSLAX46Y46*%
%MOMM*%
%LPD*%
%ADD10C,0.150000*%
G01*
D10*
'''
before={n:hashlib.sha256(b).hexdigest() for n,b in data.items()}
data['F_SilkScreen.gbr']=(header+''.join(xy(a)+'D02*\n'+xy(b)+'D01*\n' for a,b in lines)+'M02*\n').encode()
# No paste on bare contacts or printed coil terminals.
circuit=json.loads((root/'dist/index/circuit.json').read_text())
sources={e['source_component_id']:e.get('name') for e in circuit if e['type']=='source_component'}
components={e['pcb_component_id']:sources[e['source_component_id']] for e in circuit if e['type']=='pcb_component'}
no_paste=[]
for e in circuit:
if e['type'] in ['pcb_smtpad','pcb_plated_hole'] and components.get(e.get('pcb_component_id')) in ['J2','L1']:
no_paste.append((e['x'],e['y']))
removed=[]; kept=[]
for line in data['F_Paste.gbr'].decode().splitlines():
m=re.fullmatch(r'X(-?\d+)Y(-?\d+)D03\*',line)
if m and any(abs(int(m[1])/1e6-x)<.00001 and abs(int(m[2])/1e6-y)<.00001 for x,y in no_paste):
removed.append([int(m[1])/1e6,int(m[2])/1e6]);continue
kept.append(line)
assert len(removed)==6,removed
assert not any(any(abs(x-a)<.00001 and abs(y-b)<.00001 for a,b in no_paste) for (x,y),_ in read('\n'.join(kept))[1])
data['F_Paste.gbr']=('\n'.join(kept)+'\n').encode()
out=root/'fabrication/iphone-encoder-counter-jlc.zip'
with zipfile.ZipFile(out,'w',zipfile.ZIP_DEFLATED) as z:
for n,b in data.items():z.writestr(n,b)
report={'input':'native tscircuit gerbers export','paste_removed_for':['J2 bare programming pads','L1 printed coil terminals'],'paste_flashes_removed':removed,'legend_stroke_mm':.15,'legend_to_mask_mm':.2,'unchanged_files':[n for n,b in data.items() if hashlib.sha256(b).hexdigest()==before[n]],'source_legend_segments':len(segments),'finished_legend_segments':len(lines)}
(root/'validation/gerber-finishing.json').write_text(json.dumps(report,indent=2)+'\n')
for n in ['bom.csv','pick_and_place.csv']:(root/'fabrication'/n).write_bytes(data[n])
print('Finished',out.name,'; all copper, mask, drill, BOM and placement bytes unchanged.')