saltylab-firmware/chassis/payload_bay_rail.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

430 lines
19 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_rail.scad — Modular Payload Bay Rail System
// Issue: #170 Agent: sl-mechanical Date: 2026-03-01
// ============================================================
//
// Dovetail rail mounted on robot top deck. Payload modules slide on
// from either end and are retained by a spring-loaded ball detent plus
// an optional M4 thumbscrew safety lock.
//
// Dovetail geometry (60° included angle — balanced for print + load):
//
// ← RAIL_W (50 mm) →
// ┌──────────────────┐ ← rail top face (Z = RAIL_T)
// │ ╲
// │ ╲__________ │ ← dovetail slot (female, cut into top)
// │ (DOVE_SLOT) │
// └──────────────────┘ ← rail bottom face (Z = 0)
//
// DOVE_ANGLE = 30° from vertical (= 60° included).
// Slot width at top = DOVE_SLOT_TOP (open face)
// Slot width at bottom = DOVE_SLOT_BOT (inner base of slot)
// Slot depth = DOVE_H
//
// Module tongue (male dovetail) slides in with 0.3 mm clearance each side.
//
// Spring detent: Ø6 mm steel ball in module, spring behind, seats in
// Ø4.9 mm dimples drilled into rail slot bottom at DETENT_PITCH spacing.
// Provides tactile click-lock at each indexed position.
//
// Safety lock: M4 thumbscrew through module side, tightens against rail
// side wall. For vibration environments or >1 kg payload.
//
// Power+data connector: 4-pin pogo array in rail at fixed position.
// Pins: GND | +5 V | +12 V | UART (half-duplex)
// Module has matching brass target pads (Ø4 mm).
// Connector position: centred in rail length, at CONN_Y from one end.
//
// Cross-variant adapter plates (this file):
// lab_rail_adapter() — SaltyLab chassis top (Ø25 mm stem clear)
// rover_rail_adapter() — SaltyRover deck (M4 grid)
// tank_rail_adapter() — SaltyTank deck (M4 grid)
//
// Coordinate convention:
// Rail runs along Y axis. Cross-section in X-Z plane.
// Z = 0 at rail bottom face (= robot deck top face).
// Module slides in +Y direction.
//
// RENDER options:
// "assembly" rail + adapter + module ghost
// "rail_2d" DXF — dovetail cross-section (CNC/waterjet)
// "rail_stl" STL — printable rail section (PETG prototype)
// "connector_stl" STL — pogo connector housing insert
// "detent_plunger_stl" STL — spring detent plunger (print ×2 per module)
// "lab_adapter_stl" STL — SaltyLab deck adapter
// "rover_adapter_stl" STL — SaltyRover deck adapter
// "tank_adapter_stl" STL — SaltyTank deck adapter
//
// Export:
// openscad payload_bay_rail.scad -D 'RENDER="rail_2d"' -o payload_rail.dxf
// openscad payload_bay_rail.scad -D 'RENDER="rail_stl"' -o payload_rail.stl
// openscad payload_bay_rail.scad -D 'RENDER="connector_stl"' -o payload_connector.stl
// openscad payload_bay_rail.scad -D 'RENDER="detent_plunger_stl"' -o payload_detent.stl
// openscad payload_bay_rail.scad -D 'RENDER="lab_adapter_stl"' -o payload_lab_adapter.stl
// openscad payload_bay_rail.scad -D 'RENDER="rover_adapter_stl"' -o payload_rover_adapter.stl
// openscad payload_bay_rail.scad -D 'RENDER="tank_adapter_stl"' -o payload_tank_adapter.stl
// ============================================================
$fn = 64;
e = 0.01;
// ── Dovetail rail cross-section ───────────────────────────────────────────────
RAIL_W = 50.0; // rail total width (X)
RAIL_T = 12.0; // rail total height (Z)
RAIL_R = 2.0; // outer corner radius
RAIL_LEN = 200.0; // default rail section length (Y)
// Dovetail slot geometry
DOVE_ANGLE = 30.0; // degrees from vertical (60° included angle)
DOVE_H = 8.0; // slot depth (Z into rail from top)
DOVE_SLOT_BOT= 28.0; // slot width at bottom (inner)
// Derived: slot width at top = DOVE_SLOT_BOT + 2 * DOVE_H * tan(DOVE_ANGLE)
DOVE_SLOT_TOP= DOVE_SLOT_BOT + 2 * DOVE_H * tan(DOVE_ANGLE); // ≈ 37.2 mm
// Module tongue (male) clearance: 0.3 mm per side
DOVE_CLEAR = 0.3;
// → module tongue: bot_w = DOVE_SLOT_BOT - 2*DOVE_CLEAR, top_w = DOVE_SLOT_TOP + 2*DOVE_CLEAR
// ── Spring ball detent ────────────────────────────────────────────────────────
// Ø6 mm steel ball presses up through module tongue into dimples in rail slot.
DETENT_BALL_D = 6.0; // ball diameter
DETENT_HOLE_D = 4.9; // dimple bore in rail slot bottom (ball seats in)
DETENT_DEPTH = 1.5; // dimple depth (ball sinks in this far)
DETENT_PITCH = 50.0; // dimple spacing along rail (Y) — module index positions
DETENT_SPG_OD = 6.2; // plunger bore OD (ball + spring housing in module)
DETENT_SPG_L = 16.0; // spring pocket depth in module tongue
// ── Safety lock (M4 thumbscrew through module side into rail side groove) ─────
LOCK_GROOVE_D = 4.5; // groove in rail side wall (M4 thumbscrew tip seats in)
LOCK_GROOVE_DEPTH = 1.5; // groove depth into rail side
// Lock groove runs full rail length (continuous slot) for tool-free slide + lock anywhere
// ── 4-pin power+data connector ───────────────────────────────────────────────
// Pogo pin array mounted in rail body at CONN_Y from entry end.
// Pin map: 1=GND 2=+5V 3=+12V 4=UART
// Pogo: Ø2 mm spring contact (P75-E2 style), rated 2 A (power), 0.5 A (signal)
CONN_Y = RAIL_LEN / 2; // connector centred in rail section
CONN_PIN_D = 2.2; // pogo bore (2 mm pin + 0.2 mm clearance)
CONN_PIN_SPC = 5.0; // pin centre-to-centre spacing
CONN_N_PINS = 4; // GND / +5V / +12V / UART
CONN_HOUSING_W= CONN_N_PINS * CONN_PIN_SPC + 4; // housing width (X)
CONN_HOUSING_D= 8.0; // housing depth (Y, inside rail)
CONN_HOUSING_H= DOVE_H - 1.0; // housing height; sits inside slot (flush with slot floor)
// Connector pogo pins point upward into module pad targets.
// ── Deck mounting holes ───────────────────────────────────────────────────────
MOUNT_PITCH = 50.0; // M4 FHCS hole pitch along rail (countersunk from bottom)
MOUNT_INSET = 25.0; // first hole Y from rail end
MOUNT_D = 4.3; // M4 clearance
CSINK_D = 8.0; // M4 FHCS head diameter
// ── Cross-variant adapter plates ─────────────────────────────────────────────
ADAPT_T = 4.0; // adapter plate thickness
ADAPT_OVHG = 10.0; // adapter overhang past rail edge each side (flange width)
// SaltyLab deck: stem bore at centre
LAB_STEM_BORE = 26.0; // clear stem Ø25 mm
// SaltyRover deck: M4 bolt grid (spacing from rover_chassis_r2.scad)
ROVER_BOLT_SPC = 40.0;
// SaltyTank deck: M4 bolt grid (spacing from saltytank_chassis.scad)
TANK_BOLT_SPC = 40.0;
// Fasteners
M3_D = 3.3;
M4_D = 4.3;
// ============================================================
// RENDER DISPATCH
// ============================================================
RENDER = "assembly";
if (RENDER == "assembly") assembly();
else if (RENDER == "rail_2d")
projection(cut = true) translate([0, 0, -0.5])
linear_extrude(1) rail_profile_2d();
else if (RENDER == "rail_stl") rail_section(RAIL_LEN);
else if (RENDER == "connector_stl") connector_housing();
else if (RENDER == "detent_plunger_stl") detent_plunger();
else if (RENDER == "lab_adapter_stl") lab_rail_adapter();
else if (RENDER == "rover_adapter_stl") rover_rail_adapter();
else if (RENDER == "tank_adapter_stl") tank_rail_adapter();
// ============================================================
// ASSEMBLY PREVIEW
// ============================================================
module assembly() {
// Rail section
color("Silver", 0.85) rail_section(RAIL_LEN);
// Rover adapter under rail
color("SteelBlue", 0.70)
translate([0, 0, -ADAPT_T])
rover_rail_adapter();
// Ghost module sliding on
%color("OliveDrab", 0.3)
translate([0, 60, RAIL_T])
cube([RAIL_W + 10, 100, 40], center = true);
// Connector position marker
%color("Gold", 0.5)
translate([0, CONN_Y, RAIL_T - DOVE_H])
cube([CONN_HOUSING_W, CONN_HOUSING_D, CONN_HOUSING_H],
center = true);
// Detent dimple markers
for (dy = [MOUNT_INSET : DETENT_PITCH : RAIL_LEN - MOUNT_INSET])
%color("Red", 0.6)
translate([0, dy, RAIL_T - DOVE_H])
cylinder(d = DETENT_HOLE_D, h = DETENT_DEPTH);
}
// ============================================================
// RAIL CROSS-SECTION 2D (DXF export)
// ============================================================
// Outer profile minus dovetail slot.
// For CNC milling from 50×12 mm aluminium bar, or waterjet from plate.
// Also used for PETG prototype extrusion.
module rail_profile_2d() {
difference() {
// Outer rail cross-section (rounded rect)
minkowski() {
square([RAIL_W - 2*RAIL_R, RAIL_T - 2*RAIL_R],
center = true);
circle(r = RAIL_R);
}
// Dovetail slot (trapezoid, open at top)
translate([0, RAIL_T/2])
_dovetail_slot_2d();
}
}
// ── Dovetail slot 2D (trapezoid with open top) ───────────────────────────────
module _dovetail_slot_2d() {
// Trapezoid: wider at top (open face), narrower at bottom.
// Points listed clockwise from top-left:
polygon([
[-DOVE_SLOT_TOP/2, 0], // top-left
[ DOVE_SLOT_TOP/2, 0], // top-right
[ DOVE_SLOT_BOT/2, -DOVE_H], // bottom-right
[-DOVE_SLOT_BOT/2, -DOVE_H], // bottom-left
]);
}
// ── Dovetail slot for difference() operations (3D volume) ────────────────────
module _dovetail_slot_3d(length) {
translate([0, -e, RAIL_T])
linear_extrude(DOVE_H + e)
offset(delta = e)
_dovetail_slot_2d();
}
// ============================================================
// RAIL SECTION (3D — printable or aluminium)
// ============================================================
module rail_section(len = RAIL_LEN) {
difference() {
// ── Extruded profile ────────────────────────────────────────
linear_extrude(len)
rotate([0, 0, 90])
rail_profile_2d();
// ── Dovetail slot ────────────────────────────────────────────
translate([0, -e, RAIL_T])
rotate([0, 0, 0])
linear_extrude(len + 2*e)
rotate([0, 0, 90])
offset(delta = e)
_dovetail_slot_2d();
// ── Deck mounting holes (M4 FHCS, from bottom) ───────────────
for (my = [MOUNT_INSET : MOUNT_PITCH : len - MOUNT_INSET])
translate([0, my, -e])
cylinder(d = MOUNT_D, h = RAIL_T - DOVE_H + 2*e);
// Countersinks on bottom face
for (my = [MOUNT_INSET : MOUNT_PITCH : len - MOUNT_INSET])
translate([0, my, -e])
cylinder(d1 = CSINK_D, d2 = MOUNT_D,
h = (CSINK_D - MOUNT_D) / (2 * tan(41)));
// ── Spring detent dimples (slot bottom, at DETENT_PITCH) ──────
for (dy = [MOUNT_INSET : DETENT_PITCH : len - MOUNT_INSET])
translate([0, dy, RAIL_T - DOVE_H - e])
cylinder(d = DETENT_HOLE_D, h = DETENT_DEPTH + e);
// ── Safety-lock groove (continuous slot, both sides of rail) ──
// M4 thumbscrew tip seats anywhere along groove
for (sx = [-1, 1])
translate([sx * (RAIL_W/2 + e), -e,
RAIL_T - DOVE_H/2])
rotate([0, 90, 0])
hull() {
translate([0, 0, 0])
cylinder(d = LOCK_GROOVE_D, h = RAIL_W + 2*e);
}
// ── Connector housing pocket (at CONN_Y) ──────────────────────
translate([0, CONN_Y, RAIL_T - DOVE_H - e])
cube([CONN_HOUSING_W + 0.4,
CONN_HOUSING_D + 0.4,
CONN_HOUSING_H + e], center = true);
// ── Lightening slots (rail body between mounting holes) ────────
for (my = [MOUNT_INSET + 25 : MOUNT_PITCH : len - MOUNT_INSET - 25])
translate([0, my, RAIL_T/4])
cube([RAIL_W - 16, 20, RAIL_T/2 + 2*e], center = true);
}
}
// ============================================================
// CONNECTOR HOUSING (pogo-pin insert, press-fits into rail pocket)
// ============================================================
// 4× spring-loaded pogo pins (P75-E2, Ø2 mm, 6 mm travel).
// Printed housing press-fits into rail pocket; pins protrude up into module.
// Module has 4× Ø4 mm brass target pads at matching pitch.
//
// Pin map (left to right, looking at rail top from +Z):
// Pin 1: GND Pin 2: +5 V Pin 3: +12 V Pin 4: UART
module connector_housing() {
hw = CONN_HOUSING_W;
hd = CONN_HOUSING_D;
hh = CONN_HOUSING_H;
difference() {
// Housing body (press-fit into rail pocket)
cube([hw, hd, hh], center = true);
// 4× pogo pin bores (through housing, top to bottom)
for (i = [0 : CONN_N_PINS - 1]) {
px = (i - (CONN_N_PINS - 1) / 2) * CONN_PIN_SPC;
translate([px, 0, -hh/2 - e])
cylinder(d = CONN_PIN_D, h = hh + 2*e);
}
// Wire exit slot (bottom, routes into rail body)
translate([0, 0, -hh/2 - e])
cube([hw - 6, hd/2, hh/3 + e], center = true);
// Retention barbs (prevent housing pulling out of pocket)
for (sx = [-1, 1])
translate([sx * (hw/2 - 1), 0, hh/4])
rotate([0, sx * 15, 0])
cube([2, hd + 2*e, 2.5], center = true);
}
// Pin polarity label recess (on top face, GND side)
difference() {
translate([0, 0, 0]) cube([0, 0, 0]); // null
translate([-(CONN_N_PINS * CONN_PIN_SPC)/2 + 1, 0, hh/2 - 0.4])
cube([3, hd * 0.6, 0.5 + e], center = true);
}
}
// ============================================================
// DETENT PLUNGER (lives in module tongue; print 2× per module)
// ============================================================
// Press-fit into Ø6.2 mm bore in module tongue.
// Includes spring pocket; ball seated on top.
// Spring: Ø5.5 mm OD, 12 mm free length, ~2 N/mm (light detent).
// Ball: Ø6 mm steel (standard bearing ball, purchase).
module detent_plunger() {
bore_d = DETENT_BALL_D + 0.2; // 6.2 mm
body_od = DETENT_SPG_OD;
body_len = DETENT_SPG_L;
spg_d = 5.8; // spring OD
spg_pocket = 10.0; // spring pocket depth (bottom of housing)
difference() {
cylinder(d = body_od, h = body_len);
// Ball socket (top — partial sphere, retains ball)
translate([0, 0, body_len])
sphere(d = bore_d);
translate([0, 0, body_len - bore_d/4])
cylinder(d = bore_d, h = bore_d/2 + e);
// Spring pocket (bottom)
translate([0, 0, -e])
cylinder(d = spg_d + 0.3, h = spg_pocket + e);
// Retention lip (allows push-in but prevents pullout before spring seated)
translate([0, 0, spg_pocket])
cylinder(d1 = spg_d + 0.3, d2 = spg_d - 1,
h = 1.5);
}
}
// ============================================================
// CROSS-VARIANT DECK ADAPTER PLATES
// ============================================================
// Thin plates that bolt to the robot deck and provide M4 threaded
// studs (or through holes) for the rail mounting holes.
// All adapters: RAIL_LEN × (RAIL_W + 2×ADAPT_OVHG) footprint.
module _adapter_base() {
adapt_l = RAIL_LEN;
adapt_w = RAIL_W + 2*ADAPT_OVHG;
difference() {
// Plate
cube([adapt_w, adapt_l, ADAPT_T], center = true);
// Rail mounting holes (M4 FHCS up through adapter into rail bottom)
for (my = [MOUNT_INSET : MOUNT_PITCH : adapt_l - MOUNT_INSET])
translate([0, my - adapt_l/2, -ADAPT_T/2 - e])
cylinder(d = MOUNT_D, h = ADAPT_T + 2*e);
// Corner lightening
for (cx = [-1, 1]) for (cy = [-1, 1])
translate([cx * (adapt_w/2 - 12),
cy * (adapt_l/2 - 20), 0])
cylinder(d = 10, h = ADAPT_T + 2*e, center = true);
}
}
// SaltyLab adapter: clears Ø25 mm stem, 4× M4 to lab chassis ring
module lab_rail_adapter() {
difference() {
_adapter_base();
// Stem bore clearance (at centre of adapter)
cylinder(d = LAB_STEM_BORE, h = ADAPT_T + 2*e, center = true);
// 4× M4 mounting to lab chassis top ring (Ø44 mm bolt circle)
for (a = [45, 135, 225, 315])
translate([22*cos(a), 22*sin(a), -ADAPT_T/2 - e])
cylinder(d = M4_D, h = ADAPT_T + 2*e);
}
}
// SaltyRover adapter: 4× M4 to rover deck bolt grid
module rover_rail_adapter() {
adapt_l = RAIL_LEN;
adapt_w = RAIL_W + 2*ADAPT_OVHG;
difference() {
_adapter_base();
// 2 rows × 3 cols of M4 bolts into rover deck
for (rx = [-ROVER_BOLT_SPC/2, ROVER_BOLT_SPC/2])
for (ry = [-adapt_l/3, 0, adapt_l/3])
translate([rx, ry, -ADAPT_T/2 - e])
cylinder(d = M4_D, h = ADAPT_T + 2*e);
}
}
// SaltyTank adapter: M4 to tank deck; relieved for deck cable slots
module tank_rail_adapter() {
adapt_l = RAIL_LEN;
adapt_w = RAIL_W + 2*ADAPT_OVHG;
difference() {
_adapter_base();
// 2 rows × 3 cols of M4 bolts into tank deck
for (rx = [-TANK_BOLT_SPC/2, TANK_BOLT_SPC/2])
for (ry = [-adapt_l/3, 0, adapt_l/3])
translate([rx, ry, -ADAPT_T/2 - e])
cylinder(d = M4_D, h = ADAPT_T + 2*e);
// Deck cable slot clearance (tank deck has centre cable channel)
translate([0, 0, 0])
cube([10, adapt_l - 40, ADAPT_T + 2*e], center = true);
}
}