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