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