Add 2020 T-slot quick-swap sensor rail for SaltyLab/Rover/Tank variants: - sensor_rail.scad: 2020 T-slot profile, T-nut, thumbscrew, end cap, index pins, stem/post/tank clamp adapters - sensor_rail_brackets.scad: universal T-nut base + RPLIDAR A1M8, D435i, IMX219, UWB anchor, cable clip brackets (tool-free M3 thumbscrew retention) - sensor_rail_BOM.md: purchased hardware, print settings, export commands Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
534 lines
22 KiB
OpenSCAD
534 lines
22 KiB
OpenSCAD
// ============================================================
|
||
// 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);
|
||
}
|
||
}
|