From 3e4764b3eb96d0f588bac1a0cab0afead0483d30 Mon Sep 17 00:00:00 2001 From: sl-mechanical Date: Sun, 1 Mar 2026 01:21:22 -0500 Subject: [PATCH] feat: SaltyRover 4-wheel chassis design (#73) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add parametric OpenSCAD designs for the SaltyRover stable 4-wheel variant. Reuses existing 25mm stem, sensor head, and all SaltyLab sensor mounts without modification. Files: - saltyrover_chassis.scad 480×500mm deck, stem collar, FC+Orin standoffs, motor attachment holes, battery tray opening; RENDER deck_2d for waterjet/CNC DXF - rover_motor_mount.scad L-bracket + axle clamp plate per motor; uses caliper-verified axle dims from BOM.md; dropout slot for tool-free motor swap; RENDER bracket_2d for CNC DXF - rover_battery_tray.scad Slide-out tray for 2-4 × 420×88×56mm packs laid flat (low CG); T-slot rails, spring latch - rover_stem_adapter.scad Flange + split clamp locks 25mm stem to deck collar; 550mm stem option for rover height - rover_BOM.md Assembly sequence, fastener table, mass estimate (~13.4kg), height stack diagram Co-Authored-By: Claude Sonnet 4.6 --- chassis/rover_BOM.md | 228 ++++++++++++++++++++++++++++ chassis/rover_battery_tray.scad | 255 ++++++++++++++++++++++++++++++++ chassis/rover_motor_mount.scad | 240 ++++++++++++++++++++++++++++++ chassis/rover_stem_adapter.scad | 202 +++++++++++++++++++++++++ chassis/saltyrover_chassis.scad | 218 +++++++++++++++++++++++++++ 5 files changed, 1143 insertions(+) create mode 100644 chassis/rover_BOM.md create mode 100644 chassis/rover_battery_tray.scad create mode 100644 chassis/rover_motor_mount.scad create mode 100644 chassis/rover_stem_adapter.scad create mode 100644 chassis/saltyrover_chassis.scad diff --git a/chassis/rover_BOM.md b/chassis/rover_BOM.md new file mode 100644 index 0000000..cfe0c22 --- /dev/null +++ b/chassis/rover_BOM.md @@ -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, 2–4 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 | 4–8 | Battery pack retention (1–2 per pack through tray floor slots) | + +### Batteries + +| # | Part | Qty | Spec | +|---|------|-----|------| +| 19 | Battery pack | 2–4 | 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 diff --git a/chassis/rover_battery_tray.scad b/chassis/rover_battery_tray.scad new file mode 100644 index 0000000..bdc905f --- /dev/null +++ b/chassis/rover_battery_tray.scad @@ -0,0 +1,255 @@ +// ============================================================ +// rover_battery_tray.scad — SaltyRover Flat Battery Tray +// Rev A 2026-03-01 sl-mechanical +// ============================================================ +// Slide-out tray for 2–4 × 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(); +} diff --git a/chassis/rover_motor_mount.scad b/chassis/rover_motor_mount.scad new file mode 100644 index 0000000..daa9f9b --- /dev/null +++ b/chassis/rover_motor_mount.scad @@ -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(); +} diff --git a/chassis/rover_stem_adapter.scad b/chassis/rover_stem_adapter.scad new file mode 100644 index 0000000..e26ef47 --- /dev/null +++ b/chassis/rover_stem_adapter.scad @@ -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(); +} diff --git a/chassis/saltyrover_chassis.scad b/chassis/saltyrover_chassis.scad new file mode 100644 index 0000000..955901f --- /dev/null +++ b/chassis/saltyrover_chassis.scad @@ -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(); +} -- 2.47.2