saltylab-firmware/chassis/charging_dock.scad
sl-mechanical 8222a0c42e feat(mechanical): charging dock station (Issue #159)
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 <noreply@anthropic.com>
2026-03-02 10:15:14 -05:00

531 lines
24 KiB
OpenSCAD
Raw Permalink 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.

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