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)