saltylab-firmware/chassis/saltyrover_chassis_r2.scad
sl-mechanical 2fa72e169e feat: SaltyRover chassis Rev 2 — 4-wheel rover with spring suspension (#109)
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 <noreply@anthropic.com>
2026-03-02 08:42:44 -05:00

524 lines
22 KiB
OpenSCAD
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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