saltylab-firmware/chassis/saltytank_chassis.scad
sl-mechanical d93919e26f feat: SaltyTank tracked chassis — drive sprockets, tensioners, skid plate (#121)
Three new chassis design files for the SaltyTank continuous-track variant:

• saltytank_chassis.scad — Deck plate (500×360×8mm Al, DXF export), 2×
  side track frames (6mm Al, CNC/laser), idler tensioner sliding block,
  4× CSI corner camera mounts (45°/20°), D435i front bracket (8° tilt),
  stem collar (Ø25mm shared).  Drive sprocket mounts accept hoverboard hub
  motors with caliper-verified D-cut bore (16.11mm/13mm flat) + 52mm BC
  hub flange bolt pattern.  M6 tensioner bolt adjusts idler ±15mm for
  track tension. Shared FC 30.5×30.5mm + Jetson 58×49mm M3 patterns.
  Electronics bay footprint matches rover_electronics_bay.scad exactly.

• saltytank_skid_plate.scad — Sacrificial underside skid panel (360×500mm).
  4mm HDPE (DXF) or PETG print; countersunk M4 FHCS bolt-on.  4× drain/
  inspection slots; optional printed ribs (RIB_PRINT=true).  Ground
  clearance of hull between tracks: 90mm (exceeds 50mm requirement).

• saltytank_BOM.md — Full BOM: deck plate, side frames, drive sprockets,
  idler wheels + tensioners, road wheels (2/side), track belts (1109mm
  circumference calc), skid plate, sensor brackets, electronics bay
  (rover_electronics_bay.scad reused unchanged). Frame mass ≈ 2.98 kg
  (just under 3 kg target). Assembly sequence and track tensioning
  procedure included.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 09:20:41 -05:00

673 lines
30 KiB
OpenSCAD
Raw 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.

// ============================================================
// saltytank_chassis.scad — SaltyTank Tracked Chassis
// Issue: #121 Agent: sl-mechanical Date: 2026-03-01
// ============================================================
//
// Parametric tank chassis for rubber or metal continuous tracks.
//
// Structure (left-to-right cross-section):
// [left track belt]
// ← sprocket + idler + road wheels on outer face of side frame
// [left side frame plate, 8 mm Al]
// [deck plate, 8 mm Al] ← electronics bay on top
// [right side frame plate, 8 mm Al]
// → sprocket + idler + road wheels
// [right track belt]
//
// Drive: hoverboard hub motors (rear) — caliper-verified axle
// (16.11 mm OD, D-cut flat 13.00 mm, bearing seat Ø37.8 mm)
// Idler: 80 mm OD, M8 axle; tensioner slot ±15 mm fore-aft
// Road wh: 60 mm OD × 2 per side, M8 axle, fixed
// Tracks: rubber belt, 80 mm wide, 20 mm pitch (parametric)
//
// Electronics bay: reuses rover_electronics_bay.scad (same deck
// footprint, FC 30.5 × 30.5 mm M3 + Jetson 58 × 49 mm M3)
// Sensors: RPLIDAR A1M8 top (bay lid tower), D435i front,
// 4 × IMX219 / CSI at deck corners
// Stem: Ø25 mm (shared with SaltyLab / SaltyRover)
//
// Coordinate convention:
// Z = 0 deck top face
// +Y forward
// +X right
// Ground Z = -(DECK_T + FRAME_H) [= 98 mm with defaults]
//
// Ground clearance of hull (between tracks): FRAME_H = 90 mm ✓
// (exceeds 50 mm requirement with significant margin)
//
// Weight estimate — frame only (excl. motors, electronics, battery):
// Deck plate (8 mm Al, lightened) ≈ 1.55 kg
// Side frames 2 × (6 mm Al) ≈ 0.52 kg
// Skid plate (saltytank_skid_plate.scad, 4 mm HDPE) ≈ 0.56 kg
// Brackets + fasteners (PETG + SS) ≈ 0.35 kg
// Total ≈ 2.98 kg ← just under 3 kg target
//
// RENDER options:
// "assembly" full 3D preview (default)
// "deck_2d" DXF — deck plate (waterjet / CNC)
// "side_frame_2d" DXF — side frame plate (×2 mirrored; CNC)
// "side_frame_stl" STL — side frame (print 2×, mirror right)
// "idler_block_stl" STL — tensioner idler block (print 2×)
// "csi_mount_stl" STL — CSI corner bracket (print 4×)
// "d435i_mount_stl" STL — D435i front bracket (print 1×)
//
// ── Export commands ─────────────────────────────────────────
// Deck DXF:
// openscad saltytank_chassis.scad -D 'RENDER="deck_2d"' -o saltytank_deck.dxf
// Side frame DXF (cut 2×, flip one for mirror):
// openscad saltytank_chassis.scad -D 'RENDER="side_frame_2d"' -o saltytank_side_frame.dxf
// Side frame STL (print 2×; mirror one in slicer):
// openscad saltytank_chassis.scad -D 'RENDER="side_frame_stl"' -o saltytank_side_frame.stl
// Idler block STL (×2):
// openscad saltytank_chassis.scad -D 'RENDER="idler_block_stl"' -o saltytank_idler_block.stl
// CSI bracket STL (×4):
// openscad saltytank_chassis.scad -D 'RENDER="csi_mount_stl"' -o saltytank_csi_mount.stl
// D435i bracket STL (×1):
// openscad saltytank_chassis.scad -D 'RENDER="d435i_mount_stl"' -o saltytank_d435i_mount.stl
// ============================================================
$fn = 64;
e = 0.01;
// ── Deck plate ────────────────────────────────────────────────────────────────
BODY_L = 500.0; // deck fore-aft (Y)
BODY_W = 360.0; // deck left-right (X) — space between inner frame faces
DECK_T = 8.0; // deck plate thickness
DECK_R = 15.0; // corner fillet radius
// ── Side frame geometry ───────────────────────────────────────────────────────
FRAME_H = 90.0; // frame height below deck bottom = hull ground clearance
FRAME_T = 6.0; // frame plate thickness (6 mm Al laser-cut)
FRAME_R = 10.0; // frame corner fillet radius
// ── Track system (rubber or metal belt) ───────────────────────────────────────
TRACK_WID = 80.0; // track belt width
TRACK_PITCH = 20.0; // track link pitch (mm) — affects sprocket tooth count
// Track belt inner face sits at X = ±(BODY_W/2 + FRAME_T) from centre
// Track CL at X = ±(BODY_W/2 + FRAME_T + TRACK_WID/2) from centre
// ── Drive sprocket (rear) — hoverboard hub motor ──────────────────────────────
// Caliper-verified axle (matches BOM.md):
SPROCKET_AXLE_D = 16.11; // axle base OD (round section near hub)
SPROCKET_AXLE_FLAT= 13.00; // D-cut chord width
SPROCKET_AXLE_DCUT= 15.95; // D-cut OD
BEARING_OD = 37.80; // motor bearing-seat collar OD
BEARING_RECESS = 8.0; // bearing seat recess depth in frame
// Sprocket pitch circle: 10 teeth × 20 mm pitch
// PCD = pitch / sin(π/N) = 20 / sin(18°) ≈ 64.7 mm → R ≈ 32.4 mm
SPROCKET_R = 33.0; // sprocket pitch-circle radius (nominal)
SPROCKET_POS_Y = -(BODY_L/2 - SPROCKET_R - 22); // rear, Y
// Hub motor flange bolt circle (4× M5 at 90°, BC = 52 mm)
// ⚠ Verify against your motor flange before fabricating!
HUB_FLANGE_BC = 52.0; // motor hub bolt circle OD (M5 × 4)
HUB_BOLT_D = 5.3; // M5 clearance
// ── Idler wheel (front, adjustable tensioner) ─────────────────────────────────
IDLER_R = 40.0; // idler wheel radius (OD = 80 mm)
IDLER_AXLE_D = 8.5; // M8 axle clearance bore
IDLER_POS_Y_NOM = +(BODY_L/2 - IDLER_R - 22); // front nominal, +Y
// Tensioner slot: idler can move ±TENS_TRAVEL fore-aft to tension track
TENS_TRAVEL = 15.0; // ±15 mm track tension adjustment
// Tensioner bolt: M6 runs fore-aft in threaded lug at front of slot
TENS_BOLT_D = 6.5; // M6 clearance
TENS_BLOCK_L = 35.0; // sliding idler block length
TENS_BLOCK_W = TRACK_WID - 4; // block width (fits inside track)
TENS_SLOT_H = IDLER_AXLE_D + 2.0; // slot height (clearance for block)
// ── Road wheels (2× per side, between sprocket and idler) ────────────────────
ROAD_WHEEL_R = 30.0; // road wheel radius (OD = 60 mm)
ROAD_AXLE_D = 8.5; // M8 axle clearance bore
ROAD_Y_1 = -BODY_L/4; // forward road wheel (125 mm from centre)
ROAD_Y_2 = +BODY_L/4; // rearward road wheel (+125 mm from centre)
// ⚠ Reversed fore/aft: ROAD_Y_1 is near rear sprocket, ROAD_Y_2 near front idler
// ── Height stack ─────────────────────────────────────────────────────────────
// Ground Z = 0 (absolute); deck coords: Z=0 at deck top.
// In deck coords, ground is at Z = -(DECK_T + FRAME_H) = -98 mm.
// SPROCKET_CL_Z = -(DECK_T + FRAME_H - SPROCKET_R) = -(98 - 33) = -65 mm
// IDLER_CL_Z = -(DECK_T + FRAME_H - IDLER_R) = -(98 - 40) = -58 mm
// ROAD_WHEEL_Z = -(DECK_T + FRAME_H - ROAD_WHEEL_R) = -(98 - 30) = -68 mm
GROUND_Z = -(DECK_T + FRAME_H); // = -98 mm in deck coords
// ── Stem socket (deck centre, shared with SaltyLab / SaltyRover) ─────────────
STEM_BORE = 25.5; // 25 mm tube + 0.5 mm clearance
STEM_COLLAR_OD = 50.0;
STEM_COLLAR_H = 20.0; // boss height above deck top
STEM_FLANGE_BC = 40.0; // 4× M4 bolt circle
// ── Electronics bay footprint (rover_electronics_bay.scad) ───────────────────
// Bay dimensions match rover_electronics_bay.scad exactly.
// Deck holes match bay floor bolt pattern for drop-in compatibility.
BAY_L = 240.0; // bay length (X on deck = left-right)
BAY_W = 200.0; // bay width (Y on deck = fore-aft)
BAY_WALL = 3.0; // bay wall thickness
BAY_BOLT_INSET = 8.0; // bay bolt CL from bay exterior corner
// ── FC mount — 30.5 × 30.5 mm M3 (shared SaltyLab pattern) ──────────────────
FC_PITCH = 30.5;
FC_HOLE_D = 3.2;
FC_POS_Y = BAY_W/2 - 50.0; // near front edge (inside bay footprint)
// ── Jetson Orin mount — 58 × 49 mm M3 (shared SaltyLab pattern) ─────────────
ORIN_HOLE_X = 58.0;
ORIN_HOLE_Y = 49.0;
ORIN_HOLE_D = 3.2;
ORIN_POS_Y = -(BAY_W/2 - 55.0); // near rear edge
// ── CSI corner camera mounts ──────────────────────────────────────────────────
CSI_PCB = 25.0; // IMX219 PCB square side
CSI_M2_SPC = 15.0; // M2 hole pitch
CSI_TILT = 20.0; // nose-down tilt (degrees)
// ── D435i front bracket ───────────────────────────────────────────────────────
RS_TILT = 8.0; // nose-down tilt (degrees)
RS_ARM_LEN = 70.0; // arm reach forward from deck edge
RS_BASE_W = 44.0; // base plate width
// ── Fasteners ─────────────────────────────────────────────────────────────────
M2_D = 2.3;
M3_D = 3.3;
M4_D = 4.3;
M5_D = 5.3;
M6_D = 6.5;
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 == "side_frame_2d") {
// Left frame, projected flat (2D profile only — Z/Y plane)
projection(cut = true)
rotate([90, 0, 0])
translate([0, 0, BODY_L / 2])
side_frame(-1);
} else if (RENDER == "side_frame_stl") {
side_frame(-1); // mirror one in slicer for right side
} else if (RENDER == "idler_block_stl") {
idler_block();
} else if (RENDER == "csi_mount_stl") {
csi_corner_bracket();
} else if (RENDER == "d435i_mount_stl") {
d435i_front_bracket();
}
// ============================================================
// FULL ASSEMBLY
// ============================================================
module assembly() {
// Deck plate
color("Silver", 0.90) deck_plate();
// Stem collar
color("DimGray", 0.85) stem_collar();
// Side frames (left = 1, right = +1)
color("SteelBlue", 0.80) side_frame(-1);
color("SteelBlue", 0.80) side_frame(+1);
// Idler tensioner blocks (×2)
color("LightSlateGray", 0.85)
translate([-BODY_W/2 - FRAME_T, IDLER_POS_Y_NOM,
GROUND_Z + IDLER_R])
rotate([0, 90, 0]) idler_block();
color("LightSlateGray", 0.85)
translate([+BODY_W/2, IDLER_POS_Y_NOM,
GROUND_Z + IDLER_R])
rotate([0, -90, 0]) idler_block();
// 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();
// Phantom: electronics bay (rover_electronics_bay.scad)
%color("OliveDrab", 0.25)
translate([0, 0, DECK_T])
cube([BAY_L + 2*BAY_WALL,
BAY_W + 2*BAY_WALL,
84], center = true);
// Phantom: track loops (rubber belt cross-section)
for (sx = [-1, 1])
%color("Black", 0.15)
translate([sx * (BODY_W/2 + FRAME_T + TRACK_WID/2), 0, 0])
rotate([90, 0, 0])
track_loop_ghost();
// Phantom: hub motors (rear, outboard)
for (sx = [-1, 1])
%color("Orange", 0.20)
translate([sx * (BODY_W/2 + FRAME_T + 30),
SPROCKET_POS_Y,
GROUND_Z + SPROCKET_R])
rotate([0, sx*90, 0])
cylinder(d = BEARING_OD, h = 70, center = false);
}
// Ghost track loop outline for preview (no geometry output)
module track_loop_ghost() {
span = abs(IDLER_POS_Y_NOM - SPROCKET_POS_Y);
hull() {
translate([0, IDLER_POS_Y_NOM, GROUND_Z + IDLER_R])
rotate([90, 0, 0]) cylinder(d = IDLER_R*2, h = 1);
translate([0, SPROCKET_POS_Y, GROUND_Z + SPROCKET_R])
rotate([90, 0, 0]) cylinder(d = SPROCKET_R*2, h = 1);
}
}
// ============================================================
// DECK PLATE (Part A — laser-cut 8 mm 5052-H32 aluminium)
// ============================================================
// Rectangular plate spanning the gap between the two side frames.
// All electronics and sensor mounts attach to the deck top face.
// Side frames bolt to deck side edges (M5 × 4 per side).
// Weight estimate: 500×360×8 mm Al, ~45% lightened ≈ 1.55 kg
module deck_plate() {
difference() {
// ── Outer profile ─────────────────────────────────────────────
linear_extrude(DECK_T)
minkowski() {
square([BODY_L - 2*DECK_R, BODY_W - 2*DECK_R],
center = true);
circle(r = DECK_R);
}
// ── Side frame attachment slots (M5 × 4 per side) ─────────────
// Slots run fore-aft (Y) for ±10 mm lateral alignment adjustment
for (sx = [-1, 1])
for (py = [-BODY_L/4, BODY_L/4]) {
hull() {
translate([sx*(BODY_W/2 - 8), py - 12, -e])
cylinder(d = M5_D, h = DECK_T + 2*e);
translate([sx*(BODY_W/2 - 8), py + 12, -e])
cylinder(d = M5_D, h = DECK_T + 2*e);
}
}
// ── Stem bore ─────────────────────────────────────────────────
translate([0, 0, -e])
cylinder(d = STEM_BORE, h = DECK_T + 2*e);
// ── Stem flange bolts (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);
// ── Electronics bay footprint bolt holes (10× M3) ─────────────
// Matches rover_electronics_bay.scad floor flange pattern exactly
for (sx = [-1, 1])
for (sy = [-1, 1]) {
bx = sx * (BAY_L/2 + BAY_WALL - BAY_BOLT_INSET);
by = sy * (BAY_W/2 + BAY_WALL - BAY_BOLT_INSET);
translate([bx, by, -e])
cylinder(d = M3_D, h = DECK_T + 2*e);
}
// Centre long-wall bolts (2×)
for (sy = [-1, 1])
translate([0, sy*(BAY_W/2 + BAY_WALL - BAY_BOLT_INSET), -e])
cylinder(d = M3_D, h = DECK_T + 2*e);
// ── FC mount holes — 30.5×30.5 M3 (shared SaltyLab) ──────────
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 (shared SaltyLab) ─────
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);
// ── Lightening holes (between bay footprint and deck edges) ───
for (sx = [-1, 1])
for (sy = [-1, 1]) {
lx = sx * (BAY_L/2 + 40);
ly = sy * (BODY_L/4 + 10);
translate([lx, ly, -e])
cylinder(d = 50, h = DECK_T + 2*e);
}
// Centre pair flanking stem
for (sx = [-1, 1])
translate([sx * 65, 0, -e])
cylinder(d = 38, h = DECK_T + 2*e);
// ── Cable routing slots (motor phase + sensor harness) ─────────
// 4× slots near side frame attachment points
for (sx = [-1, 1])
for (sy = [-1, 1])
hull() {
translate([sx*(BODY_W/2 - 30), sy*(BODY_L/4 - 8), -e])
cylinder(d = 14, h = DECK_T + 2*e);
translate([sx*(BODY_W/2 - 30), sy*(BODY_L/4 + 8), -e])
cylinder(d = 14, h = DECK_T + 2*e);
}
// ── Skid plate attachment holes (M4 × 8, through deck underside)
// These align with saltytank_skid_plate.scad bolt pattern
for (sx = [-1, 1])
for (sy = [-1, 0, 1]) {
bx = sx * (BODY_W/2 - 20);
by = sy * (BODY_L/3);
translate([bx, by, -e])
cylinder(d = M4_D, h = DECK_T + 2*e);
}
// ── CSI corner bracket attachment holes (M3 × 2 per corner) ───
for (sx = [-1, 1])
for (sy = [-1, 1])
for (dd = [-12, 12])
translate([sx*(BODY_W/2 - 18),
sy*(BODY_L/2 - 18) + dd*sy, -e])
cylinder(d = M3_D, h = DECK_T + 2*e);
}
}
// ── Deck-top stem collar ─────────────────────────────────────────────────────
module stem_collar() {
translate([0, 0, DECK_T])
difference() {
cylinder(d = STEM_COLLAR_OD, h = STEM_COLLAR_H);
translate([0, 0, -e])
cylinder(d = STEM_BORE, h = STEM_COLLAR_H + 2*e);
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);
}
}
// ============================================================
// SIDE FRAME (Part B — laser-cut 6 mm 6061-T6 aluminium)
// ============================================================
// Vertical plate running the full body length.
// Inner face bolts to deck plate side edge.
// Outer face mounts: drive sprocket (rear) + idler slot (front)
// + road wheels (middle).
//
// `side` = -1 (left / X) or +1 (right / +X)
// The right frame is a mirror of the left — cut 2× from same DXF,
// flip one face-down before mounting.
//
// Weight estimate: 500×90×6 mm Al, ~35% lightened ≈ 0.26 kg each
// ============================================================
module side_frame(side = -1) {
sx = side; // 1 = left, +1 = right
// Frame inner face at X = sx * BODY_W/2
// Frame outer face at X = sx * (BODY_W/2 + FRAME_T)
frame_x = sx * BODY_W/2; // inner face X position
// In deck coords: sprocket / idler CL Z values
spr_z = GROUND_Z + SPROCKET_R; // = -(DECK_T + FRAME_H) + SPROCKET_R
idl_z = GROUND_Z + IDLER_R;
rw_z = GROUND_Z + ROAD_WHEEL_R;
translate([frame_x, 0, 0])
rotate([0, sx > 0 ? 180 : 0, 0]) // mirror right frame in X
translate([0, 0, 0]) {
difference() {
// ── Outer profile of side frame ───────────────────────────
// Frame plate in the Y-Z plane, FRAME_T thick in X
translate([0, -BODY_L/2, GROUND_Z])
linear_extrude(FRAME_T)
minkowski() {
square([BODY_L - 2*FRAME_R,
FRAME_H + DECK_T - 2*FRAME_R],
center = false);
circle(r = FRAME_R);
}
// ── Deck attachment slots (2× M5 per side, front + rear) ──
// Slots allow ±10 mm vertical adjustment for frame height
for (py = [-BODY_L/4, BODY_L/4])
hull() {
translate([e, py - 12, -DECK_T / 2])
rotate([0, 90, 0])
cylinder(d = M5_D, h = FRAME_T + 2*e);
translate([e, py + 12, -DECK_T / 2])
rotate([0, 90, 0])
cylinder(d = M5_D, h = FRAME_T + 2*e);
}
// ── Drive sprocket bore — D-cut (rear) ────────────────────
// Round section (base, near hub)
translate([e, SPROCKET_POS_Y, spr_z])
rotate([0, 90, 0])
cylinder(d = SPROCKET_AXLE_D + 0.4, h = FRAME_T + 2*e);
// D-cut flat (anti-rotation) — removes chord to AXLE_FLAT
dcut_h = sqrt(pow((SPROCKET_AXLE_DCUT + 0.4)/2, 2)
- pow((SPROCKET_AXLE_FLAT + 0.4)/2, 2));
translate([e, SPROCKET_POS_Y, spr_z])
rotate([0, 90, 0])
translate([0, (SPROCKET_AXLE_FLAT + 0.4)/2, 0])
cube([(SPROCKET_AXLE_DCUT + 0.4)/2,
FRAME_T + 2*e,
FRAME_T + 2*e],
center = false);
// ── Bearing seat recess (outboard face — inboard of track) ─
// Prevents Ø37.8 mm collar binding on frame face
translate([e - BEARING_RECESS, SPROCKET_POS_Y, spr_z])
rotate([0, 90, 0])
cylinder(d = BEARING_OD + 1.5,
h = BEARING_RECESS + e);
// ── Hub motor flange bolt holes (4× M5, 52 mm BC) ─────────
for (a = [45, 135, 225, 315])
translate([e,
SPROCKET_POS_Y + HUB_FLANGE_BC/2 * sin(a),
spr_z + HUB_FLANGE_BC/2 * cos(a)])
rotate([0, 90, 0])
cylinder(d = HUB_BOLT_D, h = FRAME_T + 2*e);
// ── Idler tensioner slot (front) ──────────────────────────
// Horizontal slot in Y direction; idler block slides in it
slot_y_ctr = IDLER_POS_Y_NOM;
slot_y_min = slot_y_ctr - TENS_TRAVEL;
slot_y_max = slot_y_ctr + TENS_TRAVEL;
translate([e, slot_y_min, idl_z - TENS_SLOT_H/2])
cube([FRAME_T + 2*e,
slot_y_max - slot_y_min,
TENS_SLOT_H]);
// Rounded ends
translate([e, slot_y_min, idl_z])
rotate([0, 90, 0])
cylinder(d = TENS_SLOT_H, h = FRAME_T + 2*e);
translate([e, slot_y_max, idl_z])
rotate([0, 90, 0])
cylinder(d = TENS_SLOT_H, h = FRAME_T + 2*e);
// ── Tensioner bolt bore (M6, at front end of slot) ────────
// M6 bolt threads into lug at front of slot; pushes block rearward
translate([e,
slot_y_max + 8,
idl_z])
rotate([0, 90, 0])
cylinder(d = M6_D, h = FRAME_T + 2*e);
// ── Road wheel bores (2× M8, fore/aft of centre) ──────────
for (ry = [ROAD_Y_1, ROAD_Y_2])
translate([e, ry, rw_z])
rotate([0, 90, 0])
cylinder(d = ROAD_AXLE_D, h = FRAME_T + 2*e);
// ── Lightening holes (between bore positions) ──────────────
// Row 1: between road wheel 1 and sprocket
translate([e,
(SPROCKET_POS_Y + ROAD_Y_1) / 2,
GROUND_Z + FRAME_H/2])
rotate([0, 90, 0])
cylinder(d = 45, h = FRAME_T + 2*e);
// Row 2: between road wheel 2 and idler
translate([e,
(IDLER_POS_Y_NOM + ROAD_Y_2) / 2,
GROUND_Z + FRAME_H/2])
rotate([0, 90, 0])
cylinder(d = 45, h = FRAME_T + 2*e);
// Row 3: between the two road wheels
translate([e, (ROAD_Y_1 + ROAD_Y_2) / 2, GROUND_Z + FRAME_H/2])
rotate([0, 90, 0])
cylinder(d = 35, h = FRAME_T + 2*e);
}
}
}
// ============================================================
// IDLER TENSIONER BLOCK (Part C — 3D print PETG, × 2)
// ============================================================
// Slides in the frame's tensioner slot.
// M8 bore holds the idler axle.
// A flat face at the front receives the tensioner M6 bolt.
// Lock nut (M6 nyloc) clamps block at desired track tension.
// Print orientation: flat face on bed; no supports needed.
// ============================================================
module idler_block() {
block_h = TENS_SLOT_H - 0.6; // height with slot clearance
block_l = TENS_BLOCK_L;
block_w = FRAME_T - 0.4; // thickness with clearance
difference() {
union() {
// Main block body
cube([block_w, block_l, block_h], center = true);
// Tensioner bolt lug (extends from +Y face)
translate([0, block_l/2, 0])
cube([block_w, 10, block_h], center = true);
}
// M8 axle bore through full width
translate([0, 0, 0])
rotate([0, 90, 0])
cylinder(d = IDLER_AXLE_D, h = block_w + 2*e, center = true);
// M6 tensioner bolt bore (fore-aft, through lug)
translate([0, block_l/2 + 5, 0])
rotate([90, 0, 0])
cylinder(d = M6_D, h = 16, center = true);
// M6 nyloc nut pocket (rear face of lug — captured nut)
translate([0, block_l/2 - 1, 0])
rotate([90, 0, 0])
cylinder(d = 11.5, h = 5, $fn = 6, center = false);
// ── Lightening slot (centre, removes material not in load path)
cube([block_w + 2*e, block_l/2 - IDLER_AXLE_D/2 - 4, block_h - 4],
center = true);
}
}
// ============================================================
// CSI CORNER BRACKET (Part D — 3D print PETG, × 4)
// ============================================================
// Same design as saltyrover_chassis_r2.scad.
// Mounts IMX219 / Arducam CSI camera at each deck corner,
// angled 45° outward + CSI_TILT downward.
// ============================================================
module csi_corner_bracket() {
base_l = 42;
base_w = 32;
base_t = 5;
difference() {
union() {
cube([base_l, base_w, base_t]);
translate([base_l / 2, base_w / 2, base_t])
rotate([0, CSI_TILT, 0])
translate([-CSI_PCB/2 - 3, -CSI_PCB/2 - 3, 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 M2 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 slot
translate([base_l/2 - 6, base_w/2 - 1.5, -e])
cube([12, 3, base_t + 2*e]);
}
}
module csi_bracket_placed(sx, sy) {
cx = sx * (BODY_W/2 - 22);
cy = sy * (BODY_L/2 - 22);
rot = atan2(sy, sx) * 180 / 3.14159 - 45;
translate([cx, cy, DECK_T])
rotate([0, 0, rot])
translate([-21, -16, 0])
csi_corner_bracket();
}
// ============================================================
// D435i FRONT BRACKET (Part E — 3D print PETG, × 1)
// ============================================================
// Same design as saltyrover_chassis_r2.scad.
// Arm extends forward from deck front edge.
// RS_TILT degrees nose-down. 1/4-20 captured nut for D435i.
// ============================================================
module d435i_front_bracket() {
base_d = 24;
base_h = 8;
arm_len = RS_ARM_LEN;
nut14_af = 11.1;
nut14_h = 5.6;
nut14_cl = 6.5;
difference() {
union() {
// Base plate (bolts to deck front face)
translate([-RS_BASE_W/2, 0, 0])
cube([RS_BASE_W, base_d, base_h]);
// Forward arm
translate([-13, base_d, 0])
cube([26, arm_len, base_h]);
// Tilted face plate
translate([0, base_d + arm_len, base_h/2])
rotate([0, RS_TILT, 0])
translate([-16, 0, -base_h/2])
cube([32, 14, base_h]);
}
// 2× M4 base attachment
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 UNC captured nut
translate([0, base_d + arm_len + 12, base_h/2])
rotate([0, 90, 0]) {
translate([0, 0, -nut14_h - 1])
cylinder(d = nut14_af/cos(30), h = nut14_h + 1, $fn = 6);
cylinder(d = nut14_cl, h = 20);
}
}
}
module d435i_bracket_placed() {
translate([0, BODY_L/2 + 12, DECK_T])
rotate([0, 0, 180])
d435i_front_bracket();
}