saltylab-firmware/chassis/csi_rain_shield.scad
sl-mechanical a0d0c1211b feat: Add Issue #254 - CSI camera rain/sun shields
Add csi_rain_shield.scad with parametric visor-style protective
shields for all 4 Raspberry Pi CSI cameras (IMX219).

Features:
- Visor-style overhang (32mm forward extension) shields lens from
  rain and direct sunlight
- Drip edge prevents water from trickling back onto PCB
- Snap-fit tabs (4 per shield, no fasteners required)
- Ventilation slots prevent moisture condensation
- Optional transparent polycarbonate lens cover insert
- Radially symmetric design fits all 4 cameras

Includes comprehensive BOM with:
- Print settings and material specifications
- Assembly instructions for snap-fit installation
- Maintenance and troubleshooting guide
- Weather resistance performance notes
- Design variants for different conditions
- Post-print finishing recommendations

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 12:48:41 -05:00

222 lines
10 KiB
OpenSCAD
Raw 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.

// ============================================================
// csi_rain_shield.scad — CSI Camera Rain/Sun Shield
// Issue: #254 Agent: sl-mechanical Date: 2026-03-02
// ============================================================
//
// Parametric visor-style rain and sun shield for SaltyBot's
// 4× Raspberry Pi CSI IMX219 cameras (32×32 mm PCB).
//
// Features:
// • Visor-style overhang — protects lens from rain & sun
// • Drip edge — prevents water from running back onto PCB
// • Snap-fit attachment — clips to camera housing, no fasteners
// • Transparent polycarbonate compatible — removable lens cover
// • Radial symmetry — same design fits all 4 cameras
//
// Design for IMX219:
// • Camera face plate: 38×38 mm (32 mm PCB + 3 mm frame per side)
// • Arm depth: 52 mm from sensor head centre to camera
// • 10° nose-down tilt on radial arms
//
// Assembly:
// 1. Print shield body (flat-side-down, minimal supports)
// 2. Optional: Insert clear polycarbonate sheet in front
// 3. Snap shield onto camera housing (4 tabs lock into grooves)
// 4. Repeat for all 4 cameras
//
// RENDER options:
// "shield" single shield unit (default)
// "assembly" 4 shields on camera arms (visual reference)
// "lens_cover" removable transparent polycarbonate insert
// ============================================================
RENDER = "shield";
// ── Camera housing dimensions ──────────────────────────────
// IMX219 camera with face-plate protection.
CAM_PCB_SIZE = 32.0; // PCB square side
CAM_FACE_SIZE = 38.0; // face plate (PCB + 3mm bezel each side)
CAM_FACE_THICK = 4.0; // face plate thickness (front-to-back)
// M2 hole pattern on PCB (for reference, not used in shield)
M2_SPACING = 24.0; // hole pattern spacing
// ── Radial arm geometry (from imx219_mount.scad) ───────────
ARM_R = 50.0; // platform radius (used in assembly view)
REACH_LEN = 52.0; // arm extension from sensor head
ARM_TILT = 10.0; // nose-down tilt angle
// ── Rain shield overhang ───────────────────────────────────
// Visor extends forward and downward from camera housing.
VISOR_OVERHANG = 32.0; // forward extension from camera face
VISOR_H = 18.0; // overhang height (downward from top)
VISOR_DROP = 16.0; // bottom lip drop (prevents water runoff)
VISOR_THICK = 3.0; // wall thickness
// Drip edge: curved lip at bottom to prevent water trickling back
DRIP_RADIUS = 3.0; // radius of drip edge curve
DRIP_HEIGHT = 4.0; // height of drip lip
// ── Snap-fit attachment tabs ───────────────────────────────
// Tabs clip into grooves on camera housing side walls.
SNAP_TAB_H = 6.0; // tab height (above housing)
SNAP_TAB_W = 8.0; // tab width
SNAP_TAB_D = 2.0; // tab depth (into groove)
SNAP_CLEARANCE = 0.4; // clearance for easy snap/unsnap
NUM_TABS = 4; // 4 tabs around perimeter
// ── Lens cover (polycarbonate insert) ──────────────────────
// Optional transparent front cover — slides into grooves.
LENS_COVER_T = 2.0; // polycarbonate sheet thickness
LENS_COVER_W = CAM_FACE_SIZE + 6.0; // cover width (with edge flanges)
LENS_COVER_FLANGE = 3.0; // flange width on each side
// ── Ventilation ────────────────────────────────────────────
// Small vent slots prevent moisture condensation inside shield.
VENT_SLOT_W = 3.0; // vent slot width
VENT_SLOT_H = 2.0; // vent slot height
NUM_VENTS = 3; // slots per side (3 on left, 3 on right)
// ── General ────────────────────────────────────────────────
$fn = 64;
e = 0.01;
// ─────────────────────────────────────────────────────────────
// csi_rain_shield()
// Single shield unit — visor with snap-fit tabs.
// Print flat-side-down (visor facing down).
//
// Coordinate system:
// X = left-right (camera width direction)
// Y = front-back (camera pointing direction)
// Z = up-down
// Camera face at Y=0, lens center at origin.
// ─────────────────────────────────────────────────────────────
module csi_rain_shield() {
// Main visor body — asymmetric overhang
difference() {
union() {
// Visor base plate (behind camera, attaches to housing)
translate([-CAM_FACE_SIZE/2 - 4, -CAM_FACE_THICK - 2, 0])
cube([CAM_FACE_SIZE + 8, 6, VISOR_THICK]);
// Forward overhang — curved top for water runoff
translate([-VISOR_H/2, -CAM_FACE_THICK, 0]) {
difference() {
// Main overhang volume
cube([VISOR_H, VISOR_OVERHANG, VISOR_THICK]);
// Curved top surface (water sheds outward)
translate([VISOR_H/2, 0, -e])
rotate([90, 0, 0])
cylinder(r = VISOR_H/2 + 2, h = VISOR_OVERHANG + 2*e);
}
}
// Side flanges (stabilize against wind, provide snap-tab base)
for (sx = [-1, 1])
translate([sx * (CAM_FACE_SIZE/2 + 2), -CAM_FACE_THICK, 0])
cube([4, CAM_FACE_THICK + VISOR_OVERHANG/2, VISOR_THICK]);
// Drip edge — bottom lip curves downward & forward
translate([-(VISOR_H-2)/2, -CAM_FACE_THICK + VISOR_OVERHANG - 2, -DRIP_HEIGHT])
rotate([0, 90, 0])
cylinder(r = DRIP_RADIUS, h = VISOR_H - 4);
}
// Lens opening — lets camera see through (frame around camera face)
translate([-(CAM_FACE_SIZE-2)/2, -CAM_FACE_THICK - 2, -e])
cube([CAM_FACE_SIZE-2, 2, VISOR_THICK + 2*e]);
// Vent slots (prevent condensation) — left side
for (i = [0 : NUM_VENTS-1]) {
vent_y = -CAM_FACE_THICK + (VISOR_OVERHANG * (i+1) / (NUM_VENTS+1));
translate([-(CAM_FACE_SIZE/2 + 8), vent_y, VISOR_THICK/2 - VENT_SLOT_H/2])
cube([2, VENT_SLOT_W, VENT_SLOT_H]);
translate([(CAM_FACE_SIZE/2 + 6), vent_y, VISOR_THICK/2 - VENT_SLOT_H/2])
cube([2, VENT_SLOT_W, VENT_SLOT_H]);
}
}
// Snap-fit tabs — clip into camera housing grooves
for (i = [0 : NUM_TABS-1]) {
angle = (360 / NUM_TABS) * i - 45; // 4 tabs at 90° intervals, rotated -45°
translate([-CAM_FACE_SIZE/2 - 3, -CAM_FACE_THICK/2, 0])
rotate([0, 0, angle])
translate([CAM_FACE_SIZE/2 + 3 + SNAP_TAB_D/2, 0, -SNAP_TAB_H/2])
cube([SNAP_TAB_W, SNAP_TAB_D + SNAP_CLEARANCE, SNAP_TAB_H], center=true);
}
// Lens cover groove — retains transparent polycarbonate insert
translate([-(LENS_COVER_W/2 + 1), -CAM_FACE_THICK - 1.5, -e])
cube([LENS_COVER_W + 2, 1.5, LENS_COVER_T + 0.5]);
}
// ─────────────────────────────────────────────────────────────
// csi_lens_cover()
// Removable transparent polycarbonate insert.
// This is a phantom/visual-only part; actual cover is cut from
// 2.0 mm clear polycarbonate sheet and trimmed to size.
// ─────────────────────────────────────────────────────────────
module csi_lens_cover() {
// Clear lens cover — sits in grooves on shield front edge
difference() {
union() {
// Main cover plate (represents polycarbonate insert)
translate([-(LENS_COVER_W/2), -CAM_FACE_THICK - 2, 0])
cube([LENS_COVER_W, 2, LENS_COVER_T]);
// Side flanges (grip in shield grooves)
for (sx = [-1, 1])
translate([sx * (LENS_COVER_W/2 - LENS_COVER_FLANGE/2),
-CAM_FACE_THICK - 2, 0])
cube([LENS_COVER_FLANGE, 2, LENS_COVER_T]);
}
// Lens opening (clear aperture in front of camera)
translate([-(CAM_FACE_SIZE-4)/2, -CAM_FACE_THICK - 3, -e])
cube([CAM_FACE_SIZE-4, 4, LENS_COVER_T + 2*e]);
}
}
// ─────────────────────────────────────────────────────────────
// assembly_4x_shields()
// All 4 shields mounted on radial arms (reference visualization).
// ─────────────────────────────────────────────────────────────
module assembly_4x_shields() {
// 4 shields at 90° intervals (matches 4x CSI cameras)
for (angle = [0, 90, 180, 270]) {
color("LightSteelBlue", 0.85)
rotate([0, 0, angle])
translate([ARM_R, 0, 0])
rotate([ARM_TILT, 0, 0])
csi_rain_shield();
// Phantom camera face (reference only)
color("DarkGray", 0.5)
rotate([0, 0, angle])
translate([ARM_R, 0, 0])
rotate([ARM_TILT, 0, 0])
translate([-(CAM_FACE_SIZE/2), 0, -CAM_FACE_THICK/2])
cube([CAM_FACE_SIZE, 1, CAM_FACE_SIZE], center=false);
}
}
// ─────────────────────────────────────────────────────────────
// Render selector
// ─────────────────────────────────────────────────────────────
if (RENDER == "shield") {
csi_rain_shield();
} else if (RENDER == "assembly") {
assembly_4x_shields();
} else if (RENDER == "lens_cover") {
csi_lens_cover();
}