// Provisional enclosure, mm. See README.md before printing or ordering hardware. // Saved carrier coordinates: X right, Y toward USB, Z above base floor. // Display in conventional top view via world(): X right, cable/DB9 end up. part = "assembly"; // assembly, exploded, base, lid, plunger, plungers, labels, layout, check show_references = true; // preview only: never included in STL emboss = true; $fn = 48; eps = 0.02; /* [Print and enclosure] */ wall = 2.4; floor_t = 2.4; lid_t = 2.4; side_extra = 12; // width added at EACH side of the 82 mm carrier end_clearance = 1; // inner wall to carrier at cable ends corner_r = 3; seam_clearance = 0.25; label_raise = 0.6; /* [Carrier and module stack: heights NOT independently measured] */ carrier_t = 1.6; // saved PCB setting carrier_boss_h = 5; // directly supports carrier underside; solder/prong clearance above floor module_gap = 10; // brass body height: actual carrier TOP to module UNDERSIDE esp_t = 1.6; // provisional rs232_t = 1.57; // STEP oled_t = 1.6; // provisional lid_above_carrier = 26; // lid underside relative to carrier top split_above_carrier = 6; /* [Heat inserts: EXAMPLE pocket sizes, select for actual inserts] */ m3_insert_d = 4.2; m3_insert_depth = 4; m2_insert_d = 3.2; m2_insert_depth = 3; insert_lead = 0.3; carrier_boss_d = 8; case_boss_d = 9; m3_clearance = 3.4; m2_clearance = 2.3; m3_head_d = 6.4; m3_head_depth = 2.8; // Insert bosses stop at carrier underside; brass male/female bodies sit ABOVE it. daughter_supports = true; daughter_m3_boss_d = 7; daughter_m2_boss_d = 6; brass_m3_d = 6; // reference OD only; measure purchased hardware brass_m2_d = 4; brass_m3_stud_l = 4; // UNKNOWN actual lengths: reference values, includes carrier thickness brass_m2_stud_l = 4; brass_m3_female_depth = 4; // reference only, not a hardware specification brass_m2_female_depth = 3; nozzle_d = 0.4; // analytical minimum-web checks, not a slicer simulation /* [USB: only X widths and mouth Y measured] */ usb_extra_width = 2.4; // each shell's total added width; overlap joins into one bay usb_below_module_top = 1.0; usb_above_module_top = 6.0; usb_reference_depth = 7; // preview estimate ONLY usb_reference_h = 3.5; // preview estimate ONLY /* [DB9 panel: replacement jackscrews must be verified] */ db_panel = true; // false leaves service opening; no case-to-flange retention panel_t = 2.4; // axial clamp thickness; consumes this much additional stud reach panel_flange_gap = 0; // nominal flange contact; tune after physical measurement external_fastener_clearance = 3.6; // NOT a thread designation panel_rib_w = 5; // broad forward ribs outside the flange, not insert hangers shell_margin = 0.6; // per edge on STEP maximum shell envelope panel_bottom_above_module = 0.5; /* [Display and controls] */ display_margin = 0.6; // around entire measured display, NOT active pixels button_h = 4.3; // Omron B3F-1000, +/-0.2 plunger_flange_offset = 0.5; // above switch contact face; no external travel stop plunger_d = 5; plunger_clearance = 0.5; // diametral, tune printer plunger_flange_d = 8; plunger_flange_t = 1.2; plunger_above_lid = 1.5; guide_od = 9; guide_bottom_above_button = 2.3; // Source geometry, carrier-local. Do not replace hole centres with body centres. board_outline = [[0,19],[36,19],[36,0],[82,0],[82,80],[0,80]]; carrier_holes = [[3.5,22.5],[3.5,75],[78,4],[78,76]]; rs_holes = [[44.905,17.907],[69.035,17.907]]; // 24.130 spacing, Z axes oled_holes = [[45.72,37.75],[68.22,37.75],[45.72,60.25],[68.22,60.25]]; button_centres = [[44.97,70.25],[56.97,70.25],[68.97,70.25]]; usb_centres = [15.23,26.63]; usb_mouth_y = 77.705; module_cx = 56.97; lock_x = [44.47,69.47]; // 25.000 spacing, Y axes; NOT rs_holes carrier_bottom = floor_t + carrier_boss_h; carrier_top = carrier_bottom + carrier_t; module_bottom = carrier_top + module_gap; rs_top = module_bottom + rs232_t; esp_top = module_bottom + esp_t; oled_top = module_bottom + oled_t; lid_z = carrier_top + lid_above_carrier; top_z = lid_z + lid_t; split_z = carrier_top + split_above_carrier; xmin = -side_extra; xmax = 82 + side_extra; ymin = -end_clearance-wall; ymax = 80+end_clearance+wall; case_holes = [[-side_extra/2,12],[-side_extra/2,68], [82+side_extra/2,12],[82+side_extra/2,68]]; db_axis_z = rs_top + 6.27762024; flange_y = 1.930; panel_rear_y = flange_y-panel_flange_gap; panel_front_y = panel_rear_y-panel_t; panel_low_z = rs_top+panel_bottom_above_module; recess_left = 33; recess_right = 81; shell_slot_w = 16.878038+2*shell_margin; upstand_top = db_axis_z-9.1/2-shell_margin; upstand_shoulder = panel_low_z-seam_clearance; upstand_rear = min(-0.5,panel_front_y); button_top = carrier_top+button_h; guide_low_z = button_top+guide_bottom_above_button; guide_high_z = lid_z; flange_bottom_z = button_top+plunger_flange_offset; plunger_length = top_z+plunger_above_lid-button_top; assert(side_extra >= case_boss_d+2, "Widen side rails for case bosses"); assert(carrier_boss_h > max(m3_insert_depth,m2_insert_depth), "Leave material under inserts"); assert(min(wall,floor_t,lid_t) >= 3*nozzle_d, "Shell needs at least three nozzle widths"); assert((min(carrier_boss_d,case_boss_d,daughter_m3_boss_d) -m3_insert_d-2*insert_lead)/2 >= 2*nozzle_d, "M3 entry web too thin"); assert((daughter_m2_boss_d-m2_insert_d-2*insert_lead)/2 >= 2*nozzle_d, "M2 entry web too thin"); assert((guide_od-plunger_d-plunger_clearance)/2 >= 2*nozzle_d, "Guide web too thin"); assert(brass_m3_stud_l > carrier_t && brass_m3_stud_l-carrier_t < m3_insert_depth, "M3 reference stud must cross carrier and engage without bottoming"); assert(brass_m2_stud_l > carrier_t && brass_m2_stud_l-carrier_t < m2_insert_depth, "M2 reference stud must cross carrier and engage without bottoming"); assert(module_gap > max(brass_m3_female_depth,brass_m2_female_depth)+1); assert(m3_insert_d+2 < min(carrier_boss_d,case_boss_d,daughter_m3_boss_d)); assert(m2_insert_d+2 < daughter_m2_boss_d); assert(lid_z > rs_top+12.55262024+0.8, "Lid intersects DB9 flange"); assert(panel_low_z >= rs_top+0.5, "Panel must stay above module PCB during lowering"); assert(panel_low_z < db_axis_z-external_fastener_clearance/2-0.8); assert(panel_t >= 3*nozzle_d && panel_rib_w >= 3*nozzle_d); assert(seam_clearance >= 0.2 && seam_clearance <= 0.5, "Keep printable sliding seams"); assert(shell_margin >= 0.5, "Retain shell/cable clearance"); assert(upstand_rear-ymin >= 3*nozzle_d, "Upstand wall too thin"); assert(upstand_shoulder > split_z && upstand_top > upstand_shoulder, "Stepped closure must rise above base and below shell"); assert(upstand_rear <= -0.5, "Do not enter projecting module PCB/underside"); assert(panel_flange_gap >= 0, "Panel cannot cross flange plane"); assert(34+panel_rib_w < 41.57 && 80-panel_rib_w > 72.37, "Ribs must clear flange sides"); assert(12.5-16.878038/2-shell_margin-external_fastener_clearance/2 >= 3*nozzle_d, "Closed lock holes need a solid web to the open shell slot"); assert(panel_front_y > -2.870, "Panel would obscure stock cable-lock mouth plane"); assert(guide_high_z > guide_low_z+4); assert(flange_bottom_z+plunger_flange_t < guide_low_z, "Plunger flange trapped below guide"); assert(split_z > carrier_top+2 && split_z < module_bottom-1); assert(usb_above_module_top+esp_top < lid_z, "USB bay cuts roof"); echo("PROVISIONAL overall W,D,H (without labels/plungers)",xmax-xmin,ymax-ymin,top_z); echo("carrier bottom/top, module underside, RS232 top",carrier_bottom,carrier_top,module_bottom,rs_top); echo("DB9 lock axis Z, split, lid underside",db_axis_z,split_z,lid_z); echo("CHECK stack",[carrier_bottom,carrier_top,module_bottom,rs_top,lid_z,top_z, split_z,button_top,guide_low_z,db_axis_z,panel_low_z, esp_top-usb_below_module_top,esp_top+usb_above_module_top,plunger_length]); echo("CHECK closure",[recess_left,recess_right,upstand_rear,upstand_shoulder, upstand_top,shell_slot_w-2*seam_clearance,seam_clearance]); echo("CHECK engagement",[brass_m3_stud_l-carrier_t,brass_m2_stud_l-carrier_t]); echo("CHECK connector",[panel_front_y,panel_rear_y,panel_t,panel_low_z, db_axis_z+9.1/2+shell_margin,16.878038+2*shell_margin, lock_x[1]-lock_x[0],external_fastener_clearance,panel_rib_w, 12.5-16.878038/2-shell_margin-external_fastener_clearance/2]); // Reflect carrier Y for a conventional top view; text compensates separately. module world() { translate([0,80,0]) rotate([0,0,0]) mirror([0,1,0]) children(); } module box(x,y,z,w,d,h) { translate([x,y,z]) cube([w,d,h]); } module zcyl(p,z,d,h) { translate([p[0],p[1],z]) cylinder(d=d,h=h); } module ybore(x,y,z,d,len) { translate([x,y,z]) rotate([-90,0,0]) cylinder(d=d,h=len); } module outer_2d(inset=0) { translate([xmin+inset+corner_r,ymin+inset+corner_r]) offset(r=corner_r) square([xmax-xmin-2*inset-2*corner_r, ymax-ymin-2*inset-2*corner_r]); } module outline_volume(z,h,inset=0) { translate([0,0,z]) linear_extrude(h) outer_2d(inset); } module walls(z,h) { difference() { outline_volume(z,h); outline_volume(z-eps,h+2*eps,wall); } } module insert_pocket(p,top,d,depth) { zcyl(p,top-depth,d,depth+eps); translate([p[0],p[1],top-insert_lead]) cylinder(d1=d,d2=d+2*insert_lead,h=insert_lead+eps); } module daughter_bosses() { for(p=rs_holes) zcyl(p,floor_t-eps,daughter_m3_boss_d,carrier_bottom-floor_t+eps); for(p=oled_holes) zcyl(p,floor_t-eps,daughter_m2_boss_d,carrier_bottom-floor_t+eps); } module cable_cuts() { // Whole DB9 service recess clears PCB overhang, flange and wrench approach. // Panel lives inward of this wall and ABOVE the module PCB, never through it. box(33,ymin-1,split_z+seam_clearance-eps,48, max(panel_front_y,-end_clearance)-ymin+1+eps, lid_z-split_z-seam_clearance+eps); // Wide joined bay admits overmoulds to the inboard USB mouths. No thin web. box(usb_centres[0]-(8.87+usb_extra_width)/2,usb_mouth_y-1, esp_top-usb_below_module_top, usb_centres[1]-usb_centres[0]+8.87+usb_extra_width, ymax-usb_mouth_y+2,usb_below_module_top+usb_above_module_top); } module connector_upstand(travel=0) { // Forward of the PCB edge, including its unknown underside components. // Shoulders meet the ribs/panel with a seam; tongue closes the open shell slot. box(recess_left+seam_clearance,ymin,split_z-eps-travel, recess_right-recess_left-2*seam_clearance,upstand_rear-ymin, upstand_shoulder-split_z+eps+travel); box(module_cx-shell_slot_w/2+seam_clearance,ymin,upstand_shoulder-eps-travel, shell_slot_w-2*seam_clearance,upstand_rear-ymin, upstand_top-upstand_shoulder+eps+travel); } module base() { difference() { union() { outline_volume(0,floor_t); walls(floor_t-eps,split_z-floor_t+eps); for(p=carrier_holes) zcyl(p,floor_t-eps,carrier_boss_d,carrier_boss_h+eps); for(p=case_holes) { zcyl(p,floor_t-eps,case_boss_d,split_z-floor_t+eps); // Lateral ribs connect bosses to walls; not in cable approach paths. box(p[0]<0?xmin:p[0],p[1]-2,floor_t-eps, side_extra/2,4,split_z-floor_t+eps); } if(daughter_supports) daughter_bosses(); } for(p=carrier_holes) insert_pocket(p,floor_t+carrier_boss_h,m3_insert_d,m3_insert_depth); for(p=case_holes) insert_pocket(p,split_z,m3_insert_d,m3_insert_depth); if(daughter_supports) { for(p=rs_holes) insert_pocket(p,carrier_bottom,m3_insert_d,m3_insert_depth); for(p=oled_holes) insert_pocket(p,carrier_bottom,m2_insert_d,m2_insert_depth); } cable_cuts(); } if(db_panel) connector_upstand(); } module connector_panel() { difference() { // Bottom-open slot clears the shell during straight vertical lid installation. // Solid wings and top rail meet the roof; nothing crosses the PCB plane. box(recess_left-eps,panel_front_y,panel_low_z, recess_right-recess_left+2*eps,panel_t,lid_z-panel_low_z+eps); box(module_cx-16.878038/2-shell_margin,panel_front_y-eps, panel_low_z-eps,16.878038+2*shell_margin, panel_t+2*eps,db_axis_z+9.1/2+shell_margin-panel_low_z+eps); for(x=lock_x) ybore(x,panel_front_y-eps,db_axis_z, external_fastener_clearance,panel_t+2*eps); } } module connector_ribs() { // Forward of flange/anchors, outside flange X bounds; full-height roof load path. box(recess_left-eps,ymin,panel_low_z,34+panel_rib_w-recess_left+eps, panel_rear_y-ymin,lid_z-panel_low_z+eps); box(80-panel_rib_w,ymin,panel_low_z,recess_right-(80-panel_rib_w)+eps, panel_rear_y-ymin,lid_z-panel_low_z+eps); } module guide(p) { difference() { // Flange stays BELOW this continuous guide: insert stem from underside. zcyl(p,guide_low_z,guide_od,guide_high_z-guide_low_z+eps); zcyl(p,guide_low_z-eps,plunger_d+plunger_clearance, guide_high_z-guide_low_z+2*eps); } } module lid_body() { difference() { union() { outline_volume(lid_z,lid_t); walls(split_z+seam_clearance,lid_z-split_z-seam_clearance+eps); for(p=case_holes) { zcyl(p,split_z+seam_clearance,case_boss_d,lid_z-split_z-seam_clearance+eps); box(p[0]<0?xmin:p[0],p[1]-2,split_z+seam_clearance, side_extra/2,4,lid_z-split_z-seam_clearance+eps); } for(p=button_centres) guide(p); } // Measured glass/display boundary plus margin, not a guessed active area. box(44.97-display_margin,40.5-display_margin,lid_z-eps, 24.5+2*display_margin,16.5+2*display_margin,lid_t+2*eps); for(p=button_centres) zcyl(p,lid_z-eps,plunger_d+plunger_clearance,lid_t+2*eps); for(p=case_holes) { zcyl(p,split_z-eps,m3_clearance,top_z-split_z+2*eps); // Recess spans thick boss, leaving lid thickness below screw head. zcyl(p,top_z-m3_head_depth,m3_head_d,m3_head_depth+eps); } cable_cuts(); } } module label_at(x,y,s,size=2.5) { // Undo world's Y reflection for text only: readable from USB end, DB9 above. translate([x,y,top_z-eps]) mirror([0,1,0]) linear_extrude(label_raise+eps) text(s,size=size,halign="center",valign="center",font="Liberation Sans:style=Bold"); } module labels() { label_at(module_cx,7,"RS-232",3); label_at(20.93,74,"USB",3); // native vs UART left/right NOT established label_at(module_cx,35,"DISPLAY",2.4); for(i=[0:2]) label_at(button_centres[i][0],77,["BACK","SELECT","NEXT"][i],2.1); } module lid() { lid_body(); if(db_panel) { connector_panel(); connector_ribs(); } if(emboss) labels(); } module plunger() { // Local bottom at zero, prints vertically; brim recommended for slender stem. cylinder(d=plunger_d,h=plunger_length); translate([0,0,flange_bottom_z-button_top]) cylinder(d=plunger_flange_d,h=plunger_flange_t); } module plungers() { for(i=[0:2]) translate([i*12,0,0]) plunger(); } // Preview-only unthreaded hardware dummies, never printable parts. module brass_standoff(p,od,thread_d,stud_l,female_depth) { difference() { zcyl(p,carrier_top,od,module_gap); zcyl(p,module_bottom-female_depth,thread_d,female_depth+eps); } zcyl(p,carrier_top-stud_l,thread_d,stud_l+eps); } module pcb_shape() { difference() { linear_extrude(carrier_t) polygon(board_outline); for(p=carrier_holes) zcyl(p,-eps,3.2,carrier_t+2*eps); for(p=rs_holes) zcyl(p,-eps,3,carrier_t+2*eps); for(p=oled_holes) zcyl(p,-eps,2,carrier_t+2*eps); } } module references() { // All simplified envelopes, not complete component CAD or collision certificates. color("seagreen") translate([0,0,carrier_bottom]) pcb_shape(); // Four independent carrier screws, not daughterboard standoff locations. color("silver") for(p=carrier_holes) { zcyl(p,carrier_bottom-2.4,3,carrier_t+2.4); zcyl(p,carrier_top,5.5,2); // head/engagement reference only } // ESP32 is header-supported: no supplied mounting holes, no invented fasteners. color("black") for(x=[8,33.4]) box(x-1.25,21.75,carrier_top,2.5,55.84,module_gap); color("darkgreen") { box(6.635,21.635,module_bottom,28.09,56.07,esp_t); difference() { box(41.095,0,module_bottom,31.75,29.337,rs232_t); for(p=rs_holes) zcyl(p,module_bottom-eps,3,rs232_t+2*eps); } difference() { box(43.97,36,module_bottom,26,26,oled_t); for(p=oled_holes) zcyl(p,module_bottom-eps,2,oled_t+2*eps); } } color("orange") box(12.045,14.745,module_bottom,17.63,6.89,esp_t); color("black") box(44.97,40.5,oled_top,24.5,16.5,2); // glass height unknown color("silver") { for(x=usb_centres) box(x-8.87/2,usb_mouth_y-usb_reference_depth,esp_top, 8.87,usb_reference_depth,usb_reference_h); box(41.57,1.930,rs_top+0.00262024,30.8,0.4,12.55); box(module_cx-16.878038/2,-3.870,db_axis_z-9.1/2,16.878038,5.8,9.1); } // Reseated 4.8 mm post-body reference ONLY; stud/thread and cable reach unverified. color([0.8,0.5,0.1,0.65]) for(x=lock_x) ybore(x,(db_panel?panel_front_y:flange_y)-4.8,db_axis_z,5.311622,4.8); color("gray") for(p=button_centres) { box(p[0]-3,p[1]-3,carrier_top,6,6,3.4); zcyl(p,carrier_top+3.4,3.5,button_h-3.4); } if(daughter_supports) color("gold") { for(p=rs_holes) brass_standoff(p,brass_m3_d,3,brass_m3_stud_l,brass_m3_female_depth); for(p=oled_holes) brass_standoff(p,brass_m2_d,2,brass_m2_stud_l,brass_m2_female_depth); } } module assembled(explode=0) { color("slategray") render(convexity=10) base(); color([0.8,0.85,0.9,0.7]) translate([0,0,explode]) render(convexity=10) lid(); color("orange") for(p=button_centres) translate([p[0],p[1],button_top+explode]) plunger(); if($preview && show_references) %references(); } // Conservative rectangular envelopes swept downward in the lid frame: equivalent // to lowering the lid vertically 60 mm onto fixed electronics, posts REMOVED. // Contact faces inset 0.01 only to avoid a coincident flange face in CGAL. module installation_envelopes(stock_posts=false,travel=60,upward=false) { module swept(x,y,z,w,d,h) { box(x,y,upward?z:z-travel,w,d,h+travel); } swept(module_cx-16.878038/2,-3.870,db_axis_z-9.1/2,16.878038,5.8,9.1); swept(41.57,flange_y+0.01,rs_top+0.00262024,30.8,0.39,12.55); // Connector anchor brackets: STEP solids 13/14, not invented mounting holes. for(x=[41.97,66.97]) swept(x,5.03,module_bottom-2.23,5,6.4,12.35262024); swept(41.095,0,module_bottom,31.75,29.337,rs232_t); swept(6.635,21.635,module_bottom,28.09,56.07,esp_t); swept(12.045,14.745,module_bottom,17.63,6.89,esp_t); swept(43.97,36,module_bottom,26,26,oled_t); swept(44.97,40.5,oled_top,24.5,16.5,2); for(x=usb_centres) swept(x-8.87/2,usb_mouth_y-usb_reference_depth,esp_top, 8.87,usb_reference_depth,usb_reference_h); for(p=button_centres) swept(p[0]-3,p[1]-3,carrier_top,6,6,button_h); for(p=case_holes) swept(p[0]-case_boss_d/2,p[1]-case_boss_d/2, floor_t,case_boss_d,case_boss_d,split_z-floor_t); if(stock_posts) for(x=lock_x) swept(x-5.311622/2,-2.870,db_axis_z-2.3,5.311622,4.8,4.6); } module installation_check(stock_posts=false) { // Sentinel permits exporting an otherwise empty intersection; expected volume 1. cube(1); translate([0,0,80]) { intersection() { lid(); installation_envelopes(stock_posts); } intersection() { lid(); base(); } // Sweep each step separately: a whole-part hull invents diagonal material. if(db_panel) intersection() { lid(); connector_upstand(60); } // New material must also clear electronics while boards lower into the base. if(db_panel) intersection() { connector_upstand(); installation_envelopes(false,60,true); } } } // Each printable export is on Z=0, without hardware reference geometry. if(part=="base") world() base(); else if(part=="lid") translate([0,0,-split_z-seam_clearance]) world() lid(); else if(part=="plunger") plunger(); else if(part=="plungers") plungers(); else if(part=="labels") translate([0,0,-top_z+eps]) world() labels(); else if(part=="layout") { world() base(); translate([xmax-xmin+12,0,-split_z-seam_clearance]) world() lid(); translate([0,-22,0]) plungers(); } else if(part=="installation-check") installation_check(); else if(part=="installation-blocked") installation_check(true); else if(part=="check") {} // datums/assertions only, used by validate.py else if(part=="assembly") world() assembled(); else if(part=="exploded") world() assembled(28); else assert(false,"Unknown part");