Files

448 lines
21 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, 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);
// One grouped bay label: physical UART0/CDC left/right is not established.
label_at(20.93,71,"USB",3);
label_at(20.93,76,"CDC | UART0",2.4);
label_at(20.93,32,"ESP32",5);
label_at(20.93,41,"Serial Swiss",4);
label_at(20.93,50,"Army Knife",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");