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