saltylab-firmware/chassis/bumper_frame.scad
sl-mechanical beb0af7ce6 feat: bumper + frame crash protection for SaltyBot
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 <noreply@anthropic.com>
2026-02-28 23:30:06 -05:00

261 lines
12 KiB
OpenSCAD
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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