feat(mechanical): universal sensor mount rail system (Issue #138) #151
564
chassis/sensor_rail.scad
Normal file
564
chassis/sensor_rail.scad
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
115
chassis/sensor_rail_BOM.md
Normal file
115
chassis/sensor_rail_BOM.md
Normal file
@ -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.
|
||||||
533
chassis/sensor_rail_brackets.scad
Normal file
533
chassis/sensor_rail_brackets.scad
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user