From 2fa72e169e5108f8109a750a180c808c289421e8 Mon Sep 17 00:00:00 2001 From: sl-mechanical Date: Mon, 2 Mar 2026 08:42:44 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20SaltyRover=20chassis=20Rev=202=20?= =?UTF-8?q?=E2=80=94=204-wheel=20rover=20with=20spring=20suspension=20(#10?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New chassis design files for the SaltyRover rough-terrain variant: • saltyrover_chassis_r2.scad — Deck plate (500×480×6mm Al, laser-cut DXF), 4× M3-adjustable pivot brackets, 4× CSI corner camera mounts (45° outward, 20° down), D435i front bracket (8° tilt), stem collar. All RENDER modes for STL and DXF export included. • rover_spring_arm.scad — Trailing-arm spring suspension (×4). Pivot on M8 bolt; captured 14mm OD compression spring (50mm free, ~5 N/mm); open-end axle dropout slot with retainer cap. Provides 25mm bump + 15mm droop travel. Bearing-seat recess for caliper-verified 37.8mm collar OD. • rover_electronics_bay.scad — PETG electronics bay (240×200×80mm internal). FC standoffs 30.5×30.5mm M3 and Jetson Orin 58×49mm M3 — shared SaltyLab swappable pattern. Ventilation slots all 4 walls + lid. Lid integrates 100mm RPLIDAR A1M8 tower (58mm BC, matched to rplidar_mount.scad). Split-print halves for 220mm beds included. • rover_chassis_r2_BOM.md — Full BOM, mass estimate (frame ~2.15kg; reduce to <2kg by setting DECK_T=5), assembly sequence, critical dimensions. Sensor positions: RPLIDAR top-centre on bay lid, D435i front, 4× IMX219 at deck corners. Shares 30.5mm FC + 58mm Jetson + Ø25mm stem patterns with SaltyLab for swappable electronics. Co-Authored-By: Claude Sonnet 4.6 --- chassis/rover_chassis_r2_BOM.md | 328 ++++++++++++++++++ chassis/rover_electronics_bay.scad | 400 ++++++++++++++++++++++ chassis/rover_spring_arm.scad | 272 +++++++++++++++ chassis/saltyrover_chassis_r2.scad | 523 +++++++++++++++++++++++++++++ 4 files changed, 1523 insertions(+) create mode 100644 chassis/rover_chassis_r2_BOM.md create mode 100644 chassis/rover_electronics_bay.scad create mode 100644 chassis/rover_spring_arm.scad create mode 100644 chassis/saltyrover_chassis_r2.scad diff --git a/chassis/rover_chassis_r2_BOM.md b/chassis/rover_chassis_r2_BOM.md new file mode 100644 index 0000000..755c807 --- /dev/null +++ b/chassis/rover_chassis_r2_BOM.md @@ -0,0 +1,328 @@ +# SaltyRover Chassis Rev 2 — BOM & Assembly Notes +**Issue: #109 Agent: sl-mechanical Date: 2026-03-01** + +--- + +## Overview + +Rev 2 adds spring suspension, an enclosed electronics bay, and corner CSI cameras +to the SaltyRover platform (originally designed in issue #73). +Sensor head, RPLIDAR, D435i, and vertical stem are **shared with SaltyLab** — no changes. + +``` +Top view (schematic): + + +Y (forward) + │ + [CSI] ┌────┴────┐ [CSI] + ◉──┤ ├──◉ ← front axles (suspension arms) + │ [Orin] │ + │ [ Bay ] │ + │ [RPLIDAR tower] + │ [ FC ] │ + ◉──┤ ├──◉ ← rear axles (suspension arms) + [CSI] └────┬────┘ [CSI] + │ + D435i → + +Deck footprint: 500 × 480 mm (Y × X) +Track (axle C/C): 540 mm +Wheelbase (axle C/C): 340 mm +Ground clearance: 55 mm (static sag; 30 mm at full compression) +Overall width (tyre edge to tyre edge): ~810 mm +Overall height (deck to RPLIDAR top): ~317 mm +``` + +--- + +## File Index + +| File | Description | Part | Qty | +|------|-------------|------|-----| +| `saltyrover_chassis_r2.scad` | Deck plate + pivot brackets + sensor brackets | Laser cut + 3D print | See below | +| `rover_spring_arm.scad` | Spring suspension trailing arm + retainer cap | 3D print | 4× arm + 4× cap | +| `rover_electronics_bay.scad` | Electronics bay body + lid (with RPLIDAR tower) | 3D print | 1× body + 1× lid | +| `rover_motor_mount.scad` | Motor axle L-bracket (Rev 1, unchanged) | 3D print or CNC | 4× | +| `rover_battery_tray.scad` | Under-deck battery slide tray (Rev 1) | 3D print | 1× | +| `rover_stem_adapter.scad` | Stem-to-deck collar adapter (Rev 1) | 3D print | 1× | +| `rplidar_mount.scad` | RPLIDAR anti-vibration ring (shared) | 3D print | 1× | +| `realsense_mount.scad` | D435i bracket (integrated into chassis r2) | — | — | +| `imx219_mount.scad` | IMX219 radial arm on sensor_head (unchanged) | — | — | + +--- + +## Part A — Deck Plate (`saltyrover_chassis_r2.scad` → `deck_2d`) + +| # | Spec | Qty | Notes | +|---|------|-----|-------| +| A1 | 6 mm 5052-H32 aluminium, 500×480 mm blank | 1 | Waterjet or CNC router. 6 mm preferred (vs 8 mm Rev 1) for weight target. | +| A1-alt | 8 mm PETG FDM, split in two halves, joined with M5 lap bolts | 1 | Prototype only — expect 1.5× weight, 0.5× stiffness | + +**Deck plate weight estimate:** 6 mm Al, ~50% lightening → **≈ 1.15 kg** ✓ + +Export DXF: +```bash +openscad saltyrover_chassis_r2.scad -D 'RENDER="deck_2d"' -o saltyrover_r2_deck.dxf +``` + +--- + +## Part B — Pivot Brackets (`saltyrover_chassis_r2.scad` → `pivot_bracket_stl`) + +Each bracket provides: M8 pivot for suspension arm + spring guide boss + M3 adjustment slots. + +| # | RENDER | Qty | Material | Settings | +|---|--------|-----|----------|----------| +| B1 | `pivot_bracket_stl` | 4 | PETG or PC | 5 perims, 60% gyroid | +| B1-alt | `pivot_bracket_2d` | 4 | 8 mm 6061-T6 Al | CNC router / waterjet | + +CNC export: +```bash +openscad saltyrover_chassis_r2.scad -D 'RENDER="pivot_bracket_2d"' -o rover_pivot_bracket.dxf +``` + +**Fasteners — Bracket to Deck:** + +| # | Spec | Qty | Use | +|---|------|-----|-----| +| B-f1 | M3×16 SHCS | 8 | Bracket to deck (2× per bracket) through slotted deck holes | +| B-f2 | M3 nyloc nut | 8 | Under-deck retention (accessible from below before deck install) | +| B-f3 | M3 flat washer | 16 | Both sides | + +**Fasteners — Suspension pivot:** + +| # | Spec | Qty | Use | +|---|------|-----|-----| +| B-f4 | M8×50 SHCS | 4 | Pivot pin (bracket clevis → trailing arm) | +| B-f5 | M8 nyloc nut | 4 | Pivot retention | +| B-f6 | M8 flat washer | 8 | Both sides of clevis | +| B-f7 | Flanged IGUS GFI-0810-10 bushing | 4 | Pivot arm bearing (optional, reduces wear) | + +--- + +## Part C — Spring Suspension Arms (`rover_spring_arm.scad`) + +| # | RENDER | Qty | Material | Settings | +|---|--------|-----|----------|----------| +| C1 | `arm_stl` | 4 | PC (Polycarbonate) recommended; PETG acceptable | 5 perims, 60% gyroid infill | +| C2 | `retainer_stl` | 4 | PETG | 4 perims, 40% infill | + +Export: +```bash +openscad rover_spring_arm.scad -D 'RENDER="arm_stl"' -o rover_spring_arm.stl +openscad rover_spring_arm.scad -D 'RENDER="retainer_stl"' -o rover_spring_retainer.stl +``` + +**Compression Springs (×4):** + +| # | Spec | Qty | Notes | +|---|------|-----|-------| +| C-s1 | OD 14 mm, wire Ø 1.5 mm, free length 50 mm, spring rate ~5 N/mm | 4 | Lee Spring LCI 014M 05 S or equivalent | +| C-s2 | *(Stiffer alternative)* OD 14 mm, rate ~8 N/mm | 4 | For heavier payloads >3 kg | + +**Retainer fasteners:** + +| # | Spec | Qty | Use | +|---|------|-----|-----| +| C-f1 | M3×12 SHCS | 8 | Retainer cap to arm (2× per arm; self-taps into PETG) | + +--- + +## Part D — Electronics Bay (`rover_electronics_bay.scad`) + +| # | RENDER | Qty | Material | Settings | +|---|--------|-----|----------|----------| +| D1 | `bay_stl` | 1 | PETG | 4 perims, 30% gyroid | +| D1-alt | `front_half` + `rear_half` | 1+1 | PETG | For 220 mm bed printers (split at Y=0 centreline) | +| D2 | `lid_stl` | 1 | PETG | 4 perims, 30% gyroid | + +Export: +```bash +openscad rover_electronics_bay.scad -D 'RENDER="bay_stl"' -o rover_elec_bay.stl +openscad rover_electronics_bay.scad -D 'RENDER="lid_stl"' -o rover_elec_bay_lid.stl +# For 220 mm beds: +openscad rover_electronics_bay.scad -D 'RENDER="front_half"' -o rover_elec_bay_front.stl +openscad rover_electronics_bay.scad -D 'RENDER="rear_half"' -o rover_elec_bay_rear.stl +``` + +**Bay-to-deck fasteners:** + +| # | Spec | Qty | Use | +|---|------|-----|-----| +| D-f1 | M3×12 SHCS | 10 | Bay body to deck through floor flanges | +| D-f2 | M3 flat washer | 10 | Under head | +| D-f3 | M3×8 BHCS | 4 | Lid retention (corner screws self-tap into bay rim) | + +**Electronics internal (FC + Jetson standoffs — built into bay body):** + +| # | Spec | Qty | Use | +|---|------|-----|-----| +| D-f4 | M3×8 SHCS | 8 | FC mount to bay standoffs (4×) + Jetson to bay standoffs (4×) | +| D-f5 | M3 flat washer | 8 | Under heads | +| D-f6 | Anti-vibration M3 grommet | 4 | FC isolation (silicone, M3, same as rplidar_mount.scad) | + +--- + +## Part E — CSI Corner Camera Brackets (`saltyrover_chassis_r2.scad` → `csi_mount_stl`) + +| # | RENDER | Qty | Material | Settings | +|---|--------|-----|----------|----------| +| E1 | `csi_mount_stl` | 4 | PETG | 4 perims, 30% infill | + +Export: +```bash +openscad saltyrover_chassis_r2.scad -D 'RENDER="csi_mount_stl"' -o rover_csi_mount.stl +``` + +| # | Spec | Qty | Use | +|---|------|-----|-----| +| E-f1 | M2×6 SHCS | 8 | CSI camera PCB to bracket (2× per camera) | +| E-f2 | M3×8 SHCS | 8 | Bracket to deck (2× per bracket) | +| E-c1 | 200 mm CSI FPC flat cable | 4 | IMX219 to Jetson (extended) | + +--- + +## Part F — D435i Front Bracket (`saltyrover_chassis_r2.scad` → `d435i_mount_stl`) + +| # | RENDER | Qty | Material | Settings | +|---|--------|-----|----------|----------| +| F1 | `d435i_mount_stl` | 1 | PETG | 5 perims, 40% infill | + +Export: +```bash +openscad saltyrover_chassis_r2.scad -D 'RENDER="d435i_mount_stl"' -o rover_d435i_mount.stl +``` + +| # | Spec | Qty | Use | +|---|------|-----|-----| +| F-f1 | 1/4-20 UNC hex nut | 1 | Captured in bracket face for D435i tripod socket | +| F-f2 | M4×14 SHCS | 2 | Bracket to deck front face | + +--- + +## Mass Estimate — Frame Only (excl. motors, electronics, battery) + +| Assembly | Material | Est. mass | +|----------|----------|-----------| +| Deck plate | 6 mm Al, lightened | ~1.15 kg | +| Pivot brackets × 4 | PETG | ~0.22 kg | +| Spring arms × 4 | PC | ~0.28 kg | +| Spring retainer caps × 4 | PETG | ~0.04 kg | +| Springs × 4 | Steel | ~0.04 kg | +| Electronics bay body | PETG | ~0.12 kg | +| Electronics bay lid + RPLIDAR tower | PETG | ~0.08 kg | +| CSI brackets × 4 | PETG | ~0.04 kg | +| D435i bracket × 1 | PETG | ~0.03 kg | +| Fasteners (M2–M8) | Stainless | ~0.15 kg | +| **Frame total** | | **~2.15 kg** | + +> ⚠ Target: <2 kg frame. Current estimate is 0.15 kg over. +> Options to reduce: +> 1. Switch deck from 6 mm Al → 5 mm Al saves ~0.19 kg ✓ +> 2. Or: enlarge lightening holes from Ø55 → Ø65 mm (saves ~0.12 kg) +> 3. Electronics bay in 2 mm wall PETG (saves ~0.06 kg) +> Recommend option 1: change `DECK_T = 5.0` in `saltyrover_chassis_r2.scad` +> and re-verify with waterjet quotation. + +--- + +## Assembly Sequence + +### 1. Fabrication +1. Export DXF and send deck plate to waterjet / CNC. Specify 6 mm (or 5 mm) 5052-H32 Al. +2. Export and print all STL parts (settings per table above). +3. Source springs, fasteners, and hub motors per BOM. + +### 2. Deck preparation +1. Deburr all deck holes. Tap stem-collar M4 holes if using threaded standoffs. +2. Press or thread M3 rivet-nuts into deck at battery-tray rail positions (from `rover_battery_tray.scad`). +3. Apply stem collar with 4× M4×16 FHCS; Loctite 243. + +### 3. Pivot bracket installation +1. Slide pivot bracket through deck M3 slots (under-deck side first). +2. Fit M3 washers + nyloc nuts; snug but do not torque — leave adjustable. +3. Set all 4 brackets to nominal Y position (motor corner fore-aft CL). +4. Torque M3 bolts to 1.2 N·m once alignment is confirmed (step 6). + +### 4. Suspension arm assembly +1. Drop spring into bracket spring-guide boss (compress by hand). +2. Slide trailing arm pivot boss over pivot bolt M8×50. +3. Fit IGUS bushing in pivot bore (if used). +4. Fit M8 washer + nyloc nut; torque to 6 N·m. +5. Snap retainer cap onto arm axle slot; thread 2× M3×12 by hand. + +### 5. Motor installation +1. Slide hub motor axle into arm dropout slot. +2. Fit clamp plate (from `rover_motor_mount.scad`); tighten M4 bolts 1.5 N·m. +3. Thread axle lock nut; apply Loctite 243; torque to 30 N·m. +4. Route phase cables + hall wires through deck phase pass-through hole. + +### 6. Geometry check and bracket torque +1. Set robot on flat surface; check that all 4 wheels contact ground. +2. Measure axle-to-ground on each corner. Nominal: 127 mm ± 5 mm. +3. Adjust pivot bracket fore-aft position if needed to correct height. +4. Torque all M3 bracket bolts to 1.2 N·m. + +### 7. Electronics bay + wiring +1. Thread ESC/VESC harnesses through bay floor cable pass-throughs. +2. Place electronics bay body on deck; fasten 10× M3×12 SHCS. +3. Mount FC on bay standoffs: anti-vibration grommets + M3×8 SHCS. +4. Mount Jetson Orin on bay standoffs: M3×8 SHCS. +5. Route USB/UART cables internally; cable-tie to bay walls. +6. Fit lid (with RPLIDAR tower stub); 4× M3×8 BHCS at corners. + +### 8. Sensor installation +1. **RPLIDAR A1M8**: Fit anti-vibration ring (`rplidar_mount.scad`) on tower top. + Bolt RPLIDAR with 4× M3×30 SHCS through ring. +2. **D435i**: Bolt to front bracket arm using captured 1/4-20 nut. + Confirm 8° downward tilt; tighten firmly. +3. **4× CSI cameras**: Plug CSI flex into Jetson CSI ports. + Thread M2×6 SHCS into each corner bracket PCB holes. +4. **Stem + sensor head**: Press stem through deck collar bore. + Fit stem adapter (`rover_stem_adapter.scad`); clamp at 550 mm height. + Attach sensor_head to stem top as per `sensor_head_assembly.md`. + +### 9. Final checks +- [ ] All wheels spin freely without catching wiring +- [ ] Suspension compresses and rebounds on each corner +- [ ] RPLIDAR scans 360° without obstruction (check deck edge clearance) +- [ ] D435i USB connected and streaming +- [ ] CSI cameras initialised on Jetson boot (`v4l2-ctl --list-devices`) +- [ ] FC armed and IMU reading correctly +- [ ] E-stop functional + +--- + +## Motors (unchanged from Rev 1) + +| # | Part | Qty | Spec | +|---|------|-----|------| +| M1 | Hub motor | 4 | 10×2.125" pneumatic, 36 V, ~350 W; axle OD 16.11 mm (caliper) | +| M2 | Phase cable extension | 4 | 3-wire 12 AWG, 300 mm, XT30 to VESC | +| M3 | Hall cable extension | 4 | 6-pin JST-PH, 300 mm | + +--- + +## Critical Dimensions + +| Dimension | Nominal | Tolerance | +|-----------|---------|-----------| +| Track (axle C/C) | 540 mm | ±2 mm | +| Wheelbase (axle C/C) | 340 mm | ±2 mm | +| Axle CL height | 127 mm | ±3 mm | +| Pivot bracket M3 slot pitch | 32 mm | ±0.3 mm | +| FC hole pattern | 30.5×30.5 mm | ±0.2 mm | +| Jetson hole pattern | 58×49 mm | ±0.2 mm | +| Stem bore | Ø25.5 mm | +0.3/0 | +| Spring guide boss OD | Ø14 mm | ±0.1 mm | + +--- + +## OpenSCAD Version Requirement + +Requires **OpenSCAD 2021.01 or newer** (for `linear_extrude` + `minkowski` with `$fn` in difference). + +Render command (full assembly): +```bash +openscad saltyrover_chassis_r2.scad & +``` diff --git a/chassis/rover_electronics_bay.scad b/chassis/rover_electronics_bay.scad new file mode 100644 index 0000000..ee47118 --- /dev/null +++ b/chassis/rover_electronics_bay.scad @@ -0,0 +1,400 @@ +// ============================================================ +// rover_electronics_bay.scad — SaltyRover Electronics Bay +// Issue: #109 Agent: sl-mechanical Date: 2026-03-01 +// ============================================================ +// +// Enclosed electronics housing sitting on the rover deck plate. +// Houses: • Flight Controller (FC) — 30.5×30.5 mm M3 standoffs +// • Jetson Orin NX / Nano — 58×49 mm M3 standoffs +// • Battery access slot — left side slide-in +// • RPLIDAR A1M8 tower — integrated on lid top +// • Ventilation slots — all 4 walls + lid +// +// Shared mounting patterns (swappable with SaltyLab): +// FC : 30.5 × 30.5 mm M3 (MAMBA F722S / Pixhawk) +// Jetson: 58 × 49 mm M3 (Orin NX / Nano Devkit carrier) +// +// Coordinate: bay centred at origin; Z=0 = deck top face. +// Bay body rests directly on deck top (no additional standoffs). +// +// Print: +// Material : PETG (bay body + lid) +// Settings : 4 perimeters, 30% gyroid infill +// Orientation: open top face up for body; lid printed flat. +// Note: Bay is too large to print as one piece on most 220mm beds. +// Use RENDER="front_half" and RENDER="rear_half" for split, +// joined with 3× M3 bolts and alignment pins. +// +// Export commands: +// Bay body (full, for large-bed printers): +// openscad rover_electronics_bay.scad -D 'RENDER="bay_stl"' -o rover_elec_bay.stl +// Front half: +// openscad rover_electronics_bay.scad -D 'RENDER="front_half"' -o rover_elec_bay_front.stl +// Rear half: +// openscad rover_electronics_bay.scad -D 'RENDER="rear_half"' -o rover_elec_bay_rear.stl +// Lid (with RPLIDAR tower): +// openscad rover_electronics_bay.scad -D 'RENDER="lid_stl"' -o rover_elec_bay_lid.stl +// Assembly preview: +// openscad rover_electronics_bay.scad -D 'RENDER="assembly"' +// ============================================================ + +$fn = 64; +e = 0.01; + +// ── Bay exterior dimensions ────────────────────────────────────────────────── +BAY_L = 240.0; // length left-right (X in rover coords = Y here) +BAY_W = 200.0; // width fore-aft (Y in rover = X here) +BAY_H = 80.0; // interior height +BAY_WALL = 3.0; // wall thickness (all sides) +BAY_FLOOR = 4.0; // floor thickness (rests on deck) +BAY_R = 8.0; // exterior corner radius + +// ── Ventilation slots ──────────────────────────────────────────────────────── +VENT_W = 20.0; // slot width +VENT_H = 6.0; // slot height +VENT_PITCH = 28.0; // slot centre-to-centre pitch +VENT_FROM_BOT = 12.0; // lowest vent row height above floor exterior + +// ── Lid ───────────────────────────────────────────────────────────────────── +LID_T = 4.0; // lid plate thickness +LID_RIM_H = 8.0; // lip that drops inside bay walls (retention) +LID_RIM_GAP = 0.4; // clearance between lid rim and bay inner wall + +// ── FC mount — 30.5×30.5 mm M3 (shared SaltyLab) ──────────────────────────── +FC_PITCH = 30.5; +FC_HOLE_D = 3.2; +FC_STANDOFF_H = 8.0; +FC_STANDOFF_OD = 7.0; +// FC positioned toward front-left inside bay (offset from centre) +FC_OFF_X = -BAY_L/2 + 60.0; // left side (left = cable/ESC side) +FC_OFF_Y = -BAY_W/2 + 50.0; // front side + +// ── Jetson Orin mount — 58×49 mm M3 (shared SaltyLab) ─────────────────────── +ORIN_HOLE_X = 58.0; +ORIN_HOLE_Y = 49.0; +ORIN_HOLE_D = 3.2; +ORIN_STANDOFF_H = 10.0; +ORIN_STANDOFF_OD = 7.0; +// Jetson positioned toward rear-right (toward robot rear, USB/HDMI accessible) +ORIN_OFF_X = BAY_L/2 - 70.0; // right side +ORIN_OFF_Y = BAY_W/2 - 55.0; // rear side + +// ── Battery access slot (left wall slide-in) ────────────────────────────────── +// The under-deck battery tray is separate (rover_battery_tray.scad). +// A slot in the bay left wall allows BMS cable + main power harness. +BATT_SLOT_W = 30.0; // harness slot width +BATT_SLOT_H = 20.0; // harness slot height +BATT_SLOT_Z = 20.0; // slot bottom above floor interior + +// ── RPLIDAR A1M8 tower (on lid, top centre) ─────────────────────────────────── +RPL_TOWER_OD = 28.0; // tower OD (hollow column) +RPL_TOWER_ID = 16.0; // hollow core ID (cable routing) +RPL_TOWER_H = 100.0; // tower height above lid top face +// RPLIDAR A1M8 bolt circle: 58 mm dia, 4× M3 at 45°/135°/225°/315° +RPL_BC = 58.0; +RPL_HOLE_D = 3.3; // M3 clearance +RPL_PLATFORM_D = 90.0; // platform disk at tower top + +// ── Bay-to-deck attachment ──────────────────────────────────────────────────── +// 8× M3 SHCS through bay floor flanges into deck (matching saltyrover_chassis_r2.scad) +DECK_BOLT_D = 3.3; +DECK_BOLT_INSET = 8.0; // bolt CL from exterior corner + +// ── Lid retention (M3 corner bolts) ────────────────────────────────────────── +LID_BOLT_D = 3.3; +LID_BOLT_POS = 8.0; // bolt CL from exterior wall + +M3_D = 3.3; +M4_D = 4.3; + +// ============================================================ +// RENDER DISPATCH +// ============================================================ +RENDER = "assembly"; + +if (RENDER == "assembly") { + assembly(); +} else if (RENDER == "bay_stl") { + bay_body(); +} else if (RENDER == "front_half") { + // Split along XZ plane (Y=0) — front half + intersection() { + bay_body(); + translate([0, -BAY_W/2 - BAY_WALL, 0]) + cube([BAY_L + 2*BAY_WALL + 2, BAY_W/2 + BAY_WALL + 1, + BAY_H + BAY_FLOOR + LID_T + 2]); + } +} else if (RENDER == "rear_half") { + // Split along XZ plane (Y=0) — rear half + intersection() { + bay_body(); + translate([0, 0, 0]) + cube([BAY_L + 2*BAY_WALL + 2, BAY_W/2 + BAY_WALL + 1, + BAY_H + BAY_FLOOR + LID_T + 2]); + } +} else if (RENDER == "lid_stl") { + bay_lid(); +} + +// ============================================================ +// ASSEMBLY PREVIEW +// ============================================================ +module assembly() { + color("OliveDrab", 0.80) bay_body(); + color("DarkOliveGreen", 0.70) + translate([0, 0, BAY_FLOOR + BAY_H + 1]) + bay_lid(); + + // FC standoffs + ghost board + color("LightGray", 0.60) fc_standoffs(); + %color("DarkGreen", 0.30) + translate([FC_OFF_X, FC_OFF_Y, BAY_FLOOR + FC_STANDOFF_H]) + cube([76, 42, 3], center = true); + + // Jetson standoffs + ghost board + color("LightGray", 0.60) jetson_standoffs(); + %color("DarkBlue", 0.25) + translate([ORIN_OFF_X, ORIN_OFF_Y, + BAY_FLOOR + ORIN_STANDOFF_H]) + cube([100, 80, 5], center = true); +} + +// ============================================================ +// BAY BODY (open-top box with ventilation + mounts) +// ============================================================ +module bay_body() { + outer_x = BAY_L + 2*BAY_WALL; + outer_y = BAY_W + 2*BAY_WALL; + outer_z = BAY_FLOOR + BAY_H; + + difference() { + // ── Outer shell (rounded rectangle) ──────────────────────────── + linear_extrude(outer_z) + minkowski() { + square([outer_x - 2*BAY_R, outer_y - 2*BAY_R], center = true); + circle(r = BAY_R); + } + + // ── Inner cavity ─────────────────────────────────────────────── + translate([-BAY_L/2, -BAY_W/2, BAY_FLOOR]) + cube([BAY_L, BAY_W, BAY_H + e]); + + // ── Ventilation slots — left wall (−X) ──────────────────────── + for (i = [-2:2]) + translate([-(BAY_L/2 + BAY_WALL + e), + i * VENT_PITCH - VENT_W/2, + VENT_FROM_BOT]) + cube([BAY_WALL + 2*e, VENT_W, VENT_H]); + + // ── Ventilation slots — right wall (+X) ─────────────────────── + for (i = [-2:2]) + translate([BAY_L/2 - e, + i * VENT_PITCH - VENT_W/2, + VENT_FROM_BOT]) + cube([BAY_WALL + 2*e, VENT_W, VENT_H]); + + // ── Ventilation slots — front wall (−Y) ─────────────────────── + for (i = [-2:2]) + translate([i * VENT_PITCH - VENT_W/2, + -(BAY_W/2 + BAY_WALL + e), + VENT_FROM_BOT]) + cube([VENT_W, BAY_WALL + 2*e, VENT_H]); + + // ── Ventilation slots — rear wall (+Y) ──────────────────────── + for (i = [-2:2]) + translate([i * VENT_PITCH - VENT_W/2, + BAY_W/2 - e, + VENT_FROM_BOT]) + cube([VENT_W, BAY_WALL + 2*e, VENT_H]); + + // ── Battery / harness slot (left wall) ──────────────────────── + translate([-(BAY_L/2 + BAY_WALL + e), + -BATT_SLOT_W/2, + BAY_FLOOR + BATT_SLOT_Z]) + cube([BAY_WALL + 2*e, BATT_SLOT_W, BATT_SLOT_H]); + + // ── FC mount holes through floor ────────────────────────────── + for (dx = [-FC_PITCH/2, FC_PITCH/2]) + for (dy = [-FC_PITCH/2, FC_PITCH/2]) + translate([FC_OFF_X + dx, FC_OFF_Y + dy, -e]) + cylinder(d = FC_HOLE_D, h = BAY_FLOOR + 2*e); + + // ── Jetson mount holes through floor ────────────────────────── + for (dx = [-ORIN_HOLE_X/2, ORIN_HOLE_X/2]) + for (dy = [-ORIN_HOLE_Y/2, ORIN_HOLE_Y/2]) + translate([ORIN_OFF_X + dx, ORIN_OFF_Y + dy, -e]) + cylinder(d = ORIN_HOLE_D, h = BAY_FLOOR + 2*e); + + // ── Bay-to-deck M3 bolt holes (8× corners, through floor flange) + for (sx = [-1, 1]) + for (sy = [-1, 1]) { + bx = sx * (BAY_L/2 + BAY_WALL - DECK_BOLT_INSET); + by = sy * (BAY_W/2 + BAY_WALL - DECK_BOLT_INSET); + translate([bx, by, -e]) + cylinder(d = DECK_BOLT_D, h = BAY_FLOOR + 2*e); + } + // Extra 2 bolts per long wall (centre) + for (sy = [-1, 1]) + translate([0, sy * (BAY_W/2 + BAY_WALL - DECK_BOLT_INSET), -e]) + cylinder(d = DECK_BOLT_D, h = BAY_FLOOR + 2*e); + + // ── Lid retention M3 threaded bosses cut (4× top rim corners) ── + for (sx = [-1, 1]) + for (sy = [-1, 1]) { + lx = sx * (BAY_L/2 + BAY_WALL - LID_BOLT_POS); + ly = sy * (BAY_W/2 + BAY_WALL - LID_BOLT_POS); + translate([lx, ly, outer_z - 12]) + cylinder(d = LID_BOLT_D - 0.3, h = 14); // M3 self-tap bore + } + + // ── Cable pass-through grommets slots (bottom, 2× for deck slots) + for (sy = [-1, 1]) + hull() { + translate([-15, sy * (BAY_W/2 - 6), -e]) + cylinder(d = 12, h = BAY_FLOOR + 2*e); + translate([ 15, sy * (BAY_W/2 - 6), -e]) + cylinder(d = 12, h = BAY_FLOOR + 2*e); + } + } + + // ── FC standoffs ───────────────────────────────────────────────────────── + fc_standoffs(); + + // ── Jetson standoffs ───────────────────────────────────────────────────── + jetson_standoffs(); +} + +// ── FC standoffs (inside bay, above floor) ─────────────────────────────────── +module fc_standoffs() { + for (dx = [-FC_PITCH/2, FC_PITCH/2]) + for (dy = [-FC_PITCH/2, FC_PITCH/2]) + translate([FC_OFF_X + dx, FC_OFF_Y + dy, BAY_FLOOR]) + difference() { + cylinder(d = FC_STANDOFF_OD, h = FC_STANDOFF_H); + // Threaded bore (M3 screw from above) + translate([0, 0, FC_STANDOFF_H - 6]) + cylinder(d = 2.5, h = 7); // M3 tap drill (Ø2.5) + // Through clearance from floor (to match deck FC holes) + cylinder(d = FC_HOLE_D, h = FC_STANDOFF_H - 6); + } +} + +// ── Jetson Orin standoffs (inside bay, above floor) ────────────────────────── +module jetson_standoffs() { + for (dx = [-ORIN_HOLE_X/2, ORIN_HOLE_X/2]) + for (dy = [-ORIN_HOLE_Y/2, ORIN_HOLE_Y/2]) + translate([ORIN_OFF_X + dx, ORIN_OFF_Y + dy, BAY_FLOOR]) + difference() { + cylinder(d = ORIN_STANDOFF_OD, h = ORIN_STANDOFF_H); + // M3 tap bore (top 8mm) + translate([0, 0, ORIN_STANDOFF_H - 8]) + cylinder(d = 2.5, h = 9); + // Clearance from floor + cylinder(d = ORIN_HOLE_D, h = ORIN_STANDOFF_H - 8); + } +} + +// ============================================================ +// BAY LID (with RPLIDAR A1M8 tower and ventilation) +// ============================================================ +// Lid drops over bay walls (retention lip) and is held with 4× M3 screws. +// RPLIDAR A1M8 tower rises from lid centre. +// Lid ventilation slots allow convective air circulation. +// ============================================================ +module bay_lid() { + outer_x = BAY_L + 2*BAY_WALL; + outer_y = BAY_W + 2*BAY_WALL; + + difference() { + union() { + // ── Lid plate ───────────────────────────────────────────── + linear_extrude(LID_T) + minkowski() { + square([outer_x - 2*BAY_R, outer_y - 2*BAY_R], + center = true); + circle(r = BAY_R); + } + + // ── Retention rim (drops inside bay walls) ───────────────── + rim_x = BAY_L - 2*LID_RIM_GAP; + rim_y = BAY_W - 2*LID_RIM_GAP; + translate([0, 0, -LID_RIM_H + e]) + linear_extrude(LID_RIM_H) + difference() { + minkowski() { + square([rim_x - 2*BAY_R, rim_y - 2*BAY_R], + center = true); + circle(r = BAY_R); + } + // Hollow interior + offset(r = -BAY_WALL) + minkowski() { + square([rim_x - 2*BAY_R, rim_y - 2*BAY_R], + center = true); + circle(r = BAY_R); + } + } + + // ── RPLIDAR tower (centred on lid) ───────────────────────── + translate([0, 0, LID_T]) + rplidar_tower(); + } + + // ── Lid ventilation slots (3× rows, 5 slots each) ───────────── + for (i = [-2:2]) { + translate([i * VENT_PITCH - VENT_W/2, -outer_y/2 + 20, -e]) + cube([VENT_W, outer_y - 40, LID_T + 2*e]); + } + + // ── 4× M3 lid retention bolt holes ──────────────────────────── + for (sx = [-1, 1]) + for (sy = [-1, 1]) { + lx = sx * (outer_x/2 - LID_BOLT_POS); + ly = sy * (outer_y/2 - LID_BOLT_POS); + translate([lx, ly, -e]) + cylinder(d = M3_D, h = LID_T + 2*e); + } + } +} + +// ── RPLIDAR A1M8 tower (on lid) ────────────────────────────────────────────── +// Hollow column provides height above bay for RPLIDAR 360° scan clearance. +// Anti-vibration ring (rplidar_mount.scad) sits atop the platform. +// Tower height: 100 mm above lid = ~185 mm total above deck. +module rplidar_tower() { + difference() { + union() { + // Hollow column + cylinder(d = RPL_TOWER_OD, h = RPL_TOWER_H); + + // Flared base (distributes load, improves print adhesion) + cylinder(d = RPL_TOWER_OD + 16, h = 8); + + // Top platform disk + translate([0, 0, RPL_TOWER_H]) + cylinder(d = RPL_PLATFORM_D, h = 8); + } + + // Hollow core (cable routing for RPLIDAR USB) + translate([0, 0, -e]) + cylinder(d = RPL_TOWER_ID, h = RPL_TOWER_H + 9); + + // 4× base-to-lid M3 attachment holes (through flared base) + for (a = [0, 90, 180, 270]) + rotate([0, 0, a]) + translate([(RPL_TOWER_OD + 12) / 2, 0, -e]) + cylinder(d = M3_D, h = 10); + + // RPLIDAR A1M8 mounting holes (4× M3, 58 mm BC, 45° offset) + // Matches rplidar_mount.scad / sensor_head.scad RPL_BC pattern + for (a = [45, 135, 225, 315]) + translate([RPL_BC/2 * cos(a), + RPL_BC/2 * sin(a), + RPL_TOWER_H - e]) + cylinder(d = RPL_HOLE_D, h = 10); + + // Rotation alignment slot (sets RPLIDAR scan start angle) + translate([RPL_BC/2 - 3, -2, RPL_TOWER_H - e]) + cube([8, 4, 10]); + } +} diff --git a/chassis/rover_spring_arm.scad b/chassis/rover_spring_arm.scad new file mode 100644 index 0000000..261c976 --- /dev/null +++ b/chassis/rover_spring_arm.scad @@ -0,0 +1,272 @@ +// ============================================================ +// rover_spring_arm.scad — SaltyRover Spring Suspension Arm +// Issue: #109 Agent: sl-mechanical Date: 2026-03-01 +// ============================================================ +// +// Trailing-arm spring suspension for rough terrain. +// One arm per wheel (print 4×). +// +// Mechanical principle: +// The arm pivots on an M8 bolt through the pivot bracket +// (saltyrover_chassis_r2.scad → pivot_bracket). +// A captured compression spring between the pivot bracket's +// spring boss and the arm's spring pocket provides restoring +// force. When a wheel strikes a bump, the arm swings upward +// (rotating around the pivot) and compresses the spring. +// +// Pivot bracket (chassis-fixed) +// │ +// [M8 pivot]──────────────────[Motor axle dropout] +// │ [Trailing arm] │ +// └──[Spring upper seat] │ +// │ │ +// [Spring] │ +// │ │ +// [Spring pocket in arm] │ +// +// Spring spec (wire form compression, standard size): +// OD : 14 mm (slides over bracket's 14 mm guide boss) +// ID : ~10 mm +// Free length : 50 mm +// Solid height: ~20 mm +// Travel (max): 25 mm → spring compressed to 25 mm +// Spring rate : ~5 N/mm (soft — adjust to robot mass) +// Part no. : e.g. Lee Spring LCI 014M 05 S (or equivalent) +// +// Wheel travel: +// Bump (compression): 25 mm (spring coils bind before hard stop) +// Droop (extension) : 15 mm (limited by pivot bracket flange) +// Total travel : 40 mm +// +// Axle-to-ground height at full compression (worst case bump): +// AXLE_H − TRAVEL_BUMP = 127 − 25 = 102 mm above ground ✓ +// +// Print: +// Material : PETG or PC (PC recommended for structural rigidity) +// Settings : 5 perimeters, 60 % gyroid infill +// Orientation: pivot boss flat face on build plate; no supports needed +// Qty: 4× +// +// Export commands: +// STL (main arm × 4): +// openscad rover_spring_arm.scad -D 'RENDER="arm_stl"' -o rover_spring_arm.stl +// STL (spring retainer cap × 4): +// openscad rover_spring_arm.scad -D 'RENDER="retainer_stl"' -o rover_spring_retainer.stl +// Assembly preview: +// openscad rover_spring_arm.scad -D 'RENDER="assembly"' +// ============================================================ + +$fn = 64; +e = 0.01; + +// ── Motor axle (BOM.md caliper-verified) ──────────────────────────────────── +AXLE_D = 16.11; // axle base section OD (caliper) +AXLE_FLAT = 13.00; // D-cut chord width (caliper) +AXLE_D_DCUT = 15.95; // D-cut section OD (caliper) +BEARING_OD = 37.80; // bearing seat collar OD (caliper) +BEARING_RECESS_H = 8.0; // recess depth for bearing seat on inboard face + +// ── Arm geometry ───────────────────────────────────────────────────────────── +// Pivot end is at X=0, Y=0, Z=0 (pivot CL) +// Motor axle end is at X = +ARM_REACH (outboard, positive X = outboard) +ARM_REACH = 75.0; // pivot CL to motor axle CL (outboard reach) +ARM_W = 38.0; // arm width (fore-aft / Y direction) +ARM_T = 14.0; // arm thickness (vertical / Z direction) +ARM_TAPER = 4.0; // taper at motor end (arm narrows by this amount) + +// Pivot boss +PIV_BOSS_OD = ARM_W; // boss is as wide as arm for structural continuity +PIV_BOSS_L = ARM_T; // boss length = arm thickness +PIV_D = 8.5; // M8 clearance bore through pivot + +// ── Suspension spring parameters ──────────────────────────────────────────── +SPG_OD = 14.0; // spring OD (matches bracket guide boss OD) +SPG_FREE_L = 50.0; // spring free length (see spec above) +SPG_TRAVEL = 25.0; // max bump travel / spring compression +SPG_POCKET_D = SPG_OD + 1.5; // pocket bore (spring slides in with clearance) +SPG_POCKET_H = SPG_TRAVEL + 5; // pocket depth (captures spring bottom) + +// Spring pocket CL from pivot (along arm) +SPG_POS_X = ARM_REACH * 0.45; // ~45% along arm from pivot + +// ── Motor axle dropout slot ────────────────────────────────────────────────── +// Open-end slot at motor end of arm. Retained by spring_retainer_cap. +DROP_W = AXLE_D + 1.0; // slot width (snug but not interference) +DROP_DEPTH = AXLE_D + 4.0; // slot depth from arm end inward + +// ── Spring retainer cap ────────────────────────────────────────────────────── +// Small cap that closes the open axle slot from below, screwing onto the arm. +// Prevents axle from dropping out; provides second bearing recess face. +RET_T = 6.0; // cap thickness +RET_W = ARM_W + 4.0; // cap wider than arm for alignment lip +RET_BOLT_D = M3_D; // 2× M3 bolts retain the cap + +M2_D = 2.3; +M3_D = 3.3; +M4_D = 4.3; +M5_D = 5.3; +M8_D = 8.5; + +// ============================================================ +// RENDER DISPATCH +// ============================================================ +RENDER = "assembly"; + +if (RENDER == "assembly") { + assembly(); +} else if (RENDER == "arm_stl") { + spring_arm(); +} else if (RENDER == "retainer_stl") { + spring_retainer_cap(); +} + +// ============================================================ +// FULL ASSEMBLY PREVIEW +// ============================================================ +module assembly() { + color("SteelBlue", 0.85) spring_arm(); + color("CornflowerBlue", 0.80) + translate([ARM_REACH, 0, -(ARM_T/2 + RET_T)]) + spring_retainer_cap(); + + // Phantom spring (compressed) + %color("LimeGreen", 0.5) + translate([SPG_POS_X, 0, ARM_T/2]) + cylinder(d = SPG_OD, h = SPG_FREE_L - SPG_TRAVEL); + + // Phantom motor axle + %color("Tomato", 0.3) + translate([ARM_REACH, 0, 0]) + rotate([0, 90, 0]) + cylinder(d = AXLE_D, h = 120, center = true); + + // Phantom pivot bolt (M8) + %color("Gray", 0.5) + rotate([0, 90, 0]) + cylinder(d = 8, h = ARM_T + 20, center = true); + + // Phantom bracket guide boss (from pivot_bracket) + %color("DarkGray", 0.4) + translate([SPG_POS_X, 0, ARM_T/2]) + cylinder(d = SPG_OD, h = 20); +} + +// ============================================================ +// SPRING ARM +// ============================================================ +// Pivot CL at (0, 0, 0). Arm extends toward +X. +// Pivots around Y-axis (M8 bolt runs in Y direction). +// Spring acts in Z (vertical) at SPG_POS_X along arm. +// Motor axle runs in Y direction at (ARM_REACH, 0, 0). +// ============================================================ +module spring_arm() { + w_motor = ARM_W - ARM_TAPER; // narrower at motor end + + difference() { + union() { + // ── Main arm body (tapered hull) ────────────────────────── + hull() { + // Pivot end — full-width rectangular section + translate([0, -ARM_W/2, -ARM_T/2]) + cube([e, ARM_W, ARM_T]); + // Motor end — slightly narrower + translate([ARM_REACH - DROP_DEPTH, -w_motor/2, -ARM_T/2]) + cube([e, w_motor, ARM_T]); + } + + // ── Pivot boss (cylindrical for hinge strength) ─────────── + // Cylindrical boss sits at pivot CL; flange provides washer seat. + rotate([90, 0, 0]) { + difference() { + cylinder(d = PIV_BOSS_OD, h = PIV_BOSS_L, center = true); + } + } + + // ── Spring pocket boss (raised above arm top face) ───────── + // Boss rises to meet the bracket's spring guide boss. + // The compression spring is captured between the two bosses. + translate([SPG_POS_X, 0, ARM_T/2]) + cylinder(d = SPG_OD + 8, h = 8); + + // ── Axle retention lug at motor end (prevents side loading) ─ + translate([ARM_REACH - ARM_T, -w_motor/2, -ARM_T/2]) + cube([ARM_T, w_motor, ARM_T - BEARING_RECESS_H]); + } + + // ── M8 pivot bore (through pivot boss in Y direction) ───────── + rotate([90, 0, 0]) + cylinder(d = PIV_D, h = PIV_BOSS_L + 2*e, center = true); + + // ── Spring pocket bore (from top, captures spring bottom) ───── + // Bore is slightly larger than spring OD for easy insertion. + translate([SPG_POS_X, 0, ARM_T/2 - e]) + cylinder(d = SPG_POCKET_D, h = SPG_POCKET_H + e); + + // ── Spring pocket access slot (allows spring preload assembly) ─ + // Lateral slot lets spring be pressed in from side during assembly. + translate([SPG_POS_X - SPG_OD/2, -SPG_OD/2 - 0.5, ARM_T/2 - e]) + cube([SPG_OD, SPG_OD + 1, SPG_POCKET_H + e]); + + // ── Motor axle dropout slot (open at arm tip end, +X) ───────── + // Slot width = axle OD + 1 mm; depth = DROP_DEPTH inward. + // Motor axle slides in from the open end. + translate([ARM_REACH - DROP_DEPTH, -DROP_W/2, -ARM_T/2 - e]) + cube([DROP_DEPTH + e, DROP_W, ARM_T + 2*e]); + + // Rounded bore at inner end of dropout slot (distributes load) + translate([ARM_REACH - DROP_DEPTH, 0, -ARM_T/2 - e]) + cylinder(d = DROP_W, h = ARM_T + 2*e); + + // ── Bearing seat recess (inboard face of axle slot) ────────── + // Prevents bearing collar (Ø37.8) from clashing with arm face. + translate([ARM_REACH - DROP_DEPTH - BEARING_RECESS_H, 0, -ARM_T/2 - e]) + cylinder(d = BEARING_OD + 1.5, h = BEARING_RECESS_H + e); + + // ── Retainer cap M3 bolt holes (2×, for spring_retainer_cap) ── + for (dy = [-ARM_W/4, ARM_W/4]) + translate([ARM_REACH - DROP_DEPTH/2, dy, -ARM_T/2 - e]) + cylinder(d = M3_D - 0.3, h = ARM_T/2 + e); + // Slightly tight bore — M3 self-taps into PETG at 3.0 mm + + // ── Lightening slot (mid-arm, between pivot boss and spring) ── + lx1 = PIV_BOSS_OD/2 + 5; + lx2 = SPG_POS_X - SPG_OD/2 - 5; + translate([lx1, -(ARM_W/4), -ARM_T/2 - e]) + cube([max(lx2 - lx1, 1), ARM_W/2, ARM_T + 2*e]); + } +} + +// ============================================================ +// SPRING RETAINER CAP +// ============================================================ +// Clips onto the open axle slot at the arm tip. +// Prevents motor axle from falling out of the dropout slot. +// 2× M3 bolts thread into the arm's self-tap holes. +// Also provides the outboard bearing-seat face. +// ============================================================ +module spring_retainer_cap() { + w_cap = ARM_W - ARM_TAPER + 2; + + difference() { + union() { + cube([DROP_DEPTH, w_cap, RET_T], center = true); + + // Alignment lips (engage the arm slot edges) + for (dy = [-1, 1]) + translate([0, dy * (w_cap/2 + 1), 0]) + cube([DROP_DEPTH - 2, 2, RET_T + 4], center = true); + } + + // Axle bore (clearance) — round section + cylinder(d = AXLE_D + 0.8, h = RET_T + 2*e, center = true); + + // Bearing seat recess (outboard face) + translate([0, 0, RET_T/2 - BEARING_RECESS_H/2]) + cylinder(d = BEARING_OD + 1.5, h = BEARING_RECESS_H + e, center = true); + + // 2× M3 bolt clearance holes + for (dy = [-ARM_W/4, ARM_W/4]) + translate([0, dy, 0]) + cylinder(d = M3_D, h = RET_T + 2*e, center = true); + } +} diff --git a/chassis/saltyrover_chassis_r2.scad b/chassis/saltyrover_chassis_r2.scad new file mode 100644 index 0000000..a857115 --- /dev/null +++ b/chassis/saltyrover_chassis_r2.scad @@ -0,0 +1,523 @@ +// ============================================================ +// saltyrover_chassis_r2.scad — SaltyRover 4-Wheel Chassis Rev 2 +// Issue: #109 Agent: sl-mechanical Date: 2026-03-01 +// ============================================================ +// +// Complete parametric chassis assembly for the SaltyRover 4-wheel +// rough-terrain variant. Designed to be printed (PETG), laser-cut +// (6 mm 5052-H32 Al), or CNC-routed. +// +// NEW vs Rev 1 (issue #73 / saltyrover_chassis.scad): +// • 4× trailing-arm spring-suspension corners +// • Enclosed electronics bay (rover_electronics_bay.scad) +// • M3-slot-adjustable pivot brackets (replaces fixed M5 flanges) +// • CSI camera corner brackets (4×, 45° outward tilt) +// • RPLIDAR tower stub on electronics bay lid +// • D435i front bracket arm +// • Weight target: <2 kg frame (excl. motors/electronics) +// +// Shared SaltyLab patterns (swappable electronics): +// FC : 30.5 × 30.5 mm M3 (MAMBA F722S / Pixhawk) +// Jetson: 58 × 49 mm M3 (Orin NX / Nano carrier board) +// Stem : Ø25 mm bore (sensor head unchanged) +// +// Coordinate convention (all modules): +// Z = 0 deck top face +// +Y forward +// +X right +// Ground at Z ≈ −(GND_CLR + BATT_PACK_H + BATT_FLOOR_T + DECK_T) +// +// RENDER options: +// "assembly" full 3D preview (default) +// "deck_2d" DXF — deck plate for waterjet / CNC +// "pivot_bracket_2d" DXF — pivot bracket for CNC / laser (×4) +// "pivot_bracket_stl" STL — pivot bracket (print 4×) +// "csi_mount_stl" STL — CSI corner bracket (print 4×) +// "d435i_mount_stl" STL — D435i front bracket (print 1×) +// +// ── Export commands ───────────────────────────────────────── +// Deck DXF: +// openscad saltyrover_chassis_r2.scad -D 'RENDER="deck_2d"' -o saltyrover_r2_deck.dxf +// Pivot bracket DXF (×4): +// openscad saltyrover_chassis_r2.scad -D 'RENDER="pivot_bracket_2d"' -o rover_pivot_bracket.dxf +// Pivot bracket STL (×4): +// openscad saltyrover_chassis_r2.scad -D 'RENDER="pivot_bracket_stl"' -o rover_pivot_bracket.stl +// CSI mount STL (×4): +// openscad saltyrover_chassis_r2.scad -D 'RENDER="csi_mount_stl"' -o rover_csi_mount.stl +// D435i mount STL (×1): +// openscad saltyrover_chassis_r2.scad -D 'RENDER="d435i_mount_stl"' -o rover_d435i_mount.stl +// ============================================================ + +$fn = 64; +e = 0.01; + +// ── Deck footprint ──────────────────────────────────────────────────────────── +ROVER_L = 500.0; // deck fore-aft (Y) +ROVER_W = 480.0; // deck left-right (X) +DECK_T = 6.0; // deck plate thickness (6 mm Al — weight-optimised) +DECK_R = 15.0; // corner fillet radius + +// ── Drive geometry ──────────────────────────────────────────────────────────── +// Hoverboard hub motors — caliper-verified (matches BOM.md / rover_motor_mount.scad) +TRACK_W = 540.0; // motor axle CL to CL, left-right (X) +AXLE_BASE = 340.0; // motor axle CL to CL, fore-aft (Y) +AXLE_H = 127.0; // axle CL above ground (10×2.125" tire, r=127mm) +AXLE_D = 16.11; // axle base section OD (caliper) +AXLE_FLAT = 13.00; // D-cut chord width (caliper) +BEARING_OD = 37.80; // bearing seat collar OD (caliper) + +// ── Height stack ───────────────────────────────────────────────────────────── +GND_CLR = 55.0; // min ground clearance (at static suspension sag) +BATT_FLOOR_T = 3.0; // battery tray floor thickness +BATT_PACK_H = 56.0; // battery pack height (420×88×56mm, laid flat) +DECK_BOT_H = GND_CLR + BATT_FLOOR_T + BATT_PACK_H; // 114 mm +DECK_TOP_H = DECK_BOT_H + DECK_T; // 120 mm +// Axle above deck top (in chassis SCAD coords = positive Z): +// AXLE_H - DECK_TOP_H = 127 - 120 = +7 mm ← axle is 7 mm above deck top + +// ── Battery packs (under-deck, laid flat) ──────────────────────────────────── +BATT_X_DIM = 420.0; // pack long side (left-right) +BATT_Y_DIM = 88.0; // pack fore-aft per pack +BATT_N = 2; // number of packs fore-aft (2 = 176 mm; 4 = 352 mm) +TRAY_MARGIN = 5.0; // opening margin each side + +// ── Stem socket (deck centre) ───────────────────────────────────────────────── +STEM_BORE = 25.5; // 25 mm tube + 0.5 mm FDM clearance +STEM_COLLAR_OD = 50.0; +STEM_COLLAR_H = 20.0; // raised boss height above deck top +STEM_FLANGE_BC = 40.0; // 4× M4 bolt circle for stem adapter + +// ── FC mount — MAMBA F722S / Pixhawk (30.5 × 30.5 mm M3) ──────────────────── +// Shared with SaltyLab — swappable electronics +FC_PITCH = 30.5; +FC_HOLE_D = 3.2; +FC_POS_Y = ROVER_L/2 - 65.0; // near front edge + +// ── Jetson Orin NX / Nano mount (58 × 49 mm M3) ────────────────────────────── +// Shared with SaltyLab — swappable electronics +ORIN_HOLE_X = 58.0; +ORIN_HOLE_Y = 49.0; +ORIN_HOLE_D = 3.2; +ORIN_POS_Y = -(ROVER_L/2 - 60.0); // near rear edge + +// ── Pivot bracket (motor corner mount, adjustable) ─────────────────────────── +// Each corner: one pivot bracket bolted to deck top at motor CL fore-aft. +// M3 slotted holes allow ±15 mm fore-aft and ±10 mm lateral adjustment. +PBK_L = 80.0; // bracket plate length (fore-aft / Y) +PBK_W = 55.0; // bracket plate width (lateral / X from deck edge) +PBK_T = 8.0; // bracket plate thickness +PBK_FLANGE_H = 20.0; // vertical flange height below deck bottom face + +// M3 adjustment slots (4× per bracket on the deck-top flange) +ADJ_SLOT_L = 25.0; // slot length (allows ±12 mm adjustment) +ADJ_M3_D = 3.3; // M3 clearance +ADJ_INSET_X = 12.0; // slot CL from lateral edge of bracket +ADJ_INSET_Y = 16.0; // slot CL from fore/aft ends + +// Pivot pin (M8 through-bolt; arm swings around this) +PIV_D = 8.5; // M8 clearance bore +PIV_POS_X = ROVER_W/2 + 5.0; // pivot CL from deck centre (just at edge) +// Pivot fore-aft at each motor corner (±AXLE_BASE/2) + +// Spring guide boss on bracket underside +SPG_GUIDE_OD = 14.0; // spring guide boss OD (spring slides over this) +SPG_GUIDE_H = 15.0; // guide boss height below bracket bottom + +// ── CSI camera corner brackets ──────────────────────────────────────────────── +CSI_PCB = 25.0; // IMX219 / CSI module PCB width (square) +CSI_M2_SPC = 15.0; // M2 hole pitch (±7.5 mm from centre) +CSI_TILT = 20.0; // downward tilt (degrees) for terrain view +CSI_ANGLE = 45.0; // outward rotation at each corner + +// ── D435i front bracket ─────────────────────────────────────────────────────── +RS_TILT = 8.0; // nose-down tilt (degrees) +RS_ARM_LEN = 65.0; // arm length from deck front edge to camera CL +RS_BASE_W = 40.0; // base width (left-right) + +// ── Fasteners ───────────────────────────────────────────────────────────────── +M2_D = 2.3; +M3_D = 3.3; +M4_D = 4.3; +M5_D = 5.3; +M8_D = 8.5; + +// ============================================================ +// RENDER DISPATCH +// ============================================================ +RENDER = "assembly"; + +if (RENDER == "assembly") { + assembly(); +} else if (RENDER == "deck_2d") { + projection(cut = true) + translate([0, 0, -DECK_T / 2]) + deck_plate(); +} else if (RENDER == "pivot_bracket_2d") { + projection(cut = true) + translate([0, 0, -PBK_T / 2]) + pivot_bracket_flat(); +} else if (RENDER == "pivot_bracket_stl") { + pivot_bracket(); +} else if (RENDER == "csi_mount_stl") { + csi_corner_bracket(); +} else if (RENDER == "d435i_mount_stl") { + d435i_front_bracket(); +} + +// ============================================================ +// FULL ASSEMBLY +// ============================================================ +module assembly() { + color("Silver", 0.90) deck_plate(); + color("DimGray", 0.85) stem_collar(); + + // 4× pivot brackets at motor corners + for (sx = [-1, 1]) + for (sy = [-1, 1]) + color("SteelBlue", 0.85) + translate([sx * TRACK_W/2, sy * AXLE_BASE/2, 0]) + rotate([0, 0, sx > 0 ? 0 : 180]) + pivot_bracket(); + + // 4× CSI corner brackets + for (sx = [-1, 1]) + for (sy = [-1, 1]) + color("Teal", 0.85) + csi_bracket_placed(sx, sy); + + // D435i front bracket + color("DarkSlateGray", 0.85) + d435i_bracket_placed(); + + // Electronics bay reference ghost (from rover_electronics_bay.scad) + %color("OliveDrab", 0.30) + translate([0, 0, DECK_T + 0.5]) + cube([240, 200, 80], center = true); + + // Ghost motor axle positions + for (sx = [-1, 1]) + for (sy = [-1, 1]) + %color("Tomato", 0.25) + translate([sx * TRACK_W/2, sy * AXLE_BASE/2, + AXLE_H - DECK_TOP_H]) + rotate([90, 0, 0]) + cylinder(d = AXLE_D, h = 80, center = true); + + // Ghost tyre outlines + for (sx = [-1, 1]) + for (sy = [-1, 1]) + %color("Black", 0.15) + translate([sx * TRACK_W/2, sy * AXLE_BASE/2, + -(DECK_TOP_H - AXLE_H)]) + rotate([90, 0, 0]) + cylinder(d = 254, h = 55, center = true); +} + +// ============================================================ +// DECK PLATE (Part A — laser-cut 6 mm 5052-H32 aluminium) +// ============================================================ +// Weight estimate: 480×500×6 mm Al, ~50% lightened ≈ 1.35 kg +module deck_plate() { + difference() { + // ── Outer profile — rounded rectangle ───────────────────────── + linear_extrude(DECK_T) + minkowski() { + square([ROVER_L - 2*DECK_R, ROVER_W - 2*DECK_R], center = true); + circle(r = DECK_R); + } + + // ── Battery tray opening (under-deck, centred) ───────────────── + batt_open_x = BATT_X_DIM + 2*TRAY_MARGIN; + batt_open_y = BATT_Y_DIM * BATT_N + 2*TRAY_MARGIN; + translate([0, 0, -e]) + cube([batt_open_x, batt_open_y, DECK_T + 2*e], center = true); + + // ── Stem bore ───────────────────────────────────────────────── + translate([0, 0, -e]) + cylinder(d = STEM_BORE, h = DECK_T + 2*e); + + // ── Stem collar bolt circle (4× M4 at 90°) ──────────────────── + for (a = [0, 90, 180, 270]) + rotate([0, 0, a]) + translate([STEM_FLANGE_BC/2, 0, -e]) + cylinder(d = M4_D, h = DECK_T + 2*e); + + // ── FC mount holes — 30.5×30.5 M3 (shared SaltyLab pattern) ── + for (dx = [-FC_PITCH/2, FC_PITCH/2]) + for (dy = [-FC_PITCH/2, FC_PITCH/2]) + translate([dx, FC_POS_Y + dy, -e]) + cylinder(d = FC_HOLE_D, h = DECK_T + 2*e); + + // ── Jetson Orin mount holes — 58×49 M3 ─────────────────────── + for (dx = [-ORIN_HOLE_X/2, ORIN_HOLE_X/2]) + for (dy = [-ORIN_HOLE_Y/2, ORIN_HOLE_Y/2]) + translate([dx, ORIN_POS_Y + dy, -e]) + cylinder(d = ORIN_HOLE_D, h = DECK_T + 2*e); + + // ── Pivot bracket M3 attachment slots (4× corners) ──────────── + // Two slotted holes per corner at the deck attachment flange + for (sx = [-1, 1]) + for (sy = [-1, 1]) { + bx = sx * (ROVER_W/2 - ADJ_INSET_X); + by = sy * AXLE_BASE/2; + for (offset = [-ADJ_INSET_Y, ADJ_INSET_Y]) + hull() { + translate([bx, by + offset - ADJ_SLOT_L/2, -e]) + cylinder(d = ADJ_M3_D, h = DECK_T + 2*e); + translate([bx, by + offset + ADJ_SLOT_L/2, -e]) + cylinder(d = ADJ_M3_D, h = DECK_T + 2*e); + } + } + + // ── Lightening holes — 55 mm dia, in structural corridors ───── + // Row between battery opening and pivot brackets + for (sx = [-1, 1]) + for (sy = [-1, 1]) { + lx = sx * (ROVER_W/4 + 20); + ly = sy * (ROVER_L/4 + 15); + translate([lx, ly, -e]) + cylinder(d = 55, h = DECK_T + 2*e); + } + // Additional pair flanking stem + for (sx = [-1, 1]) + translate([sx * 65, 0, -e]) + cylinder(d = 40, h = DECK_T + 2*e); + + // ── Cable routing slots (4× around electronics bay footprint) ── + for (sy = [-1, 1]) + hull() { + translate([-20, sy * 105, -e]) cylinder(d = 14, h = DECK_T+2*e); + translate([ 20, sy * 105, -e]) cylinder(d = 14, h = DECK_T+2*e); + } + for (sx = [-1, 1]) + hull() { + translate([sx * 125, -18, -e]) cylinder(d = 14, h = DECK_T+2*e); + translate([sx * 125, 18, -e]) cylinder(d = 14, h = DECK_T+2*e); + } + + // ── Motor phase cable pass-throughs at each corner ──────────── + for (sx = [-1, 1]) + for (sy = [-1, 1]) + translate([sx * (ROVER_W/2 - 25), + sy * (ROVER_L/2 - 25), -e]) + cylinder(d = 18, h = DECK_T + 2*e); + } +} + +// ── Deck-top stem collar (raised boss, 25 mm bore) ───────────────────────── +module stem_collar() { + translate([0, 0, DECK_T]) + difference() { + cylinder(d = STEM_COLLAR_OD, h = STEM_COLLAR_H); + // Bore + translate([0, 0, -e]) + cylinder(d = STEM_BORE, h = STEM_COLLAR_H + 2*e); + // Flange bolt holes + for (a = [0, 90, 180, 270]) + rotate([0, 0, a]) + translate([STEM_FLANGE_BC/2, 0, -e]) + cylinder(d = M4_D, h = STEM_COLLAR_H + 2*e); + } +} + +// ============================================================ +// PIVOT BRACKET (Part B — M3-adjustable motor corner mount) +// ============================================================ +// One per corner (×4). Mounts to deck top face via 2× M3 SHCS +// through slotted deck holes (allows ±12 mm fore/aft adjustment). +// Provides: +// • M8 pivot pin bore for suspension trailing arm +// • Spring upper seat (captured spring guide boss, 14 mm OD) +// • Vertical flange to deck edge for lateral stiffness +// +// Print: PETG 5 perims 60% infill, flat base down. +// Alt : CNC 8 mm 6061-T6 Al from pivot_bracket_2d DXF. +// +// Coordinate: bracket centred at motor corner (sx*TRACK_W/2, sy*AXLE_BASE/2) +// The deck-edge flange is on the -X side (inner face toward deck centre). +// ============================================================ +module pivot_bracket() { + // Deck-top flat base plate + translate([-PBK_W/2, -PBK_L/2, 0]) + difference() { + cube([PBK_W, PBK_L, PBK_T]); + + // 2× M3 adjustment slots (fore-aft direction) + for (s = [-1, 1]) + hull() { + translate([ADJ_INSET_X, + PBK_L/2 + s*ADJ_INSET_Y - ADJ_SLOT_L/2, + -e]) + cylinder(d = ADJ_M3_D, h = PBK_T + 2*e); + translate([ADJ_INSET_X, + PBK_L/2 + s*ADJ_INSET_Y + ADJ_SLOT_L/2, + -e]) + cylinder(d = ADJ_M3_D, h = PBK_T + 2*e); + } + + // Lightening slot (centre of bracket base) + translate([ADJ_INSET_X + 8, PBK_L/2 - 18, -e]) + cube([PBK_W - ADJ_INSET_X - 14, 36, PBK_T + 2*e]); + } + + // Outer vertical flange (at +X edge — outboard side, toward motor) + // This flange drops below the deck to form the suspension pivot clevis. + translate([PBK_W/2 - PBK_T, -PBK_L/2, -(PBK_FLANGE_H)]) + difference() { + cube([PBK_T, PBK_L, PBK_FLANGE_H + PBK_T]); + + // M8 pivot pin bore — at mid-height of flange, centred fore-aft + // The trailing arm will pivot on an M8 bolt through this hole. + pivot_z = PBK_FLANGE_H / 2; + translate([-e, PBK_L/2, pivot_z]) + rotate([0, 90, 0]) + cylinder(d = PIV_D, h = PBK_T + 2*e); + } + + // Spring upper seat boss (below bracket base, outboard side) + // Compression spring (Ø14 OD) slides over this guide boss. + // When arm swings up (bump), spring is compressed between this boss + // and the matching pocket in the trailing arm. + translate([PBK_W/2 - PBK_T/2, 0, -e]) + cylinder(d = SPG_GUIDE_OD, h = SPG_GUIDE_H + e); +} + +// Flat (2D projection source) version of pivot bracket — same profile +module pivot_bracket_flat() { + difference() { + cube([PBK_W, PBK_L, PBK_T], center = true); + // M3 slots + for (s = [-1, 1]) + hull() { + translate([0, s*ADJ_INSET_Y - ADJ_SLOT_L/2, 0]) + cylinder(d = ADJ_M3_D, h = PBK_T + 2*e, center = true); + translate([0, s*ADJ_INSET_Y + ADJ_SLOT_L/2, 0]) + cylinder(d = ADJ_M3_D, h = PBK_T + 2*e, center = true); + } + // M8 pivot bore + cylinder(d = PIV_D, h = PBK_T + 2*e, center = true); + } +} + +// ── Place pivot brackets at correct corners ─────────────────────────────────── +// Called from assembly() with (sx, sy) = (±1, ±1) + +// ============================================================ +// CSI CAMERA CORNER BRACKET (Part C — 4× corners) +// ============================================================ +// Mounts an IMX219 / Arducam CSI module at each deck corner, +// angled 45° outward and CSI_TILT° downward for terrain coverage. +// 2× M2 bolts hold camera PCB (15 mm square hole pattern). +// 2× M3 bolts mount bracket to deck top. +// +// Print: PETG 4 perims 30% infill, flat base down. +// ============================================================ +module csi_corner_bracket() { + base_l = 40; + base_w = 30; + base_t = 5; + arm_l = 30; + + difference() { + union() { + // Deck-top base plate + cube([base_l, base_w, base_t]); + + // Angled arm + camera face plate + translate([base_l / 2, base_w / 2, base_t]) + rotate([0, CSI_TILT, 0]) + translate([-CSI_PCB/2, -CSI_PCB/2, 0]) + cube([CSI_PCB + 6, CSI_PCB + 6, base_t]); + } + + // 2× M3 base attachment holes + for (dx = [8, base_l - 8]) + translate([dx, base_w / 2, -e]) + cylinder(d = M3_D, h = base_t + 2*e); + + // CSI camera M2 mounting holes (15 × 15 mm pattern) + translate([base_l / 2, base_w / 2, base_t]) + rotate([0, CSI_TILT, 0]) + for (cx = [-CSI_M2_SPC/2, CSI_M2_SPC/2]) + for (cy = [-CSI_M2_SPC/2, CSI_M2_SPC/2]) + translate([cx, cy, -e]) + cylinder(d = M2_D, h = base_t + 2*e); + + // CSI ribbon cable slot (3 mm wide, 12 mm long, centred) + translate([base_l/2 - 6, base_w/2 - 1.5, -e]) + cube([12, 3, base_t + 2*e]); + } +} + +module csi_bracket_placed(sx, sy) { + // Corner position + cx = sx * (ROVER_W/2 - 25); + cy = sy * (ROVER_L/2 - 25); + // Rotate so camera faces outward from corner + rot = atan2(sy, sx) * 180 / 3.14159 - 45; + translate([cx, cy, DECK_T]) + rotate([0, 0, rot]) + translate([-20, -15, 0]) + csi_corner_bracket(); +} + +// ============================================================ +// D435i FRONT BRACKET (Part D — 1× front mount) +// ============================================================ +// Arm extends forward from deck front edge. +// Camera face tilted RS_TILT° nose-down. +// 1/4-20 UNC captured hex nut for D435i tripod socket. +// 2× M4 bolts mount base to deck front face. +// +// Print: PETG 5 perims 40% infill, arm flat on bed. +// ============================================================ +module d435i_front_bracket() { + base_d = 22; // base depth (Y direction, into deck) + base_h = 8; // base/arm thickness + arm_len = RS_ARM_LEN; + + // 1/4-20 UNC geometry + nut14_af = 11.1; // across-flats + nut14_h = 5.6; // nut thickness + nut14_cl = 6.5; // bolt clearance bore + + difference() { + union() { + // Rear base plate (bolts to deck front face) + translate([-RS_BASE_W/2, 0, 0]) + cube([RS_BASE_W, base_d, base_h]); + + // Forward arm (+ direction is forward / +Y) + translate([-12, base_d, 0]) + cube([24, arm_len, base_h]); + + // Camera face plate (tilted RS_TILT° downward) + translate([0, base_d + arm_len, base_h / 2]) + rotate([0, RS_TILT, 0]) + translate([-15, 0, -base_h / 2]) + cube([30, 14, base_h]); + } + + // 2× M4 base attachment holes + for (dx = [-RS_BASE_W/2 + 10, RS_BASE_W/2 - 10]) + translate([dx, base_d / 2, -e]) + cylinder(d = M4_D, h = base_h + 2*e); + + // 1/4-20 captured nut pocket in face plate + translate([0, base_d + arm_len + 12, base_h / 2]) + rotate([0, 90, 0]) { + // Hex nut pocket (from back) + translate([0, 0, -nut14_h - 1]) + cylinder(d = nut14_af / cos(30), h = nut14_h + 1, $fn = 6); + // Camera bolt clearance bore + cylinder(d = nut14_cl, h = 20); + } + } +} + +module d435i_bracket_placed() { + // Mount to deck front edge, centred left-right, at deck level + translate([0, ROVER_L/2 + 10, DECK_T]) + rotate([0, 0, 180]) + d435i_front_bracket(); +} -- 2.47.2