// ============================================================ // ip54_sensor_housings.scad — IP54 Sensor Housings // Issue: #144 Agent: sl-mechanical Date: 2026-03-01 // ============================================================ // // Weatherproof housings for sensors exposed to outdoor conditions: // // Part A — imx219_dome() Clear PC dome for IMX219 CSI camera // IP54, anti-fog element pocket, gasket-sealed base ring // Part B — d435i_housing() Sealed D435i RealSense housing // IP54, IR-transparent PC window, O-ring sealed front frame // Part C — rplidar_dome() Spinning dome for RPLIDAR A1M8 scanner // Clear PC hemisphere, static base + rotary lip seal // // Window/dome materials: // Camera domes : 2 mm clear polycarbonate (Covestro Makrolon) // D435i window : 3 mm IR-transparent PC (transmits 850 nm / 930 nm IR) // e.g. Evonik PLEXIGLAS IR (Altuglas 8N, 85% IR850 T) // RPLIDAR dome : 1.5 mm clear PC hemisphere, Ø120 mm OD // (off-the-shelf: plastic Easter egg halves or custom vac-form) // // Anti-fog provision: // Each camera dome has a pocket for a 1 g silica gel packet (demountable) // and an anti-fog coating groove (optionally apply Rain-X or similar). // // Coordinate convention: // Sensor faces +Y (forward). Z = 0 at housing base / mounting face. // // All housings mount to sensor_rail_brackets.scad T-nut arm ends. // Bracket arm interfaces re-used from existing rail brackets. // // RENDER options: // "assembly" all 3 housings side by side // "imx219_dome_stl" IMX219 dome base ring (print 1× per camera) // "imx219_dome_2d" DXF — dome polycarbonate disc profile // "d435i_body_stl" D435i housing body (print 1×) // "d435i_window_2d" DXF — IR-transparent PC window profile // "d435i_frame_stl" D435i window retention frame (print 1×) // "rplidar_base_stl" RPLIDAR dome base ring (print 1×) // "rplidar_dome_2d" DXF — RPLIDAR clear PC dome spec // // Export commands: // openscad ip54_sensor_housings.scad -D 'RENDER="imx219_dome_stl"' -o imx219_dome.stl // openscad ip54_sensor_housings.scad -D 'RENDER="imx219_dome_2d"' -o imx219_dome_profile.dxf // openscad ip54_sensor_housings.scad -D 'RENDER="d435i_body_stl"' -o d435i_body.stl // openscad ip54_sensor_housings.scad -D 'RENDER="d435i_window_2d"' -o d435i_window.dxf // openscad ip54_sensor_housings.scad -D 'RENDER="d435i_frame_stl"' -o d435i_frame.stl // openscad ip54_sensor_housings.scad -D 'RENDER="rplidar_base_stl"' -o rplidar_base.stl // openscad ip54_sensor_housings.scad -D 'RENDER="rplidar_dome_2d"' -o rplidar_dome_spec.dxf // ============================================================ $fn = 64; e = 0.01; // ── Fasteners ───────────────────────────────────────────────────────────────── M2_D = 2.4; M3_D = 3.3; M4_D = 4.3; // ── O-ring groove standard (2 mm cord, 70 % compression) ───────────────────── ORING_W = 2.2; ORING_D = 1.7; // ── IMX219 sensor interface (matches sensor_rail_brackets.scad) ─────────────── IMX_PCB_W = 32.0; IMX_PCB_H = 32.0; IMX_HOLE_SPC = 24.0; // ── D435i sensor interface ──────────────────────────────────────────────────── D4_BODY_W = 90.0; D4_BODY_D = 25.0; D4_BODY_H = 25.0; D4_MOUNT_D = 6.5; // ── RPLIDAR A1M8 sensor interface ───────────────────────────────────────────── RPL_BODY_D = 70.0; RPL_BODY_H = 40.0; // approximate scan head height RPL_MOTOR_D = 30.0; // motor spindle OD (approximate, lower portion) RPL_BC_D = 58.0; RPL_BOLT_D = 3.3; // ============================================================ // ── PART A: IMX219 CLEAR DOME ───────────────────────────────────────────────── // ============================================================ // // Base ring: printed PETG, gasket-sealed, mounts to sensor rail bracket. // Dome: 2 mm clear PC disc or hemisphere (separate purchase / fabrication). // // Design overview: // • Circular base ring sized to accept the dome OD (snap or screw) // • O-ring groove at dome seating face → IP54 // • Camera PCB attaches inside on 4× M2 standoffs // • Silica gel pocket on inner wall (1 g sachet, removable via dome) // • Anti-fog groove (optional coating during assembly) // • Base flange: M3 bolt pattern matching IMX219 bracket arm IMX_DOME_OD = 55.0; // clear PC dome outer diameter IMX_DOME_RIM_T = 4.0; // base ring wall thickness IMX_DOME_H = 35.0; // inner cavity height (lens to dome apex clearance) IMX_RING_H = 18.0; // base ring height (below dome seating face) IMX_PCB_STOFF = 4.0; // PCB standoff height inside dome IMX_TILT_DEG = 10.0; // dome tilt (same as imx219 bracket) module imx219_dome() { base_od = IMX_DOME_OD + 2*IMX_DOME_RIM_T; base_id = IMX_DOME_OD - 2*IMX_DOME_RIM_T; // internal cavity OD difference() { union() { // ── Base ring cylinder ───────────────────────────────────── cylinder(d = base_od, h = IMX_RING_H); // ── Dome retention lip (retains dome from below) ─────────── // Dome rests on this lip; screw-on retainer ring holds it. translate([0, 0, IMX_RING_H]) difference() { cylinder(d = base_od, h = 4); // Dome inner bore (dome drops in) translate([0, 0, -e]) cylinder(d = IMX_DOME_OD + 0.4, h = 4 + 2*e); } // ── Base flange (mounts to bracket arm) ─────────────────── // Square flange extending below ring translate([-base_od/2, -base_od/2, -8]) cube([base_od, base_od, 8]); } // ── Internal cavity ──────────────────────────────────────────── translate([0, 0, -e]) cylinder(d = IMX_DOME_OD - 2*IMX_DOME_RIM_T + 0.4, h = IMX_RING_H + 4 + 2*e); // ── O-ring groove at dome seating face ───────────────────────── translate([0, 0, IMX_RING_H - ORING_D]) difference() { cylinder(d = IMX_DOME_OD - 2 + ORING_W, h = ORING_D + e); cylinder(d = IMX_DOME_OD - 2 - ORING_W, h = ORING_D + e); } // ── 4× M2 PCB standoff bores (square 24×24 mm pattern) ──────── for (sx = [-1, 1]) for (sy = [-1, 1]) translate([sx*IMX_HOLE_SPC/2, sy*IMX_HOLE_SPC/2, -e]) cylinder(d = M2_D, h = IMX_RING_H + 4 + 2*e); // ── Base flange M3 bolt holes (4 corners, match bracket arm) ── for (bx = [-1, 1]) for (by = [-1, 1]) translate([bx*(base_od/2 - 5), by*(base_od/2 - 5), -8 - e]) cylinder(d = M3_D, h = 8 + 2*e); // ── Silica gel pocket (inside ring wall, rear) ───────────────── // Small pocket ~25×20×5 mm accessible when dome removed translate([0, -(base_od/2 - 3), IMX_RING_H/2]) cube([20, 6, 12], center = true); // ── FFC cable exit slot (bottom of ring) ────────────────────── translate([0, 0, -8 - e]) cube([10, base_od + 2*e, 5], center = true); } // ── M2 standoffs (inside ring) ────────────────────────────────────── for (sx = [-1, 1]) for (sy = [-1, 1]) translate([sx*IMX_HOLE_SPC/2, sy*IMX_HOLE_SPC/2, 0]) difference() { cylinder(d = 5, h = IMX_PCB_STOFF); cylinder(d = M2_D, h = IMX_PCB_STOFF + e); } } // ── IMX219 dome PC disc profile (DXF) ──────────────────────────────────────── // 2D profile for laser-cutting / ordering the clear PC dome disc. // Material: 2 mm clear polycarbonate. module imx219_dome_profile_2d() { circle(d = IMX_DOME_OD); } // ── IMX219 retainer ring (screw-on, holds dome in base) ────────────────────── // M24 metric thread (modelled as cylindrical press-fit with snap grooves). module imx219_retainer_ring() { base_od = IMX_DOME_OD + 2*IMX_DOME_RIM_T; ring_h = 8.0; difference() { cylinder(d = base_od, h = ring_h); translate([0, 0, -e]) cylinder(d = IMX_DOME_OD + 0.6, h = ring_h + 2*e); // Grip notches for (i = [0:5]) rotate([0, 0, i*60]) translate([base_od/2 - 1, 0, ring_h/2]) cylinder(d = 2, h = ring_h + e, center = true); } } // ============================================================ // ── PART B: D435i REALSENSE SEALED HOUSING ─────────────────────────────────── // ============================================================ // // Body: PETG printed U-channel wraps D435i body. // Camera inserts from front; front window frame retains it. // Window: 3 mm IR-transparent PC (88 × 22 mm), O-ring sealed. // Transmits >85% at 850 nm (IR stereo projector + receiver). // Rear cap: snap-on / screw-on PETG cap, O-ring sealed. // Cable exits via PG7 gland on rear cap. // Top: flat, M3 holes for mounting to sensor rail D435i bracket. // // Internal clearance: 94 × 29 × 29 mm (D4 body + 2 mm each side). D4H_INT_W = D4_BODY_W + 4; // internal width clearance D4H_INT_D = D4_BODY_D + 4; // internal depth D4H_INT_H = D4_BODY_H + 4; // internal height D4H_WALL = 3.5; // housing wall thickness D4H_WIN_W = D4_BODY_W - 2; // window aperture width D4H_WIN_H = D4_BODY_H - 2; // window aperture height D4H_WIN_T = 3.0; // window thickness (PC) D4H_WIN_REC= 1.5; // window recess depth (sits into frame) D4H_TILT = 8.0; // nose-down tilt, matches existing mount module d435i_housing_body() { out_w = D4H_INT_W + 2*D4H_WALL; out_d = D4H_INT_D + 2*D4H_WALL; out_h = D4H_INT_H + 2*D4H_WALL; difference() { // ── Outer shell ────────────────────────────────────────────── cube([out_w, out_d, out_h], center = true); // ── Internal cavity ───────────────────────────────────────── cube([D4H_INT_W, D4H_INT_D, D4H_INT_H + 2*e], center = true); // ── Front window aperture (sensor-facing face, +Y) ────────── translate([0, out_d/2 - D4H_WALL - e, 0]) cube([D4H_WIN_W, D4H_WALL + 2*e, D4H_WIN_H], center = true); // ── Window recess (window sits flush in face) ──────────────── translate([0, out_d/2 - D4H_WIN_REC, 0]) cube([D4H_WIN_W + 2*D4H_WIN_REC, D4H_WIN_REC + e, D4H_WIN_H + 2*D4H_WIN_REC], center = true); // ── O-ring groove around front aperture ───────────────────── // Groove on front face, surrounds window aperture translate([0, out_d/2 - ORING_D - 0.5, 0]) difference() { cube([D4H_WIN_W + 2*(D4H_WIN_REC + ORING_W), ORING_D + e, D4H_WIN_H + 2*(D4H_WIN_REC + ORING_W)], center = true); cube([D4H_WIN_W + 2*D4H_WIN_REC, ORING_D + 2*e, D4H_WIN_H + 2*D4H_WIN_REC], center = true); } // ── Rear cap opening (camera inserted from rear, cap closes it) ─ translate([0, -out_d/2 - e, 0]) cube([D4H_INT_W - 2, out_d/4, D4H_INT_H - 2], center = true); // ── PG7 gland hole in rear cap mating face ─────────────────── translate([0, -out_d/2 - e, 0]) rotate([90, 0, 0]) cylinder(d = 12.7, h = D4H_WALL + 2*e); // ── 1/4-20 captured nut for tripod/bracket mount (bottom) ──── translate([0, 0, -out_h/2 - e]) rotate([180, 0, 0]) cylinder(d = 6.5, h = D4H_WALL + 2*e); translate([0, 0, -out_h/2 + 4]) cylinder(d = 11.4/cos(30), h = 5, $fn = 6); // ── M3 mounting holes on top (sensor rail bracket interface) ── for (mx = [-20, 0, 20]) translate([mx, 0, out_h/2 - e]) cylinder(d = M3_D, h = D4H_WALL + 2*e); } } // ── D435i window retention frame ───────────────────────────────────────────── // Screws onto front of housing body, sandwiches the IR-transparent PC window. // 4× M2.5 screws at corners pull frame against O-ring seal. module d435i_window_frame() { out_w = D4H_INT_W + 2*D4H_WALL; out_h = D4H_INT_H + 2*D4H_WALL; frame_t = 5.0; frame_w = out_w + 2; frame_h = out_h + 2; difference() { // Frame plate cube([frame_w, frame_t, frame_h], center = true); // Central window reveal (slightly smaller than aperture — shows window) cube([D4H_WIN_W - 4, frame_t + 2*e, D4H_WIN_H - 4], center = true); // Window recess (PC sits in this pocket on rear face) translate([0, -frame_t/2 + D4H_WIN_REC/2, 0]) cube([D4H_WIN_W + 0.4, D4H_WIN_REC + e, D4H_WIN_H + 0.4], center = true); // 4× M2.5 mounting screws at corners for (fx = [-1, 1]) for (fz = [-1, 1]) translate([fx*(frame_w/2 - 5), -frame_t/2 - e, fz*(frame_h/2 - 5)]) rotate([90, 0, 0]) cylinder(d = 2.8, h = frame_t + 2*e); } } // ── D435i window profile (DXF) ─────────────────────────────────────────────── // Profile for laser-cutting 3 mm IR-transparent PC. module d435i_window_profile_2d() { square([D4H_WIN_W, D4H_WIN_H], center = true); } // ── D435i rear cap ──────────────────────────────────────────────────────────── // O-ring sealed rear cap. Snap-over with 2× M3 retention screws. module d435i_rear_cap() { out_w = D4H_INT_W + 2*D4H_WALL; out_h = D4H_INT_H + 2*D4H_WALL; cap_t = 5.0; difference() { union() { cube([out_w, cap_t, out_h], center = true); // Lip that wraps inside housing rear opening translate([0, cap_t/2 - e, 0]) cube([D4H_INT_W - 2 - 0.4, 6, D4H_INT_H - 2 - 0.4], center = true); } // PG7 cable gland rotate([90, 0, 0]) cylinder(d = 12.7, h = cap_t + 6 + 2*e, center = true); // O-ring groove on lip perimeter translate([0, cap_t/2 + 3, 0]) difference() { cube([D4H_INT_W - 2 + ORING_W, ORING_D, D4H_INT_H - 2 + ORING_W], center = true); cube([D4H_INT_W - 2 - ORING_W, ORING_D + e, D4H_INT_H - 2 - ORING_W], center = true); } // M3 retention screw holes for (mz = [-1, 1]) translate([0, -cap_t/2 - e, mz*(out_h/2 - 8)]) rotate([90, 0, 0]) cylinder(d = M3_D, h = cap_t + 2*e); } } // ============================================================ // ── PART C: RPLIDAR SPINNING DOME ──────────────────────────────────────────── // ============================================================ // // The RPLIDAR A1M8 scan head spins continuously. A transparent PC dome // covers the entire scanner, protecting it from water and debris. // // Architecture: // • Static base ring: mounts to robot deck; RPLIDAR mounts inside on its // standard 4× M3 bolt circle (Ø58 mm BC). // • Spinning clear dome: rotates with scan head OR is statically mounted // with sufficient clearance for the scan head to spin inside. // ★ Design choice here: STATIC dome, sized Ø120 mm OD × 95 mm tall. // The scanner spins inside the static dome. No rotary seal needed. // Scan laser exits through dome walls at all angles (clear PC transmits // the 785 nm laser with <5 % absorption at 1.5 mm wall thickness). // • Dome lip sits in O-ring groove in base ring → IP54 seal. // • Dome retention: 3× M3 captive-nut clips, quarter-turn removal. // // Dome spec: custom vac-form, OR cut top from clear PC tube Ø120 mm OD. // Off-the-shelf option: clear plastic cylinder Ø120×120 mm (party supply). // Top cap: 1.5–2 mm clear PC disc, Ø120 mm. RPL_DOME_OD = 120.0; // dome outer diameter RPL_DOME_H = 95.0; // dome total height (covers scanner + motor) RPL_DOME_T = 2.0; // dome wall thickness (clear PC) RPL_BASE_H = 20.0; // base ring height RPL_BASE_WALL = 5.0; // base ring wall thickness RPL_CLEAR = 5.0; // radial clearance between scanner and dome wall // Base ring inner bore must clear RPLIDAR motor (Ø70 mm body + clearance) RPL_BASE_BORE = RPL_BODY_D + 2*RPL_CLEAR; // = 80 mm module rplidar_dome_base() { base_od = RPL_DOME_OD + 2*RPL_BASE_WALL; difference() { union() { // ── Outer base cylinder ────────────────────────────────── cylinder(d = base_od, h = RPL_BASE_H); // ── Dome seat lip (raised inner lip, dome rests on top) ── translate([0, 0, RPL_BASE_H]) difference() { cylinder(d = base_od, h = 4); // Dome drops over this; 0.5 mm radial clearance translate([0, 0, -e]) cylinder(d = RPL_DOME_OD + 1, h = 4 + 2*e); } } // ── RPLIDAR body bore (scanner sits inside base ring) ──────── translate([0, 0, -e]) cylinder(d = RPL_BASE_BORE, h = RPL_BASE_H + 4 + 2*e); // ── 4× M3 bolt holes — RPLIDAR mounting (Ø58 mm BC, 45° off) ─ for (a = [45, 135, 225, 315]) translate([RPL_BC_D/2 * cos(a), RPL_BC_D/2 * sin(a), -e]) cylinder(d = RPL_BOLT_D, h = RPL_BASE_H + 4 + 2*e); // ── O-ring groove at dome seating face ─────────────────────── // Groove in top face of base ring; dome rim presses onto O-ring. translate([0, 0, RPL_BASE_H + 4 - ORING_D]) difference() { cylinder(d = RPL_DOME_OD + ORING_W, h = ORING_D + e); cylinder(d = RPL_DOME_OD - ORING_W, h = ORING_D + e); } // ── 3× M3 dome retention clip pockets (quarter-turn inserts) ─ for (a = [0, 120, 240]) translate([RPL_DOME_OD/2 * cos(a), RPL_DOME_OD/2 * sin(a), RPL_BASE_H + 1]) rotate([0, 0, a]) { // Clip slot: L-shaped slot for quarter-turn retention translate([0, 0, 0]) cube([M3_D + 0.5, 8, 8], center = true); translate([3, 0, -3]) cube([M3_D + 0.5 + 6, 8, 3 + e], center = true); } // ── Cable pass-through (motor USB + power) ─────────────────── // Slot in base ring floor for cable routing translate([0, RPL_BASE_BORE/2, -e]) cube([12, RPL_BASE_BORE/2, 6], center = true); // ── Deck mounting holes (4× M4 on standard bolt circle) ────── for (a = [0, 90, 180, 270]) translate([(base_od/2 - 8) * cos(a), (base_od/2 - 8) * sin(a), -e]) cylinder(d = M4_D, h = RPL_BASE_H + 4 + 2*e); } } // ── RPLIDAR dome profile (DXF — cylindrical tube spec) ─────────────────────── // 2D cross-section profile for clear PC dome cylinder purchase/fabrication. // Cut a length of Ø120 mm clear PC tube; add a disc cap. module rplidar_dome_profile_2d() { // Cross-section annulus: OD = RPL_DOME_OD, wall = RPL_DOME_T difference() { circle(d = RPL_DOME_OD); circle(d = RPL_DOME_OD - 2*RPL_DOME_T); } } // ── RPLIDAR dome top cap (clear PC disc — DXF profile only) ────────────────── module rplidar_dome_cap_2d() { circle(d = RPL_DOME_OD - RPL_DOME_T); } // ── RPLIDAR dome retention clip ──────────────────────────────────────────────── // Printed clip slides into quarter-turn slot on base ring. // Captive M3 bolt tip engages hole drilled in dome wall. // Print: PETG, 5 perims, 60% infill. 3× per dome. module rplidar_dome_clip() { difference() { union() { // T-body (fits in L-slot) cube([M3_D + 2, 8, 8], center = true); // Engagement lug translate([3, 0, -3]) cube([M3_D + 8, 7, 3], center = true); } // M3 bore (bolt presses against dome wall) rotate([0, 90, 0]) cylinder(d = M3_D, h = M3_D + 10, center = true); } } // ============================================================ // RENDER DISPATCH // ============================================================ RENDER = "assembly"; if (RENDER == "assembly") { assembly(); } else if (RENDER == "imx219_dome_stl") { imx219_dome(); } else if (RENDER == "imx219_dome_2d") { projection(cut = true) translate([0, 0, -0.5]) linear_extrude(1) imx219_dome_profile_2d(); } else if (RENDER == "d435i_body_stl") { d435i_housing_body(); } else if (RENDER == "d435i_window_2d") { projection(cut = true) translate([0, 0, -0.5]) linear_extrude(1) d435i_window_profile_2d(); } else if (RENDER == "d435i_frame_stl") { d435i_window_frame(); } else if (RENDER == "rplidar_base_stl") { rplidar_dome_base(); } else if (RENDER == "rplidar_dome_2d") { projection(cut = true) translate([0, 0, -0.5]) linear_extrude(1) rplidar_dome_profile_2d(); } // ============================================================ // ASSEMBLY PREVIEW // ============================================================ module assembly() { // IMX219 dome (left) color("DodgerBlue", 0.80) translate([-120, 0, 0]) imx219_dome(); // IMX219 clear dome ghost %color("LightCyan", 0.25) translate([-120, 0, 18]) cylinder(d = IMX_DOME_OD + 0.4, h = IMX_DOME_H); // D435i housing (centre) color("DarkSlateGray", 0.85) translate([0, 0, 0]) d435i_housing_body(); // D435i window frame ghost %color("LightBlue", 0.30) translate([0, (D4H_INT_D + 2*D4H_WALL)/2, 0]) rotate([90, 0, 0]) d435i_window_frame(); // RPLIDAR dome base (right) color("OliveDrab", 0.85) translate([160, 0, 0]) rplidar_dome_base(); // RPLIDAR clear dome ghost %color("LightCyan", 0.25) translate([160, 0, RPL_BASE_H + 4]) cylinder(d = RPL_DOME_OD, h = RPL_DOME_H - RPL_BASE_H - 4); // Ghost scanner inside dome %color("Black", 0.35) translate([160, 0, RPL_BASE_H/2]) cylinder(d = RPL_BODY_D, h = RPL_BODY_H, center = true); }