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.20
License
unset
Stars
0

scripts/verify-upright-preview.py

from pathlib import Path
from collections import Counter
import json, hashlib, base64, io, numpy as np, trimesh
from scipy.spatial import cKDTree

root=Path(__file__).resolve().parent.parent
watch=json.loads((root/'dist/watch/circuit.json').read_text())
index=json.loads((root/'dist/index/circuit.json').read_text())
names={e['source_component_id']:e['name'] for e in watch if e['type']=='source_component'}
models=[e for e in watch if e['type']=='cad_component']
sha=lambda b:hashlib.sha256(b).hexdigest()
assert not any('error' in e['type'] for e in watch)
assert len([e for e in watch if e['type']=='pcb_board'])==1
assert len([e for e in watch if e['type']=='pcb_bend'])==2
assert len([e for e in watch if e['type']=='pcb_stiffener'])==6
pcb=[e for e in watch if e['type']=='pcb_component']
assert len(pcb)==162 and all(e['layer']=='top' for e in pcb)
assert 'VERIFIED_FOLDED_PCB' not in names.values()
assert not any(n in names.values() for n in ['PCB_J3','PCB_J4','PCB_J7','PCB_J8'])
def clean(v):
    if isinstance(v,dict):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]
    if isinstance(v,float):return round(v,9)
    return v
kinds=['pcb_board','pcb_bend','pcb_stiffener','pcb_component','pcb_port','pcb_smtpad','pcb_trace','pcb_via','pcb_plated_hole','pcb_hole','pcb_copper_pour','pcb_keepout','pcb_cutout']
for kind in kinds:
    a=Counter(json.dumps(clean(e),sort_keys=True) for e in index if e['type']==kind)
    b=Counter(json.dumps(clean(e),sort_keys=True) for e in watch if e['type']==kind)
    assert a==b,kind
required={'ORIGINAL_AMOLED_DISPLAY':'display','ENCLOSURE_WITH_INTEGRATED_PCB_POSTS':'case-body','REMOVABLE_TOP_BEZEL':'case-bezel','BEZEL_ADHESIVE_BONDLINE_RESERVATION':'bezel-bondline','REMOVABLE_MOTOR_SUPPORT_SHIM':'motor-support-shim','REMOVABLE_PCB_MOUNTING_CRADLE':'pcb-cradle','FRONT_LEFT_PCB_RETAINER':'retainer-front-left','FRONT_RIGHT_PCB_RETAINER':'retainer-front-right','FRONT_LEFT_M1_6_X_6_PH0_SCREW':'m1-6x6-iso7046','FRONT_RIGHT_M1_6_X_6_PH0_SCREW':'m1-6x6-iso7046','BATTERY_CABLE':'battery-harness','HAPTIC_CABLE':'haptic-harness','MOTOR_FACTORY_LEADS':'motor-factory-leads','BATTERY_NTC_CABLE':'battery-ntc-harness','COIL_NTC_CABLE':'coil-ntc-harness','BLACK_OPTICAL_LIGHT_SEAL':'optical-light-seal','SEPARATE_LED_AND_PHOTODIODE_WINDOWS':'optical-windows','FLUSH_SIDE_BUTTON_ACTUATOR':'button-actuator'}
for name,file in required.items():
    e=next(e for e in models if names[e['source_component_id']]==name)
    expected=(root/f'mechanical/{file}.ts').read_text().split('base64,')[1].split('"')[0]
    assert e['model_glb_url'].split('base64,')[1]==expected,name
scene=trimesh.load(root/'mechanical/upright-watch.glb',force='scene')
def load_asset(name):
    path=root/f'mechanical/{name}.glb'
    if path.exists():return trimesh.load(path,force='scene')
    embedded=(root/f'mechanical/{name}.ts').read_text().split('base64,')[1].split('"')[0]
    return trimesh.load(io.BytesIO(base64.b64decode(embedded)),file_type='glb',force='scene')
# Native GLTF -> native PCB XYZ -> case XYZ. The rigid frame transform is
# self-inverse. Manufacturing PCB coordinates and component layers are untouched.
def vertices(s,prefix=None,emitted=False,select=None):
    points=[]
    for node in s.graph.nodes_geometry:
        if prefix and not node.startswith(prefix):continue
        if select and not select(node):continue
        T,k=s.graph[node];v=trimesh.transform_points(s.geometry[k].vertices,T)
        if emitted:
            v=v[:,[0,2,1]];v[:,0]=-v[:,0]
            v[:,1]=-v[:,1]-47.19159126281738
            v[:,2]=-v[:,2]-7.4
        points.append(v)
    assert points,prefix
    return np.vstack(points)
