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