saltylab-firmware/chassis/rplidar_mount.scad

503 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.

// ============================================================
// rplidar_mount.scad — RPLIDAR A1 Elevated Bracket for 2020 T-Slot Rail
// Issue: #561 Agent: sl-mechanical Date: 2026-03-14
// (supersedes Rev A anti-vibration ring — ring integrated as Part 4)
// ============================================================
//
// Complete elevated mount system for RPLIDAR A1 on 2020 aluminium T-slot
// rail. Scanner raised ELEV_H mm above rail attachment point so the
// 360° laser scan plane clears the rover/tank chassis body.
//
// Architecture:
// T-nut base → clamps to 2020 rail (standard thumbscrew interface)
// Column → parametric-height hollow mast; USB cable routed inside
// Platform → disc receives RPLIDAR via 4× M3 on Ø40 mm bolt circle
// Vibe ring → anti-vibration isolation ring with silicone grommet seats
// Cable guide → snap-on clips along column for USB cable management
//
// Part catalogue:
// Part 1 — tnut_base() 2020 T-nut rail base + column stub socket
// Part 2 — column() Hollow elevation mast (parametric ELEV_H)
// Part 3 — scan_platform() RPLIDAR mounting disc + motor connector slot
// Part 4 — vibe_ring() Anti-vibration isolation ring (grommet seats)
// Part 5 — cable_guide() Snap-on cable management clip for column
// Part 6 — assembly_preview()
//
// Hardware BOM (per mount):
// 1× M3 × 16 mm SHCS + M3 hex nut rail clamp thumbscrew
// 4× M3 × 30 mm SHCS RPLIDAR → vibe_ring → platform
// 4× M3 silicone grommets (Ø6 mm) anti-vibration isolators
// 4× M3 hex nuts captured in platform underside
// 2× M4 × 12 mm SHCS column → base socket bolts
// 2× M4 hex nuts captured in base socket
// 1× USB-A cable (RPLIDAR → Jetson) routed through column bore
//
// RPLIDAR A1 interface (caliper-verified Slamtec RPLIDAR A1):
// Body diameter : Ø70 mm
// Bolt circle : Ø40 mm, 4× M3, at 45°/135°/225°/315°
// USB connector : micro-USB, right-rear quadrant, exits at 0° (front)
// Motor connector : JST 2-pin, rear centreline
// Scan plane height : 19 mm above bolt mounting face
// Min clearance : Ø80 mm cylinder around body for 360° scan
//
// Parametric constants (override for variants):
// ELEV_H — scan elevation above rail face (default 120 mm)
// COL_OD — column outer diameter (default 25 mm)
// RAIL choice — RAIL_W = 20 for 2020, = 40 for 4040 extrusion
//
// Print settings:
// Material : PETG (all parts); vibe_ring optionally in TPU 95A
// Perimeters : 5 (tnut_base, column, platform), 3 (vibe_ring, cable_guide)
// Infill : 40 % gyroid (structural), 20 % (vibe_ring, guide)
// Orientation:
// tnut_base — face-plate flat on bed (no supports)
// column — standing upright (no supports; hollow bore bridgeable)
// scan_platform — disc face down (no supports)
// vibe_ring — flat on bed (no supports)
// cable_guide — clip-open face down (no supports)
//
// Export commands:
// openscad rplidar_mount.scad -D 'RENDER="tnut_base_stl"' -o rpm_tnut_base.stl
// openscad rplidar_mount.scad -D 'RENDER="column_stl"' -o rpm_column.stl
// openscad rplidar_mount.scad -D 'RENDER="platform_stl"' -o rpm_platform.stl
// openscad rplidar_mount.scad -D 'RENDER="vibe_ring_stl"' -o rpm_vibe_ring.stl
// openscad rplidar_mount.scad -D 'RENDER="cable_guide_stl"' -o rpm_cable_guide.stl
// ============================================================
$fn = 64;
e = 0.01;
// ── Parametric elevation ──────────────────────────────────────────────────────
ELEV_H = 120.0; // scan plane elevation above rail face (mm)
// increase for taller chassis; min ~60 mm recommended
// ── RPLIDAR A1 interface constants ───────────────────────────────────────────
RPL_BODY_D = 70.0; // scanner body outer diameter
RPL_BC_D = 40.0; // mounting bolt circle diameter (4× M3 at 45° offsets)
RPL_BOLT_D = 3.3; // M3 clearance bore
RPL_SCAN_Z = 19.0; // scan plane height above mount face
RPL_CLEAR_D = 82.0; // minimum radial clearance diameter for 360° scan
// ── Rail geometry (matches 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;
// ── T-nut geometry (matches sensor_rail_brackets.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;
// ── Base plate geometry ───────────────────────────────────────────────────────
BASE_FACE_W = 38.0; // wider than rail, provides column socket footprint
BASE_FACE_H = 38.0; // height along rail Z
BASE_FACE_T = SLOT_NECK_H + 2.0; // plate depth (Y)
// ── Column geometry ───────────────────────────────────────────────────────────
COL_OD = 25.0; // column outer diameter
COL_ID = 17.0; // column inner bore (cable routing + weight saving)
COL_SOCKET_D = COL_OD + 6.0; // socket boss OD (column inserts into base)
COL_SOCKET_L = 14.0; // socket depth in base (14 mm engagement)
COL_BOLT_BC = COL_OD + 4.0; // M4 column-lock bolt span (centre-to-centre)
COL_SLOT_W = 5.0; // cable exit slot width in column base
COL_SLOT_H = 8.0; // cable exit slot height
// ── Platform geometry ─────────────────────────────────────────────────────────
PLAT_OD = RPL_CLEAR_D + 4.0; // platform disc OD (covers scan clear zone)
PLAT_T = 5.0; // platform disc thickness
PLAT_SOCKET_D = COL_OD + 0.3; // column-top socket ID (slip fit)
PLAT_SOCKET_L = 12.0; // socket depth on platform underside
PLAT_RIM_T = 3.5; // rim wall thickness around RPLIDAR body
// ── Anti-vibration ring geometry ─────────────────────────────────────────────
RING_OD = RPL_BODY_D + 12.0; // 82 mm (body + 6 mm rim)
RING_ID = 28.0; // central bore (connector/cable access)
RING_H = 4.0; // ring thickness
GROMMET_D = 7.0; // silicone grommet OD pocket
GROMMET_RECESS = 1.5; // grommet seating recess depth (bottom face)
// ── Cable guide clip geometry ─────────────────────────────────────────────────
GUIDE_CABLE_D = 6.0; // max cable OD (USB-A cable)
GUIDE_T = 2.0; // clip wall thickness
GUIDE_BODY_W = 20.0; // clip body width
GUIDE_BODY_H = 12.0; // clip body height
// ── Fastener sizes ────────────────────────────────────────────────────────────
M3_D = 3.3;
M4_D = 4.3;
M3_NUT_AF = 5.5;
M3_NUT_H = 2.4;
M4_NUT_AF = 7.0;
M4_NUT_H = 3.2;
// ============================================================
// RENDER DISPATCH
// ============================================================
RENDER = "assembly";
if (RENDER == "assembly") assembly_preview();
else if (RENDER == "tnut_base_stl") tnut_base();
else if (RENDER == "column_stl") column();
else if (RENDER == "platform_stl") scan_platform();
else if (RENDER == "vibe_ring_stl") vibe_ring();
else if (RENDER == "cable_guide_stl") cable_guide();
// ============================================================
// ASSEMBLY PREVIEW
// ============================================================
module assembly_preview() {
// Ghost 2020 rail section (250 mm)
%color("Silver", 0.28)
translate([-RAIL_W/2, -RAIL_W/2, 0])
cube([RAIL_W, RAIL_W, 250]);
// T-nut base at Z=60 on rail
color("OliveDrab", 0.85)
translate([0, 0, 60])
tnut_base();
// Column rising from base
color("SteelBlue", 0.85)
translate([0, BASE_FACE_T + COL_OD/2, 60 + BASE_FACE_H/2])
column();
// Vibe ring on top of platform
color("Teal", 0.85)
translate([0, BASE_FACE_T + COL_OD/2,
60 + BASE_FACE_H/2 + ELEV_H + PLAT_T])
vibe_ring();
// Scan platform at column top
color("DarkSlateGray", 0.85)
translate([0, BASE_FACE_T + COL_OD/2,
60 + BASE_FACE_H/2 + ELEV_H])
scan_platform();
// RPLIDAR body ghost
%color("Black", 0.35)
translate([0, BASE_FACE_T + COL_OD/2,
60 + BASE_FACE_H/2 + ELEV_H + PLAT_T + RING_H + 1])
cylinder(d = RPL_BODY_D, h = 30);
// Cable guides at 30 mm intervals along column
for (gz = [20, 50, 80])
color("DimGray", 0.75)
translate([COL_OD/2,
BASE_FACE_T + COL_OD/2,
60 + BASE_FACE_H/2 + gz])
rotate([0, -90, 0])
cable_guide();
}
// ============================================================
// PART 1 — T-NUT RAIL BASE
// ============================================================
// Standard 2020 rail T-nut attachment, matching interface used across
// all SaltyLab sensor brackets (sensor_rail_brackets.scad convention).
// Column socket boss on front face (+Y) receives column bottom.
// Column locked with 2× M4 cross-bolts through socket boss.
//
// Cable exit slot at base of socket directs RPLIDAR USB cable
// downward and rearward toward Jetson USB port.
//
// Print: face-plate flat on bed, PETG, 5 perims, 50 % gyroid.
module tnut_base() {
difference() {
union() {
// ── Face plate (flush against rail outer face, -Y) ───────────
translate([-BASE_FACE_W/2, -BASE_FACE_T, 0])
cube([BASE_FACE_W, BASE_FACE_T, BASE_FACE_H]);
// ── T-nut neck (enters rail slot) ────────────────────────────
translate([-TNUT_W/2, 0, (BASE_FACE_H - TNUT_L)/2])
cube([TNUT_W, SLOT_NECK_H + e, TNUT_L]);
// ── T-nut body (wider, locks in 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]);
// ── Column socket boss (front face, centred) ─────────────────
translate([0, -BASE_FACE_T, BASE_FACE_H/2])
rotate([-90, 0, 0])
cylinder(d = COL_SOCKET_D, h = BASE_FACE_T + COL_SOCKET_L);
}
// ── Rail clamp bolt bore (M3, centre of 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) ────────────────────────
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);
// ── Column socket bore (column inserts from +Y side) ─────────────
translate([0, -BASE_FACE_T, BASE_FACE_H/2])
rotate([-90, 0, 0])
cylinder(d = COL_OD + 0.3, h = BASE_FACE_T + COL_SOCKET_L + e);
// ── Column lock bolt bores (2× M4, horizontal through socket boss) ─
// One bolt from +X, one from -X, on COL_SOCKET_L/2 depth
for (lx = [-1, 1])
translate([lx * (COL_SOCKET_D/2 + e), COL_SOCKET_L/2, BASE_FACE_H/2])
rotate([0, 90, 0])
cylinder(d = M4_D, h = COL_SOCKET_D + 2*e,
center = true);
// ── M4 nut pockets (one side of socket boss for each bolt) ────────
for (lx = [-1, 1])
translate([lx * (COL_SOCKET_D/2 - M4_NUT_H - 1),
COL_SOCKET_L/2,
BASE_FACE_H/2])
rotate([0, 90, 0])
cylinder(d = M4_NUT_AF / cos(30),
h = M4_NUT_H + 0.5, $fn = 6);
// ── Cable exit slot (bottom of socket, cable exits downward) ──────
translate([0, COL_SOCKET_L * 0.6, BASE_FACE_H/2 - COL_SOCKET_D/2])
cube([COL_SLOT_W, COL_SOCKET_D + e, COL_SLOT_H], center = [true, false, false]);
// ── Lightening pockets in face plate ─────────────────────────────
translate([0, -BASE_FACE_T/2, BASE_FACE_H/2])
cube([BASE_FACE_W - 12, BASE_FACE_T - 2, BASE_FACE_H - 16],
center = true);
}
}
// ============================================================
// PART 2 — ELEVATION COLUMN
// ============================================================
// Hollow cylindrical mast (ELEV_H tall) raising the RPLIDAR scan
// plane above the chassis body for unobstructed 360° coverage.
// Inner bore routes USB cable from scanner to base exit slot.
// Bottom peg inserts into tnut_base socket; top peg inserts into
// scan_platform socket. Both ends are plain Ø(COL_OD) cylinders,
// interference-free slip fit into Ø(COL_OD+0.3) sockets.
//
// Three longitudinal ribs on outer surface add torsional stiffness
// without added diameter. Cable slot on one rib for cable retention.
//
// Print: standing upright, PETG, 5 perims, 20 % gyroid (hollow).
module column() {
rib_w = 3.0;
rib_h = 2.0; // rib protrusion from column OD
difference() {
union() {
// ── Hollow cylinder ───────────────────────────────────────────
cylinder(d = COL_OD, h = ELEV_H + COL_SOCKET_L);
// ── Three stiffening ribs (120° apart) ────────────────────────
for (ra = [0, 120, 240])
rotate([0, 0, ra])
translate([COL_OD/2 - e, -rib_w/2, 0])
cube([rib_h + e, rib_w, ELEV_H + COL_SOCKET_L]);
}
// ── Central cable bore (full length) ─────────────────────────────
translate([0, 0, -e])
cylinder(d = COL_ID, h = ELEV_H + COL_SOCKET_L + 2*e);
// ── Cable entry slot at column base (aligns with base exit slot) ──
translate([-COL_SLOT_W/2, COL_OD/2 - e, -e])
cube([COL_SLOT_W, COL_ID/2 + rib_h + 2, COL_SLOT_H + 2]);
// ── Cable exit slot at column top (USB exits to scanner) ──────────
translate([-COL_SLOT_W/2, COL_OD/2 - e,
ELEV_H + COL_SOCKET_L - COL_SLOT_H - 2])
cube([COL_SLOT_W, COL_ID/2 + rib_h + 2, COL_SLOT_H + 2]);
// ── Column lock flat (prevents rotation in socket) ────────────────
// Two opposed flats at column base & top socket peg
for (peg_z = [0, ELEV_H]) {
translate([-COL_OD/2 - e, COL_OD/2 - 2.0, peg_z])
cube([COL_OD + 2*e, 2.5, COL_SOCKET_L]);
}
}
}
// ============================================================
// PART 3 — SCAN PLATFORM
// ============================================================
// Disc that RPLIDAR A1 mounts to. Matches RPLIDAR A1 bolt pattern:
// 4× M3 on Ø40 mm bolt circle at 45°/135°/225°/315°.
// M3 hex nuts captured in underside pockets (blind, tool-free install).
// Column-top socket on underside receives column top peg (Ø25 slip fit).
// Motor connector slot on rear edge for JST cable exit.
// Vibe ring sits on top face between platform and RPLIDAR (separate part).
//
// Scan plane (19 mm above mount face) clears platform top by design;
// minimum platform OD = RPL_CLEAR_D (82 mm) leaves scan plane open.
//
// Print: disc face down, PETG, 5 perims, 40 % gyroid.
module scan_platform() {
difference() {
union() {
// ── Platform disc ─────────────────────────────────────────────
cylinder(d = PLAT_OD, h = PLAT_T);
// ── Column socket boss (underside, -Z) ────────────────────────
translate([0, 0, -PLAT_SOCKET_L])
cylinder(d = COL_SOCKET_D, h = PLAT_SOCKET_L + e);
}
// ── Column socket bore (column top peg inserts from below) ────────
translate([0, 0, -PLAT_SOCKET_L - e])
cylinder(d = PLAT_SOCKET_D, h = PLAT_SOCKET_L + e + 1);
// ── Column lock bores (2× M4 through socket boss) ─────────────────
for (lx = [-1, 1])
translate([lx * (COL_SOCKET_D/2 + e), 0, -PLAT_SOCKET_L/2])
rotate([0, 90, 0])
cylinder(d = M4_D, h = COL_SOCKET_D + 2*e, center = true);
// ── M4 nut pockets (one side socket boss) ─────────────────────────
translate([COL_SOCKET_D/2 - M4_NUT_H - 1, 0, -PLAT_SOCKET_L/2])
rotate([0, 90, 0])
cylinder(d = M4_NUT_AF / cos(30), h = M4_NUT_H + 0.5,
$fn = 6);
// ── 4× RPLIDAR mounting bolt holes (M3, Ø40 mm BC 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 = PLAT_T + 2*e);
// ── M3 hex nut pockets on underside (captured, tool-free) ─────────
for (a = [45, 135, 225, 315])
translate([RPL_BC_D/2 * cos(a),
RPL_BC_D/2 * sin(a), -e])
cylinder(d = M3_NUT_AF / cos(30),
h = M3_NUT_H + 0.5, $fn = 6);
// ── Motor connector slot (JST rear centreline, 10×6 mm) ──────────
translate([0, PLAT_OD/2 - 8, -e])
cube([10, 10, PLAT_T + 2*e], center = [true, false, false]);
// ── USB connector slot (micro-USB, right-rear, 12×6 mm) ──────────
translate([PLAT_OD/4, PLAT_OD/2 - 8, -e])
cube([12, 10, PLAT_T + 2*e], center = [true, false, false]);
// ── Lightening pockets (between bolt holes) ────────────────────────
for (a = [0, 90, 180, 270])
translate([(RPL_BC_D/2 + 10) * cos(a),
(RPL_BC_D/2 + 10) * sin(a), -e])
cylinder(d = 8, h = PLAT_T + 2*e);
// ── Central cable bore (USB from scanner routes down column) ──────
translate([0, 0, -e])
cylinder(d = COL_ID - 2, h = PLAT_T + 2*e);
}
}
// ============================================================
// PART 4 — VIBRATION ISOLATION RING
// ============================================================
// Flat ring sits between scan_platform top face and RPLIDAR bottom.
// Anti-vibration isolation via 4× M3 silicone FC-style grommets
// (Ø6 mm silicone, M3 bore — same type used on flight controllers).
//
// Bolt stack (bottom → top):
// M3 × 30 SHCS → platform (countersunk) → grommet (Ø7 seat) →
// ring (4 mm) → RPLIDAR threaded boss (~6 mm engagement)
//
// Grommet seats are recessed 1.5 mm into ring bottom face so grommets
// are captured and self-locating. Ring top face is flat for RPLIDAR.
//
// Print: flat on bed, PETG or TPU 95A, 3 perims, 20 % infill.
// TPU 95A provides additional compliance in axial direction.
module vibe_ring() {
difference() {
// ── Ring body ────────────────────────────────────────────────────
cylinder(d = RING_OD, h = RING_H);
// ── Central bore (cable / connector access) ───────────────────────
translate([0, 0, -e])
cylinder(d = RING_ID, h = RING_H + 2*e);
// ── 4× M3 clearance bores on Ø40 mm bolt circle ───────────────────
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 = RING_H + 2*e);
// ── Grommet seating recesses (bottom face, Ø7 mm × 1.5 mm deep) ──
for (a = [45, 135, 225, 315])
translate([RPL_BC_D/2 * cos(a),
RPL_BC_D/2 * sin(a), -e])
cylinder(d = GROMMET_D, h = GROMMET_RECESS + e);
// ── Motor connector notch (rear centreline, passes through ring) ──
translate([0, RING_OD/2 - 6, -e])
cube([10, 8, RING_H + 2*e], center = [true, false, false]);
// ── Lightening arcs ───────────────────────────────────────────────
for (a = [0, 90, 180, 270])
translate([(RPL_BC_D/2 + 9) * cos(a),
(RPL_BC_D/2 + 9) * sin(a), -e])
cylinder(d = 7, h = RING_H + 2*e);
}
}
// ============================================================
// PART 5 — CABLE GUIDE CLIP
// ============================================================
// Snap-on C-clip that presses onto column ribs to retain USB cable
// along column exterior. Cable sits in a semicircular channel;
// snap tongue grips the rib. No fasteners — push-fit on rib.
// Print multiples: one every ~30 mm along column for clean routing.
//
// Print: clip-opening face down, PETG, 3 perims, 20 % infill.
// Orientation matters — clip opening (-Y face) must face down for bridging.
module cable_guide() {
snap_t = 1.8; // snap tongue thickness (springy PETG)
snap_oc = GUIDE_CABLE_D + 2*GUIDE_T; // channel outer cylinder OD
body_h = GUIDE_BODY_H;
difference() {
union() {
// ── Clip body (flat plate on column face) ─────────────────────
translate([-GUIDE_BODY_W/2, 0, 0])
cube([GUIDE_BODY_W, GUIDE_T, body_h]);
// ── Cable channel (C-shape, opens toward +Y) ──────────────────
translate([0, GUIDE_T + snap_oc/2, body_h/2])
rotate([0, 90, 0])
difference() {
cylinder(d = snap_oc, h = GUIDE_BODY_W,
center = true);
cylinder(d = GUIDE_CABLE_D, h = GUIDE_BODY_W + 2*e,
center = true);
// Open front slot for cable insertion
translate([0, snap_oc/2 + e, 0])
cube([GUIDE_CABLE_D * 0.85,
snap_oc + 2*e,
GUIDE_BODY_W + 2*e], center = true);
}
// ── Snap-fit tongue (grips column rib, -Y side of body) ───────
// Two flexible tabs that straddle column rib
for (tx = [-GUIDE_BODY_W/2 + 2, GUIDE_BODY_W/2 - 2 - snap_t])
translate([tx, -4, 0])
cube([snap_t, 4 + GUIDE_T, body_h]);
// Snap barbs (slight overhang engages rib back edge)
for (tx = [-GUIDE_BODY_W/2 + 2, GUIDE_BODY_W/2 - 2 - snap_t])
translate([tx + snap_t/2, -4, body_h/2])
rotate([0, 90, 0])
cylinder(d = 2, h = snap_t, center = true);
}
// ── Rib slot (column rib passes through clip body) ─────────────────
translate([0, -2, body_h/2])
cube([3.5, GUIDE_T + 4 + e, body_h - 4], center = true);
}
}