diff --git a/chassis/payload_bay_BOM.md b/chassis/payload_bay_BOM.md new file mode 100644 index 0000000..4c2696f --- /dev/null +++ b/chassis/payload_bay_BOM.md @@ -0,0 +1,165 @@ +# Payload Bay BOM — Issue #170 +**Agent:** sl-mechanical | **Date:** 2026-03-01 + +Modular dovetail payload rail system. Tool-free slide-and-click module swapping. +Cross-variant: SaltyLab, SaltyRover, SaltyTank (same rail profile). + +--- + +## A. Rail Hardware + +| # | Description | Spec | Qty (per robot) | Notes | +|---|-------------|------|-----------------|-------| +| R1 | Aluminium bar stock | 50×12 mm, 6061-T6, 200 mm length | 1–2 | Preferred over printed rail for 2 kg load rating. CNC mill or route dovetail slot per `payload_rail.dxf` profile. | +| R2 | M4×10 FHCS | Stainless, countersunk | 4–8 | Rail to adapter plate (or direct to deck); FHCS sits flush below rail bottom face | +| R3 | M4 heat-set insert | M4×5.7 L, Ø5.6 OD | 4–8 | Into deck adapter plate | +| R4 | Detent ball bearing | Ø6 mm, chrome steel (GCr15) | 2 per module | Module spring detent; standard bearing ball | +| R5 | Detent spring | Ø5.5 mm OD, 12 mm free length, ~2 N/mm | 2 per module | Lee Spring LC 055A 06 S or equivalent; behind ball in plunger | +| R6 | M4 thumbscrew (knurled) | M4×12, knurled head Ø14 mm | 1 per module | Safety lock; threads into M4 nut pressed into module side | +| R7 | M4 hex nut | DIN 934, stainless | 1 per module | Captured in module body for thumbscrew | + +## B. Power + Data Connector + +| # | Description | Spec | Qty (per rail) | Notes | +|---|-------------|------|----------------|-------| +| C1 | Pogo pin | P75-E2 style, Ø2 mm, 6 mm travel, rated 2 A | 4 | Rail-side spring contacts. AliExpress "P75-E2 pogo pin" or Mill-Max 0906 series. | +| C2 | Brass target pad | Ø4 × 1.5 mm disc | 4 per module | Module-side contact pads. Machine from Ø4 mm brass rod or order PCB pads. Press-fit with Loctite 603. | +| C3 | JST-XH 2.54 mm header | 4-pin, right-angle | 1 per rail | Rail-side connector to power harness | +| C4 | JST-XH housing + crimps | 4-pin female | 1 per robot | Wires from robot PSU (5 V, 12 V, GND, UART) | +| C5 | 20 AWG silicone wire | Red / black / yellow / white, 300 mm each | 4 | Rail connector to robot bus | +| C6 | Connector housing | `payload_connector_stl` | 1 | Press-fit into rail pocket | + +### Pin Map + +| Pin | Signal | Wire colour | Max current | +|-----|--------|-------------|-------------| +| 1 | GND | Black | Return | +| 2 | +5 V | Red | 2 A | +| 3 | +12 V | Yellow | 2 A | +| 4 | UART (3.3 V) | White | 0.5 A | + +> **UART note**: Half-duplex (single wire). Module firmware connects to Jetson Orin NX UART2. Use RS-485 transceiver if module cable > 500 mm or multi-drop needed. + +## C. Deck Adapters + +| Part | File | Qty | Print | Mass est. | +|------|------|-----|-------|-----------| +| SaltyLab adapter | `payload_bay_rail.scad` `lab_adapter_stl` | 1 | PETG, 5 perims, 60% infill | ~30 g | +| SaltyRover adapter | `payload_bay_rail.scad` `rover_adapter_stl` | 1 | PETG, 5 perims, 60% infill | ~35 g | +| SaltyTank adapter | `payload_bay_rail.scad` `tank_adapter_stl` | 1 | PETG, 5 perims, 60% infill | ~35 g | + +## D. Printed Parts + +| Part | File | Qty | Print | Mass est. | +|------|------|-----|-------|-----------| +| Rail section (prototype) | `payload_bay_rail.scad` `rail_stl` | 1 | PETG, 5 perims, 60% infill, 0.2 mm layer | ~85 g | +| Connector housing | `payload_bay_rail.scad` `connector_stl` | 1 | PETG, 5 perims, 100% infill | ~4 g | +| Detent plunger | `payload_bay_rail.scad` `detent_plunger_stl` | 2 per module | PETG, 5 perims, 80% infill | ~2 g each | +| Module base (universal) | `payload_bay_modules.scad` `base_stl` | N | PETG, 5 perims, 60% infill | ~18 g | +| Cargo tray (200 mm) | `payload_bay_modules.scad` `cargo_tray_stl` | 1 | PETG, 4 perims, 30% infill | ~180 g | +| Camera boom | `payload_bay_modules.scad` `camera_boom_stl` | 1 | PETG, 5 perims, 50% infill | ~95 g | +| Cup holder | `payload_bay_modules.scad` `cup_holder_stl` | 1 | PETG, 4 perims, 25% infill | ~55 g | + +--- + +## Dovetail Rail — CNC Specification + +For aluminium production rail (preferred over printed for 2 kg rating): + +``` +Material: 6061-T6 aluminium +Stock: 50 mm × 12 mm flat bar, length to suit (200 mm, 300 mm, 400 mm) + +Dovetail slot (top face, centred): + Slot open width at top: 37.2 mm + Slot width at bottom: 28.0 mm + Slot depth: 8.0 mm + Wall angle from vertical: 30.0° (60° included angle) + Surface finish: Ra 1.6 µm (smooth for low-friction sliding) + +Detent dimples (slot floor): + Diameter: 4.9 mm (ball seats in) + Depth: 1.5 mm + Pitch: 50 mm + First dimple: 25 mm from each end + +Safety-lock groove (both side faces, continuous): + Groove diameter: 4.5 mm + Depth: 1.5 mm + Z position: RAIL_T/2 - DOVE_H/2 = 8 mm from top face + (CNC with Ø4 mm ball-nose end mill, single pass at Z = -4 mm from top) + +Mounting holes (bottom face, countersunk): + Diameter: 4.3 mm (M4 clearance) + C/sink: Ø8 mm × 82° (M4 FHCS) + Pitch: 50 mm + First hole: 25 mm from each end + +Connector pocket (slot floor, centred in rail length): + Width: 26 mm (X), Depth: 8.4 mm (Y), Height: 7 mm (Z into slot floor) + Tolerance: +0.2 / 0 mm (press-fit housing) + +DXF cross-section: export payload_rail.dxf for supplier drawing. +``` + +--- + +## Load Analysis + +| Mode | Load | Safety factor | Method | +|------|------|---------------|--------| +| Static payload (detent only) | 0.5 kg | 2× | Ball detent retention force ~10 N | +| Static payload (thumbscrew locked) | 2.0 kg | 2× | Dovetail shear area ~800 mm² Al | +| Dynamic (robot motion, 2 m/s²) | 2.0 kg | 1.5× | Inertial force = 2 kg × 2 m/s² = 4 N; detent holds 10 N | +| Dovetail shear (PETG printed) | 1.2 kg | 1.5× | PETG tensile ~50 MPa; recommend Al rail for rated 2 kg | + +> **⚠ For 2 kg payload: use machined aluminium rail. Printed PETG rail is prototype/light-duty only (<0.8 kg payload).** + +--- + +## Module Developer Guide + +### Adding a new module in 5 steps + +1. **Copy the template** at the bottom of `payload_bay_modules.scad`. +2. **Set `MY_LEN`** — must be a multiple of 50 mm (detent pitch) for repeatable positioning. +3. **Call `_module_base(MY_LEN, n_detents)`** as the first statement in your module. +4. **Build payload geometry** starting at `Z = 0` (rail top face). Keep total height ≤ 200 mm for robot clearance under doorways. +5. **Verify connector alignment** — when module is slid to its operating position, the 4 target pads on the tongue bottom must align with `CONN_Y` on the rail (default: 100 mm from rail entry end). Adjust `conn_offset` if needed. + +### Constraints + +| Parameter | Limit | +|-----------|-------| +| Module length | Min 60 mm, max 400 mm | +| Module height above rail | Max 200 mm (clearance) | +| Payload mass | ≤ 2 kg (Al rail + thumbscrew locked) | +| Module width | Max 120 mm (robot shoulder clearance) | +| Connector draw | Max 2 A per power pin (5 V or 12 V) | + +--- + +## Export Commands + +```bash +# Rail DXF (for CNC / waterjet machining) +openscad payload_bay_rail.scad -D 'RENDER="rail_2d"' -o payload_rail_profile.dxf + +# Rail STL (PETG prototype) +openscad payload_bay_rail.scad -D 'RENDER="rail_stl"' -o payload_rail_200mm.stl + +# Rail accessories +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_plunger.stl + +# Deck adapters +openscad payload_bay_rail.scad -D 'RENDER="lab_adapter_stl"' -o payload_adapter_lab.stl +openscad payload_bay_rail.scad -D 'RENDER="rover_adapter_stl"' -o payload_adapter_rover.stl +openscad payload_bay_rail.scad -D 'RENDER="tank_adapter_stl"' -o payload_adapter_tank.stl + +# Example modules +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 +openscad payload_bay_modules.scad -D 'RENDER="target_pad_2d"' -o payload_target_pad.dxf +``` diff --git a/chassis/payload_bay_modules.scad b/chassis/payload_bay_modules.scad new file mode 100644 index 0000000..1cd5f21 --- /dev/null +++ b/chassis/payload_bay_modules.scad @@ -0,0 +1,462 @@ +// ============================================================ +// 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. +// ============================================================ diff --git a/chassis/payload_bay_rail.scad b/chassis/payload_bay_rail.scad new file mode 100644 index 0000000..743aaa2 --- /dev/null +++ b/chassis/payload_bay_rail.scad @@ -0,0 +1,429 @@ +// ============================================================ +// 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); + } +}