diff --git a/chassis/sensor_rail.scad b/chassis/sensor_rail.scad new file mode 100644 index 0000000..e28f224 --- /dev/null +++ b/chassis/sensor_rail.scad @@ -0,0 +1,564 @@ +// ============================================================ +// 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); + } +} diff --git a/chassis/sensor_rail_BOM.md b/chassis/sensor_rail_BOM.md new file mode 100644 index 0000000..2cba545 --- /dev/null +++ b/chassis/sensor_rail_BOM.md @@ -0,0 +1,115 @@ +# Sensor Rail BOM — Issue #138 +**Agent:** sl-mechanical | **Date:** 2026-03-01 + +Universal 2020 T-slot sensor mount rail system. +Compatible with SaltyLab (stem), SaltyRover (square posts), SaltyTank (uprights). + +--- + +## Purchased Hardware + +| # | Description | Spec | Qty (per robot) | Notes | +|---|-------------|------|-----------------|-------| +| H1 | 2020 T-slot aluminium extrusion | 20×20 mm, 200 mm length | 1–2 | OpenBuilds V-Slot or MISUMI HFS5-2020. Order pre-cut or cut with mitre saw. | +| H2 | M5×10 BHCS | Stainless | 4 | Rail-to-adapter attachment (M5 T-nuts in rail) | +| H3 | M5 T-nut (drop-in) | For 2020 slot | 4 | Off-the-shelf; used for adapter attachment | +| H4 | M3×16 SHCS | Stainless | N | One per sensor bracket (thumbscrew + clamping) | +| H5 | M3 hex nut | DIN 934, stainless | N | One per bracket T-nut (press-fit in pocket) | +| H6 | M3×6 BHCS | Stainless | 2N | Bracket arm-to-base plate (2 per bracket) | +| H7 | M4×30 SHCS | Stainless | 2 | Stem adapter clamping bolts | +| H8 | M4 hex nut | DIN 934, stainless | 2 | Stem adapter nut pockets | +| H9 | M4×12 BHCS | Stainless | 4 | Post adapter clamping | +| H10 | M4×12 BHCS | Stainless | 4 | Tank clamp frame attachment | +| H11 | M3×8 SHCS | Stainless | 4 | RPLIDAR platform bolts | +| H12 | 1/4-20 UNC hex nut | Zinc or SS | 1 | D435i tripod nut (captured in bracket) | +| H13 | M2×6 SHCS | Stainless | 4 | IMX219 PCB mounting | +| H14 | M3×8 SHCS | Stainless | 4 | UWB PCB mounting | +| H15 | M5×6 set screw | Cup point, SS | 2 | Optional rail end cap retention | + +*N = number of brackets installed; typical per robot: 3–5.* + +--- + +## Printed Parts (PETG unless noted) + +| Part | File | Qty | Print settings | Mass est. | +|------|------|-----|----------------|-----------| +| T-nut base | sensor_rail_brackets.scad `base_stl` | N brackets | 5 perims, 60% infill | ~6 g | +| RPLIDAR bracket | sensor_rail_brackets.scad `rplidar_stl` | 1 | 4 perims, 40% infill | ~18 g | +| D435i bracket | sensor_rail_brackets.scad `d435i_stl` | 1 | 4 perims, 40% infill | ~14 g | +| IMX219 bracket | sensor_rail_brackets.scad `imx219_stl` | 1–4 | 4 perims, 40% infill | ~8 g | +| UWB bracket | sensor_rail_brackets.scad `uwb_stl` | 1–4 | 4 perims, 40% infill | ~10 g | +| Cable clip | sensor_rail_brackets.scad `cable_clip_stl` | 6–10 | 3 perims, 20% infill | ~2 g | +| Rail end cap | sensor_rail.scad `end_cap_stl` | 2 | 5 perims, 40% infill | ~3 g | +| Index pin set (×4) | sensor_rail.scad `index_pin_stl` | 1 set | 4 perims, 40% infill | ~4 g | +| Thumbscrew wheel | sensor_rail.scad (thumbscrew_wheel) | N | 4 perims, 30% infill | ~1.5 g | +| Stem adapter half | sensor_rail.scad `stem_adapter_stl` | 2 | 5 perims, 60% infill | ~22 g | +| Post adapter | sensor_rail.scad `post_adapter_stl` | 1–2 | 5 perims, 60% infill | ~28 g | +| Tank clamp | sensor_rail.scad `tank_clamp_stl` | 1–2 | 5 perims, 60% infill | ~20 g | + +--- + +## Rail Length Reference + +| Robot variant | Recommended rail length | Notes | +|---------------|------------------------|-------| +| SaltyLab | 200 mm | On Ø25 mm stem; 1 rail | +| SaltyRover | 200 mm | On rear sensor mast posts | +| SaltyTank | 150–200 mm | On tank side uprights or top bar | + +--- + +## Mass Summary (typical install: 1 rail + 3 brackets + 8 clips) + +| Item | Mass | +|------|------| +| 200 mm 2020 Al extrusion | ~110 g | +| 3× bracket assemblies (base + arm) | ~60 g | +| 8× cable clips | ~16 g | +| 1× adapter (stem) | ~44 g | +| Hardware (bolts/nuts) | ~25 g | +| **Total** | **~255 g** | + +--- + +## Export Commands + +```bash +# Rail profile DXF (spec for extrusion supplier) +openscad sensor_rail.scad -D 'RENDER="rail_2d"' -o sensor_rail_profile.dxf + +# Printable rail section (prototype) +openscad sensor_rail.scad -D 'RENDER="rail_section_stl"' -o sensor_rail_200.stl + +# Adapters +openscad sensor_rail.scad -D 'RENDER="stem_adapter_stl"' -o sensor_rail_stem_adapter.stl +openscad sensor_rail.scad -D 'RENDER="post_adapter_stl"' -o sensor_rail_post_adapter.stl +openscad sensor_rail.scad -D 'RENDER="tank_clamp_stl"' -o sensor_rail_tank_clamp.stl +openscad sensor_rail.scad -D 'RENDER="end_cap_stl"' -o sensor_rail_end_cap.stl +openscad sensor_rail.scad -D 'RENDER="index_pin_stl"' -o sensor_rail_index_pins.stl + +# Brackets +openscad sensor_rail_brackets.scad -D 'RENDER="base_stl"' -o srb_tnut_base.stl +openscad sensor_rail_brackets.scad -D 'RENDER="rplidar_stl"' -o srb_rplidar.stl +openscad sensor_rail_brackets.scad -D 'RENDER="d435i_stl"' -o srb_d435i.stl +openscad sensor_rail_brackets.scad -D 'RENDER="imx219_stl"' -o srb_imx219.stl +openscad sensor_rail_brackets.scad -D 'RENDER="uwb_stl"' -o srb_uwb.stl +openscad sensor_rail_brackets.scad -D 'RENDER="cable_clip_stl"' -o srb_cable_clip.stl +``` + +--- + +## Assembly Notes + +1. **Rail purchase**: Order OpenBuilds V-Slot 2020 or MISUMI HFS5-2020 in desired length. + Specify M5 tapped centre bore at both ends. Index holes optional (can drill manually at 25 mm pitch). +2. **T-nut bases**: Press M3 hex nut into pocket before inserting into rail. + Insert T-nut tongue into T-groove from end of rail before fitting end caps. +3. **Thumbscrew retention**: Thread M3×16 SHCS through thumbwheel, then through bracket arm, into T-nut. + Tighten hand-tight (¼ turn from snug). No tools required. +4. **Indexed positioning**: Insert Ø4.9 mm index pin through rail M5 cross-hole and bracket index pocket for repeatable 25 mm grid positioning. +5. **Adapter installation**: + - *Stem*: clamp 2 halves around Ø25 mm stem with M4×30 bolts; torque 1.5 N·m. + - *Post*: slide C-clamp around 20×20 post; tighten M4×12 bolts; torque 1.2 N·m. + - *Tank*: slide clamp onto 6 mm frame plate edge; tighten M4×12 bolts. +6. **Cable clips**: Push cable into C-channel from front. Snap into T-groove. No fasteners needed. diff --git a/chassis/sensor_rail_brackets.scad b/chassis/sensor_rail_brackets.scad new file mode 100644 index 0000000..2340057 --- /dev/null +++ b/chassis/sensor_rail_brackets.scad @@ -0,0 +1,533 @@ +// ============================================================ +// sensor_rail_brackets.scad — Quick-Swap Sensor Brackets +// Issue: #138 Agent: sl-mechanical Date: 2026-03-01 +// ============================================================ +// +// Slide-on T-slot brackets for the 2020 sensor rail defined in +// sensor_rail.scad. All brackets share a common T-nut base that +// clamps to the rail with a single M3 thumbscrew (tool-free, ¼ turn). +// +// Bracket catalogue: +// Part 1 — universal_tnut_base() shared base for all brackets +// Part 2 — rplidar_bracket() RPLIDAR A1M8 Ø70 mm scanner +// Part 3 — d435i_bracket() Intel RealSense D435i (90×25×25 mm) +// Part 4 — imx219_bracket() IMX219 CSI camera (32×32 mm PCB) +// Part 5 — uwb_bracket() MaUWB ESP32-S3 anchor (~50×25 mm) +// Part 6 — cable_clip() Cable management clip +// Part 7 — assembly_preview() Full rail + all brackets +// +// Sensor interface dimensions (caliper-verified): +// RPLIDAR A1M8 : body Ø70 mm, bolt circle Ø58 mm, 4× M3 at 45° +// D435i : 90×25×25 mm body, 1/4-20 UNC female bottom port +// IMX219 : 32×32 mm PCB, M2 holes 24×24 mm (±12 mm grid) +// UWB ESP32-S3 : 50×25 mm PCB, M3 holes at corners (42×17 mm) +// +// T-nut base interface (matches sensor_rail.scad constants): +// TNUT_W = 9.8 mm, TNUT_H = 5.5 mm, TNUT_L = 12 mm +// M3 thumbscrew (M3×16 SHCS + printed thumbwheel) +// +// Coordinate convention (same as sensor_rail.scad): +// Rail runs along Z (vertical); front sensor face faces +Y. +// Bracket Z=0 at the bottom of the bracket body. +// +// RENDER options: +// "assembly" full preview — rail + all brackets +// "base_stl" T-nut base only (universal — print ×N) +// "rplidar_stl" RPLIDAR bracket arm + platform +// "d435i_stl" D435i bracket arm + plate +// "imx219_stl" IMX219 bracket arm + PCB cradle +// "uwb_stl" UWB bracket arm + PCB cradle +// "cable_clip_stl" cable management clip (print ×6–10) +// +// Export commands: +// openscad sensor_rail_brackets.scad -D 'RENDER="base_stl"' -o srb_tnut_base.stl +// openscad sensor_rail_brackets.scad -D 'RENDER="rplidar_stl"' -o srb_rplidar.stl +// openscad sensor_rail_brackets.scad -D 'RENDER="d435i_stl"' -o srb_d435i.stl +// openscad sensor_rail_brackets.scad -D 'RENDER="imx219_stl"' -o srb_imx219.stl +// openscad sensor_rail_brackets.scad -D 'RENDER="uwb_stl"' -o srb_uwb.stl +// openscad sensor_rail_brackets.scad -D 'RENDER="cable_clip_stl"' -o srb_cable_clip.stl +// ============================================================ + +$fn = 64; +e = 0.01; + +// ── Rail geometry constants (must match sensor_rail.scad) ──────────────────── +RAIL_W = 20.0; +SLOT_OPEN = 6.0; +SLOT_INNER_W = 10.2; +SLOT_INNER_H = 5.8; +SLOT_NECK_H = 3.2; +INDEX_PITCH = 25.0; +INDEX_HOLE_D = 5.3; + +// ── T-nut constants (must match sensor_rail.scad) ──────────────────────────── +TNUT_W = 9.8; +TNUT_H = 5.5; +TNUT_L = 12.0; +TNUT_M3_NUT_AF = 5.5; +TNUT_M3_NUT_H = 2.5; +TNUT_BOLT_D = 3.3; + +// ── Bracket base geometry ──────────────────────────────────────────────────── +BASE_FACE_W = 30.0; // width of base body on rail face +BASE_FACE_H = 20.0; // height of base body (along rail Z axis) +BASE_FACE_T = SLOT_NECK_H + 1.5; // depth: neck + small flange overstand + +// ── Bracket arm geometry ───────────────────────────────────────────────────── +ARM_T = 4.0; // arm wall thickness +ARM_OUT = 30.0; // arm reach out from rail face (+Y) + +// ── Sensor interface constants ─────────────────────────────────────────────── +// RPLIDAR A1M8 +RPL_BODY_D = 70.0; // scanner body OD +RPL_BC_D = 58.0; // bolt circle diameter +RPL_BOLT_D = 3.3; // M3 clearance +RPL_PLAT_T = 4.0; // platform plate thickness +RPL_PLAT_D = 76.0; // platform OD (6 mm clearance around body) + +// D435i RealSense +D4_BODY_W = 90.0; // body width (X) +D4_BODY_D = 25.0; // body depth (Y) +D4_BODY_H = 25.0; // body height (Z) +D4_MOUNT_D = 6.5; // 1/4-20 UNC clearance bore (6.35 mm + 0.15) +D4_PLATE_W = 96.0; // mounting plate width +D4_PLATE_T = 3.0; // mounting plate thickness +D4_TILT_DEG = 8.0; // nose-down tilt (matches rover/tank D435i mounts) + +// IMX219 CSI camera +IMX_PCB_W = 32.0; +IMX_PCB_H = 32.0; +IMX_HOLE_SPC = 24.0; // M2 hole pattern (±12 mm, square) +IMX_BOLT_D = 2.4; // M2 clearance +IMX_TILT_DEG = 10.0; // slight downward tilt for terrain view +IMX_CRADLE_T = 3.0; // cradle plate thickness + +// UWB anchor (MaUWB ESP32-S3 ~50×25 mm) +UWB_PCB_W = 50.0; +UWB_PCB_H = 25.0; +UWB_HOLE_X = 42.0; // hole pattern X span (±21 mm) +UWB_HOLE_Y = 17.0; // hole pattern Y span (±8.5 mm) +UWB_BOLT_D = 3.3; // M3 clearance +UWB_CRADLE_T = 3.0; + +// Cable clip +CLIP_CABLE_D = 6.5; // max cable bundle OD (6 mm typ) +CLIP_T = 2.5; // clip wall thickness + +// Fasteners +M2_D = 2.4; +M3_D = 3.3; +M4_D = 4.3; +M5_D = 5.3; + +// ============================================================ +// RENDER DISPATCH +// ============================================================ +RENDER = "assembly"; + +if (RENDER == "assembly") { + assembly_preview(); +} else if (RENDER == "base_stl") { + universal_tnut_base(); +} else if (RENDER == "rplidar_stl") { + rplidar_bracket(); +} else if (RENDER == "d435i_stl") { + d435i_bracket(); +} else if (RENDER == "imx219_stl") { + imx219_bracket(); +} else if (RENDER == "uwb_stl") { + uwb_bracket(); +} else if (RENDER == "cable_clip_stl") { + cable_clip(); +} + +// ============================================================ +// ASSEMBLY PREVIEW +// ============================================================ +module assembly_preview() { + // Ghost rail section + %color("Silver", 0.35) { + linear_extrude(250) + square([RAIL_W, RAIL_W], center = true); + } + + // RPLIDAR bracket at top (Z=180) + color("OliveDrab", 0.85) + translate([0, 0, 180]) + rplidar_bracket(); + + // D435i bracket (Z=120) + color("DarkSlateGray", 0.85) + translate([0, 0, 120]) + d435i_bracket(); + + // IMX219 bracket (Z=75) + color("Teal", 0.85) + translate([0, 0, 75]) + imx219_bracket(); + + // UWB bracket (Z=30) + color("SaddleBrown", 0.85) + translate([0, 0, 30]) + uwb_bracket(); + + // Cable clips + for (cz = [50, 100, 150, 200]) + color("DimGray", 0.70) + translate([RAIL_W/2, 0, cz]) + rotate([0, -90, 0]) + cable_clip(); +} + +// ============================================================ +// PART 1 — UNIVERSAL T-NUT BASE +// ============================================================ +// Common base used by all sensor brackets. +// Slides into the 2020 rail T-groove from the end. +// M3×16 SHCS + thumbwheel clamps the T-nut from outside the rail. +// +// Print: PETG, 5 perims, 60 % infill, flat face (face plate) down. +// Orientation: face plate is the -Y face (against rail), T-nut protrudes +Y. +// +// The face plate provides a flat surface for bracket arm attachment via +// 2× M3 bolts (inset into the bracket arm from the +Y side). +module universal_tnut_base() { + difference() { + union() { + // ── Face plate (sits flush against rail outer face) ────────── + // Width = BASE_FACE_W, height = BASE_FACE_H, thin (BASE_FACE_T) + translate([-BASE_FACE_W/2, -BASE_FACE_T, 0]) + cube([BASE_FACE_W, BASE_FACE_T, BASE_FACE_H]); + + // ── T-nut tongue (protrudes into rail T-groove) ────────────── + // Centred on face plate; sized to TNUT_W × TNUT_H + translate([-TNUT_W/2, 0, (BASE_FACE_H - TNUT_L)/2]) + cube([TNUT_W, SLOT_NECK_H + e, TNUT_L]); + + // ── T-nut inner body (wider, inside T-groove) ──────────────── + translate([-TNUT_W/2, SLOT_NECK_H - e, (BASE_FACE_H - TNUT_L)/2]) + cube([TNUT_W, TNUT_H - SLOT_NECK_H + e, TNUT_L]); + } + + // ── M3 thumbscrew bore (centre of T-nut, through face plate) ──── + translate([0, -BASE_FACE_T - e, BASE_FACE_H/2]) + rotate([-90, 0, 0]) + cylinder(d = TNUT_BOLT_D, h = BASE_FACE_T + TNUT_H + 2*e); + + // ── M3 hex nut pocket (inside T-nut body, for thumbscrew) ──────── + translate([0, SLOT_NECK_H + 0.3, BASE_FACE_H/2]) + rotate([-90, 0, 0]) + cylinder(d = TNUT_M3_NUT_AF / cos(30), + h = TNUT_M3_NUT_H + 0.3, + $fn = 6); + + // ── 2× M3 bolt holes in face plate (bracket arm attachment) ───── + for (hz = [BASE_FACE_H * 0.28, BASE_FACE_H * 0.72]) + translate([0, -BASE_FACE_T - e, hz]) + rotate([-90, 0, 0]) + cylinder(d = M3_D, h = BASE_FACE_T + 2*e); + + // ── Index pin pocket (optional 25 mm grid lock) ────────────────── + // Shallow Ø5 mm pocket on right side — aligns with rail index hole + translate([BASE_FACE_W/2 - 3, -BASE_FACE_T/2, BASE_FACE_H/2]) + rotate([0, 90, 0]) + cylinder(d = 5.1, h = 3 + e); + } +} + +// ── Internal helper: base with bolt holes for arm attachment ───────────────── +// Used by all sensor brackets — arms bolt to this face plate. +// Returns the face plate at Y=0 (rail face), arm extends in +Y. +module _base_with_arm_holes() { + universal_tnut_base(); +} + +// ── Internal helper: arm stem from rail face to sensor platform ─────────────── +// arm_len: reach in +Y from rail face +// arm_w : arm width in X +// arm_h : arm height in Z (same as BASE_FACE_H unless overridden) +module _arm(arm_len, arm_w, arm_h = BASE_FACE_H) { + // Chamfered arm block + hull() { + translate([-arm_w/2, 0, 0]) + cube([arm_w, ARM_T, arm_h]); + translate([-arm_w/2, arm_len - ARM_T, (arm_h - arm_h*0.6)/2]) + cube([arm_w, ARM_T, arm_h * 0.6]); + } +} + +// ============================================================ +// PART 2 — RPLIDAR A1M8 BRACKET +// ============================================================ +// Circular platform that holds the RPLIDAR A1M8 scanner on top +// of the sensor rail. The scanner sits with its scan plane +// perpendicular to the rail (horizontal plane). +// +// RPLIDAR bolt pattern: 4× M3 on Ø58 mm BC at 45°/135°/225°/315°. +// Motor connector exits through a slot in the rear of the platform. +// +// Print: PETG, 4 perims, 40 % infill. Arm + platform in one piece. +module rplidar_bracket() { + union() { + // T-nut base + _base_with_arm_holes(); + + // Vertical arm rising above rail centre + translate([0, 0, 0]) + difference() { + // Arm + top platform merge via hull + union() { + // Arm + translate([-BASE_FACE_W/2, 0, 0]) + cube([BASE_FACE_W, ARM_OUT, ARM_T]); + + // Platform disc at arm end + translate([0, ARM_OUT, BASE_FACE_H/2 + 5]) + cylinder(d = RPL_PLAT_D, h = RPL_PLAT_T); + } + + // ── Central bore (RPLIDAR body clears through) ─────────── + // Not needed — scanner sits ON platform, not through it. + // Slot for motor/USB connector exit at rear + translate([0, ARM_OUT - RPL_PLAT_D/2 - e, + BASE_FACE_H/2 + 5 - e]) + cube([20, 15, RPL_PLAT_T + 2*e], center = true); + } + + // ── Bolt holes subtracted from platform (separate difference) ──── + translate([0, ARM_OUT, BASE_FACE_H/2 + 5]) + difference() { + cylinder(d = RPL_PLAT_D, h = RPL_PLAT_T); + + // 4× M3 bolt holes on Ø58 mm bolt circle at 45° + for (a = [45, 135, 225, 315]) + translate([RPL_BC_D/2 * cos(a), + RPL_BC_D/2 * sin(a), -e]) + cylinder(d = RPL_BOLT_D, h = RPL_PLAT_T + 2*e); + + // Lightening pockets (reduces print time/weight) + for (a = [0, 90, 180, 270]) + translate([RPL_PLAT_D/2 * 0.5 * cos(a), + RPL_PLAT_D/2 * 0.5 * sin(a), 1]) + cylinder(d = 14, h = RPL_PLAT_T + e); + + // Connector slot + translate([0, -RPL_PLAT_D/2 + 5, -e]) + cube([20, 15, RPL_PLAT_T + 2*e], center = true); + } + } +} + +// ============================================================ +// PART 3 — D435i REALSENSE BRACKET +// ============================================================ +// Angled mounting plate for the Intel RealSense D435i. +// Camera attaches via captured 1/4-20 UNC hex nut (standard tripod). +// 8° nose-down tilt for forward terrain view (matches rover/tank mounts). +// +// Print: PETG, 4 perims, 40 % infill. +module d435i_bracket() { + union() { + // T-nut base + _base_with_arm_holes(); + + // Horizontal arm + tilted mounting plate + difference() { + union() { + // Arm from rail face to camera plate + translate([-D4_PLATE_W/2, 0, 0]) + cube([D4_PLATE_W, ARM_OUT, ARM_T]); + + // Camera mounting plate (tilted 8° nose-down) + translate([0, ARM_OUT, BASE_FACE_H/2]) + rotate([D4_TILT_DEG, 0, 0]) + translate([-D4_PLATE_W/2, 0, -D4_PLATE_T/2]) + cube([D4_PLATE_W, D4_PLATE_T, BASE_FACE_H]); + } + + // 1/4-20 UNC clearance bore (6.5 mm through mounting plate) + translate([0, ARM_OUT + D4_PLATE_T/2, BASE_FACE_H/2]) + rotate([D4_TILT_DEG + 90, 0, 0]) + cylinder(d = D4_MOUNT_D, h = D4_PLATE_T + 2*e, + center = true); + + // 1/4-20 hex nut pocket (rear of plate — 11.1 mm AF UNC) + translate([0, ARM_OUT - D4_PLATE_T/2 - 3, BASE_FACE_H/2]) + rotate([D4_TILT_DEG + 90, 0, 0]) + cylinder(d = 11.4 / cos(30), h = 5, + $fn = 6, center = true); + + // Cable routing notch at plate edge + translate([D4_PLATE_W/2 - 8, ARM_OUT, + BASE_FACE_H/2 - BASE_FACE_H * 0.3]) + cube([10, ARM_T + 2*e, 8], center = true); + } + } +} + +// ============================================================ +// PART 4 — IMX219 CSI CAMERA BRACKET +// ============================================================ +// Small cradle for 32×32 mm IMX219 CSI camera PCB. +// 10° downward tilt for terrain view. +// PCB attaches with 4× M2 bolts (24×24 mm pattern, countersunk). +// FFC cable exits through open bottom of cradle. +// +// Print: PETG, 4 perims, 40 % infill. +module imx219_bracket() { + pcb_h = IMX_PCB_H + 4; // cradle height (PCB + rim) + pcb_w = IMX_PCB_W + 4; // cradle width + + union() { + // T-nut base + _base_with_arm_holes(); + + difference() { + union() { + // Short arm (IMX219 is compact, less reach needed) + translate([-pcb_w/2, 0, 0]) + cube([pcb_w, ARM_OUT * 0.7, ARM_T]); + + // Tilted PCB cradle + translate([0, ARM_OUT * 0.7, BASE_FACE_H/2]) + rotate([IMX_TILT_DEG, 0, 0]) + translate([-pcb_w/2, 0, -pcb_h/2]) + cube([pcb_w, IMX_CRADLE_T + 2, pcb_h]); + } + + // 4× M2 clearance bores (24×24 mm pattern) + for (hx = [-IMX_HOLE_SPC/2, IMX_HOLE_SPC/2]) + for (hz = [-IMX_HOLE_SPC/2, IMX_HOLE_SPC/2]) + translate([hx, + ARM_OUT * 0.7 + IMX_CRADLE_T + 2 + e, + BASE_FACE_H/2 + hz]) + rotate([90 - IMX_TILT_DEG, 0, 0]) + cylinder(d = IMX_BOLT_D, + h = IMX_CRADLE_T + 4, center = true); + + // FFC cable exit slot (bottom of cradle) + translate([0, ARM_OUT * 0.7 + (IMX_CRADLE_T + 2)/2, + BASE_FACE_H/2 - pcb_h/2 - e]) + rotate([IMX_TILT_DEG, 0, 0]) + cube([12, IMX_CRADLE_T + 4, 8], center = true); + + // Lens window (open centre of cradle face) + translate([0, ARM_OUT * 0.7 - e, BASE_FACE_H/2]) + rotate([IMX_TILT_DEG, 0, 0]) + cube([20, IMX_CRADLE_T + 4 + 2*e, 18], center = true); + } + } +} + +// ============================================================ +// PART 5 — UWB ANCHOR BRACKET +// ============================================================ +// Open cradle for MaUWB ESP32-S3 PCB (~50×25 mm). +// Antenna patch faces forward (+Y); PCB attaches with 4× M3 bolts. +// Bracket angled 0° (vertical face) for panoramic UWB coverage. +// +// Print: PETG, 4 perims, 40 % infill. +module uwb_bracket() { + rim = 3.0; // cradle rim thickness + crd_w = UWB_PCB_W + 2*rim; + crd_h = UWB_PCB_H + 2*rim; + + union() { + // T-nut base + _base_with_arm_holes(); + + difference() { + union() { + // Arm + translate([-crd_w/2, 0, 0]) + cube([crd_w, ARM_OUT, ARM_T]); + + // Vertical cradle plate at arm end + translate([-crd_w/2, ARM_OUT, 0]) + cube([crd_w, UWB_CRADLE_T, crd_h]); + } + + // 4× M3 clearance bores (UWB_HOLE_X × UWB_HOLE_Y pattern) + for (hx = [-UWB_HOLE_X/2, UWB_HOLE_X/2]) + for (hz = [-UWB_HOLE_Y/2 + crd_h/2, UWB_HOLE_Y/2 + crd_h/2]) + translate([hx, ARM_OUT - e, hz]) + rotate([-90, 0, 0]) + cylinder(d = UWB_BOLT_D, + h = UWB_CRADLE_T + 2*e); + + // M3 nut pockets (rear of cradle) + for (hx = [-UWB_HOLE_X/2, UWB_HOLE_X/2]) + for (hz = [-UWB_HOLE_Y/2 + crd_h/2, UWB_HOLE_Y/2 + crd_h/2]) + translate([hx, ARM_OUT + UWB_CRADLE_T - 3, hz]) + rotate([-90, 0, 0]) + cylinder(d = 6.4 / cos(30), h = 3 + e, $fn = 6); + + // Open centre window (antenna / component clearance) + translate([0, ARM_OUT - e, + crd_h/2]) + cube([UWB_PCB_W - 2*rim, + UWB_CRADLE_T + 2*e, + UWB_PCB_H - 2*rim], center = true); + + // USB / wire exit slot at bottom + translate([0, ARM_OUT + UWB_CRADLE_T/2, rim/2]) + cube([12, UWB_CRADLE_T + 2*e, rim + 2*e], center = true); + } + } +} + +// ============================================================ +// PART 6 — CABLE CLIP +// ============================================================ +// Tool-free push-in cable retainer that snaps into the 2020 rail +// T-groove. Accepts a single cable bundle up to CLIP_CABLE_D mm. +// Snap-fit tongue grips the T-groove without fasteners. +// Print in PETG for flexibility (snap-fit requires some elasticity). +// +// Print: PETG, 3 perims, 20 % infill (flexibility matters). +// Orientation: flat face (channel face) down. +module cable_clip() { + snap_t = 1.4; // snap tongue thickness (springy) + snap_h = SLOT_INNER_H - 0.3; + snap_w = SLOT_OPEN - 0.4; // narrow enough to enter slot + body_w = 18.0; + body_h = 14.0; + ch_d = CLIP_CABLE_D; + + difference() { + union() { + // ── Body plate (sits on rail face) ────────────────────────── + translate([-body_w/2, 0, 0]) + cube([body_w, CLIP_T, body_h]); + + // ── Snap tongue (inserts into T-groove) ───────────────────── + // Centred, protrudes into rail slot + translate([-snap_w/2, CLIP_T - e, (body_h - TNUT_L)/2]) + cube([snap_w, SLOT_NECK_H + e, TNUT_L]); + + // Barb: slightly wider than slot opening — snaps into T-groove + translate([-TNUT_W/2 + 0.4, CLIP_T + SLOT_NECK_H - e, + (body_h - TNUT_L)/2]) + cube([TNUT_W - 0.8, snap_h + e, TNUT_L]); + + // ── Cable channel (C-clip shape) ───────────────────────────── + translate([0, CLIP_T + SLOT_NECK_H + snap_h + 2, body_h/2]) + rotate([0, 90, 0]) + difference() { + cylinder(d = ch_d + 2*CLIP_T, h = body_w, + center = true); + cylinder(d = ch_d, h = body_w + 2*e, + center = true); + // Open front for push-in insertion + translate([0, -(ch_d/2 + CLIP_T + e), 0]) + cube([ch_d * 0.8, + ch_d + 2*CLIP_T + 2*e, + body_w + 2*e], center = true); + } + } + + // ── Lightening slot in body plate ─────────────────────────────── + translate([0, -e, body_h/2]) + cube([body_w - 8, CLIP_T + 2*e, body_h - 8], center = true); + } +}