saltylab-firmware/chassis/ip54_sensor_housings.scad
sl-mechanical a70d9a2a71 feat(mechanical): IP54 weatherproofing kit (Issue #144)
Add sealed enclosures and sensor housings for outdoor IP54 protection:
- ip54_enclosure.scad: main electronics box (Jetson/FC/ESC), O-ring lid,
  fan+filter duct, PG7/PG9 cable glands, quarter-turn latches, heat sink
  recesses; gasket DXF export
- ip54_sensor_housings.scad: IMX219 clear PC dome (O-ring + anti-fog
  pocket), D435i IR-transparent window housing (PG7 rear cap), RPLIDAR
  static clear PC dome base ring (120 mm OD, O-ring, quarter-turn clips)
- ip54_BOM.md: hardware list, thermal analysis (≤52°C at 40°C ambient),
  IP54 compliance checklist, mass ~930g total kit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 09:58:22 -05:00

551 lines
24 KiB
OpenSCAD
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ============================================================
// 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.52 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);
}