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/open-assembly.py

"""Open plate/base assembly with four screws and accessible control extensions."""
from pathlib import Path
import json, math
import cadquery as cq
import trimesh
from hardware import screw
from build import (ROOT, OUT, P, board, j, sources, mounts, keys, base, pcb,
                   pcb_top, plate_top, plate_bottom, rect, cyl, mesh, scene)

open_scene=scene.copy()
for node in list(open_scene.graph.nodes_geometry):
    if node in ['top','switch-plate'] or node.startswith(('case-M3','plate-M2')):
        _,key=open_scene.graph[node];open_scene.delete_geometry(key)
assembly=cq.Assembly(name='Macro6-open-assembly')
assembly.add(base,name='bottom',color=cq.Color(.125,.153,.188))
assembly.add(pcb,name='PCB-reference',color=cq.Color(.05,.30,.20))
plate=rect(109.6,67.6,plate_bottom,1.5,r=3)
for x,y in keys:
    plate=plate.cut(rect(14.1,14.1,plate_bottom-.1,2,x,y))
    plate=plate.cut(rect(4.2,4.2,plate_bottom-.1,2,x+10.5,y+5))
plate=plate.cut(rect(16,20,plate_bottom-.1,2,43,0))
plate=plate.cut(rect(18,17,plate_bottom-.1,2,-51,0))
plate=plate.cut(rect(9,6,plate_bottom-.1,2,38,-27))
for x,y in mounts:
    plate=plate.union(cyl(x,y,pcb_top,3.5,plate_bottom-pcb_top+.1))
    plate=plate.cut(cyl(x,y,pcb_top-.1,1.7,8))
plate=plate.cut(rect(3.2,1.6,pcb_top-.1,2.6,50,26))
for x,y in [(36,-19),(45,-19)]:
    plate=plate.cut(cyl(x,y,plate_bottom-.1,1.45,2))
assert plate.val().isValid() and len(plate.val().Solids())==1
for other in [base,pcb]:assert plate.intersect(other).val().Volume()<1e-5
assembly.add(plate,name='open-plate',color=cq.Color(.408,.494,.569))
pm=mesh(plate);pm.visual.face_colors=[104,126,145,255];open_scene.add_geometry(pm,node_name='open-plate')

solids={};colors={};screws=[]
for i,xy in enumerate(mounts):
    name=f'open-M3x10-{i+1}'
    s=screw({'xy':xy,'seat_z':plate_top,'diameter':3,'length':10,'head_diameter':6,'head_height':2.4})
    screws.append(name);solids[name]=s;colors[name]=[171,176,184,255]

# Each pushbutton uses an underside captive stem and a separate press-fit cap.
for label,x,color in [('reset',36,[205,83,75,255]),('boot',45,[76,151,196,255])]:
    y=-19
    stem=cyl(x,y,10.65,.8,.7).union(cyl(x,y,11.1,1.2,4.0)).union(cyl(x,y,11.15,2.1,.6))
    cap=cyl(x,y,14.0,2.6,1.8).edges('>Z').fillet(.4)
    cap=cap.cut(cyl(x,y,13.9,1.175,1.2))
    solids[label+'-stem']=stem;solids[label+'-cap']=cap
    colors[label+'-stem']=[189,197,205,255];colors[label+'-cap']=color

# The power switch slides along Y. A short socket/paddle extends its existing tab.
sy=1.064
paddle=rect(4.9,5.4,9.9,3.35,-53.15,sy,r=.3)
paddle=paddle.cut(rect(1.9,2.6,10.50,2.1,-51.3,sy))
paddle=paddle.union(cyl(-54.4,sy,12.8,1.6,3.4))
paddle=paddle.union(cyl(-54.4,sy,15.8,2.5,1.2).edges('>Z').fillet(.35))
# Compliant entrance ribs lightly grip the plastic tab; qualify this fit on a print.
for direction in [-1,1]:
    paddle=paddle.cut(rect(2.4,.3,9.8,3.6,-51.9,sy+direction*2.0))
    paddle=paddle.union(rect(.6,.4,10.9,1.2,-50.95,sy+direction*1.15))
solids['power-slider-extension']=paddle;colors['power-slider-extension']=[222,165,66,255]

for name,solid in solids.items():
    assert solid.val().isValid() and len(solid.val().Solids())==1,name
    assembly.add(solid,name=name,color=cq.Color(*(v/255 for v in colors[name][:3])))
    m=mesh(solid);m.visual.face_colors=colors[name];open_scene.add_geometry(m,node_name=name)

# Include the styled keycap/knob references in the STEP as well.
# Native curved BRep references come from the unchanged generation script.
from build import assembly as closed_assembly
for child in closed_assembly.children:
    if child.name.startswith('keycap-reference-') or child.name.startswith('knob-reference'):
        assembly.add(child.obj,name=child.name,color=child.color)
assembly.save(str(OUT/'open-assembly.step'))

for name,solid in {'open-plate':plate,**{n:s for n,s in solids.items() if n not in screws}}.items():
    cq.exporters.export(solid,str(OUT/(name+'.step')))
    # Caps are printed with their top face on the bed; stems stand upright.
    printing=solid.rotate((0,0,0),(1,0,0),180) if name.endswith('-cap') or name=='open-plate' else solid
    b=printing.val().BoundingBox();printing=printing.translate((-(b.xmin+b.xmax)/2,-(b.ymin+b.ymax)/2,-b.zmin))
    cq.exporters.export(printing,str(OUT/(name+'.stl')),tolerance=.06,angularTolerance=.15)
    assert trimesh.load(OUT/(name+'.stl'),force='mesh').is_watertight,name

open_scene.export(str(OUT/'open-preview.glb'))
assets={'open-plate':['open-plate'],'open-hardware':screws,
        'control-extensions':[n for n in solids if n not in screws]}
for name,nodes in assets.items():
    asset=trimesh.Scene()
    for node in nodes:
        placement,key=open_scene.graph[node];m=open_scene.geometry[key].copy();m.apply_transform(placement)
        m.apply_translation([0,0,-7.8]);asset.add_geometry(m,node_name=node)
    asset.export(str(ROOT/'enclosure/viewer'/(name+'.glb')))
report={'layout':'open plate and base, no upper housing','screws':'4 x M3x10 pan head, nominal 6 mm head diameter',
        'pcb_unchanged':True,'plate_top_mm':plate_top,'pushbutton_tip_gap_mm':.1,
        'pushbutton_cap_stop_travel_mm':.4,'pushbutton_guide_diametral_clearance_mm':.5,
        'power_slider_axis':'Y','power_slider_entrance_rib_interference_per_side_mm':.05,
        'physical_fit_verified':False,'printed_parts':[n for n in solids if n not in screws]}
(OUT/'open-assembly-checks.json').write_text(json.dumps(report,indent=2)+'\n')
print('Open assembly generated',flush=True)
from viewer_materials import prepare_viewer_materials
prepare_viewer_materials(ROOT/'enclosure/viewer')