365 lines
16 KiB
OpenSCAD
365 lines
16 KiB
OpenSCAD
// 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");
|