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-fit.py

"""Independent STEP-solid checks against actual supplier geometry where available."""
from pathlib import Path
import hashlib,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())
base=json.loads((OUT/"validation.json").read_text())
pcb_top=base["parameters_mm"]["board_bottom"]+1.6
src={e["source_component_id"]:e for e in j if e["type"]=="source_component"}
pcb={e["pcb_component_id"]:e for e in j if e["type"]=="pcb_component"}
parts={n:cq.importers.importStep(str(OUT/(n+".step"))) for n in ["bottom","top","switch-plate"]}
cache={}
report={"method":"Conservative supplier STEP bounds, followed by exact BRep intersections where bounds meet case material; conservative 2 mm-tall footprint boxes for unmodelled passives", "components":[],"unexpected_interferences":[],"key_clip_contacts":[],"access":[],"fasteners":[]}
hardware=[]
for x,y in base["mounts_xy"]:
    hardware.append(cq.Workplane("XY").circle(1.5).extrude(16).translate((x,y,4)))
    hardware.append(cq.Workplane("XY").circle(3).extrude(2.4).translate((x,y,20)))
for x,y in base["plate_mounts_xy"]:
    hardware.append(cq.Workplane("XY").circle(1).extrude(10).translate((x,y,10)))
    hardware.append(cq.Workplane("XY").circle(2).extrude(1.6).translate((x,y,20)))
    hardware.append(cq.Workplane("XY").polygon(6,4/math.cos(math.pi/6)).extrude(1.6).translate((x,y,10.1)))

def bbox_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-6 for k in ["x","y","z"])

def box(w,h,d,x,y,z):
    return cq.Workplane("XY").box(w,h,d,centered=(True,True,False)).translate((x,y,z))

for e in j:
    if e["type"]!="cad_component":continue
    source=src[e["source_component_id"]];name=source["name"];p=pcb[e["pcb_component_id"]]
    if p.get("do_not_place"):continue
    model_name="SW6" if name in [f"SW{i}" for i in range(1,7)] else name
    if name=="Q_MAIN":model_name="Q_RGB"
    if name=="SW_RESET":model_name="SW_BOOT"
    if name.startswith("U_RGB"):model_name="U_RGB6"
    file=ROOT/"work/enclosure-models"/(model_name+".step")
    if file.exists():
        if model_name not in cache:cache[model_name]=cq.importers.importStep(str(file))
        s=cache[model_name];origin=e.get("model_origin_position",{})
        s=s.translate(tuple(-origin.get(k,0) for k in ["x","y","z"]))
        s=s.rotate((0,0,0),(0,0,1),e["rotation"].get("z",0))
        pos=e["position"];s=s.translate((pos["x"],pos["y"],pcb_top))
        method="supplier_STEP"
    else:
        s=box(p["width"],p["height"],2,p["center"]["x"],p["center"]["y"],pcb_top)
        method="conservative_passive_envelope"
    row={"name":name,"geometry":method,"intersections_mm3":{}}
    b=s.val().BoundingBox();row["z_bounds_mm"]=[round(b.zmin,5),round(b.zmax,5)]
    envelope=box(b.xlen,b.ylen,b.zlen,(b.xmin+b.xmax)/2,(b.ymin+b.ymax)/2,b.zmin)
    for part,t in parts.items():
        if not bbox_overlap(s,t):v=0
        else:
            possible=envelope.intersect(t).val().Volume()
            v=0 if possible<1e-5 else possible if method=="conservative_passive_envelope" else s.intersect(t).val().Volume()
        row["intersections_mm3"][part]=round(v,7)
        if v>1e-4:
            clash={"component":name,"part":part,"volume_mm3":round(v,7)}
            ib=s.intersect(t).val().BoundingBox()
            clash["bounds"]={k:[round(getattr(ib,k+"min"),5),round(getattr(ib,k+"max"),5)] for k in ["x","y","z"]}
            if name in [f"SW{i}" for i in range(1,7)] and part=="switch-plate":
                report["key_clip_contacts"].append(clash)
            else:report["unexpected_interferences"].append(clash)
    hv=sum(s.intersect(t).val().Volume() for t in hardware if bbox_overlap(s,t))
    row["fastener_intersection_mm3"]=round(hv,7)
    if hv>1e-4:report["unexpected_interferences"].append({"component":name,"part":"fasteners","volume_mm3":round(hv,7)})
    report["components"].append(row)
    print(name,method,row["intersections_mm3"],flush=True)

# Straight tool approach, the PCB pads/button top themselves are intentionally excluded.
for name,x,y in [("reset",36,-19),("boot",45,-19)]:
    probe=cq.Workplane("XY").circle(1.2).extrude(15).translate((x,y,pcb_top+3))
    volumes={n:round(probe.intersect(parts[n]).val().Volume(),7) for n in ["top","switch-plate"]}
    report["access"].append({"name":name,"tool_diameter_mm":2.4,"intersection_mm3":volumes})
    assert not any(v>1e-5 for v in volumes.values())
isp=box(8,5,12,38,-27,pcb_top+.1)
assert isp.intersect(parts["switch-plate"]).val().Volume()<1e-5
report["access"].append({"name":"ISP","plate_opening_pass":True,"lid_removal_required":True})
plug=box(16,15,8,15,40,pcb_top+1.2-4)
for n in ["bottom","top"]:
    v=plug.intersect(parts[n]).val().Volume()
    report["access"].append({"name":"USB_cable_16x8mm_envelope","part":n,"intersection_mm3":round(v,7)})
    assert v<1e-5
for x,y in base["switch_centers_xy"]:
    sweep=box(18.5,18.5,15,x,y,pcb_top+6.5)
    assert sum(sweep.intersect(t).val().Volume() for t in hardware if bbox_overlap(sweep,t))<1e-5
report["keycap_to_fastener_clearance_pass"]=True
knob_sweep=cq.Workplane("XY").circle(9).extrude(14.5).translate((43,0,20.5))
assert knob_sweep.intersect(parts["top"]).val().Volume()<1e-5
report["knob_push_envelope"]={"diameter_mm":18,"push_travel_mm":0.5,"minimum_lid_gap_mm":0.5,"passed":True}

report["fasteners"]=[
    {"type":"M3x16 pan head","quantity":4,"base_thread_engagement_mm":3.0,"tip_z_mm":4.0,"blind_pilot_bottom_z_mm":1.2},
    {"type":"M2x10 pan head + M2 hex nut","quantity":len(base["plate_mounts_xy"]),"tip_z_mm":10.0,"pcb_top_z_mm":pcb_top,"nominal_tip_to_pcb_gap_mm":1.4},
]
report["pcb_sha256"]=hashlib.sha256((ROOT/"outputs/release/board.circuit.json").read_bytes()).hexdigest()
report["print_mesh_sha256"]={name:hashlib.sha256((OUT/(name+".stl")).read_bytes()).hexdigest() for name in parts}
report["physical_fit_verified"]=False
report["passed_without_unexpected_interference"]=not report["unexpected_interferences"]
(OUT/"component-fit.json").write_text(json.dumps(report,indent=2)+"\n")
assert not report["unexpected_interferences"],report["unexpected_interferences"]
print("Supplier/component checks completed; snap-clip contacts need coupon verification.")