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