From 893574ee79c6379b5b27754f8cd78be50200adbd Mon Sep 17 00:00:00 2001 From: Hermes Date: Fri, 22 May 2026 01:03:53 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20redesigned=20case=20=E2=80=94=20tripod-?= =?UTF-8?q?clip=20box=20for=20dual=20ESPs,=20USB=20power=20bank?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced GoPro-sleeve case design with simpler stand-mounted box: - Case clips to tripod leg/stand pole (20-35mm diameter) - No camera sleeve needed — case sits on the stand - Powered by standard USB power bank (off-the-shelf) - Holds ESP8266 + ESP32 stacked with UART wiring - Cable ports for USB in/out, LED window, ventilation Simplified BOM: ~1/node (down from 4), no buck converters needed --- hardware/README.md | 169 +++++++++-------------- hardware/case/remoterig-case.scad | 222 ------------------------------ hardware/case/tripod-case.scad | 201 +++++++++++++++++++++++++++ 3 files changed, 268 insertions(+), 324 deletions(-) delete mode 100644 hardware/case/remoterig-case.scad create mode 100644 hardware/case/tripod-case.scad diff --git a/hardware/README.md b/hardware/README.md index 6dded05..3d0a0a3 100644 --- a/hardware/README.md +++ b/hardware/README.md @@ -1,144 +1,109 @@ # RemoteRig — Camera Node Hardware Design -> **Version:** 0.1.0 | **Status:** Draft -> **Target:** GoPro Hero 3 Black/Silver + ESP32 D1 Mini + 1000mAh LiPo +> **Version:** 0.2.0 | **Status:** Draft +> **Target:** GoPro Hero 3 Black/Silver + ESP8266 + ESP32 + USB power bank ## Overview -Each camera node is a self-contained unit clipped onto a GoPro Hero 3. It provides: -- Camera control (start/stop recording) via Wi-Fi -- Status monitoring (battery, storage, recording state) -- MQTT communication to the central Pi Zero 2 W hub -- Battery power for both the ESP32 and GoPro - -## Physical Assembly +Each camera node is two ESP boards in a small case that clips to the tripod/stand. The case **does not attach to the camera** — only to the stand. Powered by a standard USB power bank. ``` -┌─────────────────────────────────┐ -│ GoPro Hero 3 │ -│ ┌─────────────────────────┐ │ -│ │ Lens (front) │ │ -│ └─────────────────────────┘ │ -│ ┌─────────────────────────┐ │ -│ │ Screen │ │ -│ └─────────────────────────┘ │ -│ ┌──────────┐ │ -│ 3D Sleeve ─────→│ ESP8266 │ │ ← Camera bridge (GoPro Wi-Fi) -│ │ D1 Mini │ │ -│ ├──────────┤ │ -│ │ ESP32 │ │ ← MQTT bridge (travel router) -│ │ Dev │ │ -│ ├──────────┤ │ -│ │ LiPo │ │ ← Shared power -│ │ 1000mAh │ │ -│ └──────────┘ │ -└─────────────────────────────────┘ + ┌─────────────────┐ + │ USB Power Bank │── USB ──→ GoPro (power only) + │ (off-the-shelf)│── USB ──→ ESP32 + ESP8266 (shared) + └─────────────────┘ + │ + ┌────────┴────────┐ + │ Tripod Case │ ← clips to stand leg + │ ┌────────────┐ │ + │ │ ESP8266 │ │ ← Wi-Fi → GoPro AP (10.5.5.1) + │ │ (camera) │ │ + │ ├────────────┤ │ ← UART between boards + │ │ ESP32 │ │ ← Wi-Fi → Travel Router + │ │ (MQTT) │ │ + │ └────────────┘ │ + └─────────────────┘ ``` ## Bill of Materials | Item | Qty | Cost | Notes | |------|-----|------|-------| -| GoPro Hero 3 Black/Silver | 1 | Already owned | Target camera | | ESP32 Dev Board | 1 | ~$5 | MQTT bridge — talks to hub | | ESP8266 D1 Mini | 1 | ~$3 | Camera bridge — talks to GoPro | -| LiPo 3.7V 1000mAh | 1 | ~$8 | 50x34x8mm typical | -| 3.3V buck converter | 1 | ~$1 | LiPo → both boards (shared VIN) | -| 5V/3A buck converter | 1 | ~$2 | LiPo → GoPro USB (power only) | -| JST-XH 2-pin connectors | 2 | ~$1 | Battery quick-disconnect | -| Micro-USB right-angle cable | 1 | ~$2 | Buck → GoPro | -| Jumper wires (female-female) | 4 | ~$0.50 | UART + GND between boards | -| Velcro strap (20cm) | 1 | ~$0.50 | Secure to GoPro | -| PETG filament | ~35g | ~$0.70 | 3D printed case | +| USB power bank (5000mAh+) | 1 | ~$10 | Powers both boards + GoPro | +| Micro-USB cable (short) | 2 | ~$2 | Power bank → boards + GoPro | +| Jumper wires F-F | 3 | ~$0.25 | UART TX/RX/GND between boards | +| PETG filament | ~25g | ~$0.50 | 3D printed case | +| Velcro strap (small) | 1 | ~$0.25 | Secure power bank to stand | -**Total per node:** ~$24 +**Total per node:** ~$21 (+ GoPro already owned) ## 3D Printed Case -The case consists of three parts (see `hardware/case/remoterig-case.scad`): +**File:** `hardware/case/tripod-case.scad` -### Part 1: GoPro Sleeve -Wraps around the GoPro body with cutouts for: -- Lens (front) -- Screen/viewfinder (back) -- USB port (side) -- Bottom mounting fingers -- Mounting ears for electronics compartment - -### Part 2: Electronics Compartment -Clips onto the sleeve's mounting ears. Holds: -- ESP32 D1 Mini board (recessed fit) -- USB cable routing (in → ESP32, out → GoPro) -- Ventilation slots (top) -- LED visibility window - -### Part 3: Battery Compartment -Slides under the GoPro. Contains: -- LiPo battery cavity -- Cable exits (to ESP32, to GoPro buck converter) -- Velcro strap slots +Three parts: +1. **Case body** — holds both boards stacked, cable ports, rail for clip +2. **Case lid** — screw-on cover with ventilation +3. **Tripod clip** — C-clamp for 20-35mm poles, slides into case rail ### Print Settings -- **Material:** PETG (outdoor/heat resistant) or PLA+ -- **Layer height:** 0.2mm -- **Infill:** 20% gyroid -- **Supports:** Yes (for cable channels) -- **Bed adhesion:** Brim (5mm) for sleeve -- **Orientation:** Print sleeve on its back, compartments flat +- **Material:** PETG (outdoor/heat) or PLA+ +- **Layer:** 0.2mm | **Infill:** 20% gyroid +- **Supports:** Yes (for clip overhang) +- **Post-processing:** M3x8mm screws for lid (4x) ## Wiring ``` -LiPo 3.7V - ├── JST-XH connector +USB Power Bank + ├── USB-A → Micro-USB cable → ESP32 USB port + │ (powers ESP32, shared 5V rail) │ - ├──→ 3.3V Buck Converter → ESP8266 VIN + GND - │ → ESP32 VIN + GND - │ (both boards share the same 3.3V rail) + ├── USB-A → Micro-USB cable → GoPro USB port + │ (power only — no data) │ - └──→ 5V/3A Buck Converter → Micro-USB right-angle → GoPro USB port - (power only — no data over USB) + └── (ESP8266 powered via ESP32 3.3V pin, or via shared USB) -UART (ESP8266 ↔ ESP32): +UART (inside case): ESP8266 TX (GPIO1) ──→ ESP32 RX (GPIO16) ESP8266 RX (GPIO3) ←── ESP32 TX (GPIO17) ESP8266 GND ─────────── ESP32 GND ``` -## Wi-Fi Topology (No Cables for Camera Control) +**Power note:** Both boards can be powered from a single USB cable if the ESP32's VIN/5V pin is bridged to the ESP8266's VIN. Alternatively, use a USB Y-splitter cable. + +## Wi-Fi Topology ``` -GoPro Hero 3 ──(Wi-Fi AP @ 10.5.5.1)──→ ESP32 STA #1 - │ -Travel Router ──(Wi-Fi AP)─────────────────→ ESP32 STA #2 -(192.168.4.1) │ - │ - └──→ MQTT → Pi Hub (192.168.4.10) +GoPro Hero 3 ──(AP @ 10.5.5.1)──→ ESP8266 (camera bridge) + │ + UART │ (inside case) + │ +Travel Router ──(AP)─────────────────→ ESP32 (MQTT bridge) +(192.168.4.1) │ + │ + MQTT │ + ▼ + Pi Hub (192.168.4.10) ``` -The ESP32 has **no wired data connection** to the GoPro. All camera control is over Wi-Fi. The USB cable is **power only**. +The ESP8266 and GoPro talk over Wi-Fi — **no data cable between them**. The only cable to the GoPro is USB power from the battery pack. -## Enclosure Dimensions +## Field Setup -| Component | W × H × D (mm) | -|-----------|-----------------| -| GoPro Hero 3 | 60 × 42 × 30 | -| ESP32 D1 Mini | 34 × 26 × 5 | -| LiPo 1000mAh | 50 × 34 × 8 | -| Full assembly | ~70 × 60 × 55 | +1. **Mount GoPro** on tripod/stand +2. **Clip case** to tripod leg +3. **Connect power bank** via USB to case + GoPro +4. **Power on** — ESP32 auto-connects to travel router, ESP8266 auto-connects to GoPro +5. **Monitor** from `http://192.168.4.10:8080` -## Usage in the Field +## Case Dimensions -1. **Pre-show:** Charge LiPos, flash ESP32 firmware, verify MQTT connectivity -2. **At venue:** Mount cameras, power on ESP32s (they auto-connect to travel router) -3. **Monitoring:** Open `http://192.168.4.10:8080` on laptop/kiosk -4. **Control:** Start/stop recording from dashboard -5. **Post-show:** Stop recording, power down, swap batteries for next session - -## Future Improvements - -- **Hot-swap battery:** Quick-release battery tray with spring contacts -- **Weather sealing:** O-ring groove in sleeve for outdoor rain protection -- **Lens hood:** Integrated sun shield for outdoor daytime recording -- **Mount adapter:** 1/4"-20 tripod mount thread on bottom -- **Antenna routing:** External antenna connector for improved Wi-Fi range in stadiums +| | W × D × H (mm) | +|---|---| +| Case external | ~64 × 38 × 27 | +| Case internal | ~57 × 31 × 18 | +| Fits poles | 20-35mm diameter | +| Total weight | ~50g (case + boards, without power bank) | diff --git a/hardware/case/remoterig-case.scad b/hardware/case/remoterig-case.scad deleted file mode 100644 index 100ad90..0000000 --- a/hardware/case/remoterig-case.scad +++ /dev/null @@ -1,222 +0,0 @@ -// RemoteRig — GoPro Hero 3 + ESP32 Camera Case -// ============================================== -// Sleeve that wraps around GoPro Hero 3 body with ESP32 + LiPo compartment. -// Designed for: ESP32 D1 Mini, 1000mAh LiPo, GoPro Hero 3 Black/Silver. -// -// Print settings: -// Material: PETG (outdoor/heat) or PLA+ (indoor) -// Layer: 0.2mm | Infill: 20% gyroid | Supports: yes (for cable channels) -// Nozzle: 0.4mm | Bed: 60°C (PLA) / 80°C (PETG) - -// ── GoPro Hero 3 Body (approximate) ── -gopro_width = 60; // mm — body width -gopro_height = 42; // mm — body height (top to bottom) -gopro_depth = 30; // mm — body depth (front to back) -gopro_lens_dia = 28; // mm — lens protrusion diameter -gopro_lens_offset = 18; // mm — lens center from top - -// ── ESP8266 D1 Mini + ESP32 Dev Board (stacked) ── -esp8266_width = 34.2; -esp8266_height = 25.6; -esp8266_thick = 5; // board + components - -esp32_width = 52; // ESP32 Dev Board is larger -esp32_height = 28; -esp32_thick = 5; - -// Combined stack -board_width = max(esp8266_width, esp32_width); -board_height = max(esp8266_height, esp32_height); -board_thick = esp8266_thick + esp32_thick + 3; // 3mm gap between boards - -// ── LiPo Battery (1000mAh typical) ── -lipo_width = 35; -lipo_height = 25; -lipo_thick = 8; - -// ── Case parameters ── -wall = 2.0; // case wall thickness -tolerance = 0.3; // print tolerance for friction fit -compartment_height = board_thick + 5; // internal compartment height for stacked boards - -// ── Cable channels ── -cable_dia = 4; // USB cable diameter -cable_channel_depth = 3; - -// ══════════════════════════════════════════════════════════════ -// MAIN ASSEMBLY -// ══════════════════════════════════════════════════════════════ - -// Uncomment the part you want to export: -gopro_sleeve(); -// translate([0, -20, 0]) electronics_compartment(); -// translate([0, 20, 0]) battery_compartment(); - -// ══════════════════════════════════════════════════════════════ -// GoPro Sleeve — wraps around the GoPro body -// ══════════════════════════════════════════════════════════════ - -module gopro_sleeve() { - union() { - // Main sleeve body — wraps around GoPro - difference() { - // Outer shell - rounded_cube( - gopro_width + wall*2, - gopro_height + wall*2, - gopro_depth + wall*2, - 4 // corner radius - ); - - // Inner cavity (GoPro body) - translate([0, 0, wall]) - rounded_cube( - gopro_width + tolerance, - gopro_height + tolerance, - gopro_depth + tolerance, - 3 - ); - - // Lens cutout (front face) - translate([0, gopro_height/2 - gopro_lens_offset, 0]) - rotate([90, 0, 0]) - cylinder(d=gopro_lens_dia + 4, h=wall*3, center=true); - - // Front screen/viewfinder cutout - translate([0, gopro_height/2 - gopro_lens_offset - 18, wall*2]) - cube([gopro_width - 10, gopro_height - 20, wall*4], center=true); - - // Bottom cutout (for GoPro mounting fingers) - translate([0, 0, gopro_depth/2 + wall]) - cube([gopro_width - 10, wall*4, wall*4], center=true); - - // USB port access (side) - translate([gopro_width/2 + wall, 0, -5]) - cube([wall*4, 16, 10], center=true); - - // Cable channel from ESP32 compartment to GoPro USB - translate([gopro_width/2 - 5, -gopro_height/2 + 10, -gopro_depth/2 + 5]) - rotate([0, 90, 0]) - cylinder(d=cable_dia, h=wall*3, center=true); - } - - // Mounting ears for electronics compartment - for (x = [-1, 1]) { - translate([x * (gopro_width/2 - 6), -gopro_height/2 - 6, 0]) - rotate([90, 0, 0]) - cylinder(d=8, h=10); - } - } -} - -// ══════════════════════════════════════════════════════════════ -// Electronics Compartment — holds ESP8266 + ESP32 stacked -// ══════════════════════════════════════════════════════════════ - -module electronics_compartment() { - comp_w = board_width + wall*2 + 8; - comp_h = compartment_height + wall*2; - comp_d = board_height + wall*2 + 8; - - difference() { - union() { - // Main box - rounded_cube(comp_w, comp_d, comp_h, 3); - - // Mounting tabs (match GoPro sleeve ears) - for (x = [-1, 1]) { - translate([x * (gopro_width/2 - 6), 0, comp_h/2]) - rotate([0, 90, 0]) - cylinder(d=6, h=4, center=true); - } - } - - // Inner cavity - translate([0, 0, wall]) - rounded_cube(comp_w - wall*2, comp_d - wall*2, comp_h - wall, 2); - - // Bottom board (ESP32 — larger) recess - translate([0, 5, wall + 1]) - cube([esp32_width + tolerance, esp32_height + tolerance, esp32_thick + 1], center=true); - - // Top board (ESP8266 — smaller) recess - translate([0, 5, wall + esp32_thick + 4]) - cube([esp8266_width + tolerance, esp8266_height + tolerance, esp8266_thick + 1], center=true); - - // UART wire channel (between boards) - translate([comp_w/2, 0, wall + esp32_thick + 1]) - cube([wall*3, 6, 3], center=true); - - // USB cable entry (power to boards) - translate([comp_w/2, 15, comp_h/2]) - rotate([0, 90, 0]) - cylinder(d=6, h=wall*3, center=true); - - // USB cable exit (to GoPro) - translate([comp_w/2, -15, comp_h/2]) - rotate([0, 90, 0]) - cylinder(d=cable_dia, h=wall*3, center=true); - - // Ventilation slots - for (y = [-1:2:1]) { - for (i = [-15:10:15]) { - translate([i, y * comp_d/3, comp_h - 2]) - cube([6, 1.5, wall*2], center=true); - } - } - - // LED windows (thin walls for ESP LEDs) - translate([0, 0, wall]) - cube([5, 5, wall], center=true); - translate([0, 0, wall + esp32_thick + 4]) - cube([5, 5, wall], center=true); - } -} - -// ══════════════════════════════════════════════════════════════ -// Battery Compartment — holds LiPo under GoPro -// ══════════════════════════════════════════════════════════════ - -module battery_compartment() { - bat_w = lipo_width + wall*2 + tolerance; - bat_d = lipo_height + wall*2 + tolerance; - bat_h = lipo_thick + wall*2 + 4; - - difference() { - // Shell - rounded_cube(bat_w, bat_d, bat_h, 3); - - // Battery cavity - translate([0, 0, wall]) - rounded_cube(lipo_width + tolerance, lipo_height + tolerance, lipo_thick + tolerance, 1); - - // Cable exit (to ESP32 compartment) - translate([0, bat_d/2, bat_h/2]) - rotate([90, 0, 0]) - cylinder(d=cable_dia, h=wall*3, center=true); - - // Cable exit (to GoPro USB) - translate([bat_w/3, -bat_d/2, bat_h/2]) - rotate([90, 0, 0]) - cylinder(d=cable_dia, h=wall*3, center=true); - - // Strap slots (velcro strap to secure to GoPro) - for (x = [-bat_w/3, bat_w/3]) { - translate([x, -bat_d/2, bat_h/2]) - cube([8, wall*4, 3], center=true); - } - } -} - -// ══════════════════════════════════════════════════════════════ -// Utility: Rounded cube (positive X/Y/Z = full dimensions) -// ══════════════════════════════════════════════════════════════ - -module rounded_cube(w, d, h, r) { - hull() { - for (x = [-1, 1], y = [-1, 1], z = [-1, 1]) { - translate([x * (w/2 - r), y * (d/2 - r), z * (h/2 - r)]) - sphere(r=r, $fn=20); - } - } -} diff --git a/hardware/case/tripod-case.scad b/hardware/case/tripod-case.scad new file mode 100644 index 0000000..9780a3a --- /dev/null +++ b/hardware/case/tripod-case.scad @@ -0,0 +1,201 @@ +// RemoteRig — Dual-ESP Tripod Case +// ================================= +// Small box that clips onto a tripod leg or light stand pole. +// Holds ESP8266 D1 Mini + ESP32 Dev Board (stacked). +// Powered by standard USB battery pack. No camera sleeve needed. +// +// Print settings: +// Material: PETG | Layer: 0.2mm | Infill: 20% gyroid +// Supports: yes (for clip overhang) | Brim: 5mm + +// ── Board dimensions ── +esp8266_w = 34.2; esp8266_d = 25.6; esp8266_h = 5; +esp32_w = 52; esp32_d = 28; esp32_h = 5; +board_gap = 3; // air gap between stacked boards +stack_h = esp8266_h + esp32_h + board_gap; +inner_w = max(esp8266_w, esp32_w); +inner_d = max(esp8266_d, esp32_d); +inner_h = stack_h + 2; + +// ── Case parameters ── +wall = 2.0; +tol = 0.4; +outer_w = inner_w + wall*2 + tol*2; +outer_d = inner_d + wall*2 + tol*2; +outer_h = inner_h + wall*2; + +// ── Tripod clip parameters ── +pole_min_dia = 20; // smallest pole +pole_max_dia = 35; // largest pole +clip_width = 12; // clip width +clip_thick = 3; // clip arm thickness +clip_grip = 2; // grip ridges + +// ── Cable ports ── +usb_port_w = 12; usb_port_h = 6; +uart_port_w = 6; uart_port_h = 4; + +// ══════════════════════════════════════════════════════════════ +// MAIN — render the full case +// ══════════════════════════════════════════════════════════════ + +// Uncomment to render individual parts: +full_case(); +// case_body(); +// case_lid(); +// tripod_clip(); + +module full_case() { + case_body(); + // Lid positioned above (for visualization) + translate([0, 0, outer_h + 2]) + case_lid(); + // Clip on the back + translate([0, outer_d/2 + pole_max_dia/2 + clip_thick, outer_h/2]) + tripod_clip(); +} + +// ══════════════════════════════════════════════════════════════ +// Case Body — holds both boards, cable ports +// ══════════════════════════════════════════════════════════════ + +module case_body() { + difference() { + // Outer shell + rounded_cube(outer_w, outer_d, outer_h, 3); + + // Inner cavity + translate([0, 0, wall]) + rounded_cube(inner_w + tol, inner_d + tol, inner_h + tol, 2); + + // ── Board recesses ── + + // Bottom: ESP32 (larger board) + translate([0, 0, wall + 1]) + cube([esp32_w + tol, esp32_d + tol, esp32_h + 1], center=true); + + // Top: ESP8266 (smaller board) + translate([0, 0, wall + esp32_h + board_gap + 1]) + cube([esp8266_w + tol, esp8266_d + tol, esp8266_h + 1], center=true); + + // ── Cable ports ── + + // USB power IN (from battery pack → ESP32) + translate([0, outer_d/2, outer_h/3]) + cube([usb_port_w, wall*3, usb_port_h], center=true); + + // USB power OUT (from battery pack → GoPro) + translate([0, -outer_d/2, outer_h/3]) + cube([usb_port_w, wall*3, usb_port_h], center=true); + + // UART wire channel (ESP8266 → ESP32 internal) + translate([outer_w/2, 0, outer_h/2]) + cube([wall*3, uart_port_w, uart_port_h], center=true); + + // ── Ventilation slots (top edge) ── + for (x = [-outer_w/4, 0, outer_w/4]) { + translate([x, 0, outer_h - wall]) + cube([8, outer_d*0.6, 2], center=true); + } + + // ── Screw posts for lid ── + for (x = [-1, 1], y = [-1, 1]) { + translate([x*(outer_w/2 - 5), y*(outer_d/2 - 5), outer_h/2]) + cylinder(d=3.2, h=outer_h, center=true, $fn=16); + } + + // ── LED window (thin spot to see board LEDs) ── + translate([-outer_w/4, -outer_d/2, wall]) + cube([6, 1, 3], center=true); + } + + // ── Tripod clip mount (rail on back) ── + translate([0, outer_d/2, outer_h/2]) + rotate([90, 0, 0]) + difference() { + cube([clip_width + 4, outer_h*0.7, 6], center=true); + // T-slot for clip to slide in + cube([clip_width + 1, outer_h*0.7 + 1, 4], center=true); + } +} + +// ══════════════════════════════════════════════════════════════ +// Case Lid — snap-fit or screw-on cover +// ══════════════════════════════════════════════════════════════ + +module case_lid() { + difference() { + rounded_cube(outer_w, outer_d, wall*2, 2); + + // Screw holes (match body posts) + for (x = [-1, 1], y = [-1, 1]) { + translate([x*(outer_w/2 - 5), y*(outer_d/2 - 5), 0]) + cylinder(d=3.2, h=wall*3, center=true, $fn=16); + } + + // Ventilation slots (match body) + for (x = [-outer_w/4, 0, outer_w/4]) { + translate([x, 0, 0]) + cube([8, outer_d*0.6, 3], center=true); + } + } +} + +// ══════════════════════════════════════════════════════════════ +// Tripod Clip — C-clamp for pole mounting +// ══════════════════════════════════════════════════════════════ + +module tripod_clip() { + difference() { + union() { + // Main body + hull() { + translate([0, -pole_max_dia/2 - clip_thick, 0]) + cube([clip_width, clip_thick*2, outer_h*0.7], center=true); + + translate([0, pole_max_dia/2 + clip_thick, 0]) + cube([clip_width, clip_thick*2, outer_h*0.7], center=true); + } + + // Top arm (flexible) + translate([0, -pole_max_dia/2 - clip_thick, outer_h*0.35]) + cube([clip_width, pole_max_dia + clip_thick*4, clip_thick], center=true); + + // Bottom arm + translate([0, -pole_max_dia/2 - clip_thick, -outer_h*0.35]) + cube([clip_width, pole_max_dia + clip_thick*4, clip_thick], center=true); + + // Mounting tab (slides into case rail) + translate([0, -pole_max_dia/2 - clip_thick*3, 0]) + cube([clip_width + 1, clip_thick*2, outer_h*0.7], center=true); + } + + // Pole hole + cylinder(d=pole_max_dia + 2, h=outer_h*1.5, center=true, $fn=32); + + // Grip ridges on inner surface + for (z = [-outer_h*0.25, 0, outer_h*0.25]) { + translate([0, 0, z]) + rotate_extrude(angle=180, $fn=32) + translate([pole_max_dia/2 + 0.5, 0]) + circle(d=1); + } + + // Entry slot (pole slides in from front) + translate([0, pole_max_dia/2 + clip_thick, 0]) + cube([clip_width + 2, pole_max_dia + 10, outer_h*0.7], center=true); + } +} + +// ══════════════════════════════════════════════════════════════ +// Utility: rounded cube +// ══════════════════════════════════════════════════════════════ + +module rounded_cube(w, d, h, r) { + hull() { + for (x = [-1, 1], y = [-1, 1], z = [-1, 1]) { + translate([x*(w/2 - r), y*(d/2 - r), z*(h/2 - r)]) + sphere(r=r, $fn=20); + } + } +}