gokul/macro-six-usbc
USB-powered ATmega32U4 controller board with protected/switched 5 V power, six tactile keys, rotary encoder, reset/boot/ISP interfaces, power LED, and six daisy-chained SK6812 RGB LEDs driven through MOSFETs.
- Version
- 0.1.6
- License
- MIT
- Stars
- 0
enclosure/check-open-fit.py
"""Validate changed open plate, fasteners and control movement against the PCB."""
from pathlib import Path
import json,math
import cadquery as cq
ROOT=Path(__file__).resolve().parents[1];OUT=ROOT/'enclosure/exports'
j=json.loads((ROOT/'outputs/release/board.circuit.json').read_text())
src={e['source_component_id']:e for e in j if e['type']=='source_component'}
pcbs={e['pcb_component_id']:e for e in j if e['type']=='pcb_component'}
plate=cq.importers.importStep(str(OUT/'open-plate.step'))
old=cq.importers.importStep(str(OUT/'switch-plate.step'))
base=cq.importers.importStep(str(OUT/'bottom.step'))
report={'components':[],'unexpected_interferences':[],'motion_checks':[],'key_plate_local_geometry_unchanged':[]}
def box(spec):
return cq.Workplane('XY').box(*spec['size'],centered=(True,True,False)).translate(spec['position'])
def overlap(a,b):
a=a.val().BoundingBox();b=b.val().BoundingBox()
return all(min(getattr(a,k+'max'),getattr(b,k+'max'))-max(getattr(a,k+'min'),getattr(b,k+'min'))>1e-5 for k in ['x','y','z'])
for e in j:
if e['type']!='cad_component':continue
name=src[e['source_component_id']]['name'];p=pcbs[e['pcb_component_id']]
if name in [f'SW{i}' for i in range(1,7)]:
x=e['position']['x'];y=e['position']['y'];region=box({'size':(16.2,16.2,20),'position':(x,y,8.6)})
a=plate.intersect(region);b=old.intersect(region)
delta=a.cut(b).val().Volume()+b.cut(a).val().Volume();assert delta<1e-5,(name,delta)
report['key_plate_local_geometry_unchanged'].append(name);continue
model='Q_RGB' if name=='Q_MAIN' else 'SW_BOOT' if name=='SW_RESET' else 'U_RGB6' if name.startswith('U_RGB') else name
path=ROOT/'work/enclosure-models'/(model+'.step')
if path.exists():
solid=cq.importers.importStep(str(path));o=e.get('model_origin_position',{})
solid=solid.translate(tuple(-o.get(k,0) for k in ['x','y','z']))
solid=solid.rotate((0,0,0),(0,0,1),e['rotation'].get('z',0)).translate((e['position']['x'],e['position']['y'],8.6))
else:solid=box({'size':(p['width'],p['height'],2),'position':(p['center']['x'],p['center']['y'],8.6)})
volume=solid.intersect(plate).val().Volume() if overlap(solid,plate) else 0
report['components'].append({'name':name,'new_plate_intersection_mm3':round(volume,7)})
if volume>1e-5:report['unexpected_interferences'].append({'name':name,'volume_mm3':volume})
print('Checked',name,round(volume,7),flush=True)
for label in ['reset','boot']:
stem=cq.importers.importStep(str(OUT/(label+'-stem.step')));cap=cq.importers.importStep(str(OUT/(label+'-cap.step')))
for dz in [0,-.1,-.3,-.4,.35]:
# Stem/cap press fit and switch contact are intentional; fixed case must remain clear.
volume=sum(s.translate((0,0,dz)).intersect(fixed).val().Volume() for s in [stem,cap] for fixed in [base,plate] if overlap(s.translate((0,0,dz)),fixed))
assert volume<1e-5,(label,dz,volume)
report['motion_checks'].append({'control':label,'vertical_range_mm':[-.4,.35],'case_clearance_pass':True})
power=cq.importers.importStep(str(OUT/'power-slider-extension.step'))
for dy in [0,-.5,-1,-1.5,-2,-2.3]:
moved=power.translate((0,dy,0));volume=sum(moved.intersect(fixed).val().Volume() for fixed in [base,plate] if overlap(moved,fixed))
assert volume<1e-5,('power',dy,volume)
report['motion_checks'].append({'control':'power','slider_sweep_mm':2.3,'case_clearance_pass':True})
report['screw_tips_mm_above_external_floor']=3.6
report['blind_pilot_floor_mm']=1.2
report['screw_thread_engagement_mm']=3.4
report['physical_fit_verified']=False
(OUT/'open-component-fit.json').write_text(json.dumps(report,indent=2)+'\n')
assert not report['unexpected_interferences'],report['unexpected_interferences']
print('Open plate and control motion checks passed',flush=True)