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>
531 lines
24 KiB
OpenSCAD
531 lines
24 KiB
OpenSCAD
// ============================================================
|
||
// 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);
|
||
}
|
||
}
|