imrishabh18/pedometer

This code defines and assembles a simple radio receiver hardware circuit using specific imported capacitors, inductors, RF connectors, and oscillator components with precise footprints and schematic attributes.

Version
1.1.3
License
unset
Stars
0

scripts/audit-bga-apertures.py

"""Check actual Gerber flashes against TI land/stencil dimensions, in mm."""
import hashlib, json, math, re
from pathlib import Path

root = Path('review/manufacturing')
c = json.loads((root / 'circuit.json').read_text())
sources = {e['source_component_id']: e for e in c if e['type'] == 'source_component'}
components = {e['pcb_component_id']: sources[e['source_component_id']]['name'] for e in c if e['type'] == 'pcb_component'}

def flashes(name):
    data = (root / name).read_text()
    assert '%FSLAX46Y46*%' in data and '%MOMM*%' in data
    apertures, result, selected = {}, [], None
    for line in data.splitlines():
        m = re.fullmatch(r'%ADD(\d+)([A-Z]+),([^*]+)\*%', line)
        if m:
            apertures[int(m[1])] = {'shape': m[2], 'dimensions': [float(n) for n in m[3].split('X')]}
        m = re.fullmatch(r'D(\d+)\*', line)
        if m: selected = int(m[1])
        m = re.fullmatch(r'X(-?\d+)Y(-?\d+)D03\*', line)
        if m:
            result.append({'x': int(m[1]) / 1e6, 'y': int(m[2]) / 1e6, **apertures[selected]})
    return result

layers = {name: flashes(f'{name}.gbr') for name in ['F_Cu', 'F_Mask', 'F_Paste']}
def at(layer, x, y):
    return [f for f in layers[layer] if math.hypot(f['x']-x, f['y']-y) < 2e-6]
def contains(fs, shape, dimensions):
    return any(f['shape'] == shape and len(f['dimensions']) == len(dimensions) and
               all(abs(a-b) < 2e-6 for a,b in zip(f['dimensions'], dimensions)) for f in fs)

report = []
for pad in c:
    if pad['type'] != 'pcb_smtpad' or components.get(pad.get('pcb_component_id')) not in ['U2','U3']: continue
    ref = components[pad['pcb_component_id']]
    assert pad['shape'] == 'circle'
    x, y = pad['x'], pad['y']
    copper = .23 if ref == 'U2' else .245
    mask = copper + 2 * pad['soldermask_margin']
    assert contains(at('F_Cu',x,y),'C',[copper]), (ref,pad['port_hints'],'copper')
    assert contains(at('F_Mask',x,y),'C',[mask]), (ref,pad['port_hints'],'mask')
    assert contains(at('F_Paste',x,y),'R',[.25,.15])
    assert contains(at('F_Paste',x,y),'R',[.15,.25])
    assert not contains(at('F_Paste',x,y),'C',[.25]), 'Old circular aperture remains'
    for dx in [-.075,.075]:
        for dy in [-.075,.075]: assert contains(at('F_Paste',x+dx,y+dy),'C',[.1])
    report.append({'reference':ref,'pad':pad['port_hints'],'copperLandDiameterMm':copper,
                   'copperFlashesAtCenter':at('F_Cu',x,y),'maskOpeningDiameterMm':mask,
                   'maskDefinition':'SMD at capped via' if pad['soldermask_margin']==0 else 'NSMD',
                   'pasteWidthMm':.25,'pasteHeightMm':.25,'pasteCornerRadiusMm':.05,
                   'pasteAreaMm2':.25**2-(4-math.pi)*.05**2})
assert len(report) == 29
bga_pads=[p for p in c if p['type']=='pcb_smtpad' and components.get(p.get('pcb_component_id')) in ['U2','U3']]
minimum_mask_bridge=min(math.hypot(p['x']-q['x'],p['y']-q['y'])-p['radius']-q['radius']-p['soldermask_margin']-q['soldermask_margin'] for i,p in enumerate(bga_pads) for q in bga_pads[i+1:])
assert minimum_mask_bridge >= .10-1e-6, ('BGA soldermask bridge',minimum_mask_bridge)
Path('review/bga-aperture-audit.json').write_text(json.dumps({
    'manufacturingCircuitSha256':hashlib.sha256((root/'circuit.json').read_bytes()).hexdigest(),
    'result':'PASS — dimensions verified in exported Gerber flashes; process qualification remains open',
    'minimumBgaMaskBridgeMm':minimum_mask_bridge,'stencilThicknessMm':.1,'lands':report,
},indent=2))
print('PASS: all 29 BGA copper/mask/stencil openings verified from Gerber flashes')