// ============================================================ // ip54_enclosure.scad — IP54 Main Electronics Enclosure // Issue: #144 Agent: sl-mechanical Date: 2026-03-01 // ============================================================ // // Sealed electronics bay for Jetson Orin NX + FC + ESC stack. // IP54 rating: dust-protected, splash-proof from all directions. // // Protection method: // • 4 mm PETG walls (5 perims, 40 % infill) // • 2 mm silicone O-ring (Ø2 mm cord) in lid groove → IP54 seal // • PG7 cable glands (Ø3–6 mm cables) on rear wall × 4 // • PG9 cable glands (Ø4–8 mm cables) on rear wall × 2 // • 40 mm axial fan + foam filter panel on lid (positive pressure) // • Thermal: 2× Al heat sink pads on lid underside over Jetson/ESC // // Internal envelope: 220 × 160 × 90 mm (W × D × H, internal) // Fits: Jetson Orin NX (58 × 49 mm), FC 30.5 × 30.5 mm, // dual ESC (~80 × 40 mm each) // // Quick-release lid: 4× spring-loaded quarter-turn latches. // Tool-free. Lid lifts straight up after 90° rotation of each latch. // // Coordinates: Z = 0 at box floor (internal), Z+ upward. // Box centred on X=0, Y=0. // // RENDER options: // "assembly" full box + lid + fans + glands preview // "body_stl" box body (print 1×) // "lid_stl" lid with fan mount (print 1×) // "fan_duct_stl" filtered fan inlet duct (print 1×) // "latch_stl" quarter-turn latch knob (print 4×) // "gasket_2d" DXF — lid O-ring groove outline + cable gland panel // // Export: // openscad ip54_enclosure.scad -D 'RENDER="body_stl"' -o ip54_body.stl // openscad ip54_enclosure.scad -D 'RENDER="lid_stl"' -o ip54_lid.stl // openscad ip54_enclosure.scad -D 'RENDER="fan_duct_stl"' -o ip54_fan_duct.stl // openscad ip54_enclosure.scad -D 'RENDER="latch_stl"' -o ip54_latch.stl // openscad ip54_enclosure.scad -D 'RENDER="gasket_2d"' -o ip54_gasket.dxf // ============================================================ $fn = 64; e = 0.01; // ── Internal cavity ─────────────────────────────────────────────────────────── INT_W = 220.0; // internal width (X) INT_D = 160.0; // internal depth (Y) INT_H = 90.0; // internal height (Z) // ── Wall / structural ───────────────────────────────────────────────────────── WALL = 4.0; // box wall thickness LID_T = 5.0; // lid thickness BOX_R = 8.0; // outer corner radius (XY) // Derived outer dims OUT_W = INT_W + 2*WALL; OUT_D = INT_D + 2*WALL; OUT_H = INT_H + WALL; // wall on floor + sides; lid closes top // ── O-ring seal ─────────────────────────────────────────────────────────────── // 2 mm cord silicone O-ring in a groove on the lid flange inner face. // Groove: 2.2 mm wide × 1.7 mm deep (standard 70 % compression for IP54). ORING_D = 2.0; ORING_GROOVE_W = 2.2; ORING_GROOVE_D = 1.7; ORING_INSET = 6.0; // groove CL from inner box wall edge // ── Lid flange (overlap joint) ──────────────────────────────────────────────── // Lid has a stepped rim that overlaps the box top edge. // Seal groove is cut into the underside of this rim. FLANGE_T = 3.0; // vertical flange depth (how far rim drops into box) FLANGE_WALL = 3.0; // rim wall thickness // ── Quarter-turn latches ────────────────────────────────────────────────────── // 4× positions: one per side (front/rear/left/right centre). // Spring-loaded bayonet latch post on box side; rotating knob on lid flange. LATCH_POST_D = 10.0; // latch post OD LATCH_BOSS_H = 8.0; // boss height above box top flange LATCH_KNOB_D = 18.0; // knob OD LATCH_SLOT_W = 2.5; // bayonet slot width // ── Cable glands ────────────────────────────────────────────────────────────── // All glands on rear wall (Y = -OUT_D/2). // PG7 thread OD = 12.5 mm; PG9 thread OD = 15.2 mm. PG7_BORE = 12.7; // drill diameter for PG7 panel hole PG9_BORE = 15.4; // drill diameter for PG9 panel hole PG7_COUNT = 4; PG9_COUNT = 2; // Gland layout on rear wall (Y = -OUT_D/2 face), evenly spaced in X. // Z centre = 30 mm from floor (lower half of box, cable routing stays low). GLAND_Z = 30.0; // ── Fan ─────────────────────────────────────────────────────────────────────── // 40 mm axial fan in lid, front-left quadrant. // Positive pressure: fan blows IN, filtered foam panel on top. // Exit: passive vent slots on rear lid (over gland panel), IP54 labyrinth. FAN_SZ = 40.0; // 40 mm fan FAN_BORE_D = 36.5; // airflow bore FAN_BOLT_SPC= 32.0; // M3 bolt square (32 × 32 mm) FAN_BOLT_D = 3.3; FAN_POS_X = -INT_W/2 + FAN_SZ/2 + 10; // fan X offset from centre (front-left) FAN_POS_Y = -INT_D/2 + FAN_SZ/2 + 10; // fan Y offset // ── Fan filter duct ─────────────────────────────────────────────────────────── DUCT_H = 20.0; // filter duct height above lid FOAM_T = 5.0; // foam filter thickness // ── Exhaust labyrinth slots ─────────────────────────────────────────────────── // Baffle-protected exhaust on lid rear. 2-row labyrinth prevents direct // water ingress while maintaining IP54 (deflects splash from all angles). EXH_SLOT_W = 3.0; // slot width EXH_SLOT_L = 30.0; // slot length EXH_ROWS = 2; // number of baffle rows EXH_BAFFLE_H= 8.0; // baffle height above lid EXH_N_SLOTS = 4; // number of exhaust slots per row // ── Internal standoffs ──────────────────────────────────────────────────────── // Jetson Orin NX: 58 × 49 mm M3 hole pattern (4 holes), Z = WALL from floor ORIN_HOLE_X = 58.0 / 2; ORIN_HOLE_Y = 49.0 / 2; ORIN_STOFF_H= 8.0; // standoff height (PCB float) ORIN_POS_X = +INT_W/4; // offset from box centre: right half ORIN_POS_Y = 0; // FC: 30.5 × 30.5 mm M3 pattern FC_HOLE_SPC = 30.5 / 2; FC_STOFF_H = 6.0; FC_POS_X = -INT_W/4; // left half FC_POS_Y = -INT_D/4; // ESC pair: 2× ESC ~80 × 40 mm; 4× M3 holes at corners ESC_W = 80.0; ESC_D = 40.0; ESC_STOFF_H = 6.0; ESC_POS_X = -INT_W/4; ESC_POS_Y = +INT_D/4; // ── Heat sink pads ──────────────────────────────────────────────────────────── // Recesses in lid underside that accept adhesive Al heat sink pads. // Thermal path: board → heat sink → lid → ambient convection + fan. // Pad size: 60 × 40 × 2 mm for Jetson, 50 × 30 × 2 mm for ESC. HSINK_JETSON_W = 60.0; HSINK_JETSON_D = 40.0; HSINK_ESC_W = 50.0; HSINK_ESC_D = 30.0; HSINK_T = 2.2; // recess depth (pad sits flush in lid) // Fasteners M3_D = 3.3; M4_D = 4.3; M5_D = 5.3; // ============================================================ // RENDER DISPATCH // ============================================================ RENDER = "assembly"; if (RENDER == "assembly") { assembly(); } else if (RENDER == "body_stl") { box_body(); } else if (RENDER == "lid_stl") { box_lid(); } else if (RENDER == "fan_duct_stl") { fan_filter_duct(); } else if (RENDER == "latch_stl") { latch_knob(); } else if (RENDER == "gasket_2d") { projection(cut = true) translate([0, 0, -0.5]) linear_extrude(1) gasket_profile_2d(); } // ============================================================ // ASSEMBLY PREVIEW // ============================================================ module assembly() { // Box body color("DarkOliveGreen", 0.85) box_body(); // Lid (lifted 5 mm to show interior) color("OliveDrab", 0.70) translate([0, 0, OUT_H + 5]) box_lid(); // Fan duct on lid color("SaddleBrown", 0.80) translate([FAN_POS_X, FAN_POS_Y, OUT_H + LID_T + 5]) fan_filter_duct(); // Ghost latch knobs for (lpos = latch_positions()) %color("DimGray", 0.60) translate([lpos[0], lpos[1], OUT_H + 3]) latch_knob(); // Ghost Jetson PCB %color("Green", 0.3) translate([ORIN_POS_X, ORIN_POS_Y, WALL + ORIN_STOFF_H]) cube([58, 49, 3], center = true); // Ghost FC %color("Orange", 0.3) translate([FC_POS_X, FC_POS_Y, WALL + FC_STOFF_H]) cube([30.5, 30.5, 3], center = true); // Ghost ESC %color("Red", 0.3) translate([ESC_POS_X, ESC_POS_Y, WALL + ESC_STOFF_H]) cube([ESC_W, ESC_D, 3], center = true); // Index annotations (cable gland markers) for (gpos = cable_gland_positions()) %color("Yellow", 0.5) translate([gpos[0], -OUT_D/2 - 5, gpos[1]]) rotate([90, 0, 0]) cylinder(d = gpos[2], h = 3); } // ============================================================ // BOX BODY // ============================================================ module box_body() { difference() { union() { // ── Outer shell (rounded rect, open top) ──────────────────── _rounded_box(OUT_W, OUT_D, OUT_H, BOX_R); // ── Latch posts on top flange ──────────────────────────────── for (lpos = latch_positions()) translate([lpos[0], lpos[1], OUT_H]) _latch_post(); // ── Cable gland boss pads (rear wall reinforcement) ────────── translate([0, -OUT_D/2, GLAND_Z]) rotate([90, 0, 0]) _gland_boss_array(); } // ── Internal cavity ────────────────────────────────────────────── translate([0, 0, WALL]) cube([INT_W, INT_D, INT_H + e], center = true); // ── Cable gland holes (rear wall) ──────────────────────────────── for (gpos = cable_gland_positions()) translate([gpos[0], -OUT_D/2 - e, GLAND_Z]) rotate([90, 0, 0]) cylinder(d = gpos[2], h = WALL + 2*e); // ── Internal standoffs (subtracted from floor thickness) ───────── // These are ADDED in the union; we only subtract if needed for wires. } // ── Internal PCB standoffs (added in separate union) ───────────────── // Jetson Orin NX standoffs for (sx = [-1, 1]) for (sy = [-1, 1]) translate([ORIN_POS_X + sx*ORIN_HOLE_X, ORIN_POS_Y + sy*ORIN_HOLE_Y, WALL]) difference() { cylinder(d = 7, h = ORIN_STOFF_H); cylinder(d = M3_D, h = ORIN_STOFF_H + e); } // FC standoffs for (sx = [-1, 1]) for (sy = [-1, 1]) translate([FC_POS_X + sx*FC_HOLE_SPC, FC_POS_Y + sy*FC_HOLE_SPC, WALL]) difference() { cylinder(d = 7, h = FC_STOFF_H); cylinder(d = M3_D, h = FC_STOFF_H + e); } // ESC standoffs (4 corners of ESC_W × ESC_D) for (sx = [-1, 1]) for (sy = [-1, 1]) translate([ESC_POS_X + sx*(ESC_W/2 - 5), ESC_POS_Y + sy*(ESC_D/2 - 5), WALL]) difference() { cylinder(d = 7, h = ESC_STOFF_H); cylinder(d = M3_D, h = ESC_STOFF_H + e); } } // ── Rounded box shell (open top) ───────────────────────────────────────────── module _rounded_box(w, d, h, r) { linear_extrude(h) minkowski() { square([w - 2*r, d - 2*r], center = true); circle(r = r); } } // ── Latch post (on top rim of box) ─────────────────────────────────────────── module _latch_post() { difference() { cylinder(d = LATCH_POST_D + 4, h = LATCH_BOSS_H); // Central latch bore translate([0, 0, -e]) cylinder(d = LATCH_POST_D, h = LATCH_BOSS_H + 2*e); // Bayonet slot (cross-slot in post top, for knob lug engagement) for (a = [0, 90]) rotate([0, 0, a]) translate([0, 0, LATCH_BOSS_H/2]) cube([LATCH_SLOT_W, LATCH_POST_D + 2, LATCH_BOSS_H], center = true); } } // ── Cable gland boss array (pad behind gland holes on outer wall) ───────────── module _gland_boss_array() { for (gpos = cable_gland_positions()) translate([gpos[0], 0, 0]) cylinder(d = gpos[2] + 8, h = 2); } // ── Latch positions (4 sides, centred) ─────────────────────────────────────── function latch_positions() = [ [ 0, +OUT_D/2 - 3 ], // front [ 0, -OUT_D/2 + 3 ], // rear [ +OUT_W/2 - 3, 0 ], // right [ -OUT_W/2 + 3, 0 ], // left ]; // ── Cable gland positions [x, z, bore_d] on rear wall ──────────────────────── // 4× PG7 + 2× PG9, arranged in a row at GLAND_Z height. // PG7 for signal / small power; PG9 for main drive harness. function cable_gland_positions() = [ [ -INT_W/2 + 15, GLAND_Z, PG7_BORE ], // PG7 #1 [ -INT_W/2 + 40, GLAND_Z, PG7_BORE ], // PG7 #2 [ -INT_W/2 + 65, GLAND_Z, PG7_BORE ], // PG7 #3 [ -INT_W/2 + 90, GLAND_Z, PG7_BORE ], // PG7 #4 [ INT_W/2 - 30, GLAND_Z, PG9_BORE ], // PG9 #1 (main battery harness) [ INT_W/2 - 60, GLAND_Z, PG9_BORE ], // PG9 #2 (motor harness) ]; // ============================================================ // BOX LID // ============================================================ module box_lid() { difference() { union() { // ── Top plate ─────────────────────────────────────────────── _rounded_box(OUT_W, OUT_D, LID_T, BOX_R); // ── Flanged rim (overlaps box top; O-ring groove cut into it) ─ difference() { translate([0, 0, -FLANGE_T]) _rounded_box(INT_W + 2*FLANGE_WALL, INT_D + 2*FLANGE_WALL, FLANGE_T + e, BOX_R - 1); // Hollow interior of flange (sits over box rim) translate([0, 0, -FLANGE_T - e]) cube([INT_W, INT_D, FLANGE_T + 2*e], center = true); } // ── Exhaust labyrinth baffles on rear of lid ───────────────── _exhaust_baffles(); } // ── Fan bore (through lid) ──────────────────────────────────────── translate([FAN_POS_X, FAN_POS_Y, -e]) cylinder(d = FAN_BORE_D, h = LID_T + 2*e); // ── Fan bolt holes ──────────────────────────────────────────────── for (fx = [-1, 1]) for (fy = [-1, 1]) translate([FAN_POS_X + fx*FAN_BOLT_SPC/2, FAN_POS_Y + fy*FAN_BOLT_SPC/2, -e]) cylinder(d = FAN_BOLT_D, h = LID_T + 2*e); // ── O-ring groove in flange underside ───────────────────────────── // Groove runs along the inner perimeter of the flange. translate([0, 0, -ORING_GROOVE_D]) difference() { _rounded_box( INT_W + 2*FLANGE_WALL - 2*(FLANGE_WALL - ORING_INSET), INT_D + 2*FLANGE_WALL - 2*(FLANGE_WALL - ORING_INSET), ORING_GROOVE_D + e, BOX_R - 2); _rounded_box( INT_W + 2*FLANGE_WALL - 2*(FLANGE_WALL - ORING_INSET) - 2*ORING_GROOVE_W, INT_D + 2*FLANGE_WALL - 2*(FLANGE_WALL - ORING_INSET) - 2*ORING_GROOVE_W, ORING_GROOVE_D + 2*e, BOX_R - 3); } // ── Latch knob counterbores in lid flange (knob sits flush) ────── for (lpos = latch_positions()) translate([lpos[0], lpos[1], -FLANGE_T - e]) cylinder(d = LATCH_KNOB_D + 1, h = FLANGE_T + 2*e); // ── Heat sink pad recesses (underside of lid) ───────────────────── // Jetson pad recess translate([ORIN_POS_X, ORIN_POS_Y, -e]) cube([HSINK_JETSON_W, HSINK_JETSON_D, HSINK_T + e], center = true); // ESC pad recess translate([ESC_POS_X, ESC_POS_Y, -e]) cube([HSINK_ESC_W, HSINK_ESC_D, HSINK_T + e], center = true); // ── Exhaust labyrinth slot through-holes ────────────────────────── for (slot = exhaust_slot_positions()) translate([slot[0], slot[1], -e]) cube([EXH_SLOT_W, EXH_SLOT_L, LID_T + 2*e], center = true); } // ── Latch knob receiver rings in flange ───────────────────────────────── for (lpos = latch_positions()) translate([lpos[0], lpos[1], -FLANGE_T]) difference() { cylinder(d = LATCH_KNOB_D, h = FLANGE_T - 0.5); // Bayonet lugs engage latch post slot translate([0, 0, FLANGE_T - 0.5 - 3]) cylinder(d = LATCH_POST_D + 0.4, h = 3 + e); cylinder(d = LATCH_POST_D - 3, h = FLANGE_T + e); } } // ── Exhaust baffle array ────────────────────────────────────────────────────── // Raised wall baffles on lid rear-right quadrant provide labyrinth exhaust path. module _exhaust_baffles() { exh_x = INT_W/4; exh_y = INT_D/2 - 40; for (row = [0 : EXH_ROWS - 1]) translate([exh_x, exh_y - row * (EXH_SLOT_W + 4), LID_T]) cube([EXH_N_SLOTS * (EXH_SLOT_W + 6), EXH_BAFFLE_H/3, EXH_BAFFLE_H]); } // ── Exhaust slot positions [x, y] (in lid top surface) ─────────────────────── function exhaust_slot_positions() = [ let(base_x = INT_W/4, base_y = INT_D/2 - 40) for (i = [0 : EXH_N_SLOTS - 1]) [base_x - (EXH_N_SLOTS - 1)/2 * (EXH_SLOT_W + 6) + i * (EXH_SLOT_W + 6), base_y - EXH_SLOT_L/2] ]; // ============================================================ // FAN FILTER DUCT (Part C — print 1×) // ============================================================ // Sits on top of lid fan bore. Contains 5 mm foam filter pad. // Labyrinth inlet around sides prevents direct splash ingress. module fan_filter_duct() { duct_od_w = FAN_SZ + 2*WALL; foam_slot = FOAM_T + 0.5; // foam insert slot depth difference() { // Outer duct body cube([duct_od_w, duct_od_w, DUCT_H], center = true); // Foam filter slot (open at top for insert/remove) translate([0, 0, DUCT_H/2 - foam_slot - 1]) cube([FAN_SZ, FAN_SZ, foam_slot + 2*e], center = true); // Airflow bore below foam (connects to fan bore in lid) translate([0, 0, -DUCT_H/2 - e]) cylinder(d = FAN_BORE_D, h = DUCT_H/2 + 2*e); // Inlet slots on all 4 sides (labyrinth — no direct top-spray path) for (a = [0, 90, 180, 270]) rotate([0, 0, a]) translate([0, duct_od_w/2 - WALL/2, 0]) { // Three inlet slots, staggered vertically for (sz = [-DUCT_H/2 + 5, 0, DUCT_H/2 - 8]) translate([0, 0, sz]) cube([FAN_SZ * 0.5, WALL + 2*e, 5], center = true); } // Fan bolt holes (align to lid bolt holes) for (fx = [-1, 1]) for (fy = [-1, 1]) translate([fx*FAN_BOLT_SPC/2, fy*FAN_BOLT_SPC/2, -DUCT_H/2 - e]) cylinder(d = FAN_BOLT_D, h = DUCT_H + 2*e); } } // ============================================================ // QUARTER-TURN LATCH KNOB (Part D — print 4×) // ============================================================ // Screws onto latch post from outside. 90° rotation latches/unlatches. // Spring washer (not printed) provides axial preload. module latch_knob() { knob_h = 12.0; grip_h = 8.0; difference() { union() { // Body disc cylinder(d = LATCH_KNOB_D, h = knob_h); // Grip ridges (6×, for finger purchase) for (i = [0:5]) rotate([0, 0, i * 60]) translate([LATCH_KNOB_D/2 - 1, 0, 0]) cylinder(d = 2.5, h = grip_h); } // Latch post bore (clearance) translate([0, 0, -e]) cylinder(d = LATCH_POST_D + 0.5, h = knob_h + 2*e); // Bayonet lug slot (catches latch post cross-slot) // 2 lugs at 180° — quarter-turn locks both simultaneously for (a = [0, 180]) rotate([0, 0, a]) translate([LATCH_POST_D/2 + LATCH_SLOT_W/2, 0, 5]) cube([LATCH_SLOT_W + 0.3, LATCH_POST_D, knob_h], center = true); } } // ============================================================ // GASKET PROFILE 2D (for DXF export) // ============================================================ // Outputs the O-ring groove path as a 2D outline suitable for // laser-cutting a flat silicone sheet gasket (alternative to O-ring cord). // Sheet gasket: 2 mm silicone sheet, cut to this profile. module gasket_profile_2d() { oring_cl_offset = FLANGE_WALL - ORING_INSET; outer_w = INT_W + 2*FLANGE_WALL - 2*oring_cl_offset; inner_w = outer_w - 2*ORING_GROOVE_W; r_outer = BOX_R - 2; r_inner = r_outer - ORING_GROOVE_W; difference() { minkowski() { square([outer_w - 2*r_outer, INT_D + 2*FLANGE_WALL - 2*oring_cl_offset - 2*r_outer], center = true); circle(r = r_outer); } minkowski() { square([inner_w - 2*r_inner, INT_D + 2*FLANGE_WALL - 2*oring_cl_offset - 2*r_inner - 2*ORING_GROOVE_W], center = true); circle(r = r_inner); } } }