Add parametric enclosure concept and checks

This commit is contained in:
2026-09-26 13:17:33 +02:00
parent 5255d6ff5e
commit f925293d3f
11 changed files with 168589 additions and 0 deletions
+364
View File
@@ -0,0 +1,364 @@
// 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, panel, 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;
panel_flange_gap = 0; // nominal flange contact; tune after physical measurement
external_fastener_clearance = 3.6; // NOT a thread designation
panel_mount_x = [38,76]; // separate M3 panel-to-lid screws, NOT connector locks
panel_mount_boss_d = 6.4;
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;
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,panel_mount_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 < db_axis_z-external_fastener_clearance/2-0.8);
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 engagement",[brass_m3_stud_l-carrier_t,brass_m2_stud_l-carrier_t]);
// 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,module_bottom-0.6,48,panel_front_y-ymin+1,lid_z-module_bottom+0.6);
// 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 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();
}
}
module panel() {
difference() {
// Separate externally fitted part: a closed shell hole cannot lower over DB9.
box(34,panel_front_y,panel_low_z,46,panel_t,lid_z-panel_low_z-seam_clearance);
box(module_cx-16.878038/2-shell_margin,panel_front_y-eps,
db_axis_z-9.1/2-shell_margin,16.878038+2*shell_margin,
panel_t+2*eps,9.1+2*shell_margin);
for(x=lock_x) ybore(x,panel_front_y-eps,db_axis_z,
external_fastener_clearance,panel_t+2*eps);
for(x=panel_mount_x) ybore(x,panel_front_y-eps,db_axis_z,m3_clearance,panel_t+2*eps);
}
}
module panel_hangers() {
for(x=panel_mount_x) difference() {
box(x-panel_mount_boss_d/2,panel_rear_y,db_axis_z-panel_mount_boss_d/2,
panel_mount_boss_d,m3_insert_depth+2,lid_z-db_axis_z+panel_mount_boss_d/2+eps);
ybore(x,panel_rear_y-eps,db_axis_z,m3_insert_d,m3_insert_depth+eps);
translate([x,panel_rear_y-eps,db_axis_z]) rotate([-90,0,0])
cylinder(d1=m3_insert_d+2*insert_lead,d2=m3_insert_d,h=insert_lead+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);
if(db_panel) panel_hangers();
}
// 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(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);
}
// Stock hex envelopes conflict with panel intentionally: must be replaced/reseated.
color([0.8,0.5,0.1,0.65]) for(x=lock_x)
ybore(x,-2.870,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") base();
color([0.8,0.85,0.9,0.7]) translate([0,0,explode]) lid();
if(db_panel) color("slategray") translate([0,-explode/2,0]) panel();
color("orange") for(p=button_centres) translate([p[0],p[1],button_top+explode]) plunger();
if($preview && show_references) %references();
}
// 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=="panel") rotate([90,0,0]) translate([-34,-panel_front_y,-panel_low_z]) panel();
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();
translate([60,-20,0]) rotate([90,0,0]) translate([-34,-panel_front_y,-panel_low_z]) panel();
}
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");