Merge pull request 'feat(mechanical): universal charging dock station (Issue #159)' (#163) from sl-mechanical/issue-159-charging-dock into main

This commit is contained in:
sl-jetson 2026-03-02 10:26:05 -05:00
commit cc67e33003
3 changed files with 1063 additions and 0 deletions

530
chassis/charging_dock.scad Normal file
View File

@ -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 (LR): 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);
}
}

View File

@ -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, 36 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) | ~3038 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
```

View File

@ -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);
}
}
}