Merge pull request 'feat: SaltyRover chassis Rev 2 — 4-wheel rover with spring suspension (#109)' (#116) from sl-mechanical/issue-109-rover-chassis into main

This commit is contained in:
sl-jetson 2026-03-02 09:03:12 -05:00
commit 5bb1ec6d3e
4 changed files with 1523 additions and 0 deletions

View File

@ -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 (M2M8) | 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 &
```

View File

@ -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]);
}
}

View File

@ -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);
}
}

View File

@ -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();
}