saltylab-firmware/chassis/sensor_rail.scad
sl-mechanical 3992b80bcb feat(mechanical): universal sensor mount rail system (Issue #138)
Add 2020 T-slot quick-swap sensor rail for SaltyLab/Rover/Tank variants:
- sensor_rail.scad: 2020 T-slot profile, T-nut, thumbscrew, end cap, index
  pins, stem/post/tank clamp adapters
- sensor_rail_brackets.scad: universal T-nut base + RPLIDAR A1M8, D435i,
  IMX219, UWB anchor, cable clip brackets (tool-free M3 thumbscrew retention)
- sensor_rail_BOM.md: purchased hardware, print settings, export commands

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

565 lines
24 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.

// ============================================================
// sensor_rail.scad — Universal Sensor Mount Rail System
// Issue: #138 Agent: sl-mechanical Date: 2026-03-01
// ============================================================
//
// T-slot 20×20 mm rail system for quick-swap sensor mounting
// across all SaltyLab robot variants.
//
// Rail profile: compatible with OpenBuilds 2020 / MISUMI HFS5-2020
// standard aluminium extrusion (off-the-shelf preferred).
// Printable PETG sections provided for rapid prototyping only.
//
// Height indexing: M5 cross-holes every 25 mm along rail length.
// Brackets can be positioned at any height (stepless) and locked
// with a thumbscrew, or indexed to 25 mm grid by aligning to holes.
//
// Bracket retention: M3 thumbscrew (no tools, ¼ turn).
// Each bracket has a printed T-nut in the rail T-groove.
// Thumbscrew clamps T-nut against groove walls from outside.
//
// Cross-variant base adapters (this file):
// • stem_adapter() — Ø25 mm stem (SaltyLab / SaltyRover mast)
// • post_adapter() — square tube (SaltyRover vertical posts)
// • tank_clamp() — flat plate clamp (SaltyTank uprights)
//
// Sensor brackets → see sensor_rail_brackets.scad
//
// Coordinate convention:
// Rail runs along Z (vertical).
// Rail cross-section in X-Y plane.
// Front face (sensor side) faces +Y.
// Z = 0 at rail bottom.
//
// RENDER options:
// "assembly" full rail + adapters + bracket ghosts (default)
// "rail_2d" DXF — rail profile cross-section (spec for extrusion)
// "rail_section_stl" STL — printable rail section (prototype only)
// "stem_adapter_stl" STL — Ø25 mm stem adapter (print 1×)
// "post_adapter_stl" STL — square-tube post adapter (print 1×)
// "tank_clamp_stl" STL — flat-plate tank upright clamp (print 1×)
// "end_cap_stl" STL — rail end cap (print 2× per rail)
// "index_pin_stl" STL — 25 mm index pin set (print 1 set)
//
// ── Export commands ─────────────────────────────────────────
// Rail profile DXF (send to extrusion supplier):
// openscad sensor_rail.scad -D 'RENDER="rail_2d"' -o sensor_rail_profile.dxf
// Printable rail section STL (200 mm, prototype):
// openscad sensor_rail.scad -D 'RENDER="rail_section_stl"' -o sensor_rail_200.stl
// Stem adapter:
// openscad sensor_rail.scad -D 'RENDER="stem_adapter_stl"' -o sensor_rail_stem_adapter.stl
// Post adapter:
// openscad sensor_rail.scad -D 'RENDER="post_adapter_stl"' -o sensor_rail_post_adapter.stl
// Tank clamp:
// openscad sensor_rail.scad -D 'RENDER="tank_clamp_stl"' -o sensor_rail_tank_clamp.stl
// End cap:
// openscad sensor_rail.scad -D 'RENDER="end_cap_stl"' -o sensor_rail_end_cap.stl
// ============================================================
$fn = 64;
e = 0.01;
// ── 2020 T-slot profile geometry ─────────────────────────────────────────────
// Matches OpenBuilds V-Slot 2020 / MISUMI HFS5-2020 / standard 2020 T-slot.
// ⚠ Do NOT modify these — they must match the aluminium extrusion you purchase.
RAIL_W = 20.0; // outer width/height of profile
SLOT_OPEN = 6.0; // T-groove opening width at outer face
SLOT_INNER_W = 10.2; // T-groove inner width
SLOT_INNER_H = 5.8; // T-groove inner height (depth)
SLOT_NECK_H = 3.2; // distance from outer face to T-groove inner
CENTRAL_BORE = 4.2; // central M5 bore diameter (tap drill)
CORNER_NOTCH = 1.6; // corner chamfer radius (standard 2020)
// ── Rail section parameters ───────────────────────────────────────────────────
RAIL_LEN = 200.0; // default section length (printable prototype)
// For aluminium: order in 200 / 250 / 300 mm lengths as needed.
// ── Index holes ───────────────────────────────────────────────────────────────
INDEX_PITCH = 25.0; // index hole spacing (mm) — height adjustment grid
INDEX_HOLE_D = 5.3; // M5 clearance through rail (perpendicular to Z)
// Index holes are on the LEFT and RIGHT faces of the rail (±X faces),
// perpendicular to Z (rail axis). Bracket T-nut has an M5 registration
// peg that drops into these holes for repeatable indexed positioning.
// ── Printable T-nut (used by sensor_rail_brackets.scad) ──────────────────────
// These dims define the T-nut that slides in the SLOT_INNER T-groove.
TNUT_W = SLOT_INNER_W - 0.4; // 9.8 mm — 0.4 clearance per side
TNUT_H = SLOT_INNER_H - 0.3; // 5.5 mm
TNUT_L = 12.0; // T-nut body length
TNUT_M3_NUT_AF = 5.5; // M3 hex nut across-flats (DIN 934)
TNUT_M3_NUT_H = 2.5; // M3 hex nut thickness
TNUT_BOLT_D = 3.3; // M3 clearance bore through T-nut
// ── Thumbscrew (M3 × 16 SHCS + printed thumbwheel) ───────────────────────────
THUMB_D = 16.0; // thumbwheel OD
THUMB_H = 8.0; // thumbwheel height
THUMB_KNURL = 12; // number of knurl ridges on thumbwheel
// ── Stem adapter (Ø25 mm — SaltyLab / SaltyRover mast) ───────────────────────
STEM_OD = 25.0; // SaltyLab / SaltyRover stem OD
STEM_BORE = 25.4; // collar bore with clearance
STEM_COL_OD = 46.0; // collar outer diameter
STEM_COL_H = 40.0; // collar height
STEM_BOLT_X = 17.0; // M4 clamping bolt CL from stem axis
STEM_RAIL_W = 60.0; // rail-mounting flange width
STEM_RAIL_H = 30.0; // rail-mounting flange height
// ── Post adapter (square tube, SaltyRover frame posts) ───────────────────────
POST_W = 20.0; // square tube OD (20×20 mm aluminium post)
POST_WALL = 2.0; // tube wall thickness
POST_CLAMP_T = 4.0; // clamp plate thickness
POST_CLAMP_W = 50.0; // clamp plate width
POST_CLAMP_H = 60.0; // clamp plate height
// ── Tank clamp (flat plate, SaltyTank side frame uprights) ───────────────────
TANK_PLATE_T = 6.0; // SaltyTank side frame plate thickness (matches FRAME_T)
TANK_CLAMP_W = 50.0; // clamp body width
TANK_CLAMP_H = 60.0; // clamp body height
TANK_BOLT_SPC= 30.0; // M4 bolt spacing for clamp-to-frame attachment
// ── Fasteners ─────────────────────────────────────────────────────────────────
M3_D = 3.3;
M4_D = 4.3;
M5_D = 5.3;
// ============================================================
// RENDER DISPATCH
// ============================================================
RENDER = "assembly";
if (RENDER == "assembly") {
assembly();
} else if (RENDER == "rail_2d") {
projection(cut = true)
translate([0, 0, -RAIL_W/2])
rotate([90, 0, 0])
rail_section(RAIL_W);
} else if (RENDER == "rail_section_stl") {
rail_section(RAIL_LEN);
} else if (RENDER == "stem_adapter_stl") {
stem_adapter();
} else if (RENDER == "post_adapter_stl") {
post_adapter();
} else if (RENDER == "tank_clamp_stl") {
tank_clamp();
} else if (RENDER == "end_cap_stl") {
rail_end_cap();
} else if (RENDER == "index_pin_stl") {
index_pin_set();
}
// ============================================================
// ASSEMBLY PREVIEW
// ============================================================
module assembly() {
// Rail section (200 mm, vertical)
color("Silver", 0.85) rail_section(200);
// Stem adapter at base
color("SteelBlue", 0.80)
translate([0, 0, -STEM_COL_H - 10])
stem_adapter();
// End cap at top
color("DimGray", 0.80)
translate([0, 0, 200])
rail_end_cap();
// Ghost sensor brackets (from sensor_rail_brackets.scad)
// RPLIDAR at top
%color("OliveDrab", 0.3)
translate([0, RAIL_W/2 + 5, 170])
cube([90, 20, 60], center = true);
// D435i at middle
%color("DarkSlateGray", 0.3)
translate([0, RAIL_W/2 + 5, 100])
cube([95, 20, 30], center = true);
// IMX219 at lower
%color("Teal", 0.3)
translate([0, RAIL_W/2 + 5, 50])
cube([40, 20, 40], center = true);
// Index hole markers
for (z = [0 : INDEX_PITCH : 200])
%color("Red", 0.4)
translate([RAIL_W/2 + 2, 0, z])
rotate([0, 90, 0])
cylinder(d = 3, h = 4);
}
// ============================================================
// RAIL SECTION (Part A — aluminium extrusion or PETG print)
// ============================================================
// Standard 2020 T-slot profile extruded along Z axis.
// This OpenSCAD module is primarily for:
// 1. DXF export to spec the extrusion cross-section for a supplier
// 2. Printable prototype sections (print in PETG, 5 perims, 60% infill)
//
// For production: purchase OpenBuilds V-Slot 2020 in desired length.
// Search: "2020 V-slot aluminium extrusion" or "2020 T-slot rail"
// Cut to length with a mitre saw. Tap central M5 bore at ends for
// end-mounting and rail-to-adapter connections.
//
// Index holes: drilled/tapped M5 on LEFT and RIGHT faces (±X)
// at 25 mm pitch. Specify when ordering pre-drilled extrusion,
// or drill manually after cutting.
module rail_section(length = RAIL_LEN) {
difference() {
// ── Extrude the 2020 profile ─────────────────────────────────
linear_extrude(length)
tslot_profile_2d();
// ── Central bore (M5 tap drill, both ends) ────────────────────
translate([0, 0, -e])
cylinder(d = CENTRAL_BORE, h = length + 2*e);
// ── Index holes (M5 clearance, ±X faces, every 25 mm) ─────────
for (z = [INDEX_PITCH/2 : INDEX_PITCH : length - INDEX_PITCH/2])
translate([-RAIL_W/2 - e, 0, z])
rotate([0, 90, 0])
cylinder(d = INDEX_HOLE_D, h = RAIL_W + 2*e);
}
}
// ── 2020 T-slot cross-section (2D profile for linear_extrude) ─────────────────
// Matches OpenBuilds V-Slot 2020 profile.
module tslot_profile_2d() {
difference() {
// Outer square with corner notches
difference() {
square([RAIL_W, RAIL_W], center = true);
// Corner notches (standard 2020 chamfer)
for (cx = [-1, 1])
for (cy = [-1, 1])
translate([cx * (RAIL_W/2 - CORNER_NOTCH/2),
cy * (RAIL_W/2 - CORNER_NOTCH/2)])
rotate([0, 0, 45])
square([CORNER_NOTCH * 1.41, CORNER_NOTCH * 1.41],
center = true);
}
// Central lightening bore
circle(d = CENTRAL_BORE);
// Internal corner channels (weight reduction, standard in 2020)
for (cx = [-1, 1])
for (cy = [-1, 1])
translate([cx * (RAIL_W/4 + 0.5), cy * (RAIL_W/4 + 0.5)])
circle(d = 3.2);
// 4× T-grooves (one per face)
for (rot = [0, 90, 180, 270])
rotate([0, 0, rot])
tslot_groove_2d(face_dist = RAIL_W/2);
}
}
// ── Single T-groove profile (2D, centred on face at face_dist from origin) ────
module tslot_groove_2d(face_dist) {
// Outer slot opening (tapered/chamfered entry)
translate([0, face_dist - SLOT_NECK_H])
square([SLOT_OPEN, SLOT_NECK_H + e], center = true);
// Inner T-groove
translate([0, face_dist - SLOT_NECK_H - SLOT_INNER_H + e])
square([SLOT_INNER_W, SLOT_INNER_H + e], center = true);
}
// ============================================================
// PRINTABLE T-NUT (Part B — print ×N as needed in PETG)
// ============================================================
// Slides into the T-groove of the 2020 rail.
// Captured M3 hex nut allows a thumbscrew to clamp from outside.
// The T-nut has a registration peg that drops into index holes.
//
// Print: PETG, flat face down, 5 perims, 60% infill.
// Standard M3 hex nut pressed in from top after printing.
module printable_tnut() {
difference() {
union() {
// Main body (fits inside T-groove)
cube([TNUT_W, TNUT_L, TNUT_H], center = true);
// Wings that sit behind slot opening (wider than SLOT_OPEN)
// These wings bear against the T-groove inner walls
translate([0, 0, TNUT_H/2 - 0.8])
cube([TNUT_W, TNUT_L, 1.6], center = true);
}
// M3 hex nut pocket (press-fit from top)
translate([0, 0, TNUT_H/2 - TNUT_M3_NUT_H - 0.3])
cylinder(d = TNUT_M3_NUT_AF / cos(30),
h = TNUT_M3_NUT_H + 0.4,
$fn = 6);
// M3 clearance bore (through T-nut, for thumbscrew shank)
cylinder(d = TNUT_BOLT_D, h = TNUT_H + 2*e, center = true);
}
}
// ============================================================
// THUMBSCREW WHEEL (Part C — print ×N as needed in PETG)
// ============================================================
// Press-fit onto M3×16 SHCS head. Provides finger grip for
// tool-free tightening. One per bracket.
// Press-fit bore: 5.6 mm (M3 SHCS head hex socket OD ≈ 5.5 mm)
module thumbscrew_wheel() {
difference() {
union() {
cylinder(d = THUMB_D, h = THUMB_H);
// Knurl ridges (cosmetic grooves — helps grip)
for (i = [0 : THUMB_KNURL - 1])
rotate([0, 0, i * 360 / THUMB_KNURL])
translate([THUMB_D/2 - 1, 0, 0])
cylinder(d = 2.0, h = THUMB_H);
}
// M3 SHCS head hex socket bore (press-fit)
translate([0, 0, -e])
cylinder(d = 5.7, h = 4);
// M3 shank clearance bore (bolt passes through)
cylinder(d = M3_D, h = THUMB_H + 2*e);
}
}
// ============================================================
// RAIL END CAP (Part D — print 2× per rail in PETG)
// ============================================================
// Safety end cap — prevents T-nuts from sliding off end.
// Snap-friction fit (no fasteners needed for prototyping).
// Or: drill M5 through cap + rail end, use M5×10 set screw.
module rail_end_cap() {
cap_h = 8;
cap_plug_h = 6; // depth inserted into rail end
difference() {
union() {
// Outer flange (≥ rail OD, prevents over-insertion)
cylinder(d = RAIL_W + 6, h = cap_h);
// Inner plug (fits inside rail with 0.3 mm clearance per side)
translate([0, 0, cap_h - e])
cube([RAIL_W - 0.6, RAIL_W - 0.6, cap_plug_h], center = true);
}
// Central M5 bore (for optional end-bolt)
cylinder(d = M5_D, h = cap_h + cap_plug_h + e);
// Lightening cutout
translate([0, 0, 2])
cube([RAIL_W - 4, RAIL_W - 4, cap_h + cap_plug_h], center = true);
}
}
// ============================================================
// INDEX PIN SET (Part E — print 1 set in PETG)
// ============================================================
// Small pins that insert through M5 index holes on the rail.
// When a bracket T-nut has a matching recess, the pin provides
// positive indexed positioning (true 25 mm grid lock).
// For non-indexed positioning: leave pins out, use thumbscrew only.
module index_pin_set() {
// Print 4 pins on a carrier plate for easy identification
for (i = [0:3])
translate([i * 20, 0, 0])
index_pin();
}
module index_pin() {
pin_od = 4.9; // M5 hole clearance (5.3 - 0.4 mm)
pin_len = RAIL_W + 2; // spans full rail width + 1 mm each side
head_od = 8.0; // knurled head OD (finger pull)
head_h = 5.0;
union() {
// Pin shaft
cylinder(d = pin_od, h = pin_len);
// Knurled head (at one end)
translate([0, 0, pin_len])
difference() {
cylinder(d = head_od, h = head_h);
for (j = [0:7])
rotate([0, 0, j*45])
translate([head_od/2 - 1.2, 0, 0])
cylinder(d = 2, h = head_h + e);
}
}
}
// ============================================================
// STEM ADAPTER (Part F — SaltyLab / SaltyRover Ø25 mm mast)
// ============================================================
// Split collar clamps to the robot's Ø25 mm vertical stem.
// Two rail-mounting flanges (front + rear) allow one or two
// sensor rails to be mounted at 180° apart.
//
// Print 2× halves (front + rear), join with 2× M4×30 SHCS.
// Each half is printed flat-face-down with no supports needed.
module stem_adapter() {
// Front half only — print 2× and assemble around stem
stem_adapter_half("front");
}
module stem_adapter_half(side = "front") {
sy = (side == "front") ? 1 : -1;
difference() {
union() {
// ── Collar half (semicircle) ─────────────────────────────
rotate_extrude(angle = 180)
translate([STEM_COL_OD/2 - (STEM_COL_OD - STEM_BORE)/2, 0, 0])
square([(STEM_COL_OD - STEM_BORE)/2,
STEM_COL_H], center = false);
// ── Rail mounting flange (extends forward from collar) ────
// Single flange on front face, centred left-right
translate([-STEM_RAIL_W/2,
sy * (STEM_COL_OD/2 - 2),
STEM_COL_H/2 - STEM_RAIL_H/2])
cube([STEM_RAIL_W, STEM_COL_OD/4 + 5, STEM_RAIL_H]);
}
// ── Stem bore ────────────────────────────────────────────────
translate([0, 0, -e])
cylinder(d = STEM_BORE, h = STEM_COL_H + 2*e);
// ── M4 clamping bolt holes (through collar flanges) ───────────
for (sx = [-1, 1])
translate([sx * STEM_BOLT_X, 0, STEM_COL_H / 2])
rotate([90, 0, 0])
cylinder(d = M4_D, h = STEM_COL_OD + 2*e, center = true);
// ── M4 nut pocket (rear half) ─────────────────────────────────
if (side == "rear") {
for (sx = [-1, 1])
translate([sx * STEM_BOLT_X, -STEM_COL_OD/2 + 4,
STEM_COL_H/2])
rotate([90, 0, 0]) {
cylinder(d = 7.5/cos(30), h = 4, $fn = 6); // M4 nut
}
}
// ── Rail attachment slots in flange (M5 × 2) ─────────────────
// Slots allow ±5 mm front-back fine adjustment of rail position
for (rz = [-STEM_RAIL_H/4, STEM_RAIL_H/4])
translate([0,
sy * (STEM_COL_OD/2 - 2 + STEM_COL_OD/8 + 5/2),
STEM_COL_H/2 + rz])
rotate([90, 0, 0])
hull() {
translate([-6, 0, 0])
cylinder(d = M5_D, h = 10, center = true);
translate([+6, 0, 0])
cylinder(d = M5_D, h = 10, center = true);
}
}
}
// ============================================================
// POST ADAPTER (Part G — SaltyRover 20×20 mm square post)
// ============================================================
// C-clamp style bracket that wraps around a 20×20 mm aluminium
// post (common in rover extrusion frames).
// M4 × 2 clamping bolts through the clamp flanges.
// Rail mounts forward via 2× M5 slots in the front face.
module post_adapter() {
// Outer clamp body
difference() {
union() {
// Main C-clamp body
translate([-POST_CLAMP_W/2, -POST_W/2 - POST_CLAMP_T,
0])
cube([POST_CLAMP_W,
POST_W + 2*POST_CLAMP_T + POST_CLAMP_W/3,
POST_CLAMP_H]);
}
// Post cavity (POST_W × POST_W + clearance)
translate([0, 0, -e])
cube([POST_W + 0.4, POST_W + 0.4, POST_CLAMP_H + 2*e],
center = true);
// C-clamp opening (rear gap — allows clamping around post)
translate([-POST_W/2 - POST_CLAMP_T/2 - e, -POST_W/2 - e, -e])
cube([POST_CLAMP_T + 2*e,
POST_W + 0.2,
POST_CLAMP_H + 2*e]);
// M4 clamping bolt holes
for (cz = [POST_CLAMP_H/4, 3*POST_CLAMP_H/4])
translate([-POST_CLAMP_W/2 - e, 0, cz])
rotate([0, 90, 0])
cylinder(d = M4_D, h = POST_CLAMP_W + 2*e);
// M4 nut pockets (right flange)
for (cz = [POST_CLAMP_H/4, 3*POST_CLAMP_H/4])
translate([POST_CLAMP_W/2 - 5, 0, cz])
rotate([0, 90, 0])
cylinder(d = 7.5/cos(30), h = 5, $fn = 6);
// Rail mounting slots (M5 × 2, front face)
for (cz = [POST_CLAMP_H/3, 2*POST_CLAMP_H/3])
translate([0,
POST_W/2 + POST_CLAMP_T + POST_CLAMP_W/3 - e,
cz])
rotate([90, 0, 0])
hull() {
translate([-5, 0, 0])
cylinder(d = M5_D,
h = POST_CLAMP_T + 2*e);
translate([+5, 0, 0])
cylinder(d = M5_D,
h = POST_CLAMP_T + 2*e);
}
}
}
// ============================================================
// TANK CLAMP (Part H — SaltyTank side frame upright)
// ============================================================
// Flat plate bracket bolts directly to SaltyTank side frame
// (6 mm Al plate) via 4× M4 SHCS.
// Rail attaches to the front face via 2× M5 slots.
module tank_clamp() {
difference() {
union() {
// Main back plate (bolts to tank frame)
translate([-TANK_CLAMP_W/2, -TANK_PLATE_T - 2, 0])
cube([TANK_CLAMP_W,
TANK_PLATE_T + RAIL_W + 10,
TANK_CLAMP_H]);
}
// Tank frame plate slot (open at rear, receives frame edge)
translate([-TANK_CLAMP_W/2 + 5, -TANK_PLATE_T - e, -e])
cube([TANK_CLAMP_W - 10,
TANK_PLATE_T + 0.5,
TANK_CLAMP_H + 2*e]);
// 4× M4 frame attachment bolts
for (bx = [-TANK_BOLT_SPC/2, TANK_BOLT_SPC/2])
for (bz = [TANK_CLAMP_H/4, 3*TANK_CLAMP_H/4])
translate([bx, -TANK_PLATE_T/2, bz])
rotate([90, 0, 0])
cylinder(d = M4_D, h = TANK_PLATE_T + 4, center = true);
// Rail mounting slots (M5 × 2, front face)
for (bz = [TANK_CLAMP_H/3, 2*TANK_CLAMP_H/3])
translate([0, RAIL_W + 9 - e, bz])
rotate([90, 0, 0])
hull() {
translate([-6, 0, 0])
cylinder(d = M5_D, h = 10);
translate([+6, 0, 0])
cylinder(d = M5_D, h = 10);
}
// Weight-reduction pockets (back plate)
translate([0, -TANK_PLATE_T/2 - 1, TANK_CLAMP_H/2])
cube([TANK_CLAMP_W - 20,
4, TANK_CLAMP_H - 30],
center = true);
}
}