From beb0af7ce6c78e3e641f6982c79132131c9d9e8a Mon Sep 17 00:00:00 2001 From: sl-mechanical Date: Sat, 28 Feb 2026 23:30:02 -0500 Subject: [PATCH] feat: bumper + frame crash protection for SaltyBot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Protects expensive sensors when robot tips over during testing. Files added: chassis/bumper_frame.scad — roll cage (25 mm stem collar + 4 inclined posts + crown ring + TPU snap pad). Posts at 45/135/225/315° between cameras. Crown at Z=148 mm above cage collar, 18 mm above RPLIDAR top. RPLIDAR FOV unobstructed. chassis/base_bumper.scad — clip-on bumper ring for 270×240 mm base plate. 50 mm tall, 15 mm standoff, rounded corners. 4× corner + 4× side clip brackets. TPU snap edge caps. FC status LEDs remain visible. chassis/stem_protector.scad — split TPU sleeve for 25 mm stem. Snap-fit closure (no hardware). Install 3–4 sleeves along stem at ~200 mm intervals. chassis/bumper_BOM.md — full BOM, print settings, install heights, mass estimate (~500 g total, balanced). All parts print without supports. Fully removable (clip/snap/clamp). Co-Authored-By: Claude Sonnet 4.6 --- chassis/base_bumper.scad | 291 ++++++++++++++++++++++++++++++++++++ chassis/bumper_BOM.md | 169 +++++++++++++++++++++ chassis/bumper_frame.scad | 260 ++++++++++++++++++++++++++++++++ chassis/stem_protector.scad | 119 +++++++++++++++ 4 files changed, 839 insertions(+) create mode 100644 chassis/base_bumper.scad create mode 100644 chassis/bumper_BOM.md create mode 100644 chassis/bumper_frame.scad create mode 100644 chassis/stem_protector.scad diff --git a/chassis/base_bumper.scad b/chassis/base_bumper.scad new file mode 100644 index 0000000..3f8509a --- /dev/null +++ b/chassis/base_bumper.scad @@ -0,0 +1,291 @@ +// ============================================================ +// base_bumper.scad — Base Plate Impact Bumper Rev A 2026-03-01 +// Agent: sl-mechanical +// ============================================================ +// Clip-on bumper system for the 270×240 mm SaltyBot base plate. +// Protects wheels, FC board, and wiring when robot tips. +// +// System overview: +// corner_bracket() — L-clip, grips plate corner, print 4× +// side_bracket() — straight clip for mid-sides, print 4× +// tpu_edge_cap() — soft snap-on edge cap, print in TPU +// (same profile fits corner and side) +// bumper_rail_2d() — 2-D cross-section DXF for straight rails +// +// Assembly: 4 corner brackets + 4 side brackets clip onto the +// plate bottom edge without drilling. Rails (printed or cut from +// 25×8 mm aluminium bar) bridge between clips. TPU edge caps +// snap onto rail outer face. +// +// FC status LEDs: FC is front-centre of plate. Bumper is below +// and outside the plate footprint — LEDs remain visible. +// +// RENDER options: +// "assembly" 3-D view with phantom plate (default) +// "corner" one corner bracket (print 4×) +// "side" one side bracket (print 4×) +// "tpu_cap" TPU edge cap cross-section extruded (print N×) +// "rail_2d" flat rail profile → DXF +// ============================================================ + +RENDER = "assembly"; + +// ── Base plate (reference only — not printed) ───────────────── +PLATE_W = 270.0; // width (motor axle direction, X) +PLATE_D = 240.0; // depth (fore-aft, Y) +PLATE_H = 6.0; // plate thickness +PLATE_R = 10.0; // plate corner radius + +// ── Bumper geometry ────────────────────────────────────────── +BUMP_H = 50.0; // bumper height below plate bottom face +BUMP_T = 8.0; // rail/bracket wall thickness +BUMP_CR = 22.0; // outer corner radius of bumper frame +BUMP_EXTEND = 15.0; // how far bumper extends beyond plate edge + // (absorbs impact before plate corner hits) + +// ── Clip channel (grips plate edge) ────────────────────────── +CLIP_SLOT_W = PLATE_H + 0.6; // slot width (6 mm plate + 0.6 clearance) +CLIP_SLOT_D = 12.0; // how deep clip grips plate edge +CLIP_GRIP_T = 4.0; // clip wall above plate (keeps clip aligned) + +// ── Corner bracket ──────────────────────────────────────────── +CORNER_ARM = 45.0; // length of each L-arm along plate edge +CORNER_H = BUMP_H + CLIP_GRIP_T + CLIP_SLOT_W; + +// ── Side bracket ───────────────────────────────────────────── +SIDE_ARM = 30.0; // side bracket total width along plate edge +SIDE_H = CORNER_H; + +// ── Rail (reference dims for procurement / DXF) ─────────────── +RAIL_W = BUMP_T; // 8 mm +RAIL_H = BUMP_H; // 50 mm + +// ── TPU edge cap ───────────────────────────────────────────── +CAP_W = BUMP_T + 4.0; // wraps around rail outer face +CAP_H = 14.0; // cap height (bottom of bumper profile) +CAP_R = 8.0; // outer impact radius +CAP_CLIP_W = BUMP_T + 0.6; // inner clip width (snaps on rail) +CAP_CLIP_D = 5.0; // clip depth + +// ── Fasteners ───────────────────────────────────────────────── +M4_D = 4.5; // M4 clearance hole (rail → bracket) +M3_D = 3.3; // M3 set screw (lock bracket on plate) + +$fn = 48; +e = 0.01; + +// ───────────────────────────────────────────────────────────── +// clip_slot_2d() +// 2-D profile of the plate-edge gripping channel. +// Origin at plate bottom face, +Y toward plate interior. +// ───────────────────────────────────────────────────────────── +module clip_slot_2d() { + // Slot opens from outside (-Y), grips plate edge + translate([0, -(BUMP_EXTEND + BUMP_T)]) + square([CLIP_SLOT_W, CLIP_SLOT_D + BUMP_EXTEND + BUMP_T]); +} + +// ───────────────────────────────────────────────────────────── +// corner_bracket() +// L-shaped clip bracket. Internal coordinate: +// Corner at origin, arms along +X and +Y. +// Plate sits at Z = BUMP_H (above bracket base). +// ───────────────────────────────────────────────────────────── +module corner_bracket() { + arm_w = BUMP_T; // wall thickness + arm_h = CORNER_H; // full height + + difference() { + union() { + // X arm (along plate width edge) + translate([-BUMP_EXTEND, -BUMP_EXTEND, 0]) + hull() { + cube([CORNER_ARM + BUMP_EXTEND, arm_w, arm_h]); + translate([0, arm_w, 0]) + cylinder(r = BUMP_CR, h = arm_h); + translate([CORNER_ARM + BUMP_EXTEND, arm_w, 0]) + cylinder(r = BUMP_CR, h = arm_h); + } + + // Y arm (along plate depth edge) + translate([-BUMP_EXTEND, -BUMP_EXTEND, 0]) + hull() { + cube([arm_w, CORNER_ARM + BUMP_EXTEND, arm_h]); + translate([arm_w, 0, 0]) + cylinder(r = BUMP_CR, h = arm_h); + translate([arm_w, CORNER_ARM + BUMP_EXTEND, 0]) + cylinder(r = BUMP_CR, h = arm_h); + } + } + + // Clip slot in X arm (grips plate edge) + // Slot at Z = BUMP_H (plate bottom face height) + translate([CORNER_ARM - CLIP_SLOT_D, -(BUMP_EXTEND + arm_w + e), BUMP_H]) + cube([CLIP_SLOT_D + BUMP_EXTEND + e, + arm_w + 2*e, CLIP_SLOT_W]); + + // Clip slot in Y arm + translate([-(BUMP_EXTEND + arm_w + e), CORNER_ARM - CLIP_SLOT_D, BUMP_H]) + cube([arm_w + 2*e, + CLIP_SLOT_D + BUMP_EXTEND + e, CLIP_SLOT_W]); + + // M3 set screw through clip above slot (X arm, 2 per arm) + for (xoff = [CORNER_ARM * 0.3, CORNER_ARM * 0.7]) { + translate([xoff - BUMP_EXTEND, -(BUMP_EXTEND + arm_w/2), BUMP_H + CLIP_SLOT_W + CLIP_GRIP_T/2]) + rotate([90, 0, 0]) + cylinder(d = M3_D, h = arm_w + 2*e); + } + for (yoff = [CORNER_ARM * 0.3, CORNER_ARM * 0.7]) { + translate([-(BUMP_EXTEND + arm_w/2), yoff - BUMP_EXTEND, BUMP_H + CLIP_SLOT_W + CLIP_GRIP_T/2]) + rotate([0, 90, 0]) + cylinder(d = M3_D, h = arm_w + 2*e); + } + + // M4 holes for rail attachment (outer face, pairs per arm) + for (xoff = [15, CORNER_ARM - 10]) { + translate([xoff - BUMP_EXTEND, -(BUMP_EXTEND + arm_w + e), BUMP_H * 0.3]) + rotate([90, 0, 0]) + cylinder(d = M4_D, h = arm_w + 2*e); + translate([xoff - BUMP_EXTEND, -(BUMP_EXTEND + arm_w + e), BUMP_H * 0.7]) + rotate([90, 0, 0]) + cylinder(d = M4_D, h = arm_w + 2*e); + } + } +} + +// ───────────────────────────────────────────────────────────── +// side_bracket() +// Straight clip for mid-side. Grips plate edge, M4 holes +// for rail, M3 set screws. Placed between corner brackets. +// ───────────────────────────────────────────────────────────── +module side_bracket() { + arm_w = BUMP_T; + arm_h = SIDE_H; + + difference() { + // Outer body + translate([-SIDE_ARM/2, -(BUMP_EXTEND + arm_w), 0]) + hull() { + cube([SIDE_ARM, arm_w, arm_h]); + translate([0, arm_w, 0]) + cylinder(r = BUMP_CR * 0.6, h = arm_h); + translate([SIDE_ARM, arm_w, 0]) + cylinder(r = BUMP_CR * 0.6, h = arm_h); + } + + // Clip slot + translate([-SIDE_ARM/2 + 5, -(BUMP_EXTEND + arm_w + e), BUMP_H]) + cube([SIDE_ARM - 10, arm_w + 2*e, CLIP_SLOT_W]); + + // M3 set screws (2×) + for (xoff = [-SIDE_ARM/4, SIDE_ARM/4]) { + translate([xoff, -(BUMP_EXTEND + arm_w/2), BUMP_H + CLIP_SLOT_W + CLIP_GRIP_T/2]) + rotate([90, 0, 0]) + cylinder(d = M3_D, h = arm_w + 2*e); + } + + // M4 rail holes (2 heights × 1 column) + for (zh = [BUMP_H * 0.3, BUMP_H * 0.7]) { + translate([0, -(BUMP_EXTEND + arm_w + e), zh]) + rotate([90, 0, 0]) + cylinder(d = M4_D, h = arm_w + 2*e); + } + } +} + +// ───────────────────────────────────────────────────────────── +// tpu_edge_cap() +// Snap-on soft edge cap — print in TPU. +// Snaps onto outer face of rail (or corner bracket outer face). +// Orient: cap opening faces -Y (toward bracket), print flat. +// ───────────────────────────────────────────────────────────── +module tpu_edge_cap() { + // Length — 40 mm segment; print multiple, install end-to-end + cap_len = 40.0; + + difference() { + union() { + // Rounded outer impact face + hull() { + translate([-CAP_W/2, 0, 0]) + cylinder(r = CAP_R, h = cap_len); + translate([ CAP_W/2, 0, 0]) + cylinder(r = CAP_R, h = cap_len); + translate([-CAP_W/2, CAP_H - CAP_R, 0]) + cylinder(r = CAP_R, h = cap_len); + translate([ CAP_W/2, CAP_H - CAP_R, 0]) + cylinder(r = CAP_R, h = cap_len); + } + } + + // Inner clip slot (snaps onto rail outer face) + translate([-CAP_CLIP_W/2, -e, -e]) + cube([CAP_CLIP_W, CAP_CLIP_D + e, cap_len + 2*e]); + } +} + +// ───────────────────────────────────────────────────────────── +// bumper_rail_2d() +// 2-D profile for straight bumper rail sections. +// Extrude to length per side: front/rear = PLATE_W, sides = PLATE_D. +// Use "rail_2d" RENDER to export DXF. +// ───────────────────────────────────────────────────────────── +module bumper_rail_2d() { + // Simple rectangular profile 8 × 50 mm + square([RAIL_W, RAIL_H]); +} + +// ───────────────────────────────────────────────────────────── +// Phantom plate (reference, assembly view only) +// ───────────────────────────────────────────────────────────── +module phantom_plate() { + color("Gray", 0.25) + linear_extrude(PLATE_H) + offset(r = PLATE_R, $fn = 32) + offset(-PLATE_R) + square([PLATE_W, PLATE_D], center = true); +} + +// ───────────────────────────────────────────────────────────── +// Render selector +// ───────────────────────────────────────────────────────────── +if (RENDER == "assembly") { + // Phantom plate + translate([0, 0, BUMP_H]) phantom_plate(); + + // 4 corner brackets (NW / NE / SW / SE) + half_w = PLATE_W/2; + half_d = PLATE_D/2; + color("SteelBlue", 0.9) { + // NW corner (X−, Y+) → rotate 90° + translate([-half_w, half_d, 0]) rotate([0,0,180]) corner_bracket(); + // NE corner (X+, Y+) → rotate 90° + translate([ half_w, half_d, 0]) rotate([0,0,270]) corner_bracket(); + // SW corner (X−, Y−) → no rotation + translate([-half_w,-half_d, 0]) rotate([0,0, 90]) corner_bracket(); + // SE corner (X+, Y−) + translate([ half_w,-half_d, 0]) corner_bracket(); + } + + // 4 side brackets (front, rear, left, right midpoints) + color("CornflowerBlue", 0.9) { + translate([0, half_d, 0]) rotate([0,0,180]) side_bracket(); // front + translate([0, -half_d, 0]) side_bracket(); // rear + translate([-half_w, 0, 0]) rotate([0,0, 90]) side_bracket(); // left + translate([ half_w, 0, 0]) rotate([0,0,270]) side_bracket(); // right + } + +} else if (RENDER == "corner") { + corner_bracket(); + +} else if (RENDER == "side") { + side_bracket(); + +} else if (RENDER == "tpu_cap") { + tpu_edge_cap(); + +} else if (RENDER == "rail_2d") { + projection(cut = false) + linear_extrude(1) + bumper_rail_2d(); +} diff --git a/chassis/bumper_BOM.md b/chassis/bumper_BOM.md new file mode 100644 index 0000000..189cc6b --- /dev/null +++ b/chassis/bumper_BOM.md @@ -0,0 +1,169 @@ +# SaltyBot Bumper & Crash Protection — BOM + Assembly Notes +**Rev A — 2026-03-01 — sl-mechanical** + +--- + +## System Overview + +Three independent protection layers: + +| Layer | File | Protects | +|-------|------|----------| +| Sensor head roll cage | `bumper_frame.scad` | RPLIDAR, RealSense, 4× IMX219 | +| Base plate bumper ring | `base_bumper.scad` | FC, motors, wiring, wheels | +| Stem impact sleeves | `stem_protector.scad` | 25 mm stem tube along length | + +--- + +## Design Constraints Met + +| Constraint | How addressed | +|-----------|---------------| +| RPLIDAR 360° FOV | Roll cage posts at 45°/135°/225°/315° (between cameras). At RPLIDAR scan-plane height (~105 mm from cage collar), posts are ~78 mm from stem CL — well outside RPLIDAR body (35 mm radius). Crown ring sits at Z=148 mm, above RPLIDAR top. | +| Camera views | Posts avoid all four camera directions (0°/90°/180°/270°). | +| FC status LEDs | Base bumper clips below plate edge — does not cover plate top face or sides. LEDs remain visible. | +| Removable | All parts clip or clamp; no permanent fasteners into robot structure. | +| No supports | All parts print flat-face-down (collar halves, posts, crown ring, brackets, sleeves). | +| Lightweight | PLA frame + TPU pads only where needed; crown ring is hollow, brackets are thin-wall. | + +--- + +## Part A — Sensor Head Roll Cage (`bumper_frame.scad`) + +### Printed parts + +| # | RENDER | Qty | Material | Settings | Notes | +|---|--------|-----|----------|----------|-------| +| 1 | `collar_front` | 1 | PETG | 5 perims, 40% infill | Flat-face-down | +| 2 | `collar_rear` | 1 | PETG | 5 perims, 40% infill | Flat-face-down; mirror of front | +| 3 | `post` | 4 | PETG or PLA | 4 perims, 25% infill | Lay flat on narrow face | +| 4 | `crown` | 1 | PETG or PLA | 4 perims, 30% infill | Flat (natural print orientation) | +| 5 | `tpu_pad` | 1 | TPU 95A | 3 perims, 20% infill, 25 mm/s | Snap-on crown impact ring | + +### Fasteners + +| # | Spec | Qty | Use | +|---|------|-----|-----| +| 6 | M4×25 SHCS | 2 | Cage collar clamping bolts | +| 7 | M4 hex nut | 2 | Captured in collar rear half | +| 8 | M4×10 set screw | 1 | Height/rotation lock (front collar) | +| 9 | M4×20 SHCS | 8 | Post-to-collar (2 per post, through boss) | +| 10 | M4×16 SHCS | 8 | Post-to-crown (2 per post, through crown top) | +| 11 | M4 hex nut | 8 | Under collar for post bolts | +| 12 | M4 flat washer | 16 | All M4 bolt heads | + +### Cage installation — height reference + +``` +Z from cage collar base: + Z = 0 mm — cage collar bottom (clamp to stem here) + Z = 24 mm — cage collar top + Z = 54 mm — sensor-head collar bottom (≈30 mm above cage collar) + Z = 90 mm — sensor-head collar top + platform + Z = 100 mm — RPLIDAR base (above anti-vibration ring) + Z = 130 mm — RPLIDAR top + Z = 148 mm — cage crown ring (12 mm clear of RPLIDAR) +``` + +> **Install the cage collar ~30 mm BELOW the sensor-head collar on the same 25 mm stem.** +> Slide collar down stem, position posts, attach crown ring, then tighten collar clamp bolts. + +--- + +## Part B — Base Plate Bumper (`base_bumper.scad`) + +### Printed parts + +| # | RENDER | Qty | Material | Notes | +|---|--------|-----|----------|-------| +| 13 | `corner` | 4 | PETG | Print upright (Z = tall dimension); 4 perims, 40% infill | +| 14 | `side` | 4 | PETG | Same orientation; 4 perims, 40% infill | +| 15 | `tpu_cap` | ~14 | TPU 95A | 40 mm segments; install end-to-end along all rails | + +### Rails (straight sections between clips) + +Rails can be 3D-printed from SCAD profile or procured: + +| # | Description | Qty | Size | Alternative | +|---|-------------|-----|------|-------------| +| 16 | Front rail | 1 | 8×50×270 mm PLA/PETG extrusion | 25×8 mm aluminium flat bar | +| 17 | Rear rail | 1 | 8×50×270 mm | same | +| 18 | Left side rail | 1 | 8×50×240 mm | same | +| 19 | Right side rail | 1 | 8×50×240 mm | same | + +> **DXF export:** `RENDER="rail_2d"` gives the 8×50 mm cross-section. Extrude to required length in CAM software. + +### Fasteners + +| # | Spec | Qty | Use | +|---|------|-----|-----| +| 20 | M4×16 SHCS | 16 | Rail to corner bracket (2 heights × 2 rails per corner = 4 per corner × 4) | +| 21 | M4×16 SHCS | 8 | Rail to side bracket (2 per side bracket × 4) | +| 22 | M3×8 set screw | 16 | Bracket lock to plate edge (2 per clip × 8 clips) | +| 23 | M4 hex nut | 24 | All M4 rail bolts | + +### Installation — clip orientation + +``` +TOP VIEW of base plate corner: + + plate edge (Y direction) + │ + ───────────┤ + │◄── corner_bracket arm sits here, M3 set screw bites plate edge + plate │ + ───────────┘ + ↑ + plate edge (X direction) +``` + +The clip slot engages the BOTTOM face of the plate. Slide bracket onto corner from below; tighten M3 set screws finger-tight → hand-tight. + +--- + +## Part C — Stem Protector (`stem_protector.scad`) + +### Printed parts + +| # | RENDER | Qty | Material | Notes | +|---|--------|-----|----------|-------| +| 24 | `front` | 3–4 | TPU 95A | Flat-face-down; qty = no. of stem zones to protect | +| 25 | `rear` | 3–4 | TPU 95A | Mirror; snap together around stem | + +### Recommended placement (25 mm stem, ~1000 mm total stem) + +| Position | Z from base plate | Purpose | +|----------|-----------------|---------| +| Sleeve 1 | 50–130 mm | Lowest zone — hit first in a side-fall | +| Sleeve 2 | 300–380 mm | Mid stem (near battery carousel) | +| Sleeve 3 | 600–680 mm | Upper stem | +| Sleeve 4 | 850–930 mm | Below sensor head collar | + +### No fasteners required +Sleeves snap closed around stem. For extra security on smooth aluminium stem: wrap one loop of 25 mm Velcro strap over each sleeve before snapping closed. + +--- + +## Tools Required for Assembly + +- M2.5 / M3 / M4 hex drivers +- Torque wrench: M4 cage-collar bolts 2 N·m, M3 set screws 0.8 N·m +- Thread locker (Loctite 243 blue) on cage crown M4 bolts + +--- + +## Mass Estimate + +| Part | Material | Est. mass | +|------|----------|-----------| +| Roll cage collar (×2 halves) | PETG | ~60 g | +| Roll cage posts (×4) | PETG | ~80 g | +| Crown ring | PETG | ~45 g | +| TPU crown pad | TPU | ~15 g | +| Base bumper (×4 corners + ×4 sides) | PETG | ~160 g | +| Base bumper TPU caps | TPU | ~40 g | +| Stem sleeves (×8 halves) | TPU | ~60 g | +| Fasteners | SS | ~40 g | +| **Total** | | **~500 g** | + +> **Balance note:** All protection hardware is symmetric about the robot's fore-aft axis. Roll cage is symmetric about all axes. Net CG shift is negligible (<5 mm vertical, <2 mm lateral). diff --git a/chassis/bumper_frame.scad b/chassis/bumper_frame.scad new file mode 100644 index 0000000..5d72d94 --- /dev/null +++ b/chassis/bumper_frame.scad @@ -0,0 +1,260 @@ +// ============================================================ +// bumper_frame.scad — Sensor Head Roll Cage Rev A 2026-03-01 +// Agent: sl-mechanical +// ============================================================ +// Roll cage for SaltyBot sensor head (25 mm OD stem). +// Protects RPLIDAR A1M8, RealSense D435i, and 4× IMX219 cameras +// when the robot tips over. +// +// Cage posts sit at 45°/135°/225°/315° (between camera arms at +// 0°/90°/180°/270°), minimising camera FOV obstruction. +// Crown ring sits ABOVE the RPLIDAR top — no scan-plane crossing. +// Posts lean outward so they are outside RPLIDAR body radius at +// scan-plane height. +// +// Component overview: +// cage_collar_half() — split stem collar, print 2× +// cage_post() — inclined rib, print 4×, lay flat +// cage_crown() — flat ring above RPLIDAR, print 1× +// tpu_crown_pad() — snap-on TPU impact strip, print in TPU +// +// RENDER options: +// "assembly" full 3-D view (default) +// "collar_front" front collar half for slicing +// "collar_rear" rear collar half for slicing +// "post" single post for slicing (print 4×) +// "crown" crown ring for slicing +// "tpu_pad" TPU snap strip for slicing +// ============================================================ + +RENDER = "assembly"; + +// ── Stem ───────────────────────────────────────────────────── +STEM_OD = 25.0; +STEM_BORE = 25.5; // +0.5 mm clearance (collar slides easily) + +// ── Cage collar ────────────────────────────────────────────── +COL_OD = 52.0; +COL_H = 24.0; // height (shorter than sensor-head collar) +COL_BOLT_X = 19.0; // M4 clamping bolt CL from stem axis (X) +COL_BOLT_D = 4.5; // M4 clearance +COL_NUT_W = 7.0; // M4 hex nut A/F +COL_NUT_H = 3.4; // M4 hex nut thickness + +// Post attachment boss on collar outer surface +// Boss at 45°/135°/225°/315°, protrudes POST_BOSS_H radially +POST_BOSS_H = 6.0; +POST_BOSS_W = 14.0; // boss width (tangential) +POST_BOSS_Z = 6.0; // boss bottom offset from collar base +POST_BOSS_HT = COL_H - POST_BOSS_Z - 4; // boss height on collar + +// ── Cage post ──────────────────────────────────────────────── +POST_BASE_R = COL_OD/2 + POST_BOSS_H; // radial start of post CL +POST_TIP_R = 108.0; // radial end of post at crown height +CROWN_Z = 148.0; // height of crown ring (above collar base) + // RPLIDAR top is ~120 mm above cage collar +POST_W = 12.0; // post width (tangential) +POST_T = 8.0; // post thickness (radial) +POST_LEN = sqrt(pow(POST_TIP_R - POST_BASE_R, 2) + pow(CROWN_Z, 2)); +POST_LEAN = atan((POST_TIP_R - POST_BASE_R) / CROWN_Z); // ~25° from vertical +// At RPLIDAR scan height (~105 mm), post is at radius: +// POST_BASE_R + (POST_TIP_R - POST_BASE_R) * (105/CROWN_Z) ≈ 78 mm +// >> RPLIDAR body radius 35 mm — no scan-plane intersection + +POST_BOLT_D = 4.5; // M4 clearance at each post end + +// ── Crown ring ─────────────────────────────────────────────── +CROWN_OD = 230.0; // crown outer diameter +CROWN_ID = 206.0; // crown inner diameter +CROWN_RH = 12.0; // crown ring height (thickness) +CROWN_PAD_D = 10.0; // TPU pad snap channel diameter (semi-circle) +CROWN_PAD_INSET = 5.0; // inset from crown outer edge + +// Post socket: slot in crown ring where post tip inserts +SOCK_W = POST_W + 0.4; // slot width (clearance) +SOCK_D = POST_T + 0.4; // slot depth +SOCK_H = CROWN_RH; // full ring thickness + +// ── TPU crown pad ──────────────────────────────────────────── +PAD_ID = CROWN_OD - 2 * CROWN_PAD_INSET; +PAD_OD = CROWN_OD + 2.0; +PAD_H = CROWN_PAD_D + 2.0; + +$fn = 64; +e = 0.01; + +// ───────────────────────────────────────────────────────────── +// cage_collar_half(side) +// side = "front" (+Y) | "rear" (-Y) +// Print flat-face-down. +// ───────────────────────────────────────────────────────────── +module cage_collar_half(side = "front") { + y_front = (side == "front"); + y_sign = y_front ? 1 : -1; + + difference() { + union() { + // D-shaped half body + intersection() { + cylinder(d = COL_OD, h = COL_H); + translate([-COL_OD/2, y_front ? 0 : -COL_OD/2, 0]) + cube([COL_OD, COL_OD/2, COL_H]); + } + + // Post attachment bosses at 45° and 135° (front) / 225° and 315° (rear) + for (a = y_front ? [45, -45] : [135, -135]) { + rotate([0, 0, a]) + translate([COL_OD/2, -POST_BOSS_W/2, POST_BOSS_Z]) + cube([POST_BOSS_H, POST_BOSS_W, POST_BOSS_HT]); + } + } + + // Stem bore + translate([0, 0, -e]) + cylinder(d = STEM_BORE, h = COL_H + 2*e); + + // M4 clamping bolt holes (Y direction through mating face) + for (bx = [-COL_BOLT_X, COL_BOLT_X]) { + translate([bx, y_front ? COL_OD/2 : 0, COL_H/2]) + rotate([90, 0, 0]) + cylinder(d = COL_BOLT_D, h = COL_OD/2 + e); + } + + // M4 hex nut pockets in rear half + if (!y_front) { + for (bx = [-COL_BOLT_X, COL_BOLT_X]) { + translate([bx, -(COL_OD/4 + e), COL_H/2]) + rotate([90, 0, 0]) + cylinder(d = COL_NUT_W / cos(30), h = COL_NUT_H + e, $fn = 6); + } + } + + // M4 set screw (front only, outer face → stem bore) + if (y_front) { + translate([0, COL_OD/2, COL_H * 0.7]) + rotate([90, 0, 0]) + cylinder(d = COL_BOLT_D, h = COL_OD/2 - STEM_BORE/2 + e); + } + + // M4 post attachment hole through each boss (radial direction) + for (a = y_front ? [45, -45] : [135, -135]) { + rotate([0, 0, a]) + translate([COL_OD/2 + e, 0, POST_BOSS_Z + POST_BOSS_HT/2]) + rotate([0, -90, 0]) + cylinder(d = POST_BOLT_D, h = POST_BOSS_H + COL_OD/8 + e); + } + } +} + +// ───────────────────────────────────────────────────────────── +// cage_post() +// For assembly: placed radially at angle a. +// For slicing ("post" RENDER): printed lying flat on XY plane. +// ───────────────────────────────────────────────────────────── +module cage_post_geometry() { + // Post spans from base (r = POST_BASE_R, z = 0) to tip (r = POST_TIP_R, z = CROWN_Z) + // Width POST_W in tangential direction, thickness POST_T in radial direction + difference() { + hull() { + translate([POST_BASE_R, -POST_W/2, 0]) + cube([POST_T, POST_W, 1]); + translate([POST_TIP_R, -POST_W/2, CROWN_Z - 1]) + cube([POST_T, POST_W, 1]); + } + + // M4 hole at base end (through POST_T, radially) + translate([POST_BASE_R + POST_T/2, 0, POST_BOSS_Z + POST_BOSS_HT/2]) + rotate([0, 90, 0]) + cylinder(d = POST_BOLT_D, h = POST_T + 2*e, center = true); + + // M4 hole at tip end + translate([POST_TIP_R + POST_T/2, 0, CROWN_Z - CROWN_RH/2]) + rotate([0, 90, 0]) + cylinder(d = POST_BOLT_D, h = POST_T + 2*e, center = true); + + // TPU pad snap groove on outer (high-r) face — runs along post length + // Groove: 5 mm wide × 2.5 mm deep semi-channel at midspan + translate([POST_TIP_R + POST_T - 0.1, -2.5, CROWN_Z * 0.3]) + cube([3, 5, CROWN_Z * 0.4]); + } +} + +// ───────────────────────────────────────────────────────────── +// cage_crown() +// Flat ring. 4 post sockets at 45/135/225/315°. +// TPU snap channel on outer rim. +// ───────────────────────────────────────────────────────────── +module cage_crown() { + difference() { + cylinder(d = CROWN_OD, h = CROWN_RH, $fn = 8); + + // Hollow centre + translate([0, 0, -e]) + cylinder(d = CROWN_ID, h = CROWN_RH + 2*e); + + // 4 post-tip sockets at 45/135/225/315° + for (a = [45, 135, 225, 315]) { + rotate([0, 0, a]) + translate([CROWN_ID/2 + (CROWN_OD-CROWN_ID)/4 - SOCK_D/2, + -SOCK_W/2, -e]) + cube([SOCK_D + e, SOCK_W, SOCK_H + 2*e]); + } + + // 2× M4 bolts per post socket (through ring top to post) + for (a = [45, 135, 225, 315]) { + rotate([0, 0, a]) + for (dw = [-POST_W/4, POST_W/4]) + translate([CROWN_ID/2 + (CROWN_OD-CROWN_ID)/4, + dw, -e]) + cylinder(d = POST_BOLT_D, h = CROWN_RH + 2*e); + } + + // TPU snap channel — semicircular groove on outer rim top face + translate([0, 0, CROWN_RH - CROWN_PAD_D/2]) + rotate_extrude() + translate([CROWN_OD/2 - CROWN_PAD_INSET, 0]) + circle(d = CROWN_PAD_D); + } +} + +// ───────────────────────────────────────────────────────────── +// tpu_crown_pad() — print in TPU, snaps into crown channel +// ───────────────────────────────────────────────────────────── +module tpu_crown_pad() { + // Ring with round cross-section slightly larger than channel + rotate_extrude() + translate([PAD_ID/2 + (PAD_OD - PAD_ID)/2 - CROWN_PAD_INSET, 0]) + circle(d = CROWN_PAD_D * 1.15); // slight interference for snap +} + +// ───────────────────────────────────────────────────────────── +// Render selector +// ───────────────────────────────────────────────────────────── +if (RENDER == "assembly") { + color("SkyBlue", 0.9) cage_collar_half("front"); + color("DodgerBlue", 0.9) mirror([0,1,0]) cage_collar_half("rear"); + for (a = [45, 135, 225, 315]) + color("LightGray", 0.85) + rotate([0, 0, a]) cage_post_geometry(); + color("Silver", 0.9) translate([0, 0, CROWN_Z]) cage_crown(); + color("OrangeRed", 0.7) translate([0, 0, CROWN_Z + CROWN_RH - CROWN_PAD_D/2]) + tpu_crown_pad(); + +} else if (RENDER == "collar_front") { + cage_collar_half("front"); + +} else if (RENDER == "collar_rear") { + cage_collar_half("rear"); + +} else if (RENDER == "post") { + // Orient flat for slicing: long axis along X, flat face on Z=0 + rotate([0, 0, -45]) // un-rotate from assembly angle + rotate([-90, 0, 0]) // lay flat + cage_post_geometry(); + +} else if (RENDER == "crown") { + cage_crown(); + +} else if (RENDER == "tpu_pad") { + tpu_crown_pad(); +} diff --git a/chassis/stem_protector.scad b/chassis/stem_protector.scad new file mode 100644 index 0000000..4cf569b --- /dev/null +++ b/chassis/stem_protector.scad @@ -0,0 +1,119 @@ +// ============================================================ +// stem_protector.scad — Clip-on Stem Sleeve Rev A 2026-03-01 +// Agent: sl-mechanical +// ============================================================ +// Split-sleeve impact protector for the 25 mm OD vertical stem. +// Print in TPU (Shore 95A) for energy absorption. +// Snap-fit closure — no tools, no hardware. +// Install 3–4 sleeves along the stem at 200 mm spacing. +// +// RENDER options: +// "assembly" both halves closed around stem (default) +// "front" front half for slicing +// "rear" rear half for slicing +// ============================================================ + +RENDER = "assembly"; + +// ── Stem ───────────────────────────────────────────────────── +STEM_OD = 25.0; +STEM_BORE = 25.6; // +0.6 mm — TPU flexible, snaps closed + +// ── Sleeve ─────────────────────────────────────────────────── +SLEEVE_OD = 38.0; // outer diameter +SLEEVE_H = 80.0; // height (print and trim to taste) + +WALL = (SLEEVE_OD - STEM_BORE) / 2; // ~6.2 mm wall + +// ── Snap-fit ───────────────────────────────────────────────── +// Tab on front half snaps into recess on rear half. +// Placed at both Z = SLEEVE_H*0.25 and Z = SLEEVE_H*0.75. +SNAP_W = 8.0; // snap tab width +SNAP_H = 3.0; // snap tab height (protrusion) +SNAP_T = 2.5; // snap tab thickness +SNAP_CL = 0.3; // recess clearance for TPU flex + +// ── Ribbing ────────────────────────────────────────────────── +// Optional radial ribs for grip and stiffness +RIB_N = 8; // number of ribs around circumference +RIB_H = 1.2; // rib height +RIB_W = 2.0; // rib width + +$fn = 64; +e = 0.01; + +// ───────────────────────────────────────────────────────────── +// sleeve_half(side) +// side = "front" (+Y) | "rear" (-Y) +// Split at Y=0 plane. Print flat-face-down. +// ───────────────────────────────────────────────────────────── +module sleeve_half(side = "front") { + y_front = (side == "front"); + y_sign = y_front ? 1 : -1; + + difference() { + union() { + // Main D-shaped half cylinder + intersection() { + cylinder(d = SLEEVE_OD, h = SLEEVE_H); + translate([-SLEEVE_OD/2, y_front ? 0 : -SLEEVE_OD/2, 0]) + cube([SLEEVE_OD, SLEEVE_OD/2, SLEEVE_H]); + } + + // Radial grip ribs on outer curved face + for (i = [0 : RIB_N/2 - 1]) { + ang = (i / (RIB_N/2 - 1)) * 150 - 75; // spread 150° on each half + rotate([0, 0, y_sign * ang + (y_front ? 0 : 180)]) + translate([SLEEVE_OD/2, -RIB_W/2, 0]) + cube([RIB_H, RIB_W, SLEEVE_H]); + } + + // Snap tab (front half) — 2 positions + if (y_front) { + for (zh = [SLEEVE_H * 0.25, SLEEVE_H * 0.75]) { + translate([-SNAP_W/2, SLEEVE_OD/2 - SNAP_T, zh - SNAP_H/2]) + cube([SNAP_W, SNAP_T + SNAP_H, SNAP_H]); + } + } + } + + // Stem bore + translate([0, 0, -e]) + cylinder(d = STEM_BORE, h = SLEEVE_H + 2*e); + + // Snap tab recess in rear half + if (!y_front) { + for (zh = [SLEEVE_H * 0.25, SLEEVE_H * 0.75]) { + translate([-(SNAP_W + SNAP_CL)/2, + SLEEVE_OD/2 - (SNAP_T + SNAP_H + SNAP_CL + e), + zh - (SNAP_H + SNAP_CL)/2]) + cube([SNAP_W + SNAP_CL, SNAP_H + SNAP_CL + e, SNAP_H + SNAP_CL]); + } + } + + // Mating face: very small 0.2 mm chamfer to prevent elephant-foot binding + translate([0, 0, -e]) + rotate([0, 0, y_front ? 0 : 180]) + translate([-SLEEVE_OD/2, -0.2, 0]) + cube([SLEEVE_OD, 0.2, SLEEVE_H + 2*e]); + } +} + +// ───────────────────────────────────────────────────────────── +// Render selector +// ───────────────────────────────────────────────────────────── +if (RENDER == "assembly") { + color("OrangeRed", 0.85) sleeve_half("front"); + color("Tomato", 0.85) mirror([0,1,0]) sleeve_half("rear"); + + // Reference stem + color("Silver", 0.3) + translate([0, 0, -10]) + cylinder(d = STEM_OD, h = SLEEVE_H + 20); + +} else if (RENDER == "front") { + sleeve_half("front"); + +} else if (RENDER == "rear") { + sleeve_half("rear"); +} -- 2.47.2