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/prepare-pcb-mounts.py
"""Four case-integrated supports and a cradle with two purchased screw mounts."""
from pathlib import Path
import json, base64
import numpy as np
import trimesh
from mount_mesh_utils import printable_mesh
from trimesh.visual.material import PBRMaterial
root = Path(__file__).resolve().parent.parent
hardware=json.loads((root/'mechanical/mount-hardware.json').read_text())
supports, cradle, retainers, screws = trimesh.Scene(), trimesh.Scene(), trimesh.Scene(), trimesh.Scene()
plastic = PBRMaterial(baseColorFactor=[70,84,105,255],roughnessFactor=.65)
bar_material = PBRMaterial(baseColorFactor=[120,170,210,255],roughnessFactor=.55)
metal = PBRMaterial(baseColorFactor=[175,180,185,255],roughnessFactor=.3,metallicFactor=.9)
items = []
def add(scene, mesh, name, material, role):
mesh.visual = trimesh.visual.TextureVisuals(material=material)
scene.add_geometry(mesh,geom_name=name,node_name=name)
items.append({'name':name,'role':role,'boundsMM':mesh.bounds.tolist()})
def box(x0,x1,y0,y1,z0,z1):
return trimesh.creation.box(extents=[x1-x0,y1-y0,z1-z0],
transform=trimesh.transformations.translation_matrix([(x0+x1)/2,(y0+y1)/2,(z0+z1)/2]))
def cylinder(radius,z0,z1,x,y):
mesh=trimesh.creation.cylinder(radius=radius,height=z1-z0,sections=48)
mesh.apply_translation([x,y,(z0+z1)/2]);return mesh
# Two accessible front screws retain the cradle; rear posts are solid locators.
# The support faces retain the original 0.65mm FR4/adhesive/flex stack.
for side in [-1,1]:
for end,x,y,toe in [('FRONT',16.5,-20.5,-16.0),('REAR',16.3,22.0,12.7)]:
x *= side
label=f'{end}_{"LEFT" if side<0 else "RIGHT"}'
radius=1.5 if end=='FRONT' else 1.0
add(supports,cylinder(radius if end=='FRONT' else .7,-9.12,-9.02,x,y),label+'_FLOOR_ANCHOR',plastic,'integrated support')
bore_bottom=hardware['pilotBottomZMM'] if end=='FRONT' else -4.75
add(supports,cylinder(radius,-9.02,bore_bottom,x,y),label+'_FLOOR_POST',plastic,'integrated support')
if end=='FRONT':
depth=hardware['postTopZMM']-bore_bottom
boss=trimesh.creation.annulus(r_min=.65,r_max=radius,height=depth,sections=48)
boss.apply_translation([x,y,(hardware['postTopZMM']+bore_bottom)/2])
add(supports,boss,label+'_BLIND_PILOT_BOSS',plastic,'integrated support')
else:
add(supports,cylinder(radius,-4.75,-1.35,x,y),label+'_SOLID_LOCATOR_POST',plastic,'integrated support')
if end=='FRONT':
seat=trimesh.creation.annulus(r_min=.95,r_max=radius,height=1.2,sections=48)
seat.apply_translation([x,y,-.75])
else:
seat=cylinder(radius,-1.35,-.15,x,y)
add(cradle,seat,label+'_CRADLE_SEAT',plastic,'removable cradle')
direction=1 if toe>y else -1
lo,hi=sorted([y+direction*.75,toe])
# Deep supports remain outside the battery envelope, with a 0.65mm
# bearing overlap under each supported PCB edge.
xa,xb=sorted([side*15.85,side*17.95])
add(cradle,box(xa,xb,lo,hi,-1.35,-.15),label+'_STIFFENER_LEDGE',plastic,'intentional PCB support contact')
xa,xb=sorted([side*16.1,side*17.3])
rlo,rhi=sorted([y+direction*(1.65 if end=='FRONT' else 1.15),toe])
add(cradle,box(xa,xb,rlo,rhi,-6.6,-1.35),label+'_SUPPORT_RIB',plastic,'removable cradle')
add(cradle,box(*sorted([side*16.75,side*17.95]),toe-.6,toe+.6,-.15,.25),label+'_SIDE_STOP',plastic,'Stiffener locator, nominal 0.25mm side gap')
sy0,sy1=(-18.19159126281738,-16.99159126281738) if end=='FRONT' else (13.60840873718262,14.80840873718262)
# Rear pocket ends retain the 0.3mm rear gap; front travel
# clearance is 0.3mm, sides 0.25mm. Fit sample checks these interfaces.
add(cradle,box(*sorted([side*15.85,side*17.95]),sy0,sy1,-.15,.25),label+'_END_STOP',plastic,'Stiffener locator')
if end=='REAR':
# Integrated rear pockets replace tiny tapped posts and loose tabs.
add(cradle,cylinder(1.0,-.15,1.9,x,y),label+'_POCKET_PILLAR',plastic,'solid rear retention pillar')
add(cradle,box(*sorted([side*15.85,side*17.95]),12.4,20.0,.7,1.9),label+'_POCKET_ROOF',plastic,'rear PCB retention, 0.2mm nominal vertical gap')
add(cradle,box(x-.6,x+.6,19.4,22.3,.7,1.9),label+'_POCKET_BEAM',plastic,'1.2mm rear cantilever beam')
continue
# Removable front arm with flush M1.6 countersunk screw. The modeled
# screw head bounds reserve the ISO 7046 diameter/depth, not threads.
stem0,stem1=sorted([y+direction*1.0,toe+direction*.3])
add(retainers,box(*sorted([side*15.85,side*17.95]),stem0,stem1,.55,1.75),label+'_REMOVABLE_RETAINER',bar_material,'removable retainer')
eye=trimesh.creation.annulus(r_min=.95,r_max=1.6,height=1.2,sections=48)
eye.apply_translation([x,y,1.15])
# Countersinks are cut after all primitives are constructed.
add(retainers,eye,label+'_COUNTERSUNK_SCREW_EYE',bar_material,'removable retainer')
spacer=trimesh.creation.annulus(r_min=.95,r_max=1.5,height=.7,sections=48)
spacer.apply_translation([x,y,.2])
add(retainers,spacer,label+'_CRADLE_CLAMP_SPACER',bar_material,'removable retainer')
add(retainers,box(*sorted([side*15.85,side*16.5]),toe-.3,toe+.3,.5,.55),label+'_EDGE_CONTACT_PAD',bar_material,'intentional PCB retention contact')
# Detailed purchased screws are generated separately from the shared
# hardware definition; they are never part of the printable union.
for side in [-1,1]:
add(cradle,box(*sorted([side*16.75,side*17.95]),-20.8,21.2,-1.35,-.15),f'CRADLE_SIDE_RAIL_{side}',plastic,'removable cradle')
for name,y in [('FRONT',-20.5),('REAR',20.5)]:
add(cradle,box(-17.95,17.95,y-.6,y+.6,-1.35,-.15),f'CRADLE_{name}_CROSSBAR',plastic,'removable cradle')
# Open cable passage below the main stiffener, away from its corner supports.
rail=cradle.geometry['CRADLE_SIDE_RAIL_1'];rail.merge_vertices(merge_tex=True,merge_norm=True)
opening=box(16.3,18.4,-17.6,-11.5,-1.6,.1)
notched=trimesh.boolean.difference([rail,opening],engine='manifold');notched.visual=rail.visual
cradle.geometry['CRADLE_SIDE_RAIL_1']=notched
# Captive inline-connector pocket above the motor. The main FR4 stiffener
# closes its top when assembled; wires enter through the middle of both ends.
add(cradle,box(-17.225,-10.175,-3.6,1.9,-5.1,-3.9),'INLINE_CONNECTOR_POCKET_BED',plastic,'inline connector support contact')
for name,x0,x1 in [('LEFT',-17.225,-16.025),('RIGHT',-11.375,-10.175)]:
add(cradle,box(x0,x1,-10.1,1.9,-3.9,-.3),f'INLINE_CONNECTOR_{name}_SIDE',plastic,'inline connector locator')
for end,y0,y1 in [('FRONT',-10.1,-8.9),('REAR',.7,1.9)]:
for side,x0,x1 in [('LEFT',-16.3,-14.8),('RIGHT',-12.6,-11.1)]:
add(cradle,box(x0,x1,y0,y1,-3.9,-.3),f'INLINE_CONNECTOR_{end}_{side}_STOP',plastic,'inline connector end stop; central wire exit remains open')
add(cradle,box(-17.225,-16.025,y0,y1,-3.9,-.15),f'INLINE_CONNECTOR_{end}_HANGER',plastic,'pocket attachment to existing cradle rail')
for side in [-1,1]:
hole=cylinder(.95,-7.0,2.0,side*16.5,-20.5)
countersink=trimesh.creation.cone(radius=1.5,height=1.5,sections=48)
countersink.apply_transform(trimesh.transformations.rotation_matrix(np.pi,[1,0,0]))
countersink.apply_translation([side*16.5,-20.5,1.49])
for scene in [cradle]:
for key,mesh in list(scene.geometry.items()):
tool_bounds=hole.bounds.copy()
if scene is retainers:
tool_bounds[0]=np.minimum(tool_bounds[0],countersink.bounds[0])
tool_bounds[1]=np.maximum(tool_bounds[1],countersink.bounds[1]);tool_bounds[1,2]=2.0
if np.any(mesh.bounds[1]<tool_bounds[0]) or np.any(mesh.bounds[0]>tool_bounds[1]):continue
if key.endswith(('_CRADLE_CLAMP_SPACER','_CRADLE_SEAT')):continue
material=mesh.visual.material;mesh.merge_vertices(merge_tex=True,merge_norm=True)
assert mesh.is_volume, (key,mesh.is_watertight,mesh.volume)
cut=trimesh.boolean.difference([mesh]+([] if key.endswith('_COUNTERSUNK_SCREW_EYE') else [hole])+([countersink,cylinder(1.5,1.49,2.0,side*16.5,-20.5)] if scene is retainers else []),engine='manifold')
cut.visual=trimesh.visual.TextureVisuals(material=material);scene.geometry[key]=cut
# Export each finished front arm as one boolean-united manufacturing solid,
# avoiding coplanar primitive seams in small countersink features.
finished=trimesh.Scene()
for side in [-1,1]:
label='FRONT_'+('LEFT' if side<0 else 'RIGHT')
raw=[]
for key,mesh in retainers.geometry.items():
if key.startswith(label):
m=mesh.copy();m.merge_vertices(merge_tex=True,merge_norm=True);raw.append(m)
united=trimesh.boolean.union(raw,engine='manifold')
hole=cylinder(.95,-7.0,2.0,side*16.5,-20.5)
cone=trimesh.creation.cone(radius=1.5,height=1.5,sections=48)
cone.apply_transform(trimesh.transformations.rotation_matrix(np.pi,[1,0,0]));cone.apply_translation([side*16.5,-20.5,1.49])
solid=trimesh.boolean.difference([united,hole,cone,cylinder(1.5,1.49,2.0,side*16.5,-20.5)],engine='manifold')
assert solid.is_volume and solid.body_count==1,label
solid=printable_mesh(solid)
add(finished,solid,label+'_COMPLETE_RETAINER',bar_material,'finished removable front arm')
retainers=finished
for side in ['LEFT','RIGHT']:
part=trimesh.Scene()
key='FRONT_'+side+'_COMPLETE_RETAINER'
part.add_geometry(retainers.geometry[key],geom_name=key,node_name=key)
binary=part.export(file_type='glb');name='retainer-front-'+side.lower()
(root/f'mechanical/{name}.glb').write_bytes(binary)
(root/f'mechanical/{name}.ts').write_text('export default "data:model/gltf-binary;base64,'+base64.b64encode(binary).decode()+'"\n')
for name,scene in [('pcb-supports',supports),('pcb-cradle',cradle),('pcb-retainers',retainers)]:
binary=scene.export(file_type='glb');(root/f'mechanical/{name}.glb').write_bytes(binary)
(root/f'mechanical/{name}.ts').write_text('export default "data:model/gltf-binary;base64,'+base64.b64encode(binary).decode()+'"\n')
report={'version':'0.2.10','mountType':'Four integrated posts, a 1.2mm rail/ledge cradle, two integrated rear pockets and two removable front retaining arms. PCB holes are unnecessary.',
'fasteners':{'reference':'https://www.ettinger.de/en/product-datasheet/d38aa1001cc64e52632fd7c2c5e78a17/create','quantity':2,'thread':'M1.6 x 0.35','type':'90deg countersunk, ISO 7046 dimensional envelope','overallLengthMM':6,'headDiameterEnvelopeMM':3.0,'headHeightEnvelopeMM':.96,'drive':'Phillips PH0','pilotDepthMM':3.8,'minimumFinishedThreadDepthMM':3.1,'screwTipClearanceMM':.9,'postPilotDiameterMM':1.3,'retainerClearanceDiameterMM':1.9},
'pcbSupportZMM':-.15,'pcbRetentionZMM':.5,'capturedStiffenedPcbStackMM':.65,'nominalSideLocatorGapMM':.25,'nominalFrontLocatorGapMM':.3,'nominalRearLocatorGapMM':.3,'rearPocketVerticalGapMM':.2,'railWidthAndThicknessMM':[1.2,1.2],'retainerThicknessMM':1.2,
'mountItems':items,'assembly':'Fit the stiffened main panel into the rear pockets before completing the folds. Lower the populated PCB/cradle onto the four posts, then install the two front arms and M1.6 countersunk screws. The fixed posts leave the folded frame unobstructed during insertion. Finish the empty case pilots to 3.8mm depth and use an M1.6 x 0.35 bottoming tap to finish at least 3.1mm of thread; clean all debris before assembly. Install the top bezel and display last.',
'qualification':'Prototype CAD mount. Strength, thread material, clamp preload, manufacturing tolerances and repeated assembly require physical qualification.'}
(root/'reports/pcb-mount-design.json').write_text(json.dumps(report,indent=2)+'\n')
print('Generated thicker cradle, rear pockets and two front screw mounts; clearance audit required.')