saltylab-firmware/chassis/payload_bay_modules.scad
sl-mechanical f952ca2d0b feat(mechanical): modular payload bay system (Issue #170)
Dovetail rail + tool-free swappable payload modules for all variants:
- payload_bay_rail.scad: 50×12 mm 60° dovetail rail (DXF for CNC Al bar),
  spring ball detent (Ø6 mm, 50 mm pitch), continuous safety-lock groove
  (M4 thumbscrew), 4-pin pogo connector housing (GND/5V/12V/UART),
  lab/rover/tank deck adapter plates
- payload_bay_modules.scad: universal _module_base() (male tongue, detent
  bore, 4× Ø4 mm target pads, lock bore) + 3 example modules: cargo tray
  (200×100 mm, Velcro slots, bungee cord slots), camera boom (120 mm mast +
  80 mm arm, 2020-rail-compatible head, 3-position tilt), cup holder
  (Ø80 mm tapered, 8-slot flex grip). Includes copy-paste module template.
- payload_bay_BOM.md: hardware list, CNC spec (dovetail dimensions, surface
  finish, connector pocket), load analysis (2 kg rated with Al rail + lock),
  module developer guide with constraints table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 10:34:17 -05:00

463 lines
22 KiB
OpenSCAD
Raw 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.

// ============================================================
// payload_bay_modules.scad — Payload Bay Module Template + Examples
// Issue: #170 Agent: sl-mechanical Date: 2026-03-01
// ============================================================
//
// ── HOW TO CREATE A NEW MODULE ──────────────────────────────────────────────
//
// 1. Copy the "MODULE TEMPLATE" section at the bottom of this file.
// 2. Set MODULE_L to your module's Y length (min 60 mm).
// 3. Add your payload geometry on top of the _module_base() call.
// 4. The _module_base() provides:
// • Male dovetail tongue (slides into rail slot)
// • Spring detent bore(s) (for Ø6 mm ball + spring plunger)
// • Connector target pads (4× Ø4 mm brass, matching rail pogo pins)
// • Safety-lock M4 thumbscrew bore (side of tongue)
// • Bottom-face flush with rail top (Z = 0 at rail top / module base)
// 5. Your payload geometry sits at Z ≥ 0 (above the rail top face).
// 6. Add a RENDER dispatch entry and export command.
//
// ── DOVETAIL TONGUE GEOMETRY ────────────────────────────────────────────────
//
// TONGUE_BOT = DOVE_SLOT_BOT - 2*DOVE_CLEAR = 28.0 - 0.6 = 27.4 mm
// TONGUE_TOP = DOVE_SLOT_TOP + 2*DOVE_CLEAR = 37.2 + 0.6 = 37.8 mm
// TONGUE_H = DOVE_H + 0.2 (slight extra depth, no binding at corners)
//
// Tongue runs full module length (-Y to +Y in module coords).
// Module body sits on top of tongue at Z = 0.
//
// ── CONNECTOR PADS ──────────────────────────────────────────────────────────
// 4× Ø4 mm brass discs press-fit into tongue bottom face.
// Pad positions: must align with rail connector at CONN_Y when module
// is slid to its intended position (any detent step).
// Default: pads centred in module length → module must be placed so its
// centre aligns with CONN_Y on rail. Or: set MODULE_CONN_OFFSET to shift.
//
// ── PAYLOAD RATING ──────────────────────────────────────────────────────────
// 2 kg rated payload when safety-lock thumbscrew is tightened.
// Detent-only (no thumbscrew): ~0.5 kg (impact / vibration condition).
//
// ── RENDERED EXAMPLES ────────────────────────────────────────────────────────
// Part 1 — cargo_tray() 200×100 mm cargo tray, 30 mm walls
// Part 2 — camera_boom() L-arm with sensor_rail-compatible head
// Part 3 — cup_holder() Ø80 mm tapered cup cradle
//
// RENDER options:
// "assembly" all 3 modules on ghost rail
// "base_stl" module base / tongue only (universal)
// "cargo_tray_stl" cargo tray module
// "camera_boom_stl" camera boom module
// "cup_holder_stl" cup holder module
// "target_pad_2d" DXF — Ø4 mm brass target pad profile
//
// Export:
// openscad payload_bay_modules.scad -D 'RENDER="base_stl"' -o payload_base.stl
// openscad payload_bay_modules.scad -D 'RENDER="cargo_tray_stl"' -o payload_cargo_tray.stl
// openscad payload_bay_modules.scad -D 'RENDER="camera_boom_stl"' -o payload_camera_boom.stl
// openscad payload_bay_modules.scad -D 'RENDER="cup_holder_stl"' -o payload_cup_holder.stl
// ============================================================
$fn = 64;
e = 0.01;
// ── Rail geometry constants (must match payload_bay_rail.scad) ────────────────
RAIL_W = 50.0;
RAIL_T = 12.0;
DOVE_ANGLE = 30.0;
DOVE_H = 8.0;
DOVE_SLOT_BOT = 28.0;
DOVE_SLOT_TOP = DOVE_SLOT_BOT + 2 * DOVE_H * tan(DOVE_ANGLE);
DOVE_CLEAR = 0.3;
DETENT_PITCH = 50.0;
DETENT_BALL_D = 6.0;
DETENT_SPG_OD = 6.2;
DETENT_SPG_L = 16.0;
CONN_PIN_SPC = 5.0;
CONN_N_PINS = 4;
CONN_HOUSING_D = 8.0;
// ── Module tongue (male dovetail) geometry ────────────────────────────────────
TONGUE_BOT = DOVE_SLOT_BOT - 2*DOVE_CLEAR; // 27.4 mm
TONGUE_TOP = DOVE_SLOT_TOP + 2*DOVE_CLEAR; // 37.8 mm
TONGUE_H = DOVE_H + 0.2; // 8.2 mm (slight extra depth)
// ── Connector target pad ──────────────────────────────────────────────────────
TARGET_PAD_D = 4.0; // brass pad OD (slightly larger than pogo Ø2 mm)
TARGET_PAD_T = 1.5; // brass pad thickness
TARGET_PAD_RECESS = 1.3; // press-fit recess depth (pad is 0.2 mm proud)
// 4 pads at CONN_PIN_SPC pitch, centred in module tongue
CONN_SPAN = (CONN_N_PINS - 1) * CONN_PIN_SPC; // 15 mm
// ── Module safety lock ────────────────────────────────────────────────────────
LOCK_BOLT_D = 4.3; // M4 thumbscrew bore through tongue side
LOCK_BOLT_Z = TONGUE_H/2; // Z of thumbscrew CL from tongue bottom
// Thumbscrew tightens against continuous groove in rail side.
// Fasteners
M3_D = 3.3;
M4_D = 4.3;
// ============================================================
// RENDER DISPATCH
// ============================================================
RENDER = "assembly";
if (RENDER == "assembly") assembly();
else if (RENDER == "base_stl") _module_base(80);
else if (RENDER == "cargo_tray_stl") cargo_tray();
else if (RENDER == "camera_boom_stl") camera_boom();
else if (RENDER == "cup_holder_stl") cup_holder();
else if (RENDER == "target_pad_2d") {
projection(cut = true) translate([0, 0, -0.5])
linear_extrude(1) circle(d = TARGET_PAD_D);
}
// ============================================================
// ASSEMBLY PREVIEW
// ============================================================
module assembly() {
// Ghost rail
%color("Silver", 0.25)
translate([0, 0, -RAIL_T])
cube([RAIL_W, 600, RAIL_T], center = true);
// Cargo tray at Y=50 (first detent position)
color("OliveDrab", 0.85)
translate([0, 50, 0])
cargo_tray();
// Cup holder at Y=300
color("RoyalBlue", 0.85)
translate([0, 300, 0])
cup_holder();
// Camera boom at Y=500
color("DarkSlateGray", 0.85)
translate([0, 500, 0])
camera_boom();
}
// ============================================================
// UNIVERSAL MODULE BASE (_module_base)
// ============================================================
// All modules use this as their foundation.
// Provides: male dovetail tongue, detent bore, connector pads, lock bore.
//
// module_len : module length in Y (≥ 60 mm)
// n_detents : how many ball detent bores to include (1 or 2)
// conn_offset: Y offset of connector pads from module centre (default 0)
//
// After calling _module_base(), add payload geometry at Z = 0 and above.
// The tongue occupies Z = -(TONGUE_H) to Z = 0.
// Rail top face is at Z = 0.
module _module_base(module_len, n_detents = 1, conn_offset = 0) {
ml = module_len;
difference() {
// ── Dovetail tongue (male) ────────────────────────────────────
translate([0, ml/2, -TONGUE_H/2])
linear_extrude(TONGUE_H, center = true, twist = 0,
convexity = 2)
_tongue_profile_2d();
// ── Spring detent bore(s) (up through tongue top face) ────────
// 1 detent: at module centre; 2 detents: at ±25 mm from centre
for (i = [0 : n_detents - 1]) {
dy = (n_detents == 1)
? ml/2
: ml/2 + (i - (n_detents-1)/2) * DETENT_PITCH;
translate([0, dy, -(TONGUE_H/2) - DETENT_SPG_L/2])
cylinder(d = DETENT_SPG_OD,
h = DETENT_SPG_L + TONGUE_H/2 + e);
}
// ── Connector target pad recesses (tongue bottom face) ────────
for (i = [0 : CONN_N_PINS - 1]) {
px = (i - (CONN_N_PINS-1)/2) * CONN_PIN_SPC;
translate([px,
ml/2 + conn_offset,
-TONGUE_H + e])
cylinder(d = TARGET_PAD_D + 0.1,
h = TARGET_PAD_RECESS + e);
}
// ── Safety-lock M4 thumbscrew bore (right side of tongue) ─────
// Bore exits tongue right face, tip bears on rail side groove
translate([TONGUE_TOP/2 + e, ml/2, LOCK_BOLT_Z - TONGUE_H])
rotate([0, 90, 0])
cylinder(d = LOCK_BOLT_D,
h = (TONGUE_TOP - TONGUE_BOT)/2 + 6 + e);
// ── Lead-in chamfer on entry end of tongue ────────────────────
// 2 mm chamfer on bottom corners of tongue at Y=0 end
translate([0, 0, -TONGUE_H])
rotate([45, 0, 0])
cube([TONGUE_TOP + 2*e, 4, 4], center = true);
}
}
// ── Tongue 2D cross-section (male dovetail, trapezoid wider at bottom) ────────
// Module tongue: narrower at top (entering slot), wider at bottom (interlocking).
// Note orientation: tongue points DOWN (-Z), so wider face is at bottom (-Z).
module _tongue_profile_2d() {
polygon([
[-TONGUE_TOP/2, 0], // top-left (at Z = 0, flush with rail top)
[ TONGUE_TOP/2, 0], // top-right
[ TONGUE_BOT/2, -TONGUE_H], // bottom-right (interlocking face)
[-TONGUE_BOT/2, -TONGUE_H], // bottom-left
]);
}
// ============================================================
// PART 1 — CARGO TRAY
// ============================================================
// 200×100 mm open tray for transporting small items.
// 30 mm walls, chamfered rim, 4× drainage holes.
// Two Velcro strip slots on tray floor for cargo retention.
// Module length: 200 mm (4 detent positions).
// Weight budget: tray printed ~180 g; payload up to 2 kg.
TRAY_L = 200.0; // module Y length
TRAY_W = 100.0; // tray interior width (X)
TRAY_WALL = 3.0; // tray wall thickness
TRAY_H = 30.0; // tray wall height above module base
TRAY_FLOOR_T = 3.0; // tray floor thickness
module cargo_tray() {
// Mount base on rail (2 detents at ±50 mm from module centre)
_module_base(TRAY_L, n_detents = 2);
difference() {
union() {
// ── Tray body on top of base ────────────────────────────
translate([-TRAY_W/2 - TRAY_WALL, 0,
0])
cube([TRAY_W + 2*TRAY_WALL,
TRAY_L,
TRAY_FLOOR_T + TRAY_H]);
// ── Corner gussets (stiffening for 2 kg load) ───────────
for (cx = [-1, 1]) for (cy = [0, 1])
hull() {
translate([cx * TRAY_W/2,
cy * (TRAY_L - TRAY_WALL),
0])
cube([TRAY_WALL + e, TRAY_WALL + e,
TRAY_FLOOR_T + TRAY_H * 0.6], center = true);
translate([cx * (TRAY_W/2 - 10),
cy * (TRAY_L - TRAY_WALL),
0])
cylinder(d = TRAY_WALL * 2, h = e);
}
}
// ── Tray interior cavity ─────────────────────────────────────
translate([-TRAY_W/2, TRAY_WALL,
TRAY_FLOOR_T])
cube([TRAY_W, TRAY_L - 2*TRAY_WALL,
TRAY_H + e]);
// ── Drainage holes (4× Ø8 mm in floor) ──────────────────────
for (dx = [-TRAY_W/4, TRAY_W/4])
for (dy = [TRAY_L/4, 3*TRAY_L/4])
translate([dx, dy, -e])
cylinder(d = 8, h = TRAY_FLOOR_T + 2*e);
// ── Velcro slot × 2 (25 mm wide grooves in floor) ────────────
for (dy = [TRAY_L/3, 2*TRAY_L/3])
translate([0, dy, TRAY_FLOOR_T - 1.5])
cube([TRAY_W - 10, 25, 2 + e], center = true);
// ── Rim chamfer (top inner edge, ergonomic) ───────────────────
translate([0, TRAY_L/2, TRAY_FLOOR_T + TRAY_H + e])
cube([TRAY_W + 2*TRAY_WALL + 2*e,
TRAY_L + 2*e, 4], center = true);
// ── Side slots for bungee cord retention (3× each long side) ─
for (sx = [-1, 1])
for (sy = [TRAY_L/4, TRAY_L/2, 3*TRAY_L/4])
translate([sx * (TRAY_W/2 + TRAY_WALL/2),
sy, TRAY_FLOOR_T + TRAY_H/2])
cube([TRAY_WALL + 2*e, 8, 6], center = true);
}
}
// ============================================================
// PART 2 — CAMERA BOOM
// ============================================================
// L-shaped arm: vertical mast + horizontal boom.
// Boom head accepts sensor_rail 2020 T-slot (RAIL_W/2 bolt pattern).
// Sensor head can be rotated 0/90/180° and locked with M4 bolt.
// Module length: 80 mm; arm rises 120 mm, boom extends 80 mm forward.
BOOM_MODULE_L = 80.0;
BOOM_MAST_H = 120.0; // mast height above rail top (Z)
BOOM_ARM_L = 80.0; // horizontal boom length (+Y forward)
BOOM_ARM_W = 20.0; // arm cross-section width
BOOM_ARM_T = 20.0; // arm cross-section height
BOOM_HEAD_W = 50.0; // sensor head width (matches 2020 rail flange)
BOOM_HEAD_H = 20.0; // sensor head plate height
BOOM_HEAD_T = 5.0; // sensor head plate thickness
module camera_boom() {
_module_base(BOOM_MODULE_L, n_detents = 1);
difference() {
union() {
// ── Mast (vertical column from module body) ──────────────
translate([-BOOM_ARM_W/2, BOOM_MODULE_L/2 - BOOM_ARM_T/2, 0])
cube([BOOM_ARM_W, BOOM_ARM_T, BOOM_MAST_H]);
// ── Horizontal boom (from mast top, extends +Y) ──────────
translate([-BOOM_ARM_W/2,
BOOM_MODULE_L/2 - BOOM_ARM_T/2,
BOOM_MAST_H - BOOM_ARM_W])
cube([BOOM_ARM_W, BOOM_ARM_L, BOOM_ARM_W]);
// ── Sensor head plate (at boom tip) ──────────────────────
translate([-BOOM_HEAD_W/2,
BOOM_MODULE_L/2 - BOOM_ARM_T/2 + BOOM_ARM_L,
BOOM_MAST_H - BOOM_ARM_W - BOOM_HEAD_H/2 + BOOM_ARM_W/2])
cube([BOOM_HEAD_W, BOOM_HEAD_T, BOOM_HEAD_H]);
// ── Junction gussets (mast + horizontal boom) ────────────
translate([-BOOM_ARM_W/2,
BOOM_MODULE_L/2 - BOOM_ARM_T/2,
BOOM_MAST_H - BOOM_ARM_W])
rotate([45, 0, 0])
cube([BOOM_ARM_W, BOOM_ARM_W * 0.7, BOOM_ARM_W * 0.7]);
}
// ── 2020 sensor-rail bolt pattern in head plate ───────────────
// 2× M5 slots (matches sensor_rail.scad tank_clamp slot geometry)
for (sz = [-BOOM_HEAD_H/4, BOOM_HEAD_H/4])
translate([0,
BOOM_MODULE_L/2 - BOOM_ARM_T/2 + BOOM_ARM_L + BOOM_HEAD_T + e,
BOOM_MAST_H - BOOM_ARM_W/2 + sz])
rotate([90, 0, 0])
hull() {
translate([-6, 0, 0])
cylinder(d = 5.3, h = BOOM_HEAD_T + 2*e);
translate([+6, 0, 0])
cylinder(d = 5.3, h = BOOM_HEAD_T + 2*e);
}
// ── Tilt angle slots (3 positions: 0°, ±15°) ─────────────────
for (ta = [-15, 0, 15]) {
translate([0,
BOOM_MODULE_L/2 - BOOM_ARM_T/2 + BOOM_ARM_L/2,
BOOM_MAST_H - BOOM_ARM_W/2])
rotate([ta, 0, 0])
translate([0, BOOM_ARM_L/2, 0])
cylinder(d = 4.3, h = BOOM_ARM_W + 2*e,
center = true);
}
// ── Cable tie slots in mast ───────────────────────────────────
for (cz = [BOOM_MAST_H * 0.3, BOOM_MAST_H * 0.6])
translate([0, BOOM_MODULE_L/2, cz])
cube([BOOM_ARM_W + 2*e, 4, 4], center = true);
// ── Lightening pockets in mast ────────────────────────────────
translate([0, BOOM_MODULE_L/2, BOOM_MAST_H * 0.4])
cube([BOOM_ARM_W - 6, BOOM_ARM_T - 6,
BOOM_MAST_H * 0.4], center = true);
}
}
// ============================================================
// PART 3 — CUP HOLDER
// ============================================================
// Tapered cup cradle for standard travel mugs / water bottles.
// Inner diameter: 80 mm at top, 68 mm at bottom (matches Ø70 mm typical mug).
// Flexible gripper ribs (cut slots) provide spring retention.
// Drain hole at bottom for condensation.
// Module length: 80 mm.
CUP_MODULE_L = 80.0;
CUP_INNER_TOP = 80.0; // inner bore OD at top
CUP_INNER_BOT = 68.0; // inner bore OD at bottom (taper for grip)
CUP_OUTER_T = 4.0; // wall thickness
CUP_H = 80.0; // cup holder height
CUP_GRIP_SLOTS = 8; // number of flex slots (spring grip)
CUP_SLOT_W = 2.5; // flex slot width
CUP_SLOT_H = 40.0; // flex slot height (from top)
module cup_holder() {
_module_base(CUP_MODULE_L, n_detents = 1);
difference() {
union() {
// ── Outer shell (tapered cylinder) ───────────────────────
cylinder(d1 = CUP_INNER_BOT + 2*CUP_OUTER_T,
d2 = CUP_INNER_TOP + 2*CUP_OUTER_T,
h = CUP_H);
// ── Base flange (connects to module body footprint) ───────
hull() {
cylinder(d = CUP_INNER_BOT + 2*CUP_OUTER_T + 4,
h = 4);
translate([0, CUP_MODULE_L/2, 0])
cube([RAIL_W, CUP_MODULE_L, 4], center = true);
}
}
// ── Inner bore (tapered cup cavity) ──────────────────────────
translate([0, 0, CUP_OUTER_T])
cylinder(d1 = CUP_INNER_BOT, d2 = CUP_INNER_TOP,
h = CUP_H + e);
// ── Base drain hole ──────────────────────────────────────────
translate([0, 0, -e])
cylinder(d = 12, h = CUP_OUTER_T + 2*e);
// ── Flex grip slots (from top down) ──────────────────────────
// Slots allow upper rim to flex inward and grip cup body
for (i = [0 : CUP_GRIP_SLOTS - 1]) {
angle = i * 360 / CUP_GRIP_SLOTS;
rotate([0, 0, angle])
translate([CUP_INNER_TOP/2 + CUP_OUTER_T/2, 0, CUP_H - CUP_SLOT_H])
cube([CUP_OUTER_T + 2*e, CUP_SLOT_W, CUP_SLOT_H + e],
center = true);
}
// ── Exterior branding recess (optional label area) ────────────
translate([CUP_INNER_BOT/2 + CUP_OUTER_T/2 - 0.5, 0, CUP_H/2])
rotate([0, 90, 0])
cube([25, 40, 1 + e], center = true);
}
}
// ============================================================
// ══ MODULE TEMPLATE ═══════════════════════════════════════════
// ══ Copy this block to create a new payload module ════════════
// ============================================================
//
// module my_new_module() {
// MY_LEN = 120.0; // module length — must be multiple of DETENT_PITCH (50 mm)
//
// // Always start with the base (provides tongue, pads, detent bore)
// _module_base(MY_LEN, n_detents = 2);
//
// // Add your payload geometry here.
// // Z = 0 is the rail top face / module mounting face.
// // Build upward from Z = 0.
//
// difference() {
// union() {
// // Example: a simple platform
// translate([-(RAIL_W + 10)/2, 0, 0])
// cube([RAIL_W + 10, MY_LEN, 10]);
//
// // Add your geometry...
// }
//
// // Add your cutouts...
// }
// }
//
// ── Don't forget to: ──────────────────────────────────────────
// 1. Add else if (RENDER == "my_module_stl") my_new_module();
// in the RENDER DISPATCH block above.
// 2. Add export command in BOM / README.
// 3. Test: verify tongue fits rail slot (should slide with 0.3 mm clearance).
// 4. Verify connector pad positions align with CONN_Y on rail.
// ============================================================