From 8222a0c42e18592c053be452a9352210555249b8 Mon Sep 17 00:00:00 2001 From: sl-mechanical Date: Mon, 2 Mar 2026 10:15:14 -0500 Subject: [PATCH] feat(mechanical): charging dock station (Issue #159) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Universal 5 V/5 A charging dock for SaltyLab/Rover/Tank robots: - charging_dock.scad: weighted base (ballast pockets, floor anchors), back wall with 2× 5 A pogo pin housing + wiring channel, V-guide funnel rails (±20 mm alignment tolerance), ArUco marker mast (100×100 mm, 15° tilt), PSU bracket (IRM-30-5), 4-LED status bezel (Searching/Aligned/Charging/Full) - charging_dock_receiver.scad: 3-variant robot-side contact plate with Ø12 mm brass pad press-fit, V-nose self-alignment; SaltyLab stem collar, SaltyRover deck flange, SaltyTank skid-plate mount - charging_dock_BOM.md: hardware list, ASCII wiring diagram, INA219 current-sense LED state logic, pogo height cross-variant shim table, assembly sequence, export commands Co-Authored-By: Claude Sonnet 4.6 --- chassis/charging_dock.scad | 530 ++++++++++++++++++++++++++++ chassis/charging_dock_BOM.md | 202 +++++++++++ chassis/charging_dock_receiver.scad | 331 +++++++++++++++++ 3 files changed, 1063 insertions(+) create mode 100644 chassis/charging_dock.scad create mode 100644 chassis/charging_dock_BOM.md create mode 100644 chassis/charging_dock_receiver.scad diff --git a/chassis/charging_dock.scad b/chassis/charging_dock.scad new file mode 100644 index 0000000..8c18e78 --- /dev/null +++ b/chassis/charging_dock.scad @@ -0,0 +1,530 @@ +// ============================================================ +// charging_dock.scad — SaltyLab Charging Dock Station +// Issue: #159 Agent: sl-mechanical Date: 2026-03-01 +// ============================================================ +// +// Universal charging dock for SaltyLab / SaltyRover / SaltyTank. +// Robot drives forward into V-guide funnel; spring-loaded pogo pins +// make contact with the robot receiver plate (charging_dock_receiver.scad). +// +// Power: 5 V / 5 A (25 W) via 2× high-current pogo pins (+/-) +// Alignment tolerance: ±20 mm lateral (V-guide funnels to centre) +// +// Dock architecture (top view): +// +// ┌─────────────────────────────────┐ ← back wall (robot stops here) +// │ PSU shelf │ +// │ [PSU] [LED ×4] │ +// │ [POGO+][POGO-] │ ← pogo face (robot contact) +// └────\ /────────┘ +// \ V-guide rails / +// \ / +// ╲ ╱ ← dock entry, ±20 mm funnel +// +// Components (this file): +// Part A — dock_base() weighted base plate with ballast pockets +// Part B — back_wall() upright back panel + pogo housing + LED bezel +// Part C — guide_rail(side) V-funnel guide rail, L/R (print 2×) +// Part D — aruco_mount() ArUco marker frame at dock entrance +// Part E — psu_bracket() PSU retention bracket (rear of base) +// Part F — led_bezel() 4-LED status bezel +// +// Robot-side receiver → see charging_dock_receiver.scad +// +// Coordinate system: +// Z = 0 at dock floor (base plate top face) +// Y = 0 at back wall front face (robot approaches from +Y) +// X = 0 at dock centre +// Robot drives in -Y direction to dock. +// +// RENDER options: +// "assembly" full dock preview (default) +// "base_stl" base plate (print 1×) +// "back_wall_stl" back wall + pogo housing (print 1×) +// "guide_rail_stl" V-guide rail (print 2×, mirror for R side) +// "aruco_mount_stl" ArUco marker frame (print 1×) +// "psu_bracket_stl" PSU mounting bracket (print 1×) +// "led_bezel_stl" LED status bezel (print 1×) +// +// Export commands: +// openscad charging_dock.scad -D 'RENDER="base_stl"' -o dock_base.stl +// openscad charging_dock.scad -D 'RENDER="back_wall_stl"' -o dock_back_wall.stl +// openscad charging_dock.scad -D 'RENDER="guide_rail_stl"' -o dock_guide_rail.stl +// openscad charging_dock.scad -D 'RENDER="aruco_mount_stl"' -o dock_aruco_mount.stl +// openscad charging_dock.scad -D 'RENDER="psu_bracket_stl"' -o dock_psu_bracket.stl +// openscad charging_dock.scad -D 'RENDER="led_bezel_stl"' -o dock_led_bezel.stl +// ============================================================ + +$fn = 64; +e = 0.01; + +// ── Base plate dimensions ───────────────────────────────────────────────────── +BASE_W = 280.0; // base width (X) +BASE_D = 250.0; // base depth (Y, extends behind and in front of back wall) +BASE_T = 12.0; // base thickness +BASE_R = 10.0; // corner radius + +// Ballast pockets (for steel hex bar / bolt weights): +// 4× pockets in base underside, accept M20 hex nuts (30 mm AF) stacked +BALLAST_N = 4; +BALLAST_W = 32.0; // pocket width (hex nut AF + 2 mm) +BALLAST_D = 32.0; // pocket depth +BALLAST_T = 8.0; // pocket depth (≤ BASE_T/2) +BALLAST_INSET_X = 50.0; +BALLAST_INSET_Y = 40.0; + +// Floor bolt holes (M8, for bolting dock to bench/floor — optional) +FLOOR_BOLT_D = 8.5; +FLOOR_BOLT_INSET_X = 30.0; +FLOOR_BOLT_INSET_Y = 25.0; + +// ── Back wall (upright panel) ───────────────────────────────────────────────── +WALL_W = 250.0; // wall width (X) — same as guide entry span +WALL_H = 85.0; // wall height (Z) +WALL_T = 10.0; // wall thickness (Y) + +// Back wall Y position relative to base rear edge +// Wall sits at Y=0 (its front face); base extends behind it (-Y) and in front (+Y) +BASE_REAR_Y = -80.0; // base rear edge Y coordinate + +// ── Pogo pin housing (in back wall front face) ──────────────────────────────── +// High-current pogo pins: Ø5.5 mm body, 20 mm long (compressed), 4 mm spring travel +// Rated 5 A each; 2× pins for +/- power +POGO_D = 5.5; // pogo pin body OD +POGO_BORE_D = 5.7; // bore diameter (0.2 mm clearance) +POGO_L = 20.0; // pogo full length (uncompressed) +POGO_TRAVEL = 4.0; // spring travel +POGO_FLANGE_D = 8.0; // pogo flange / retention shoulder OD +POGO_FLANGE_T = 1.5; // flange thickness +POGO_SPACING = 20.0; // CL-to-CL spacing between + and - pins +POGO_Z = 35.0; // pogo CL height above dock floor +POGO_PROTRUDE = 8.0; // pogo tip protrusion beyond wall face (uncompressed) +// Wiring channel behind pogo (runs down to base) +WIRE_CH_W = 8.0; +WIRE_CH_H = POGO_Z + 5; + +// ── LED bezel (4 status LEDs in back wall, above pogo pins) ─────────────────── +// LED order (left to right): Searching | Aligned | Charging | Full +// Colours (suggested): Red | Yellow | Blue | Green +LED_D = 5.0; // 5 mm through-hole LED +LED_BORE_D = 5.2; // bore diameter +LED_BEZEL_W = 80.0; // bezel plate width +LED_BEZEL_H = 18.0; // bezel plate height +LED_BEZEL_T = 4.0; // bezel plate thickness +LED_SPACING = 16.0; // LED centre-to-centre +LED_Z = 65.0; // LED centre height above floor +LED_INSET_D = 2.0; // LED recess depth (LED body recessed for protection) + +// ── V-guide rails ───────────────────────────────────────────────────────────── +// Robot receiver width (contact block): 30 mm. +// Alignment tolerance: ±20 mm → entry gap = 30 + 2×20 = 70 mm. +// Guide rail tapers from 70 mm entry (at Y = GUIDE_L) to 30 mm exit (at Y=0). +// Each rail is a wedge-shaped wall. +GUIDE_L = 100.0; // guide rail length (Y depth, from back wall) +GUIDE_H = 50.0; // guide rail height (Z) +GUIDE_T = 8.0; // guide rail wall thickness +RECV_W = 30.0; // robot receiver contact block width +ENTRY_GAP = 70.0; // guide entry gap (= RECV_W + 2×20 mm tolerance) +EXIT_GAP = RECV_W + 2.0; // guide exit gap (2 mm clearance on each side) +// Derived: half-gap at entry = 35 mm, at exit = 16 mm; taper = 19 mm over 100 mm +// Half-angle = atan(19/100) ≈ 10.8° — gentle enough for reliable self-alignment + +// ── ArUco marker mount ──────────────────────────────────────────────────────── +// Mounted at dock entry arch (forward of guide rails), tilted 15° back. +// Robot camera acquires marker for coarse approach alignment. +// Standard ArUco marker size: 100×100 mm (printed/laminated on paper). +ARUCO_MARKER_W = 100.0; +ARUCO_MARKER_H = 100.0; +ARUCO_FRAME_T = 3.0; // frame plate thickness +ARUCO_FRAME_BDR = 10.0; // frame border around marker +ARUCO_SLOT_T = 1.5; // marker slip-in slot depth +ARUCO_MAST_H = 95.0; // mast height above base (centres marker at camera height) +ARUCO_MAST_W = 10.0; +ARUCO_TILT = 15.0; // backward tilt (degrees) — faces approaching robot +ARUCO_Y = GUIDE_L + 60; // mast Y position (in front of guide entry) + +// ── PSU bracket ─────────────────────────────────────────────────────────────── +// Meanwell IRM-30-5 (or similar): 63×45×28 mm body +// Bracket sits behind back wall, on base plate. +PSU_W = 68.0; // bracket internal width (+5 mm clearance) +PSU_D = 50.0; // bracket internal depth +PSU_H = 32.0; // bracket internal height +PSU_T = 3.0; // bracket wall thickness +PSU_Y = BASE_REAR_Y + PSU_D/2 + PSU_T + 10; // PSU Y centre + +// ── Fasteners ───────────────────────────────────────────────────────────────── +M3_D = 3.3; +M4_D = 4.3; +M5_D = 5.3; +M8_D = 8.5; + +// ============================================================ +// RENDER DISPATCH +// ============================================================ +RENDER = "assembly"; + +if (RENDER == "assembly") assembly(); +else if (RENDER == "base_stl") dock_base(); +else if (RENDER == "back_wall_stl") back_wall(); +else if (RENDER == "guide_rail_stl") guide_rail("left"); +else if (RENDER == "aruco_mount_stl") aruco_mount(); +else if (RENDER == "psu_bracket_stl") psu_bracket(); +else if (RENDER == "led_bezel_stl") led_bezel(); + +// ============================================================ +// ASSEMBLY PREVIEW +// ============================================================ +module assembly() { + // Base plate + color("SaddleBrown", 0.85) dock_base(); + + // Back wall + color("Sienna", 0.85) + translate([0, 0, BASE_T]) + back_wall(); + + // Left guide rail + color("Peru", 0.85) + translate([0, 0, BASE_T]) + guide_rail("left"); + + // Right guide rail (mirror in X) + color("Peru", 0.85) + translate([0, 0, BASE_T]) + mirror([1, 0, 0]) + guide_rail("left"); + + // ArUco mount + color("DimGray", 0.85) + translate([0, 0, BASE_T]) + aruco_mount(); + + // PSU bracket + color("DarkSlateGray", 0.80) + translate([0, PSU_Y, BASE_T]) + psu_bracket(); + + // LED bezel + color("LightGray", 0.90) + translate([0, -WALL_T/2, BASE_T + LED_Z]) + led_bezel(); + + // Ghost robot receiver approaching from +Y + %color("SteelBlue", 0.25) + translate([0, GUIDE_L + 30, BASE_T + POGO_Z]) + cube([RECV_W, 20, 8], center = true); + + // Ghost pogo pins + for (px = [-POGO_SPACING/2, POGO_SPACING/2]) + %color("Gold", 0.60) + translate([px, -POGO_PROTRUDE, BASE_T + POGO_Z]) + rotate([90, 0, 0]) + cylinder(d = POGO_D, h = POGO_L); +} + +// ============================================================ +// PART A — DOCK BASE PLATE +// ============================================================ +module dock_base() { + difference() { + // ── Main base block (rounded rect) ────────────────────────── + linear_extrude(BASE_T) + minkowski() { + square([BASE_W - 2*BASE_R, + BASE_D - 2*BASE_R], center = true); + circle(r = BASE_R); + } + + // ── Ballast pockets (underside) ────────────────────────────── + // 4× pockets: 2 front, 2 rear + for (bx = [-1, 1]) + for (by = [-1, 1]) + translate([bx * (BASE_W/2 - BALLAST_INSET_X), + by * (BASE_D/2 - BALLAST_INSET_Y), + -e]) + cube([BALLAST_W, BALLAST_D, BALLAST_T + e], center = true); + + // ── Floor bolt holes (M8, 4 corners) ──────────────────────── + for (bx = [-1, 1]) + for (by = [-1, 1]) + translate([bx * (BASE_W/2 - FLOOR_BOLT_INSET_X), + by * (BASE_D/2 - FLOOR_BOLT_INSET_Y), -e]) + cylinder(d = FLOOR_BOLT_D, h = BASE_T + 2*e); + + // ── Back wall attachment slots (M4, top face) ───────────────── + for (bx = [-WALL_W/2 + 30, 0, WALL_W/2 - 30]) + translate([bx, -BASE_D/4, BASE_T - 3]) + cylinder(d = M4_D, h = 4 + e); + + // ── Guide rail attachment holes (M4) ────────────────────────── + for (side = [-1, 1]) + for (gy = [20, GUIDE_L - 20]) + translate([side * (EXIT_GAP/2 + GUIDE_T/2), gy, BASE_T - 3]) + cylinder(d = M4_D, h = 4 + e); + + // ── Cable routing slot (from pogo wires to PSU, through base) ─ + translate([0, -WALL_T - 5, -e]) + cube([WIRE_CH_W, 15, BASE_T + 2*e], center = true); + + // ── Anti-skid texture (front face chamfer) ─────────────────── + // Chamfer front-bottom edge for easy robot approach + translate([0, BASE_D/2 + e, -e]) + rotate([45, 0, 0]) + cube([BASE_W + 2*e, 5, 5], center = true); + } +} + +// ============================================================ +// PART B — BACK WALL (upright panel) +// ============================================================ +module back_wall() { + difference() { + union() { + // ── Wall slab ──────────────────────────────────────────── + translate([-WALL_W/2, -WALL_T, 0]) + cube([WALL_W, WALL_T, WALL_H]); + + // ── Pogo pin housing bosses (front face) ───────────────── + for (px = [-POGO_SPACING/2, POGO_SPACING/2]) + translate([px, -WALL_T, POGO_Z]) + rotate([90, 0, 0]) + cylinder(d = POGO_FLANGE_D + 6, + h = POGO_PROTRUDE); + + // ── Wiring channel reinforcement (inside wall face) ─────── + translate([-WIRE_CH_W/2 - 2, -WALL_T, 0]) + cube([WIRE_CH_W + 4, 4, WIRE_CH_H]); + } + + // ── Pogo pin bores (through wall into housing boss) ─────────── + for (px = [-POGO_SPACING/2, POGO_SPACING/2]) + translate([px, POGO_PROTRUDE + e, POGO_Z]) + rotate([90, 0, 0]) { + // Main bore (full depth through wall + boss) + cylinder(d = POGO_BORE_D, + h = WALL_T + POGO_PROTRUDE + 2*e); + // Flange shoulder counterbore (retains pogo from pulling out) + translate([0, 0, WALL_T + POGO_PROTRUDE - POGO_FLANGE_T - 1]) + cylinder(d = POGO_FLANGE_D + 0.4, + h = POGO_FLANGE_T + 2); + } + + // ── Wiring channel (vertical slot, inside face → base cable hole) ─ + translate([-WIRE_CH_W/2, 0 + e, 0]) + cube([WIRE_CH_W, WALL_T/2, WIRE_CH_H]); + + // ── LED bezel recess (in front face, above pogo) ────────────── + translate([-LED_BEZEL_W/2, -LED_BEZEL_T, LED_Z - LED_BEZEL_H/2]) + cube([LED_BEZEL_W, LED_BEZEL_T + e, LED_BEZEL_H]); + + // ── M4 base attachment bores (3 through bottom of wall) ─────── + for (bx = [-WALL_W/2 + 30, 0, WALL_W/2 - 30]) + translate([bx, -WALL_T/2, -e]) + cylinder(d = M4_D, h = 8 + e); + + // ── Cable tie slots (in wall body, for neat wire routing) ───── + for (cz = [15, POGO_Z - 15]) + translate([WIRE_CH_W/2 + 3, -WALL_T/2, cz]) + cube([4, WALL_T + 2*e, 3], center = true); + + // ── Lightening cutout (rear face pocket) ────────────────────── + translate([-WALL_W/2 + 40, 0, 20]) + cube([WALL_W - 80, WALL_T/2 + e, WALL_H - 30]); + } +} + +// ============================================================ +// PART C — V-GUIDE RAIL +// ============================================================ +// Print 2×; mirror in X for right side. +// Rail tapers from ENTRY_GAP/2 (at Y=GUIDE_L) to EXIT_GAP/2 (at Y=0). +// Inner (guiding) face is angled; outer face is vertical. +module guide_rail(side = "left") { + // Inner face X at back wall = EXIT_GAP/2 + // Inner face X at entry = ENTRY_GAP/2 + x_back = EXIT_GAP/2; // 16 mm + x_entry = ENTRY_GAP/2; // 35 mm + + difference() { + union() { + // ── Main wedge body ───────────────────────────────────── + // Hull between two rectangles: narrow at Y=0, wide at Y=GUIDE_L + hull() { + // Back end (at Y=0, flush with back wall) + translate([x_back, 0, 0]) + cube([GUIDE_T, e, GUIDE_H]); + // Entry end (at Y=GUIDE_L) + translate([x_entry, GUIDE_L, 0]) + cube([GUIDE_T, e, GUIDE_H]); + } + + // ── Entry flare (chamfered lip at guide entry for bump-entry) ─ + hull() { + translate([x_entry, GUIDE_L, 0]) + cube([GUIDE_T, e, GUIDE_H]); + translate([x_entry + 15, GUIDE_L + 20, 0]) + cube([GUIDE_T, e, GUIDE_H * 0.6]); + } + } + + // ── M4 base attachment bores ───────────────────────────────── + for (gy = [20, GUIDE_L - 20]) + translate([x_back + GUIDE_T/2, gy, -e]) + cylinder(d = M4_D, h = 8 + e); + + // ── Chamfer on inner top corner (smooth robot entry) ───────── + translate([x_back - e, -e, GUIDE_H - 5]) + rotate([0, -45, 0]) + cube([8, GUIDE_L + 30, 8]); + } +} + +// ============================================================ +// PART D — ArUco MARKER MOUNT +// ============================================================ +// Free-standing mast at dock entry. Mounts to base plate. +// Marker face tilted 15° toward approaching robot. +// Accepts 100×100 mm printed/laminated paper marker in slot. +module aruco_mount() { + frame_w = ARUCO_MARKER_W + 2*ARUCO_FRAME_BDR; + frame_h = ARUCO_MARKER_H + 2*ARUCO_FRAME_BDR; + mast_y = ARUCO_Y; + + union() { + // ── Mast column ─────────────────────────────────────────────── + translate([-ARUCO_MAST_W/2, mast_y - ARUCO_MAST_W/2, 0]) + cube([ARUCO_MAST_W, ARUCO_MAST_W, ARUCO_MAST_H]); + + // ── Marker frame (tilted back ARUCO_TILT°) ──────────────────── + translate([0, mast_y, ARUCO_MAST_H]) + rotate([-ARUCO_TILT, 0, 0]) { + difference() { + // Frame plate + translate([-frame_w/2, -ARUCO_FRAME_T, -frame_h/2]) + cube([frame_w, ARUCO_FRAME_T, frame_h]); + + // Marker window (cutout for marker visibility) + translate([-ARUCO_MARKER_W/2, -ARUCO_FRAME_T - e, + -ARUCO_MARKER_H/2]) + cube([ARUCO_MARKER_W, + ARUCO_FRAME_T + 2*e, + ARUCO_MARKER_H]); + + // Marker slip-in slot (insert from side) + translate([-frame_w/2 - e, + -ARUCO_SLOT_T - 0.3, + -ARUCO_MARKER_H/2]) + cube([frame_w + 2*e, + ARUCO_SLOT_T + 0.3, + ARUCO_MARKER_H]); + } + } + + // ── Mast base foot (M4 bolts to dock base) ──────────────────── + difference() { + translate([-20, mast_y - 20, 0]) + cube([40, 40, 5]); + for (fx = [-12, 12]) for (fy = [-12, 12]) + translate([fx, mast_y + fy, -e]) + cylinder(d = M4_D, h = 6 + e); + } + } +} + +// ============================================================ +// PART E — PSU BRACKET +// ============================================================ +// Open-top retention bracket for PSU module. +// PSU slides in from top; 2× M3 straps or cable ties retain it. +// Bracket bolts to base plate via 4× M4 screws. +module psu_bracket() { + difference() { + union() { + // ── Outer bracket box (open top) ───────────────────────── + _box_open_top(PSU_W + 2*PSU_T, + PSU_D + 2*PSU_T, + PSU_H + PSU_T); + + // ── Base flange ────────────────────────────────────────── + translate([-(PSU_W/2 + PSU_T + 8), + -(PSU_D/2 + PSU_T + 8), -PSU_T]) + cube([PSU_W + 2*PSU_T + 16, + PSU_D + 2*PSU_T + 16, PSU_T]); + } + + // ── PSU cavity ─────────────────────────────────────────────── + translate([0, 0, PSU_T]) + cube([PSU_W, PSU_D, PSU_H + e], center = true); + + // ── Ventilation slots (sides) ───────────────────────────────── + for (a = [0, 90, 180, 270]) + rotate([0, 0, a]) + translate([0, (PSU_D/2 + PSU_T)/2, PSU_H/2 + PSU_T]) + for (sz = [-PSU_H/4, 0, PSU_H/4]) + translate([0, 0, sz]) + cube([PSU_W * 0.5, PSU_T + 2*e, 5], + center = true); + + // ── Cable exit slot (bottom) ────────────────────────────────── + translate([0, 0, -e]) + cube([15, PSU_D + 2*PSU_T + 2*e, PSU_T + 2*e], + center = true); + + // ── Base flange M4 bolts ────────────────────────────────────── + for (fx = [-1, 1]) for (fy = [-1, 1]) + translate([fx * (PSU_W/2 + PSU_T + 4), + fy * (PSU_D/2 + PSU_T + 4), + -PSU_T - e]) + cylinder(d = M4_D, h = PSU_T + 2*e); + + // ── Cable tie slots ─────────────────────────────────────────── + for (sz = [PSU_H/3, 2*PSU_H/3]) + translate([0, 0, PSU_T + sz]) + cube([PSU_W + 2*PSU_T + 2*e, 4, 4], center = true); + } +} + +module _box_open_top(w, d, h) { + difference() { + cube([w, d, h], center = true); + translate([0, 0, PSU_T + e]) + cube([w - 2*PSU_T, d - 2*PSU_T, h], center = true); + } +} + +// ============================================================ +// PART F — LED STATUS BEZEL +// ============================================================ +// 4 × 5 mm LEDs in a row. Press-fits into recess in back wall. +// LED labels (L→R): SEARCHING | ALIGNED | CHARGING | FULL +// Suggested colours: Red | Yellow | Blue | Green +module led_bezel() { + difference() { + // Bezel plate + cube([LED_BEZEL_W, LED_BEZEL_T, LED_BEZEL_H], center = true); + + // 4× LED bores + for (i = [-1.5, -0.5, 0.5, 1.5]) + translate([i * LED_SPACING, -LED_BEZEL_T - e, 0]) + rotate([90, 0, 0]) { + // LED body bore (recess, not through) + cylinder(d = LED_BORE_D + 1, + h = LED_INSET_D + e); + // LED pin bore (through bezel) + translate([0, 0, LED_INSET_D]) + cylinder(d = LED_BORE_D, + h = LED_BEZEL_T + 2*e); + } + + // Label recesses between LEDs (for colour-dot stickers or printed inserts) + for (i = [-1.5, -0.5, 0.5, 1.5]) + translate([i * LED_SPACING, LED_BEZEL_T/2, LED_BEZEL_H/2 - 3]) + cube([LED_SPACING - 3, 1 + e, 5], center = true); + + // M3 mounting holes (2× into back wall) + for (mx = [-LED_BEZEL_W/2 + 6, LED_BEZEL_W/2 - 6]) + translate([mx, -LED_BEZEL_T - e, 0]) + rotate([90, 0, 0]) + cylinder(d = M3_D, h = LED_BEZEL_T + 2*e); + } +} diff --git a/chassis/charging_dock_BOM.md b/chassis/charging_dock_BOM.md new file mode 100644 index 0000000..ce07d39 --- /dev/null +++ b/chassis/charging_dock_BOM.md @@ -0,0 +1,202 @@ +# Charging Dock BOM — Issue #159 +**Agent:** sl-mechanical | **Date:** 2026-03-01 + +Cross-variant charging dock: 5 V / 5 A pogo-pin contact, V-guide funnel, ArUco marker, LED status. + +--- + +## A. Dock Station Hardware + +| # | Description | Spec | Qty | Notes / Source | +|---|-------------|------|-----|----------------| +| D1 | High-current pogo pin | OD 5.5 mm, 5 A rated, 20 mm length, 4 mm travel | 2 | Generic HC pogo "PBX-5" type; AliExpress or Preci-Dip 821 series. Specify press-fit shoulder. | +| D2 | Brass contact pad | Ø12 × 2 mm, bare brass | 2 | Machine from Ø12 mm brass rod, or order PCB contact pad. Robot-side receiver. | +| D3 | Meanwell IRM-30-5 PSU | 5 V / 5 A (30 W), open-frame | 1 | Or equivalent: Hi-Link HLK-30M05, Recom RAC30-05SK | +| D4 | PG7 cable gland | IP67, M12 thread, 3–6 mm cable | 2 | PSU mains in + DC out to pogo; fits bracket cable exit | +| D5 | M20 hex nut (ballast) | Steel, 30 mm AF, ~86 g each | 8 | Stack in 4× base pockets (2 nuts/pocket) for ~690 g ballast | +| D6 | M4×16 SHCS | Stainless | 12 | Back wall + guide rail attachment to base | +| D7 | M4×10 BHCS | Stainless | 8 | ArUco mast foot + PSU bracket to base | +| D8 | M4 T-nut or insert | Heat-set, M4 | 20 | Into base plate slots | +| D9 | M3×16 SHCS | Stainless | 4 | LED bezel to back wall | +| D10 | M3 hex nut | DIN 934 | 4 | LED bezel | +| D11 | M8×40 BHCS | Zinc | 4 | Optional floor anchor bolts | +| D12 | Rubber foot | Ø20 × 5 mm, self-adhesive | 4 | Underside of base plate (no floor bolts variant) | +| D13 | 16 AWG silicone wire | Red + black, 300 mm each | 2 | Pogo pin to PSU | +| D14 | Crimp ring terminal | M3, for 16 AWG | 4 | Pogo pin terminal connection | +| D15 | Silicone sleeve | 4 mm ID, 100 mm | 2 | Wire strain relief in cable routing channel | + +## B. LED Status Circuit Components + +| # | Description | Spec | Qty | Notes | +|---|-------------|------|-----|-------| +| L1 | 5 mm LED — Red | Vf ≈ 2.0 V, 20 mA | 1 | SEARCHING state | +| L2 | 5 mm LED — Yellow | Vf ≈ 2.1 V, 20 mA | 1 | ALIGNED state | +| L3 | 5 mm LED — Blue | Vf ≈ 3.2 V, 20 mA | 1 | CHARGING state | +| L4 | 5 mm LED — Green | Vf ≈ 2.1 V, 20 mA | 1 | FULL state | +| L5 | Current limiting resistor | 150 Ω 1/4 W (for 5 V rail) | 4 | R = (5 V − Vf) / 20 mA | +| L6 | TP4056 or MCU GPIO | LED driver / controller | 1 | GPIO from Jetson Orin NX via I2C LED driver, OR direct GPIO with resistors | +| L7 | 2.54 mm pin header | 1×6, right-angle | 1 | LED→controller connection | + +> **LED current calc:** R = (5.0 − 2.1) / 0.020 = 145 Ω → use 150 Ω for red/yellow/green. +> Blue: R = (5.0 − 3.2) / 0.020 = 90 Ω → use 100 Ω. + +## C. Robot Receiver Hardware (per robot) + +| # | Description | Spec | Qty | Notes | +|---|-------------|------|-----|-------| +| R1 | Brass contact pad | Ø12 × 2 mm | 2 | Press-fit into receiver housing (0.1 mm interference) | +| R2 | 16 AWG silicone wire | Red + black, 300 mm | 2 | From pads to battery charging PCB | +| R3 | M4×10 SHCS | Stainless | 4 | Receiver to chassis (rover/tank flange bolts) | +| R4 | M4×16 SHCS | Stainless | 2 | Lab variant stem collar clamp | +| R5 | Solder lug, M3 | For wire termination to pad | 2 | Solder to brass pad rear face | + +## D. ArUco Marker + +| # | Description | Spec | Qty | Notes | +|---|-------------|------|-----|-------| +| A1 | ArUco marker print | 100×100 mm, ID = 0 (DICT_4X4_50) | 1 | Print on photo paper or laminate; slip into frame slot | +| A2 | Lamination pouch | A5, 80 micron | 1 | Weather-protect printed marker | + +--- + +## Printed Parts + +| Part | File | Qty | Print settings | Mass est. | +|------|------|-----|----------------|-----------| +| Dock base plate | `charging_dock.scad` `base_stl` | 1 | PETG, 5 perims, 60% infill (heavy = stable), 0.3 mm layer | ~380 g | +| Back wall | `charging_dock.scad` `back_wall_stl` | 1 | PETG, 5 perims, 40% infill | ~120 g | +| Guide rail | `charging_dock.scad` `guide_rail_stl` | 2 (mirror R) | PETG, 5 perims, 60% infill | ~45 g each | +| ArUco mast | `charging_dock.scad` `aruco_mount_stl` | 1 | PETG, 4 perims, 40% infill | ~55 g | +| PSU bracket | `charging_dock.scad` `psu_bracket_stl` | 1 | PETG, 4 perims, 30% infill | ~35 g | +| LED bezel | `charging_dock.scad` `led_bezel_stl` | 1 | PETG, 4 perims, 40% infill | ~10 g | +| Lab receiver | `charging_dock_receiver.scad` `lab_stl` | 1 | PETG, 5 perims, 60% infill | ~28 g | +| Rover receiver | `charging_dock_receiver.scad` `rover_stl` | 1 | PETG, 5 perims, 60% infill | ~32 g | +| Tank receiver | `charging_dock_receiver.scad` `tank_stl` | 1 | PETG, 5 perims, 60% infill | ~38 g | + +--- + +## Mass Summary + +| Assembly | Mass | +|----------|------| +| Dock printed parts (all) | ~690 g | +| Steel ballast (8× M20 hex nuts) | ~690 g | +| PSU + hardware | ~250 g | +| **Dock total** | **~1630 g** | +| Receiver (per robot) | ~30–38 g | + +--- + +## Pogo Pin Contact Height — Cross-Variant Alignment + +The dock `POGO_Z = 35 mm` (contact height above dock floor) is set for **SaltyLab** (stem receiver height ≈ 35 mm). + +| Robot | Chassis floor-to-contact height | Dock adapter | +|-------|--------------------------------|--------------| +| SaltyLab | ~35 mm (stem base) | None — direct fit | +| SaltyRover | ~55 mm (deck belly) | 20 mm ramp shim under dock base | +| SaltyTank | ~90 mm (hull floor) | 55 mm ramp shim under dock base | + +> **Ramp shim:** print `dock_base.scad` with `BASE_T` increased, or print a separate shim block (not included — cut from plywood or print as needed). +> **Alternative:** in firmware, vary approach Z offset per variant. Single dock at POGO_Z = 60 mm midpoint ± 25 mm spring travel gives rough cross-variant coverage. + +--- + +## Wiring Diagram + +``` +MAINS INPUT (AC) + │ + ▼ +┌─────────────────┐ +│ IRM-30-5 PSU │ 5 V / 5 A out +│ 63×45×28 mm │ +└────────┬────────┘ + │ 5 V (RED 16 AWG) 0 V (BLK 16 AWG) + │ │ + ▼ ▼ + ┌───────────┐ ┌───────────┐ + │ POGO + │ (spring-loaded) │ POGO - │ + │ pin │◄────────────────────►│ pin │ + └─────┬─────┘ robot docks └─────┬─────┘ + │ │ + ─ ─ ─ ┼ ─ ─ ─ ─ ─ DOCK/ROBOT GAP ─ ─ ─ ┼ ─ ─ ─ + │ │ + ┌─────▼─────┐ ┌─────▼─────┐ + │ CONTACT + │ (brass pad Ø12 mm) │ CONTACT - │ + └─────┬─────┘ └─────┬─────┘ + │ │ + └──────────────┬────────────────────┘ + │ + ┌────────▼────────┐ + │ Robot charging │ + │ PCB / BMS │ + │ (on robot) │ + └────────┬────────┘ + │ + [Battery pack] + +LED STATUS CIRCUIT (dock side): + │ 5 V from PSU + │ + ┌─────────────┼──────────────────────┐ + │ │ │ + [R] [R] [R] + 150 Ω 150 Ω 100 Ω 150 Ω + │ │ │ │ + [LED1] [LED2] [LED3] [LED4] + RED YELLOW BLUE GREEN + SEARCHING ALIGNED CHARGING FULL + │ │ │ │ + └─────────────┴─────────────┴─────────┘ + │ + GPIO (Jetson Orin NX) + or TP4056 charge state output + +CURRENT SENSING (optional — recommended): + Insert INA219 (I2C) in series with POGO+ line. + I2C addr 0x40; reads dock current to detect: + 0 mA → SEARCHING (no robot contact) + >10 mA → ALIGNED (contact made, BMS pre-charge) + >1000 mA → CHARGING + <50 mA → FULL (BMS trickle / float) +``` + +--- + +## Assembly Sequence + +1. **Print all parts** (see table above). Base at 60% infill for mass. +2. **Press ballast nuts** into base pockets from underside. Optional: fill pockets with epoxy to lock. +3. **Install heat-set M4 inserts** in base plate slots (back wall × 3, guide × 4 each side, ArUco foot × 4, PSU × 4). +4. **Press pogo pins** into back wall bores from the front face. Flange seats against counterbore shoulder. Apply drop of Loctite 243 to bore wall. +5. **Solder 16 AWG wires** to pogo pin terminals. Route down wiring channel. Thread through base cable slot. +6. **Assemble PSU bracket** to base (rear). Connect pogo wires to PSU DC terminals (observe polarity: POGO+ → V+, POGO- → COM). Route AC mains input via PG7 glands on bracket. +7. **Install LED bezel**: press 5 mm LEDs through bores (body recessed 2 mm), solder resistors and wires on rear, plug into controller header. +8. **Bolt back wall** to base (3× M4×16 from underneath). +9. **Bolt guide rails** to base (2× M4 each side). Mirror the right rail — print a second copy, insert STL mirrored in slicer OR use `mirror([1,0,0])` in OpenSCAD. +10. **Mount ArUco mast** to base front (4× M4×10). +11. **Insert ArUco marker** (laminated 100×100 mm, ID=0, DICT_4X4_50) into frame slot from side. +12. **Attach rubber feet** (or drill floor anchor holes). +13. **Robot receiver**: press brass contact pads into bores (interference fit, apply Loctite 603 retaining compound). Solder wires to pad rear lugs before pressing in. +14. **Mount receiver to robot**: align pads to dock pogo height, fasten M4 bolts. + +--- + +## Export Commands + +```bash +# Dock parts +openscad charging_dock.scad -D 'RENDER="base_stl"' -o dock_base.stl +openscad charging_dock.scad -D 'RENDER="back_wall_stl"' -o dock_back_wall.stl +openscad charging_dock.scad -D 'RENDER="guide_rail_stl"' -o dock_guide_rail.stl +openscad charging_dock.scad -D 'RENDER="aruco_mount_stl"' -o dock_aruco_mount.stl +openscad charging_dock.scad -D 'RENDER="psu_bracket_stl"' -o dock_psu_bracket.stl +openscad charging_dock.scad -D 'RENDER="led_bezel_stl"' -o dock_led_bezel.stl + +# Robot receivers +openscad charging_dock_receiver.scad -D 'RENDER="lab_stl"' -o receiver_lab.stl +openscad charging_dock_receiver.scad -D 'RENDER="rover_stl"' -o receiver_rover.stl +openscad charging_dock_receiver.scad -D 'RENDER="tank_stl"' -o receiver_tank.stl +openscad charging_dock_receiver.scad -D 'RENDER="contact_pad_2d"' -o contact_pad.dxf +``` diff --git a/chassis/charging_dock_receiver.scad b/chassis/charging_dock_receiver.scad new file mode 100644 index 0000000..af02c60 --- /dev/null +++ b/chassis/charging_dock_receiver.scad @@ -0,0 +1,331 @@ +// ============================================================ +// charging_dock_receiver.scad — Robot-Side Charging Receiver +// Issue: #159 Agent: sl-mechanical Date: 2026-03-01 +// ============================================================ +// +// Robot-side contact plate that mates with the charging dock pogo pins. +// Each robot variant has a different mounting interface; the contact +// geometry is identical across all variants (same pogo pin spacing). +// +// Variants: +// A — lab_receiver() SaltyLab — mounts to underside of stem base ring +// B — rover_receiver() SaltyRover — mounts to chassis belly (M4 deck holes) +// C — tank_receiver() SaltyTank — mounts to skid plate / hull floor +// +// Contact geometry (common across variants): +// 2× brass contact pads, Ø12 mm × 2 mm (press-fit into PETG housing) +// Pad spacing: 20 mm CL-to-CL (matches dock POGO_SPACING exactly) +// Contact face Z height matches dock pogo pin Z when robot is level +// Polarity: marked + on top pin (conventional: positive = right when +// facing dock; negative = left) — must match dock wiring. +// +// Approach guide nose: +// A chamfered V-nose on the forward face guides the receiver block +// into the dock's V-funnel. Taper half-angle ≈ 14° matches guide rails. +// Nose width = RECV_W = 30 mm (matches dock EXIT_GAP - 2 mm clearance). +// +// Coordinate convention: +// Z = 0 at receiver mounting face (against robot chassis/deck underside). +// +Z points downward (toward dock floor). +// Contact pads face +Y (toward dock back wall when docked). +// Receiver centred on X = 0 (robot centreline). +// +// RENDER options: +// "assembly" all 3 receivers side by side +// "lab_stl" SaltyLab receiver (print 1×) +// "rover_stl" SaltyRover receiver (print 1×) +// "tank_stl" SaltyTank receiver (print 1×) +// "contact_pad_2d" DXF — Ø12 mm brass pad profile (order from metal shop) +// +// Export: +// openscad charging_dock_receiver.scad -D 'RENDER="lab_stl"' -o receiver_lab.stl +// openscad charging_dock_receiver.scad -D 'RENDER="rover_stl"' -o receiver_rover.stl +// openscad charging_dock_receiver.scad -D 'RENDER="tank_stl"' -o receiver_tank.stl +// openscad charging_dock_receiver.scad -D 'RENDER="contact_pad_2d"' -o contact_pad.dxf +// ============================================================ + +$fn = 64; +e = 0.01; + +// ── Contact geometry (must match charging_dock.scad) ───────────────────────── +POGO_SPACING = 20.0; // CL-to-CL (dock POGO_SPACING) +PAD_D = 12.0; // contact pad OD (brass disc) +PAD_T = 2.0; // contact pad thickness +PAD_RECESS = 1.8; // pad pressed into housing (0.2 mm proud for contact) +PAD_PROUD = 0.2; // pad face protrudes from housing face + +// ── Common receiver body geometry ──────────────────────────────────────────── +RECV_W = 30.0; // receiver body width (X) — matches dock EXIT_GAP inner +RECV_D = 25.0; // receiver body depth (Y, docking direction) +RECV_H = 12.0; // receiver body height (Z, from mount face down) +RECV_R = 3.0; // corner radius +// V-nose geometry (front Y face — faces dock back wall) +NOSE_CHAMFER = 10.0; // chamfer depth on X corners of front face + +// Polarity indicator slot (on top/mount face: + on right, - on left) +POL_SLOT_W = 4.0; +POL_SLOT_D = 8.0; +POL_SLOT_H = 1.0; + +// Fasteners +M2_D = 2.4; +M3_D = 3.3; +M4_D = 4.3; + +// ── Mounting patterns ───────────────────────────────────────────────────────── +// SaltyLab stem base ring (Ø25 mm stem, 4× M3 in ring at Ø40 mm BC) +LAB_BC_D = 40.0; +LAB_BOLT_D = M3_D; +LAB_COLLAR_H = 15.0; // collar height above receiver body + +// SaltyRover deck (M4 grid pattern, 30.5×30.5 mm matching FC pattern on deck) +// Receiver uses 4× M4 holes at ±20 mm from centre (clear of deck electronics) +ROVER_BOLT_SPC = 40.0; + +// SaltyTank skid plate (M4 holes matching skid plate bolt pattern) +// Uses 4× M4 at ±20 mm X, ±10 mm Y (inset from skid plate M4 positions) +TANK_BOLT_SPC_X = 40.0; +TANK_BOLT_SPC_Y = 20.0; +TANK_NOSE_L = 20.0; // extended nose for tank (wider hull) + +// ============================================================ +// RENDER DISPATCH +// ============================================================ +RENDER = "assembly"; + +if (RENDER == "assembly") assembly(); +else if (RENDER == "lab_stl") lab_receiver(); +else if (RENDER == "rover_stl") rover_receiver(); +else if (RENDER == "tank_stl") tank_receiver(); +else if (RENDER == "contact_pad_2d") { + projection(cut = true) translate([0, 0, -0.5]) + linear_extrude(1) circle(d = PAD_D); +} + +// ============================================================ +// ASSEMBLY PREVIEW +// ============================================================ +module assembly() { + // SaltyLab receiver + color("RoyalBlue", 0.85) + translate([-80, 0, 0]) + lab_receiver(); + + // SaltyRover receiver + color("OliveDrab", 0.85) + translate([0, 0, 0]) + rover_receiver(); + + // SaltyTank receiver + color("SaddleBrown", 0.85) + translate([80, 0, 0]) + tank_receiver(); +} + +// ============================================================ +// COMMON RECEIVER BODY +// ============================================================ +// Internal helper: the shared contact housing + V-nose. +// Orientation: mount face = +Z top; contact face = +Y front. +// All variant-specific modules call this, then add their mount interface. +module _receiver_body() { + difference() { + union() { + // ── Main housing block (rounded) ───────────────────────── + linear_extrude(RECV_H) + _recv_profile_2d(); + + // ── V-nose chamfer reinforcement ribs ───────────────────── + // Two diagonal ribs at 45° reinforce the chamfered corners + for (sx = [-1, 1]) + hull() { + translate([sx*(RECV_W/2 - NOSE_CHAMFER), + RECV_D/2, 0]) + cylinder(d = 3, h = RECV_H * 0.6); + translate([sx*(RECV_W/2), RECV_D/2 - NOSE_CHAMFER, 0]) + cylinder(d = 3, h = RECV_H * 0.6); + } + } + + // ── Contact pad bores (2× Ø12 mm, press-fit) ───────────────── + // Pads face +Y; bores from Y face into housing + for (px = [-POGO_SPACING/2, POGO_SPACING/2]) + translate([px, RECV_D/2 + e, RECV_H/2]) + rotate([90, 0, 0]) { + // Pad press-fit bore + cylinder(d = PAD_D + 0.1, + h = PAD_RECESS + e); + // Wire bore (behind pad, to mount face) + translate([0, 0, PAD_RECESS]) + cylinder(d = 3.0, + h = RECV_D + 2*e); + } + + // ── Polarity indicator slots on top face ────────────────────── + // "+" slot: right pad (+X side) + translate([POGO_SPACING/2, 0, -e]) + cube([POL_SLOT_W, POL_SLOT_D, POL_SLOT_H + e], center = true); + // "-" indent: left pad (no slot = negative) + + // ── Wire routing channel (on mount face / underside) ────────── + // Trough connecting both pad bores for neat wire run + translate([0, RECV_D/2 - POGO_SPACING/2, RECV_H - 3]) + cube([POGO_SPACING + 6, POGO_SPACING, 4], center = true); + } +} + +// ── 2D profile of receiver body with chamfered V-nose ──────────────────────── +module _recv_profile_2d() { + hull() { + // Rear corners (full width) + for (sx = [-1, 1]) + translate([sx*(RECV_W/2 - RECV_R), -RECV_D/2 + RECV_R]) + circle(r = RECV_R); + // Front corners (chamfered — narrowed by NOSE_CHAMFER) + for (sx = [-1, 1]) + translate([sx*(RECV_W/2 - NOSE_CHAMFER - RECV_R), + RECV_D/2 - RECV_R]) + circle(r = RECV_R); + } +} + +// ============================================================ +// PART A — SALTYLAB RECEIVER +// ============================================================ +// Mounts to the underside of the SaltyLab chassis stem base ring. +// Split collar grips Ø25 mm stem; receiver body hangs below collar. +// Z height set so contact pads align with dock pogo pins when robot +// rests on flat surface (robot wheel-to-contact-pad height calibrated). +// +// Receiver height above floor: tune LAB_CONTACT_Z in firmware (UWB/ArUco +// approach). Mechanically: receiver sits ~35 mm above ground (stem base +// height), matching dock POGO_Z = 35 mm. +module lab_receiver() { + collar_od = 46.0; // matches sensor_rail.scad STEM_COL_OD + collar_h = LAB_COLLAR_H; + + union() { + // ── Common receiver body ──────────────────────────────────── + _receiver_body(); + + // ── Stem collar (split, 2 halves joined with M4 bolts) ─────── + // Only the front half printed here; rear half is mirror. + translate([0, -RECV_D/2, RECV_H]) + difference() { + // Half-collar cylinder + rotate_extrude(angle = 180) + translate([collar_od/2 - 8, 0, 0]) + square([8, collar_h]); + + // Stem bore clearance + translate([0, 0, -e]) + cylinder(d = 25.5, h = collar_h + 2*e); + + // 2× M4 clamping bolt bores (through collar flanges) + for (cx = [-collar_od/2 + 4, collar_od/2 - 4]) + translate([cx, 0, collar_h/2]) + rotate([90, 0, 0]) + cylinder(d = M4_D, + h = collar_od + 2*e, + center = true); + } + + // ── M3 receiver-to-collar bolts ─────────────────────────────── + // 4× M3 holes connecting collar flange to receiver body top + // (These are mounting holes for assembly; not holes in the part) + } +} + +// ============================================================ +// PART B — SALTYOVER RECEIVER +// ============================================================ +// Mounts to the underside of the SaltyRover deck plate. +// 4× M4 bolts into deck underside (blind holes tapped in deck). +// Receiver sits flush with deck belly; contact pads protrude 5 mm below. +// Dock pogo Z = 35 mm must equal ground-to-deck-belly height for rover +// (approximately 60 mm chassis clearance — shim with spacer if needed). +module rover_receiver() { + mount_h = 5.0; // mounting flange thickness + + union() { + // ── Common receiver body ──────────────────────────────────── + _receiver_body(); + + // ── Mounting flange (attaches to deck belly) ───────────────── + difference() { + translate([-(ROVER_BOLT_SPC/2 + 12), + -RECV_D/2 - 10, + RECV_H]) + cube([ROVER_BOLT_SPC + 24, + RECV_D + 20, + mount_h]); + + // 4× M4 bolt holes + for (fx = [-1, 1]) for (fy = [-1, 1]) + translate([fx*ROVER_BOLT_SPC/2, + fy*(RECV_D/2 + 5), + RECV_H - e]) + cylinder(d = M4_D, + h = mount_h + 2*e); + + // Weight-reduction pockets + for (sx = [-1, 1]) + translate([sx*(ROVER_BOLT_SPC/4 + 6), + 0, RECV_H + 1]) + cube([ROVER_BOLT_SPC/2 - 4, RECV_D - 4, mount_h], + center = true); + } + } +} + +// ============================================================ +// PART C — SALTYTANK RECEIVER +// ============================================================ +// Mounts to SaltyTank hull floor or replaces a section of skid plate. +// Extended front nose (TANK_NOSE_L) for tank's wider hull approach. +// Contact pads exposed through skid plate via a 30×16 mm slot. +// Ground clearance: tank chassis = 90 mm; dock POGO_Z = 35 mm. +// Use ramp shim (see BOM) under dock base to elevate pogo pins to 90 mm +// OR set POGO_Z = 90 in dock for a tank-specific dock configuration. +// ⚠ Cross-variant dock: set POGO_Z per robot if heights differ. +// Compromise: POGO_Z = 60 mm with 25 mm ramp for tank, 25 mm spacer for lab. +module tank_receiver() { + mount_h = 4.0; + nose_l = RECV_D/2 + TANK_NOSE_L; + + union() { + // ── Common receiver body ──────────────────────────────────── + _receiver_body(); + + // ── Extended nose for tank approach ────────────────────────── + // Additional chamfered wedge ahead of standard receiver body + hull() { + // Receiver front face corners + for (sx = [-1, 1]) + translate([sx*(RECV_W/2 - NOSE_CHAMFER), RECV_D/2, 0]) + cylinder(d = 2*RECV_R, h = RECV_H * 0.5); + // Extended nose tip (narrowed to 20 mm) + for (sx = [-1, 1]) + translate([sx*10, RECV_D/2 + TANK_NOSE_L, 0]) + cylinder(d = 2*RECV_R, h = RECV_H * 0.4); + } + + // ── Mounting flange (bolts to tank skid plate) ──────────────── + difference() { + translate([-(TANK_BOLT_SPC_X/2 + 10), + -RECV_D/2 - 8, + RECV_H]) + cube([TANK_BOLT_SPC_X + 20, + RECV_D + 16, + mount_h]); + + // 4× M4 bolt holes + for (fx = [-1, 1]) for (fy = [-1, 1]) + translate([fx*TANK_BOLT_SPC_X/2, + fy*TANK_BOLT_SPC_Y/2, + RECV_H - e]) + cylinder(d = M4_D, + h = mount_h + 2*e); + } + } +}