feat: bumper + frame crash protection (roll cage, base bumper, stem sleeves) #56

Merged
seb merged 1 commits from sl-mechanical/bumper-protection into main 2026-03-01 00:21:13 -05:00
4 changed files with 839 additions and 0 deletions

291
chassis/base_bumper.scad Normal file
View File

@ -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();
}

169
chassis/bumper_BOM.md Normal file
View File

@ -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` | 34 | TPU 95A | Flat-face-down; qty = no. of stem zones to protect |
| 25 | `rear` | 34 | TPU 95A | Mirror; snap together around stem |
### Recommended placement (25 mm stem, ~1000 mm total stem)
| Position | Z from base plate | Purpose |
|----------|-----------------|---------|
| Sleeve 1 | 50130 mm | Lowest zone — hit first in a side-fall |
| Sleeve 2 | 300380 mm | Mid stem (near battery carousel) |
| Sleeve 3 | 600680 mm | Upper stem |
| Sleeve 4 | 850930 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).

260
chassis/bumper_frame.scad Normal file
View File

@ -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();
}

119
chassis/stem_protector.scad Normal file
View File

@ -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 34 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");
}