def distance(a,b):
    return max(float(cKDTree(a).query(b)[0].max()),float(cKDTree(b).query(a)[0].max()))
# Float32 GLTF positions at the native frame's roughly 100mm coordinates can
# differ by nanometers after two exports. This bound is only serialization error.
tolerance=.000015
errors={}
for prefix,file in [('ORIGINAL_AMOLED_DISPLAY','assembly-display'),('ENCLOSURE_WITH_INTEGRATED_PCB_POSTS','case-body'),('REMOVABLE_TOP_BEZEL','case-bezel'),('FRONT_LEFT_PCB_RETAINER','retainer-front-left'),('FRONT_RIGHT_PCB_RETAINER','retainer-front-right'),('REMOVABLE_PCB_MOUNTING_CRADLE','pcb-cradle'),('REMOVABLE_MOTOR_SUPPORT_SHIM','motor-support-shim'),('BATTERY_CABLE','battery-harness'),('HAPTIC_CABLE','haptic-harness'),('BATTERY_NTC_CABLE','battery-ntc-harness'),('COIL_NTC_CABLE','coil-ntc-harness'),('MOTOR_FACTORY_LEADS','motor-factory-leads'),('BLACK_OPTICAL_LIGHT_SEAL','optical-light-seal'),('SEPARATE_LED_AND_PHOTODIODE_WINDOWS','optical-windows')]:
    a=vertices(scene,prefix,True);b=vertices(load_asset(file))
    err=distance(a,b);assert err<tolerance,(prefix,err);errors[prefix]=err
pcb_names={names[e['source_component_id']] for e in pcb}
def pcb_mesh(node):
    return node.startswith('Box0_') or node.startswith('pcb_stiffener_') or any(node==n or node.startswith(n+'_') for n in pcb_names)
a=vertices(scene,emitted=True,select=pcb_mesh)
b=vertices(trimesh.load(root/'mechanical/upright-pcb.glb',force='scene'))
err=distance(a,b);assert err<tolerance,('NATIVE_FOLDED_PCB',err);errors['NATIVE_FOLDED_PCB']=err
h=json.loads((root/'mechanical/mount-hardware.json').read_text())
for mount in h['mounts']:
    offset=np.array([mount['x'],mount['y'],h['headTopZMM']])
    a=vertices(scene,mount['name'],True);b=vertices(trimesh.load(root/'mechanical/m1-6x6-iso7046.glb',force='scene'))+offset
    err=distance(a,b);assert err<tolerance,(mount['name'],err);errors[mount['name']]=err
inspection=json.loads((root/'dist/assembly/circuit.json').read_text())
assert not any(e.get('name') in ['ORIGINAL_AMOLED_DISPLAY','REMOVABLE_TOP_BEZEL','BEZEL_ADHESIVE_BONDLINE_RESERVATION'] for e in inspection)
assert len([e for e in inspection if e['type']=='pcb_bend'])==2
v=vertices(scene,emitted=True);dims=v.max(0)-v.min(0)
assert np.all(dims<=[39.000015,50.500015,14.500015]),dims
sensor=vertices(scene,'U14',True);assert sensor[:,2].max()<-7.49
report={'status':'PASS','cadComponents':len(models),'nativeErrors':0,'nativePcbBoards':1,'nativePcbComponents':162,'nativePcbBends':2,'nativePcbStiffeners':6,'pcbIsImportedGlb':False,'allPcbGeometryMatchesManufacturingCircuit':True,'circuitSha256':sha((root/'dist/watch/circuit.json').read_bytes()),'glbSha256':sha((root/'mechanical/upright-watch.glb').read_bytes()),'maximumModelVertexImportErrorsMM':errors,'meshComparisonToleranceMM':tolerance,'modeledOutsideDimensionsMM':dims.tolist(),'sensorBoundsInCaseMM':[sensor.min(0).tolist(),sensor.max(0).tolist()],'selfContainedCableModelCount':5,'cablesUseActualPcbConnectorIdentities':['J3','J4','J7','J8'],'embeddedModelsMatchSource':True,'displayIsAboveElectronicsInCaseFrame':True,'sensorIsOnWristSide':True,'referencePattern':'https://tscircuit.com/muse/book-reading-clip-lamp#3d'}
(root/'reports/emitted-preview.json').write_text(json.dumps(report,indent=2)+'\n')
(root/'reports/native-assembly.json').write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps(report,indent=2))