abse/smartwatch-flex-top-only

Three-panel flex smartwatch with two parallel R3.9 bends, all components flat-top, low-profile JST connector harnesses, split heartbeat optical windows and screw-retained PCB mounting. Modeled 39 × 50.5 × 14.5mm prototype; 100% routing, zero native DRC and shorts. Hardware qualification pending.

Version
0.2.22
License
unset
Stars
0

scripts/verify-schematic.py

"""Check the annotated schematic against the unchanged published PCB design."""
from pathlib import Path
from collections import Counter
import json, hashlib, sys

root = Path(__file__).resolve().parents[1]
baseline = Path('/workspace/smartwatch-flex-schematic')
name = sys.argv[1] if len(sys.argv) > 1 else 'index'
candidate = Path(sys.argv[2]) if len(sys.argv) > 2 else root / 'dist' / name / 'circuit.json'
a = json.loads((baseline / 'dist' / name / 'circuit.json').read_text())
b = json.loads(candidate.read_text())

def clean(v):
    if isinstance(v, dict):
        # Port hints are an alias set. Vertical capacitor symbols enumerate the
        # same aliases in a different order; pin numbers and net IDs stay fixed.
        if v.get('type') == 'source_port' and 'port_hints' in v:
            v = {**v, 'port_hints': sorted(v['port_hints'])}
        return {k: clean(x) for k, x in v.items() if not k.endswith('_id')}
    if isinstance(v, list):
        return [clean(x) for x in v]
    return round(v, 9) if isinstance(v, float) else v

types = {e['type'] for e in a if e['type'].startswith(('pcb_', 'source_'))} | {'cad_component'}
types.discard('source_project_metadata')
for typ in sorted(types):
    old = Counter(json.dumps(clean(e), sort_keys=True) for e in a if e['type'] == typ)
    new = Counter(json.dumps(clean(e), sort_keys=True) for e in b if e['type'] == typ)
    assert old == new, (typ, sum((old - new).values()), sum((new - old).values()), list((new-old))[:1])

issues = Counter(e['type'] for e in b if 'error' in e['type'] or e['type'].endswith('_warning'))
assert not issues, issues
symbols = [e for e in b if e['type'] == 'schematic_component']
ports = [e for e in b if e['type'] == 'schematic_port']
labels = [e for e in b if e['type'] == 'schematic_net_label']
sheets = [e for e in b if e['type'] == 'schematic_sheet']
assert len(symbols) >= 162 and len(ports) >= 500 and labels and sheets
assert len({(e['schematic_sheet_id'], e['center']['x'], e['center']['y']) for e in symbols}) == len(symbols), 'Stacked symbols'
assert all(e.get('schematic_sheet_id') for e in symbols), 'Unassigned schematic symbols'
source_ports = {e['source_port_id']:e for e in b if e['type']=='source_port'}
net_ids = {}
for e in b:
    if e['type'] == 'source_trace':
        for p in e['connected_source_port_ids']:
            net_ids.setdefault(p,set()).update(e['connected_source_net_ids'])
traces = [e for e in b if e['type']=='schematic_trace']
def close(a,b):
    return abs(a['x']-b['x']) < 1e-8 and abs(a['y']-b['y']) < 1e-8
represented = set()
for p in ports:
    pid = p['source_port_id']
    if pid not in net_ids:
        continue
    key = source_ports[pid].get('subcircuit_connectivity_map_key')
    sheet = p['schematic_sheet_id']
    has_wire = any(t.get('schematic_sheet_id') == sheet and t.get('subcircuit_connectivity_map_key') == key
                   and any(close(p['center'],e['from']) or close(p['center'],e['to']) for e in t['edges']) for t in traces)
    has_label = any(l.get('schematic_sheet_id') == sheet and l.get('source_net_id') in net_ids[pid]
                    and close(p['center'],l['anchor_position']) for l in labels)
    assert has_wire or has_label, ('Functional pin missing from schematic', source_ports[pid])
    represented.add(pid)
components = {e['source_component_id']:e for e in b if e['type']=='source_component'}
nets = {e['source_net_id']:e for e in b if e['type']=='source_net'}
documented_case_lands = set()
for pid in net_ids.keys() - represented:
    p = source_ports[pid]
    comp = components[p['source_component_id']]
    assert comp['name'] in ('SW2','SW3') and p['pin_number'] == 5
    assert {nets[n]['name'] for n in net_ids[pid]} == {'GND'}
    documented_case_lands.add(pid)
assert len(represented) == 498 and len(documented_case_lands) == 2
assert any(e['type']=='schematic_text' and 'metal case land 5 is connected to GND' in e['text'] for e in b), 'Missing case-land annotation'
vias = [e for e in b if e['type'] == 'pcb_via']
assert len(vias) == 623 and all(abs(v['hole_diameter']-.3) < 1e-9 and abs(v['outer_diameter']-.45) < 1e-9 for v in vias)
result = {'status':'PASS', 'entry':name, 'schematicSymbols':len(symbols), 'schematicPorts':len(ports),
          'schematicLabels':len(labels), 'schematicSheets':len(sheets), 'contactPinsRepresented':len(represented),
          'documentedGroundedCaseLands':len(documented_case_lands), 'all500ConnectedPadsAccountedFor':True, 'nativeIssues':dict(issues),
          'allPcbCadAndElectricalSourceRecordsUnchanged':True,
          'sourcePortHintSetsUnchanged':True, 'sourcePortHintOrderIgnored':True,
          'pcbAndCadComparedWithoutAliasNormalization':True, 'viaHoleMm':.3, 'viaPadMm':.45,
          'routeCacheSha256':hashlib.sha256((root/'routing/final/cache.json').read_bytes()).hexdigest(),
          'circuitSha256':hashlib.sha256(candidate.read_bytes()).hexdigest()}
out = root / 'reports' / 'schematic'
out.mkdir(parents=True, exist_ok=True)
(out / f'{name}-verification.json').write_text(json.dumps(result,indent=2)+'\n')
print(json.dumps(result), flush=True)