mohan-bee/stm32-midi-keys

Clips rectangular ground-region geometry to fit within the board’s scaled rounded-edge outline using polygon intersection.

Version
1.0.2
License
unset
Stars
0

scripts/verify-jlcpcb.py

"""Check every physical pad's net after substituting JLCPCB components."""
import json, math, re
from pathlib import Path

actual = json.loads(Path('dist/index/circuit.json').read_text())
reference = json.loads(Path('board.circuit.json').read_text())
geometry = json.loads(Path('source/geometry.json').read_text())
parts = json.loads(Path('source/jlcpcb-parts.json').read_text())
components = {e['source_component_id']: e for e in actual if e['type'] == 'source_component'}
pcb_components = {components[e['source_component_id']]['name']: e for e in actual if e['type'] == 'pcb_component'}
old_names = {e['source_component_id']: e['name'] for e in reference if e['type'] == 'source_component'}
old_components = {old_names[e['source_component_id']]: e for e in reference if e['type'] == 'pcb_component'}
ports = {e['pcb_port_id']: e for e in actual if e['type'] == 'pcb_port'}
net_names = {e['source_net_id']: e['name'] for e in actual if e['type'] == 'source_net'}
port_nets = {}
for trace in (e for e in actual if e['type'] == 'source_trace'):
    for port in trace['connected_source_port_ids']:
        port_nets.setdefault(port, set()).update(net_names[n] for n in trace['connected_source_net_ids'])

def normalized(name):
    return re.sub('_+', '_', re.sub('[^A-Za-z0-9_]', '_', name.replace('+', '_P').replace('-', '_'))).strip('_')

errors = []
checked = 0
for footprint in geometry['footprints']:
    name = footprint['name']
    original = [e for e in reference if e.get('pcb_component_id') == old_components[name]['pcb_component_id'] and e['type'] in ['pcb_smtpad', 'pcb_plated_hole']]
    pads = [e for e in actual if e.get('pcb_component_id') == pcb_components[name]['pcb_component_id'] and e['type'] in ['pcb_smtpad', 'pcb_plated_hole']]
    for pad in original:
        number = pad['port_hints'][0]
        net = next(p['net'] for p in footprint['pads'] if p['number'] == number)
        if not net or net.startswith('unconnected'): continue
        if pad.get('shape') == 'polygon':
            matches = [p for p in pads if p.get('shape') == 'polygon' and len(p['points']) == len(pad['points']) and all(math.hypot(a['x']-b['x'], a['y']-b['y']) < 1e-6 for a,b in zip(p['points'],pad['points']))]
        else:
            matches = [p for p in pads if 'x' in p and math.hypot(p['x']-pad['x'],p['y']-pad['y']) < 1e-6]
        found = set()
        for p in matches:
            port = ports.get(p.get('pcb_port_id'), {})
            found.update(port_nets.get(port.get('source_port_id'), set()))
        checked += 1
        if found != {normalized(net)}: errors.append({'reference':name,'pad':number,'expected':normalized(net),'actual':sorted(found)})

by_name = {c['name']: c for c in components.values()}
supplier_errors = []
for part, refs in parts.items():
    for ref in refs.split():
        if part not in by_name[ref].get('supplier_part_numbers', {}).get('jlcpcb', []): supplier_errors.append(ref)
summary = {'checkedConnectedPads': checked, 'padNetMismatches': errors, 'jlcpcbUniqueParts': len(parts), 'jlcpcbPlacements': sum(len(refs.split()) for refs in parts.values()), 'missingSupplierAssignments':supplier_errors}
Path('validation/jlcpcb-parity.json').write_text(json.dumps(summary,indent=2)+'\n')
print(json.dumps(summary,indent=2))
assert not errors and not supplier_errors