Merge pull request 'feat: SaltyRover 4-wheel chassis (#73)' (#79) from sl-mechanical/rover-chassis into saltyrover-dev

This commit is contained in:
seb 2026-03-01 01:29:34 -05:00
commit 0d4599e12e
5 changed files with 1143 additions and 0 deletions

228
chassis/rover_BOM.md Normal file
View File

@ -0,0 +1,228 @@
# SaltyRover Chassis — BOM + Assembly Notes
**Rev A — 2026-03-01 — sl-mechanical**
**Issue: #73**
---
## Overview
SaltyRover is the stable 4-wheel variant of SaltyLab.
Low CG, wide track — designed not to tip over.
Reuses the 25 mm vertical stem, sensor head, RPLIDAR, RealSense,
IMX219 camera mounts, and roll cage from SaltyLab without modification.
```
Top view (schematic):
Forward (+Y)
◉─────────────────────◉ ← front axle (AXLE_BASE/2 = 170 mm forward)
│ [Orin NX] │
│ [battery] │ ← flat packs below deck (low CG)
│ [battery] │
│ [FC] │
◉─────────────────────◉ ← rear axle
stem (centre)
Dimensions (approx):
Robot width (tyre-to-tyre): ~810 mm (TRACK_W=540 + 2×TIRE_W/2)
Robot length (axle-to-axle): ~340 mm (AXLE_BASE)
Deck width: 480 mm
Deck length: 500 mm
Ground clearance: ~50 mm (deck bottom to ground)
Overall height (with 550 mm stem + sensor head): ~800 mm
```
---
## Height Stack
```
Z from ground:
Z = 0 mm — ground
Z = 50 mm — chassis bottom (battery tray floor)
Z = 109 mm — deck bottom face
Z = 117 mm — deck top face (Z=0 in SCAD coords)
Z = 127 mm — motor axle CL (+10 mm above deck top)
Z = 139 mm — deck stem collar top (117+22)
Z = 145 mm — stem adapter flange top (139+6)
Z = 173 mm — stem clamp top (145+28)
Z = 723 mm — stem top with 550 mm rover stem
```
---
## File Index
| File | Description | RENDER |
|------|-------------|--------|
| `saltyrover_chassis.scad` | Main deck plate, stem collar, standoffs | `assembly` / `deck` / `deck_2d` |
| `rover_motor_mount.scad` | L-bracket + axle clamp plate, 4× | `assembly` / `bracket` / `clamp_plate` / `bracket_2d` |
| `rover_battery_tray.scad` | Slide-out battery tray, 24 packs | `assembly` / `tray` / `rail` / `latch` / `tray_2d` |
| `rover_stem_adapter.scad` | Flange + split clamp, locks stem to deck | `assembly` / `base_flange` / `clamp_front` / `clamp_rear` |
---
## Part A — Deck Plate (`saltyrover_chassis.scad`)
### Structural parts
| # | RENDER | Qty | Material | Process | Notes |
|---|--------|-----|----------|---------|-------|
| 1 | `deck_2d` | 1 | 8 mm 5052-H32 Al | Waterjet or CNC router | 480×500 mm blank |
| — | — | — | **or** 8 mm PETG FDM | Print in sections, bolted lap joints | Prototype only |
### Fasteners (deck to motor brackets)
| # | Spec | Qty | Use |
|---|------|-----|-----|
| 2 | M5×20 SHCS | 16 | Motor bracket flange to deck (4 per corner × 4) |
| 3 | M5 hex nut | 16 | Captured on deck underside |
| 4 | M4×16 SHCS | 4 | Stem adapter flange to deck collar |
| 5 | M4 flat washer | 4 | Under bolt head |
---
## Part B — Motor Brackets (`rover_motor_mount.scad`)
Print or CNC 4×. Each bracket handles one drive motor.
### Printed parts
| # | RENDER | Qty | Material | Settings | Notes |
|---|--------|-----|----------|----------|-------|
| 6 | `bracket` | 4 | PETG or PC | 5 perims, 60% infill | Print flange face down; add M5 inserts or use nuts |
| 7 | `clamp_plate` | 4 | PETG | 4 perims, 40% infill | Axle retention; print flat |
**CNC alternative:** `bracket_2d` → DXF, CNC route from 10 mm 6061-T6 Al plate.
### Fasteners
| # | Spec | Qty | Use |
|---|------|-----|-----|
| 8 | M4×20 SHCS | 8 | Axle clamp plate to bracket (2 per motor × 4) |
| 9 | M4 hex nut | 8 | Captured in bracket |
| 10 | Axle lock nut | 4 | Axle tip retention — **verify axle thread before ordering** (BOM.md: Ø≈10 mm tip) |
| 11 | M5×20 SHCS | 16 | Bracket flange to deck (from item 2 above) |
> ⚠ **Axle note:** BOM.md caliper values: base OD 16.11 mm, D-cut OD 15.95 mm, flat chord 13.00 mm.
> Verify AXLE_D / AXLE_FLAT / BEARING_OD in `rover_motor_mount.scad` before printing.
---
## Part C — Battery Tray (`rover_battery_tray.scad`)
### Printed parts
| # | RENDER | Qty | Material | Settings | Notes |
|---|--------|-----|----------|----------|-------|
| 12 | `tray` | 1 | PETG | 4 perims, 30% infill | Print open-top face up; no supports |
| 13 | `rail` | 2 | PETG | 5 perims, 40% infill | T-slot slide rail; print top-face down |
| 14 | `latch` | 1 | PETG or TPU 95A | 4 perims, 30% infill | Spring retention clip; TPU gives better snap feel |
### Fasteners
| # | Spec | Qty | Use |
|---|------|-----|-----|
| 15 | M4×12 FHCS | 6 | Slide rail to deck underside (3 per rail, countersunk) |
| 16 | M4 T-nut or nyloc | 6 | Captured under deck for rail bolts |
| 17 | M3×10 SHCS | 2 | Spring latch to tray end |
| 18 | Velcro strap 25 mm × 500 mm | 48 | Battery pack retention (12 per pack through tray floor slots) |
### Batteries
| # | Part | Qty | Spec |
|---|------|-----|------|
| 19 | Battery pack | 24 | 24 V, **420×88×56 mm** (BOM.md caliper-verified) |
| 20 | BMS board | 1 | Matched to cell chemistry; mount to deck underside near Y+ wall |
---
## Part D — Stem Adapter (`rover_stem_adapter.scad`)
| # | RENDER | Qty | Material | Settings | Notes |
|---|--------|-----|----------|----------|-------|
| 21 | `base_flange` | 1 | PETG | 5 perims, 40% infill | Print flat; add M4 brass inserts or use through-bolts |
| 22 | `clamp_front` | 1 | PETG | 5 perims, 40% infill | Flat-face down |
| 23 | `clamp_rear` | 1 | PETG | 5 perims, 40% infill | Mirror; flat-face down |
### Fasteners
| # | Spec | Qty | Use |
|---|------|-----|-----|
| 24 | M4×20 SHCS | 2 | Stem clamp clamping bolts |
| 25 | M4 hex nut | 2 | Captured in clamp rear half |
| 26 | M4×8 set screw | 1 | Rotation lock (front half) |
| 27 | M4×16 FHCS | 4 | Flange to deck collar (countersunk) |
### Stem tube
| # | Part | Qty | Spec | Notes |
|---|------|-----|------|-------|
| 28 | Vertical stem | 1 | 25 mm OD × 1.5 mm wall 6061-T6 Al, **550 mm length** | Cut from 1 m stock; SaltyLab uses 1000 mm |
> Sensor head, RPLIDAR, cameras, and roll cage from SaltyLab mount
> directly onto this 25 mm stem — no modifications required.
---
## Motors
Same hoverboard hub motors as SaltyLab. **4× required for rover.**
| # | Part | Qty | Spec |
|---|------|-----|------|
| 29 | Hub motor | 4 | 10×2.125" tire, 36V nominal, ~350W; axle OD 16.11mm (caliper) |
| 30 | Motor cable extension | 4 | 6-pin JST-PH 300 mm hall sensor + 3-phase power |
---
## Mass Estimate
| Assembly | Est. mass |
|----------|-----------|
| Al deck plate | ~1.4 kg |
| Motor brackets × 4 (PETG) | ~0.5 kg |
| Hub motors × 4 | ~7.2 kg |
| Battery packs × 2 | ~1.4 kg |
| Battery tray (PETG) | ~0.3 kg |
| Stem + sensor head (from SaltyLab) | ~1.5 kg |
| Electronics (FC + Orin + wiring) | ~0.6 kg |
| Stem adapter + hardware | ~0.2 kg |
| Fasteners | ~0.3 kg |
| **Total estimate** | **~13.4 kg** |
> Wide 540 mm track + low 117 mm deck height → tip-over angle > 45°.
> CG is at approximately Z = 200 mm with packs flat and full payload.
---
## Assembly Sequence
1. **Deck plate:** Cut or print `deck_2d`. Waterjet preferred for Al.
2. **Motor brackets:** Print (or CNC) 4× `bracket` + 4× `clamp_plate`.
- Test-fit axle D-cut bore before assembling to deck.
3. **Slide rails:** Print 2× `rail`. Bolt to deck underside.
4. **Stem adapter:** Print `base_flange` + `clamp_front` + `clamp_rear`.
- Insert stem through deck collar bore; seat flange on collar top.
- Bolt flange with 4× M4×16 FHCS.
- Slide clamp halves on stem above flange; tighten M4 clamping bolts (1.5 N·m).
- Tighten M4 set screw.
5. **Motor brackets:** Slide flange into deck edge slot; 4× M5×20 bolts.
- Do not torque until axle alignment is confirmed.
6. **Motors:** Drop axle into bracket dropout slot. Install clamp plate.
- Tighten clamp M4 bolts. Axle lock nut finger-tight + ¼ turn.
7. **Battery tray:** Slide tray in from +X with packs loaded.
- Latch snaps at full insertion.
8. **Sensor head + stem accessories:** Install from SaltyLab as-is.
- Recommend 550 mm stem for rover (sensor head at ~720 mm, stable).
---
## Tools Required
- M3/M4/M5 hex drivers
- Torque wrench: M5 structural bolts 4 N·m, M4 clamp bolts 1.5 N·m
- Thread locker (Loctite 243 blue) on axle lock nuts and stem clamp set screw
- Dial caliper — verify axle dimensions before bracket print/CNC

View File

@ -0,0 +1,255 @@
// ============================================================
// rover_battery_tray.scad SaltyRover Flat Battery Tray
// Rev A 2026-03-01 sl-mechanical
// ============================================================
// Slide-out tray for 24 × 420×88×56 mm battery packs
// laid FLAT below the rover deck plate.
//
// Packs orient with 420 mm left-right (X), 88 mm fore-aft
// per pack (Y), 56 mm tall (Z). BATT_N packs stack fore-aft.
//
// Tray slides out to the RIGHT (+X) for battery swap.
// Two slide rails (T-slot profile) run fore-aft inside
// the deck opening. Tray body rides on these rails.
// A spring-clip latch on the +X end retains the tray.
//
// Base plate attachment:
// Rail mounts bolt to deck underside (4× M4 per rail,
// counter-sunk flat-head).
//
// VERIFY:
// BATT_PACK_L / W / H from caliper.
// RAIL_SLOT_W / H from your chosen extrusion or print.
//
// RENDER options:
// "assembly" tray + phantom packs (default)
// "tray" tray body for printing
// "rail" one T-slot slide rail (print 2×)
// "latch" spring latch for tray end (print 1×)
// "tray_2d" floor projection DXF
// ============================================================
RENDER = "assembly";
// Battery packs (BOM.md caliper-verified)
BATT_PACK_L = 420.0; // left-right (X)
BATT_PACK_W = 88.0; // fore-aft per pack (Y)
BATT_PACK_H = 56.0; // height when flat (Z)
BATT_N = 2; // packs arranged fore-aft (2 or 4)
// Tray geometry
TRAY_WALL = 2.5; // wall thickness
TRAY_FLOOR = 3.0; // floor thickness
TRAY_PAD = 2.0; // per-side clearance inside tray (pack-to-wall)
// Inner cavity
TRAY_INN_W = BATT_PACK_L + 2*TRAY_PAD;
TRAY_INN_D = BATT_PACK_W * BATT_N + 2*TRAY_PAD;
TRAY_INN_H = BATT_PACK_H + TRAY_PAD; // open top
// Outer tray body
TRAY_OUT_W = TRAY_INN_W + 2*TRAY_WALL;
TRAY_OUT_D = TRAY_INN_D + 2*TRAY_WALL;
TRAY_OUT_H = TRAY_INN_H + TRAY_FLOOR;
// Slide rail
// T-profile rail runs fore-aft (Y), mounts to deck underside.
// Tray has matching T-slots on its side walls.
RAIL_L = TRAY_OUT_D + 40.0; // rail longer than tray for stop
RAIL_W = 14.0; // rail body width
RAIL_H = 8.0; // rail height below deck
RAIL_T_W = 10.0; // T-slot head width on tray side
RAIL_T_H = 4.0; // T-slot head height (captured in tray slot)
RAIL_CL = 0.3; // running clearance
RAIL_M4_SPC = 80.0; // M4 deck attachment bolt spacing along rail
// Tray T-slot channel (cut into tray outer wall)
SLOT_W = RAIL_T_W + 2*RAIL_CL;
SLOT_H = RAIL_T_H + RAIL_CL;
// Rail Y-spacing: rails at ±RAIL_Y from tray centre (fore-aft)
RAIL_Y_SPC = TRAY_INN_W - 20.0; // rails near X edges of tray
// Note: rails run in Y, so RAIL_Y_SPC is the X spread between them
// Strap slots
STRAP_W = 20.0; // Velcro strap width
STRAP_T = 3.0; // slot depth
// Latch
LATCH_L = 40.0; // latch body length
LATCH_W = 20.0;
LATCH_T = 4.0; // plate thickness
LATCH_FLEX = 25.0; // spring arm length
LATCH_TAB_H = 5.0; // retention tab height (hooks over deck edge)
LATCH_BOLT_D= 3.3; // M3 attachment bolt
// Fasteners
M3_D = 3.2;
M4_D = 4.3;
M4_CS_D = 7.0; // flat-head countersink diam
$fn = 48;
e = 0.01;
//
// battery_tray()
// Z=0 at tray bottom face. Slides in +X direction.
// Inner cavity origin at (TRAY_WALL, TRAY_WALL, TRAY_FLOOR).
//
module battery_tray() {
difference() {
union() {
// Outer tray body
cube([TRAY_OUT_W, TRAY_OUT_D, TRAY_OUT_H]);
// Pull tab on +X face
translate([TRAY_OUT_W, TRAY_OUT_D/2 - 15, TRAY_FLOOR])
cube([18, 30, TRAY_OUT_H - TRAY_FLOOR]);
}
// Inner cavity (open top)
translate([TRAY_WALL, TRAY_WALL, TRAY_FLOOR])
cube([TRAY_INN_W, TRAY_INN_D, TRAY_INN_H + e]);
// Pack divider slot (for multi-pack separation)
if (BATT_N > 1)
for (pn = [1 : BATT_N - 1])
translate([TRAY_WALL - e,
TRAY_WALL + BATT_PACK_W * pn - 1,
TRAY_FLOOR])
cube([TRAY_INN_W + 2*e, 2, TRAY_INN_H + e]);
// Strap slots through floor (×2 per pack)
for (pn = [0 : BATT_N - 1])
for (sx = [TRAY_WALL + TRAY_INN_W*0.25,
TRAY_WALL + TRAY_INN_W*0.75])
translate([sx - STRAP_W/2,
TRAY_WALL + BATT_PACK_W * pn + 10,
-e])
cube([STRAP_W, BATT_PACK_W - 20, TRAY_FLOOR + 2*e]);
// T-slot channels on left (+Y) and right (-Y) faces
// Tray left wall: Y+ face rail rides in from -X end
for (face_y=[TRAY_OUT_D - TRAY_WALL - e, -e]) {
translate([TRAY_OUT_W*0.2 - SLOT_W/2, face_y,
TRAY_FLOOR + TRAY_INN_H/2 - SLOT_H/2])
cube([TRAY_OUT_W*0.6, TRAY_WALL + 2*e, SLOT_H]);
// T-head channel
translate([TRAY_OUT_W*0.2 - RAIL_T_W/2 - RAIL_CL,
face_y,
TRAY_FLOOR + TRAY_INN_H/2 - SLOT_H/2 - RAIL_CL])
cube([RAIL_T_W + 2*RAIL_CL,
TRAY_WALL + 2*e,
SLOT_H + 2*RAIL_CL]);
}
// Ventilation / weight-save holes in floor (×4)
for (vx=[TRAY_OUT_W*0.25, TRAY_OUT_W*0.75])
for (vy=[TRAY_WALL + TRAY_INN_D*0.25,
TRAY_WALL + TRAY_INN_D*0.75])
translate([vx, vy, -e])
cylinder(d=25, h=TRAY_FLOOR + 2*e);
}
}
//
// slide_rail()
// Mounts to deck underside. Runs fore-aft (Y).
// T-head protrudes inward (X direction) into tray slot.
//
module slide_rail() {
difference() {
union() {
// Rail body
translate([-RAIL_W/2, 0, 0])
cube([RAIL_W, RAIL_L, RAIL_H]);
// T-head flange (protrudes toward tray)
translate([-RAIL_T_W/2, 0, RAIL_H - RAIL_T_H])
cube([RAIL_T_W, RAIL_L, RAIL_T_H]);
}
// M4 countersunk attachment holes to deck (×3 along rail)
for (ry = [15, RAIL_L/2, RAIL_L - 15])
translate([0, ry, RAIL_H + e])
rotate([180, 0, 0]) {
cylinder(d=M4_D, h=RAIL_H + 2*e);
cylinder(d1=M4_CS_D, d2=M4_D, h=3.5 + e);
}
// Rail end stop slot (tray cannot slide past rail end)
translate([-RAIL_W/2 - e, RAIL_L - 8, -e])
cube([RAIL_W + 2*e, 8 + e, RAIL_H - RAIL_T_H + e]);
}
}
//
// spring_latch()
// Clips to tray +X end face. Spring arm hooks over
// deck opening edge to retain tray. Squeeze tab to release.
//
module spring_latch() {
difference() {
union() {
// Base plate (bolts to tray end face)
cube([LATCH_W, LATCH_T, LATCH_L]);
// Spring arm
translate([LATCH_W/2 - 4, LATCH_T, LATCH_L - LATCH_FLEX])
cube([8, LATCH_T + LATCH_TAB_H, LATCH_FLEX + LATCH_TAB_H]);
// Retention tab
translate([LATCH_W/2 - 6, LATCH_T, LATCH_L - 2])
cube([12, LATCH_T + LATCH_TAB_H + 4, LATCH_TAB_H + 2]);
}
// M3 attachment holes (×2)
for (lz=[10, LATCH_L - 20])
translate([LATCH_W/2, -e, lz])
rotate([-90,0,0])
cylinder(d=LATCH_BOLT_D, h=LATCH_T + 2*e);
}
}
//
// Render selector
//
if (RENDER == "assembly") {
color("SteelBlue", 0.88) battery_tray();
// Phantom battery packs inside tray
for (pn = [0 : BATT_N - 1])
color("Gold", 0.35)
translate([TRAY_WALL + TRAY_PAD,
TRAY_WALL + TRAY_PAD + pn * BATT_PACK_W,
TRAY_FLOOR])
cube([BATT_PACK_L, BATT_PACK_W, BATT_PACK_H]);
// Slide rails
color("Silver", 0.80) {
translate([0, -20, TRAY_OUT_H])
rotate([0, 90, 0])
slide_rail();
translate([0, TRAY_OUT_D + 20 - RAIL_L, TRAY_OUT_H])
rotate([0, 90, 0])
slide_rail();
}
// Latch at +X end
color("OrangeRed", 0.85)
translate([TRAY_OUT_W + 18,
TRAY_OUT_D/2 - LATCH_W/2,
TRAY_OUT_H/2 - LATCH_L/2])
rotate([0, -90, 0])
spring_latch();
} else if (RENDER == "tray") {
battery_tray();
} else if (RENDER == "rail") {
slide_rail();
} else if (RENDER == "latch") {
spring_latch();
} else if (RENDER == "tray_2d") {
projection(cut=true)
translate([0, 0, -TRAY_FLOOR/2])
battery_tray();
}

View File

@ -0,0 +1,240 @@
// ============================================================
// rover_motor_mount.scad SaltyRover Drive Motor Brackets
// Rev A 2026-03-01 sl-mechanical
// ============================================================
// L-bracket that bolts to the deck edge and positions the drive
// motor axle at the correct height. Print or CNC 4×.
//
// Default values from BOM.md (caliper-verified hoverboard motor):
// Axle base OD: 16.11 mm
// D-cut OD: 15.95 mm (flat chord 13.00 mm)
// Bearing seat OD: 37.80 mm
// Axle CL above gnd: 127 mm
// Tire OD: 254 mm (10×2.125")
//
// Bracket geometry (all dims in mm):
// Vertical flange bolts to deck edge (2× M5 SHCS)
// Horizontal arm extends BRKT_REACH beyond deck edge
// to position axle CL at TRACK_W/2
// Axle channel open-bottom dropout slot for easy
// motor install/removal; retained by
// a separate clamping plate (×1 per bracket)
// Bearing recess 37.8 mm recess on inboard face
//
// VERIFY before printing / CNC:
// AXLE_D, AXLE_FLAT, BEARING_OD from caliper measurement.
// DECK_BOT_H = GND_CLR + BATT_FLOOR_T + BATT_PACK_H.
// Adjust BRKT_REACH so TRACK_W matches chassis layout.
//
// Print orientation: flange face flat on bed.
// Material: PETG or PC, 5 perims, 60% infill.
// Alternative: CNC-routed 10mm 6061-T6 aluminium plate.
//
// RENDER options:
// "assembly" bracket + clamp plate + phantom motor axle
// "bracket" main L-bracket for printing/CNC
// "clamp_plate" axle retention clamp plate (print 4×)
// "bracket_2d" bracket floor projection DXF
// ============================================================
RENDER = "assembly";
// Axle / motor (BOM.md caliper-verified)
AXLE_D = 16.11; // axle base section OD (round)
AXLE_FLAT = 13.00; // D-cut flat chord width
AXLE_D_DCUT = 15.95; // D-cut section OD
BEARING_OD = 37.80; // bearing seat collar OD
BEARING_D = BEARING_OD + 1.0; // bore clearance for bearing seat
BEARING_H = 8.0; // bearing recess depth on inboard face
AXLE_H = 127.0; // axle CL above ground
TIRE_OD = 254.0; // 10×2.125" tire outer diameter
// Dropout slot dims
DROP_W = AXLE_D + 2.0; // slot width (axle + 1 mm each side)
DROP_D = AXLE_H; // slot depth from bracket bottom (full height)
// Bracket geometry
// Deck geometry (must match saltyrover_chassis.scad)
GND_CLR = 50.0;
BATT_FLOOR_T = 3.0;
BATT_PACK_H = 56.0;
DECK_T = 8.0;
DECK_BOT_H = GND_CLR + BATT_FLOOR_T + BATT_PACK_H; // 109 mm
DECK_TOP_H = DECK_BOT_H + DECK_T; // 117 mm
// Axle height relative to deck top:
AXLE_ABOVE_DECK = AXLE_H - DECK_TOP_H; // ~10 mm
// Bracket reach beyond deck edge to motor axle CL
BRKT_REACH = 40.0; // motor CL is BRKT_REACH mm outside deck edge
// (TRACK_W/2 - ROVER_W/2 = 270 - 240 = 30 mm +
// bearing housing clearance margin = 40 mm)
// Bracket plate dimensions
BRKT_T = 10.0; // bracket plate thickness
BRKT_H_ABOVE = AXLE_ABOVE_DECK + BEARING_OD/2 + 8.0; // above deck top
BRKT_H_BELOW = DECK_T + 12.0; // below deck bottom
BRKT_TOTAL_H = BRKT_H_ABOVE + BRKT_H_BELOW; // full height
// Width of bracket arm (fore-aft direction)
BRKT_W = 60.0; // covers motor fore-aft attachment bolts
// Flange for deck attachment
FLANGE_T = 8.0; // flange plate thickness
FLANGE_DEPTH = 20.0; // how deep flange sits on deck face (Y direction)
// M5 bolt holes: 2× through flange (deck attachment)
M5_D = 5.3;
MOT_BOLT_SPC = 30.0; // matches saltyrover_chassis.scad MOT_BOLT_SPC
// Clamping plate (retains axle in dropout slot)
CLAMP_T = 6.0; // clamp plate thickness
CLAMP_W = DROP_W + 12.0;
CLAMP_H = BEARING_D + 12.0;
CLAMP_BOLT_D= 4.3; // M4 clearance
CLAMP_BOLT_SPC = CLAMP_W - 10.0;
// Gusset (triangular fillet between flange and arm)
GUSSET_T = 8.0;
$fn = 64;
e = 0.01;
//
// motor_bracket()
// Main L-bracket. Coordinate: Z=0 at deck top.
// Bracket outboard face at X=0 (deck edge).
// Bracket arm extends +X toward motor.
//
module motor_bracket() {
axle_z = AXLE_ABOVE_DECK; // axle CL in bracket coords
difference() {
union() {
// Vertical arm (extends +X from deck edge)
translate([0, -BRKT_W/2, -BRKT_H_BELOW])
cube([BRKT_REACH + BRKT_T, BRKT_W, BRKT_TOTAL_H]);
// Vertical flange (sits against deck edge, -X side)
translate([-FLANGE_DEPTH, -BRKT_W/2, -BRKT_H_BELOW])
cube([FLANGE_DEPTH, BRKT_W, BRKT_TOTAL_H]);
// Gusset (arm to flange transition)
translate([0, -BRKT_W/2, axle_z - GUSSET_T/2])
linear_extrude(GUSSET_T)
polygon([
[0, 0],
[BRKT_REACH/2, 0],
[0, BRKT_W]
]);
}
// Axle dropout slot (open at bottom, centered on arm tip)
translate([BRKT_REACH, -DROP_W/2, -BRKT_H_BELOW - e])
cube([BRKT_T + e, DROP_W, AXLE_H + 2*e]);
// Axle round bore at slot top
translate([BRKT_REACH, 0, axle_z])
rotate([0, 90, 0])
cylinder(d=AXLE_D + 1.0, h=BRKT_T + 2*e);
// D-cut anti-rotation flat (matches axle flat chord)
translate([BRKT_REACH - e, -AXLE_FLAT/2, axle_z - AXLE_D/2 - e])
cube([BRKT_T + 2*e, AXLE_FLAT, AXLE_D/2 + e]);
// Bearing seat recess (inboard face, X=BRKT_T)
translate([BRKT_REACH + BRKT_T - BEARING_H, 0, axle_z])
rotate([0, 90, 0])
cylinder(d=BEARING_D, h=BEARING_H + e);
// Clamping plate bolt holes (×2, retain axle)
for (dz=[-CLAMP_BOLT_SPC/2, CLAMP_BOLT_SPC/2])
translate([BRKT_REACH - e, 0, axle_z + dz])
rotate([0, 90, 0])
cylinder(d=CLAMP_BOLT_D, h=BRKT_T + 2*e);
// Deck attachment holes through flange (M5 × 2)
for (bz=[-MOT_BOLT_SPC/2, MOT_BOLT_SPC/2])
translate([-FLANGE_DEPTH - e, 0, bz])
rotate([0, 90, 0])
cylinder(d=M5_D, h=FLANGE_DEPTH + 2*e);
// Lightening pocket on arm (inboard face)
translate([BRKT_T, -BRKT_W/2 + 8, -BRKT_H_BELOW/2])
cube([BRKT_REACH - BRKT_T - 8,
BRKT_W - 16,
BRKT_H_BELOW/2 - 4]);
}
}
//
// axle_clamp_plate()
// Bolts to bracket outboard face to close dropout slot.
// Print flat face down. 1× per bracket.
//
module axle_clamp_plate() {
axle_z = AXLE_ABOVE_DECK;
difference() {
translate([-CLAMP_W/2, 0, axle_z - CLAMP_H/2])
cube([CLAMP_W, CLAMP_T, CLAMP_H]);
// Axle bore
translate([0, -e, axle_z])
rotate([-90, 0, 0])
cylinder(d=AXLE_D + 0.5, h=CLAMP_T + 2*e);
// Bearing seat relief
translate([0, CLAMP_T - BEARING_H + e, axle_z])
rotate([-90, 0, 0])
cylinder(d=BEARING_D, h=BEARING_H + e);
// M4 bolt holes (×2)
for (dz=[-CLAMP_BOLT_SPC/2, CLAMP_BOLT_SPC/2])
translate([0, -e, axle_z + dz])
rotate([-90, 0, 0])
cylinder(d=CLAMP_BOLT_D, h=CLAMP_T + 2*e);
}
}
//
// Render selector
//
if (RENDER == "assembly") {
color("DimGray", 0.90) motor_bracket();
color("SteelBlue", 0.85)
translate([BRKT_REACH + BRKT_T, 0, 0])
axle_clamp_plate();
// Phantom axle stub
color("Silver", 0.4)
translate([BRKT_REACH, 0, AXLE_ABOVE_DECK])
rotate([0, 90, 0])
cylinder(d=AXLE_D, h=80);
// Phantom tire (semi-transparent outline)
color("Black", 0.12)
translate([BRKT_REACH + 40, 0, AXLE_ABOVE_DECK])
rotate([0, 90, 0])
cylinder(d=TIRE_OD, h=54);
// Reference deck edge
color("Gold", 0.25)
translate([-FLANGE_DEPTH - 20, -BRKT_W/2, -BRKT_H_BELOW])
cube([20, BRKT_W, BRKT_TOTAL_H]);
} else if (RENDER == "bracket") {
motor_bracket();
} else if (RENDER == "clamp_plate") {
// Orient for printing: lay flat, rotate upright
translate([0, CLAMP_T, -AXLE_ABOVE_DECK])
rotate([90, 0, 0])
axle_clamp_plate();
} else if (RENDER == "bracket_2d") {
projection(cut=true)
translate([0, 0, -BRKT_H_BELOW - BRKT_T/2])
motor_bracket();
}

View File

@ -0,0 +1,202 @@
// ============================================================
// rover_stem_adapter.scad SaltyRover Vertical Stem Adapter
// Rev A 2026-03-01 sl-mechanical
// ============================================================
// Secures the 25 mm OD vertical stem to the rover deck.
//
// Two-part system:
// base_flange() annular flange plate bolts to deck top
// (4× M4 SHCS into deck; deck already has
// 25 mm stem bore through its centre collar)
// stem_clamp() split collar clamps on stem above the
// deck collar; two M4 clamping bolts
// lock stem position / rotation
//
// Reuses the existing sensor head, RPLIDAR, camera mounts, and
// roll cage from SaltyLab stem OD 25 mm is unchanged.
//
// Stem length options:
// SaltyLab 1000 mm (balance robot tall for CG)
// SaltyRover 550 mm (rover sensors visible, compact)
// The adapter is identical; only the purchased tube differs.
//
// Ensure the deck stem collar (saltyrover_chassis.scad,
// STEM_COLLAR_OD=50 mm, H=22 mm) is the primary lateral
// support. The flange + clamp provide torque/axial lock.
//
// RENDER options:
// "assembly" flange + clamp + stem stub
// "base_flange" flange plate for printing
// "clamp_front" clamp front half for printing
// "clamp_rear" clamp rear half for printing
// "flange_2d" flange projection DXF
// ============================================================
RENDER = "assembly";
// Stem
STEM_OD = 25.0;
STEM_BORE = 25.4; // +0.4 clearance (same as sensor mounts)
STEM_L_ROVER = 550; // recommended rover stem length (mm)
// Base flange
// Sits on deck collar top. 4× M4 bolt through flange into deck.
FLANGE_OD = 80.0; // outer diameter
FLANGE_T = 6.0; // plate thickness
FLANGE_BOLT_BC = 65.0; // M4 bolt circle diameter
FLANGE_BOLT_D = 4.3; // M4 clearance
FLANGE_BOLT_N = 4; // number of bolts (at 90°)
// Deck collar height (must match saltyrover_chassis.scad STEM_COLLAR_H)
DECK_COLLAR_H = 22.0;
// Split stem clamp
// Sits on top of flange; clamped M4 bolts lock stem.
COL_OD = 52.0; // clamp outer diameter
COL_H = 28.0; // clamp height (above flange)
COL_BOLT_X = 19.0; // M4 clamping bolt CL from stem axis
COL_BOLT_D = 4.5; // M4 clearance hole
COL_NUT_W = 7.0; // M4 hex nut across-flats
COL_NUT_H = 3.4; // hex nut height
// Set screw for rotation lock (front half)
SET_SCREW_D = 4.5; // M4 set screw
// Fasteners
M4_D = 4.3;
$fn = 64;
e = 0.01;
//
// base_flange()
// Sits on top of the deck stem collar.
// Z=0 at deck top (collar rises from here).
//
module base_flange() {
difference() {
union() {
// Annular flange plate (sits on collar top)
translate([0, 0, DECK_COLLAR_H])
cylinder(d=FLANGE_OD, h=FLANGE_T);
// Short skirt that drops inside/over collar
translate([0, 0, DECK_COLLAR_H - 4])
cylinder(d=FLANGE_OD - 8, h=4);
}
// Stem bore
translate([0, 0, DECK_COLLAR_H - 4 - e])
cylinder(d=STEM_BORE, h=FLANGE_T + 4 + 2*e);
// M4 bolts through flange + down into deck (×4)
for (ang=[0, 90, 180, 270])
rotate([0, 0, ang])
translate([FLANGE_BOLT_BC/2, 0, DECK_COLLAR_H - e])
cylinder(d=FLANGE_BOLT_D, h=FLANGE_T + 2*e);
// Countersink on top face
for (ang=[0, 90, 180, 270])
rotate([0, 0, ang])
translate([FLANGE_BOLT_BC/2, 0, DECK_COLLAR_H + FLANGE_T - 3.5])
cylinder(d1=FLANGE_BOLT_D, d2=8.5, h=3.5 + e);
}
}
//
// stem_clamp_half(side)
// Split collar clamps on stem above the flange.
// Print flat-face-down. side = "front" | "rear"
//
module stem_clamp_half(side="front") {
y_front = (side == "front");
// Clamp Z origin: on top of flange
clamp_z0 = DECK_COLLAR_H + FLANGE_T;
difference() {
// D-shaped half
intersection() {
translate([0, 0, clamp_z0])
cylinder(d=COL_OD, h=COL_H);
translate([-COL_OD/2,
y_front ? 0 : -COL_OD/2,
clamp_z0])
cube([COL_OD, COL_OD/2, COL_H]);
}
// Stem bore
translate([0, 0, clamp_z0 - e])
cylinder(d=STEM_BORE, h=COL_H + 2*e);
// M4 clamping bolt holes (Y direction, through front half)
for (bx=[-COL_BOLT_X, COL_BOLT_X])
translate([bx,
y_front ? COL_OD/2 : 0,
clamp_z0 + COL_H/2])
rotate([90, 0, 0])
cylinder(d=COL_BOLT_D, h=COL_OD/2 + e);
// M4 hex nut pockets in rear half
if (!y_front)
for (bx=[-COL_BOLT_X, COL_BOLT_X])
translate([bx,
-(COL_OD/4 + e),
clamp_z0 + COL_H/2])
rotate([90, 0, 0])
cylinder(d=COL_NUT_W/cos(30),
h=COL_NUT_H + e, $fn=6);
// M4 set screw hole (front half, mid-height, horizontal)
if (y_front)
translate([0, COL_OD/2,
clamp_z0 + COL_H * 0.65])
rotate([90, 0, 0])
cylinder(d=SET_SCREW_D,
h=COL_OD/2 - STEM_BORE/2 + e);
// Mating face chamfer (0.2 mm, prevents elephant-foot binding)
translate([0, 0, clamp_z0 - e])
rotate([0, 0, y_front ? 0 : 180])
translate([-COL_OD/2, -0.2, 0])
cube([COL_OD, 0.2, COL_H + 2*e]);
}
}
//
// Render selector
//
if (RENDER == "assembly") {
// Phantom deck collar reference
color("Gray", 0.15)
difference() {
cylinder(d=50, h=DECK_COLLAR_H);
translate([0,0,-e]) cylinder(d=STEM_BORE, h=DECK_COLLAR_H+2*e);
}
color("SteelBlue", 0.90) base_flange();
color("CornflowerBlue", 0.90) stem_clamp_half("front");
color("SlateBlue", 0.90)
mirror([0,1,0]) stem_clamp_half("rear");
// Phantom stem stub
color("Silver", 0.30)
translate([0, 0, DECK_COLLAR_H + FLANGE_T + COL_H])
cylinder(d=STEM_OD, h=STEM_L_ROVER);
} else if (RENDER == "base_flange") {
// Print flat; rotate flange down
translate([0, 0, -(DECK_COLLAR_H - 4)])
base_flange();
} else if (RENDER == "clamp_front") {
translate([0, 0, -(DECK_COLLAR_H + FLANGE_T)])
stem_clamp_half("front");
} else if (RENDER == "clamp_rear") {
translate([0, 0, -(DECK_COLLAR_H + FLANGE_T)])
stem_clamp_half("rear");
} else if (RENDER == "flange_2d") {
projection(cut=true)
translate([0, 0, -(DECK_COLLAR_H + FLANGE_T/2)])
base_flange();
}

View File

@ -0,0 +1,218 @@
// ============================================================
// saltyrover_chassis.scad SaltyRover 4-Wheel Base Plate
// Rev A 2026-03-01 sl-mechanical
// ============================================================
// Parametric deck plate for the stable 4-wheel SaltyRover.
// Low/wide/stable design reuses sensor head + 25 mm stem.
//
// Coordinate convention:
// Z = 0 deck top face
// +Y forward
// +X right (motor side)
//
// Battery orientation:
// Packs laid FLAT (56 mm tall, 420 mm running left-right,
// 88 mm per pack fore-aft). N packs arranged fore-aft.
// 2-pack tray: 430 × 186 mm opening.
// 4-pack tray: 430 × 362 mm opening.
//
// Motor positions are at ±TRACK_W/2 (X), ±AXLE_BASE/2 (Y).
// Motor brackets (rover_motor_mount.scad) bolt to deck edge
// and extend BRKT_REACH mm outward to hold axle at AXLE_H.
//
// RENDER options:
// "assembly" full deck + phantom batteries + stem stub
// "deck" deck plate only (review)
// "deck_2d" floor projection DXF for waterjet/CNC
// ============================================================
RENDER = "assembly";
// Deck footprint
ROVER_L = 500.0; // fore-aft (Y)
ROVER_W = 480.0; // left-right (X) sized around battery width
DECK_T = 8.0; // deck thickness
DECK_R = 15.0; // corner fillet radius
// Drive geometry (caliper-verified motor data from BOM.md)
TRACK_W = 540.0; // motor axle CL to CL, left-right
AXLE_BASE = 340.0; // motor axle CL to CL, fore-aft
AXLE_H = 127.0; // axle CL above ground (10×2.125" wheel, caliper)
GND_CLR = 50.0; // minimum chassis ground clearance
// Height of deck bottom above ground:
// GND_CLR + battery_tray_floor + battery_height
BATT_FLOOR_T = 3.0;
BATT_PACK_H = 56.0; // flat-laid pack height
DECK_BOT_H = GND_CLR + BATT_FLOOR_T + BATT_PACK_H; // 109 mm
// Axle above deck top = AXLE_H - (DECK_BOT_H + DECK_T) +10 mm
// Battery packs (BOM.md caliper-verified: 420×88×56 mm)
// Laid flat, 420 mm running left-right (X), 88 mm per pack (Y),
// 56 mm tall (Z). BATT_N packs arranged side-by-side fore-aft.
BATT_X_DIM = 420.0;
BATT_Y_DIM = 88.0; // per-pack fore-aft depth
BATT_N = 2; // packs fore-aft (2 = 176 mm; 4 = 352 mm)
TRAY_MARGIN = 5.0; // extra margin on each side of tray opening
// Stem socket (deck centre)
STEM_BORE = 25.5; // 25 mm tube + 0.5 clearance
STEM_COLLAR_OD = 50.0;
STEM_COLLAR_H = 22.0; // raised boss height above deck top
// FC mount (MAMBA F722S, 30.5×30.5 M3)
FC_SPACING = 30.5;
FC_HOLE_D = 3.2;
FC_STANDOFF_H= 6.0;
// FC centred forward of battery tray
FC_X = 0.0;
FC_Y = ROVER_L/2 - 60.0; // near front edge
// Jetson Orin NX mount
// Verify hole pattern against your carrier board.
// Default: 58×49 mm M3 (same as RPi HAT pattern used on Orin NX cards).
ORIN_HOLE_X = 58.0;
ORIN_HOLE_Y = 49.0;
ORIN_HOLE_D = 3.2;
ORIN_STANDOFF = 8.0;
ORIN_X = 0.0;
ORIN_Y = -(ROVER_L/2 - 55.0); // near rear edge
// Motor bracket attachment bolt pattern
// 2× M5 bolts through deck edge at each motor corner.
MOT_BOLT_D = 5.3; // M5 clearance
MOT_BOLT_SPC = 30.0; // bolt spacing fore-aft at each motor position
// Lightening holes
LH_D = 55.0;
// Fasteners
M3_D = 3.2;
M4_D = 4.3;
M5_D = 5.3;
$fn = 64;
e = 0.01;
//
// rounded_plate_2d()
//
module rounded_plate_2d(l, w, r) {
offset(r=r, $fn=32) offset(r=-r)
square([l, w], center=true);
}
//
// rover_deck()
//
module rover_deck() {
tray_w = BATT_X_DIM + 2*TRAY_MARGIN; // 430 mm
tray_l = BATT_Y_DIM * BATT_N + 2*TRAY_MARGIN; // 186 or 362 mm
difference() {
// Deck plate
translate([0, 0, -DECK_T])
linear_extrude(DECK_T)
rounded_plate_2d(ROVER_W, ROVER_L, DECK_R);
// Battery tray opening (centred, through deck)
translate([-tray_w/2, -tray_l/2, -DECK_T - e])
cube([tray_w, tray_l, DECK_T + 2*e]);
// Lightening holes
// 3 columns × 2 rows, outside battery opening
for (hx = [-ROVER_W/4, 0, ROVER_W/4])
for (hy = [-(tray_l/2 + LH_D/2 + 20),
(tray_l/2 + LH_D/2 + 20)])
if (abs(hy) < ROVER_L/2 - LH_D/2 - 10)
translate([hx, hy, -DECK_T - e])
cylinder(d=LH_D, h=DECK_T + 2*e);
// FC mount holes
for (fx=[-FC_SPACING/2, FC_SPACING/2])
for (fy=[-FC_SPACING/2, FC_SPACING/2])
translate([FC_X + fx, FC_Y + fy, -DECK_T - e])
cylinder(d=FC_HOLE_D, h=DECK_T + 2*e);
// Orin mount holes
for (ox=[-ORIN_HOLE_X/2, ORIN_HOLE_X/2])
for (oy=[-ORIN_HOLE_Y/2, ORIN_HOLE_Y/2])
translate([ORIN_X + ox, ORIN_Y + oy, -DECK_T - e])
cylinder(d=ORIN_HOLE_D, h=DECK_T + 2*e);
// Motor bracket bolt holes (4 corners × 2 bolts)
for (sx=[-1,1], sy=[-1,1])
for (by=[-MOT_BOLT_SPC/2, MOT_BOLT_SPC/2])
translate([sx*ROVER_W/2 - sx*(DECK_T/2 + e),
sy*AXLE_BASE/2 + by,
-DECK_T - e])
rotate([0, 90, 0])
cylinder(d=MOT_BOLT_D, h=DECK_T + 2*e);
// Stem bore through deck
translate([0, 0, -DECK_T - e])
cylinder(d=STEM_BORE, h=DECK_T + 2*e);
// Cable routing slots (×2 per side, fore and aft)
for (cy=[-ROVER_L/2 - e, ROVER_L/2 - 16])
for (cx=[-55, 55])
translate([cx - 7, cy, -DECK_T - e])
cube([14, 16, DECK_T + 2*e]);
}
// Stem collar (raised boss)
difference() {
cylinder(d=STEM_COLLAR_OD, h=STEM_COLLAR_H);
translate([0, 0, -e])
cylinder(d=STEM_BORE, h=STEM_COLLAR_H + 2*e);
}
// FC standoff posts (×4)
for (fx=[-FC_SPACING/2, FC_SPACING/2])
for (fy=[-FC_SPACING/2, FC_SPACING/2])
translate([FC_X + fx, FC_Y + fy, 0])
cylinder(d=6, h=FC_STANDOFF_H);
// Orin standoff posts (×4)
for (ox=[-ORIN_HOLE_X/2, ORIN_HOLE_X/2])
for (oy=[-ORIN_HOLE_Y/2, ORIN_HOLE_Y/2])
translate([ORIN_X + ox, ORIN_Y + oy, 0])
cylinder(d=6, h=ORIN_STANDOFF);
}
//
// Render selector
//
if (RENDER == "assembly") {
color("Silver", 0.90) rover_deck();
// Phantom battery packs (2 × laid flat)
tray_w = BATT_X_DIM + 2*TRAY_MARGIN;
tray_l = BATT_Y_DIM * BATT_N + 2*TRAY_MARGIN;
for (pn = [0 : BATT_N - 1])
color("Gold", 0.30)
translate([-BATT_X_DIM/2,
-BATT_Y_DIM*BATT_N/2 + pn*BATT_Y_DIM,
-(DECK_T + BATT_FLOOR_T + BATT_PACK_H)])
cube([BATT_X_DIM, BATT_Y_DIM, BATT_PACK_H]);
// Phantom stem stub
color("Silver", 0.25)
translate([0, 0, STEM_COLLAR_H])
cylinder(d=25, h=400);
// Motor position markers (spheres at axle CLs)
axle_z = AXLE_H - (DECK_BOT_H + DECK_T); // above deck top
for (sx=[-1,1], sy=[-1,1])
color("Tomato", 0.5)
translate([sx*TRACK_W/2, sy*AXLE_BASE/2, axle_z])
sphere(d=20);
} else if (RENDER == "deck") {
rover_deck();
} else if (RENDER == "deck_2d") {
projection(cut=true)
translate([0, 0, -DECK_T/2])
rover_deck();
}