From 5556c061531c088fb0798f755d40eaec2eeb37b5 Mon Sep 17 00:00:00 2001 From: sl-mechanical Date: Sat, 14 Mar 2026 12:18:37 -0400 Subject: [PATCH] feat: Battery holder bracket (Issue #588) --- chassis/battery_holder.scad | 410 ++++++++++++++++++++++++++++++++++++ 1 file changed, 410 insertions(+) create mode 100644 chassis/battery_holder.scad diff --git a/chassis/battery_holder.scad b/chassis/battery_holder.scad new file mode 100644 index 0000000..5b077e7 --- /dev/null +++ b/chassis/battery_holder.scad @@ -0,0 +1,410 @@ +// ============================================================ +// battery_holder.scad — 6S LiPo Battery Holder for 2020 T-Slot Chassis +// Issue: #588 Agent: sl-mechanical Date: 2026-03-14 +// ============================================================ +// +// Parametric bracket holding a 6S 5000 mAh LiPo pack on 2020 aluminium +// T-slot rails. Designed for low centre-of-gravity mounting: pack sits +// flat between the two chassis rails, as close to ground as clearance +// allows. Quick-release via captive Velcro straps — battery swap in +// under 60 s without tools. +// +// Architecture: +// Tray → flat floor + perimeter walls, battery sits inside +// Rail saddles→ two T-nut feet drop onto 2020 rails, thumbscrew locks +// Strap slots → four pairs of slots for 25 mm Velcro strap loops +// XT60 window → cut-out in rear wall for XT60 connector exit +// Balance port→ open channel in front wall for balance lead routing +// QR tab → front-edge pull tab for one-handed battery extraction +// +// Part catalogue: +// Part 1 — battery_tray() Main tray body (single-piece print) +// Part 2 — rail_saddle() T-nut saddle foot (print x2 per tray) +// Part 3 — strap_guide() 25 mm Velcro strap guide (print x4) +// Part 4 — assembly_preview() +// +// Hardware BOM: +// 2× M3 × 16 mm SHCS + M3 hex nut T-nut rail clamp thumbscrews +// 2× 25 mm × 250 mm Velcro strap battery retention (hook + loop) +// 1× XT60 female connector (mounted on ESC/PDB harness) +// — battery slides in from front, Velcro strap over top — +// +// 6S LiPo target pack (verify with calipers — packs vary by brand): +// BATT_L = 155 mm (length, X axis in tray) +// BATT_W = 48 mm (width, Y axis in tray) +// BATT_H = 52 mm (height, Z axis in tray) +// Clearance 1 mm each side added automatically (BATT_CLEAR) +// +// Mounting: +// Rail span : RAIL_SPAN — distance between 2020 rail centrelines +// Default 80 mm; adjust to chassis rail spacing +// Saddle height: SADDLE_H — total height of saddle (tray floor above rail) +// Keep low for CoG; default 8 mm +// +// RENDER options: +// "assembly" full assembly preview (default) +// "tray_stl" Part 1 — battery tray +// "saddle_stl" Part 2 — rail saddle (print x2) +// "strap_guide_stl" Part 3 — strap guide (print x4) +// +// Export commands: +// openscad battery_holder.scad -D 'RENDER="tray_stl"' -o bh_tray.stl +// openscad battery_holder.scad -D 'RENDER="saddle_stl"' -o bh_saddle.stl +// openscad battery_holder.scad -D 'RENDER="strap_guide_stl"' -o bh_strap_guide.stl +// +// Print settings (all parts): +// Material : PETG +// Perimeters : 5 (tray, saddle), 3 (strap_guide) +// Infill : 40 % gyroid (tray floor, saddle), 20 % (strap_guide) +// Orientation: +// tray — floor flat on bed (no supports needed) +// saddle — flat face on bed (no supports) +// strap_guide — flat face on bed (no supports) +// ============================================================ + +$fn = 64; +e = 0.01; + +// ── Battery pack dimensions (verify with calipers) ──────────────────────────── +BATT_L = 155.0; // pack length (X) +BATT_W = 48.0; // pack width (Y) +BATT_H = 52.0; // pack height (Z) +BATT_CLEAR = 1.0; // per-side fit clearance + +// ── Tray geometry ───────────────────────────────────────────────────────────── +TRAY_FLOOR_T = 4.0; // tray floor thickness +TRAY_WALL_T = 4.0; // tray perimeter wall thickness +TRAY_WALL_H = 20.0; // tray wall height (Z) — cradles lower half of pack +TRAY_FILLET_R = 3.0; // inner corner radius + +// Inner tray cavity (battery + clearance) +TRAY_INN_L = BATT_L + 2*BATT_CLEAR; +TRAY_INN_W = BATT_W + 2*BATT_CLEAR; + +// Outer tray footprint +TRAY_OUT_L = TRAY_INN_L + 2*TRAY_WALL_T; +TRAY_OUT_W = TRAY_INN_W + 2*TRAY_WALL_T; +TRAY_TOTAL_H = TRAY_FLOOR_T + TRAY_WALL_H; + +// ── Rail interface ───────────────────────────────────────────────────────────── +RAIL_SPAN = 80.0; // distance between 2020 rail centrelines (Y) +RAIL_W = 20.0; // 2020 extrusion width +SLOT_NECK_H = 3.2; // T-slot neck height +SLOT_OPEN = 6.0; // T-slot opening width +SLOT_INN_W = 10.2; // T-slot inner width +SLOT_INN_H = 5.8; // T-slot inner height + +// ── T-nut / saddle geometry ─────────────────────────────────────────────────── +TNUT_W = 9.8; +TNUT_H = 5.5; +TNUT_L = 12.0; +TNUT_NUT_AF = 5.5; // M3 hex nut across-flats +TNUT_NUT_H = 2.4; +TNUT_BOLT_D = 3.3; // M3 clearance + +SADDLE_W = 30.0; // saddle foot width (X, along rail) +SADDLE_T = 8.0; // saddle body thickness (Z, above rail top face) +SADDLE_PAD_T = 2.0; // rubber-pad recess depth (optional anti-slip) + +// ── Velcro strap slots ──────────────────────────────────────────────────────── +STRAP_W = 26.0; // 25 mm strap + 1 mm clearance +STRAP_T = 4.0; // slot through-thickness (tray wall) +// Four slot pairs: one near each end of tray (X), one each side (Y) +// Slots run through side walls (Y direction) — strap loops over battery top + +// ── XT60 connector window (rear wall) ───────────────────────────────────────── +XT60_W = 14.0; // XT60 body width +XT60_H = 18.0; // XT60 body height (with cable exit) +XT60_OFFSET_Z = 4.0; // height above tray floor + +// ── Balance lead port (front wall) ──────────────────────────────────────────── +BAL_W = 40.0; // balance lead bundle width (6S = 7 wires) +BAL_H = 6.0; // balance lead channel height +BAL_OFFSET_Z = 8.0; // height above tray floor + +// ── Quick-release pull tab (front edge) ────────────────────────────────────── +QR_TAB_W = 30.0; // tab width +QR_TAB_H = 12.0; // tab height above front wall top +QR_TAB_T = 4.0; // tab thickness +QR_HOLE_D = 10.0; // finger-loop hole diameter + +// ── Strap guide clip ───────────────────────────────────────────────────────── +GUIDE_OD = STRAP_W + 6.0; +GUIDE_T = 3.0; +GUIDE_BODY_H = 14.0; + +// ── Fasteners ───────────────────────────────────────────────────────────────── +M3_D = 3.3; + +// ============================================================ +// RENDER DISPATCH +// ============================================================ +RENDER = "assembly"; + +if (RENDER == "assembly") assembly_preview(); +else if (RENDER == "tray_stl") battery_tray(); +else if (RENDER == "saddle_stl") rail_saddle(); +else if (RENDER == "strap_guide_stl") strap_guide(); + +// ============================================================ +// ASSEMBLY PREVIEW +// ============================================================ +module assembly_preview() { + // Ghost 2020 rails (Y direction, RAIL_SPAN apart) + for (ry = [-RAIL_SPAN/2, RAIL_SPAN/2]) + %color("Silver", 0.28) + translate([-TRAY_OUT_L/2 - 30, ry - RAIL_W/2, -SADDLE_T - TNUT_H]) + cube([TRAY_OUT_L + 60, RAIL_W, RAIL_W]); + + // Rail saddles (left and right) + for (sy = [-RAIL_SPAN/2, RAIL_SPAN/2]) + color("DimGray", 0.85) + translate([0, sy, -SADDLE_T]) + rail_saddle(); + + // Battery tray (sitting on saddles) + color("OliveDrab", 0.85) + battery_tray(); + + // Battery ghost + %color("SaddleBrown", 0.35) + translate([-BATT_L/2, -BATT_W/2, TRAY_FLOOR_T]) + cube([BATT_L, BATT_W, BATT_H]); + + // Strap guides (4×, two each end) + for (sx = [-TRAY_OUT_L/2 + STRAP_W/2 + TRAY_WALL_T + 8, + TRAY_OUT_L/2 - STRAP_W/2 - TRAY_WALL_T - 8]) + for (sy = [-1, 1]) + color("SteelBlue", 0.75) + translate([sx, sy*(TRAY_OUT_W/2), TRAY_TOTAL_H + 2]) + rotate([sy > 0 ? 0 : 180, 0, 0]) + strap_guide(); +} + +// ============================================================ +// PART 1 — BATTERY TRAY +// ============================================================ +// Single-piece tray: flat floor, four perimeter walls, T-nut saddle +// attachment bosses on underside, Velcro strap slots through side walls, +// XT60 window in rear wall, balance lead channel in front wall, and +// quick-release pull tab on front edge. +// +// Battery inserts from the front (−X end) — front wall is lower than +// rear wall so the pack slides in and the rear wall stops it. +// Velcro straps loop over the top of the pack through the side slots. +// +// Coordinate convention: +// X: along battery length (−X = front/plug-end, +X = rear/balance-end) +// Y: across battery width (centred, ±TRAY_OUT_W/2) +// Z: vertical (Z=0 = tray floor top face; −Z = underside → saddles) +// +// Print: floor flat on bed, PETG, 5 perims, 40% gyroid. No supports. +module battery_tray() { + // Short rear wall height (XT60 connector exits here — full wall height) + // Front wall is lower to allow battery slide-in + front_wall_h = TRAY_WALL_H * 0.55; // 55% height — battery slides over + + difference() { + union() { + // ── Floor ──────────────────────────────────────────────────── + translate([-TRAY_OUT_L/2, -TRAY_OUT_W/2, -TRAY_FLOOR_T]) + cube([TRAY_OUT_L, TRAY_OUT_W, TRAY_FLOOR_T]); + + // ── Rear wall (+X, full height) ─────────────────────────────── + translate([TRAY_INN_L/2, -TRAY_OUT_W/2, 0]) + cube([TRAY_WALL_T, TRAY_OUT_W, TRAY_WALL_H]); + + // ── Front wall (−X, lowered for slide-in) ──────────────────── + translate([-TRAY_INN_L/2 - TRAY_WALL_T, -TRAY_OUT_W/2, 0]) + cube([TRAY_WALL_T, TRAY_OUT_W, front_wall_h]); + + // ── Side walls (±Y) ─────────────────────────────────────────── + for (sy = [-1, 1]) + translate([-TRAY_OUT_L/2, + sy*(TRAY_INN_W/2 + (sy>0 ? 0 : -TRAY_WALL_T)), + 0]) + cube([TRAY_OUT_L, + TRAY_WALL_T, + TRAY_WALL_H]); + + // ── Quick-release pull tab (front wall top edge) ────────────── + translate([-TRAY_INN_L/2 - TRAY_WALL_T - e, + -QR_TAB_W/2, front_wall_h]) + cube([QR_TAB_T, QR_TAB_W, QR_TAB_H]); + + // ── Saddle attachment bosses (underside, one per rail) ───────── + // Bosses drop into saddle sockets; M3 bolt through floor + for (sy = [-RAIL_SPAN/2, RAIL_SPAN/2]) + translate([-SADDLE_W/2, sy - SADDLE_W/2, -TRAY_FLOOR_T - SADDLE_T/2]) + cube([SADDLE_W, SADDLE_W, SADDLE_T/2 + e]); + } + + // ── Battery cavity (hollow interior) ────────────────────────────── + translate([-TRAY_INN_L/2, -TRAY_INN_W/2, -e]) + cube([TRAY_INN_L, TRAY_INN_W, TRAY_WALL_H + 2*e]); + + // ── XT60 connector window (rear wall) ───────────────────────────── + // Centred on rear wall, low position so cable exits cleanly + translate([TRAY_INN_L/2 - e, -XT60_W/2, XT60_OFFSET_Z]) + cube([TRAY_WALL_T + 2*e, XT60_W, XT60_H]); + + // ── Balance lead channel (front wall) ───────────────────────────── + // Wide slot for 6S balance lead (7-pin JST-XH ribbon) + translate([-TRAY_INN_L/2 - TRAY_WALL_T - e, + -BAL_W/2, BAL_OFFSET_Z]) + cube([TRAY_WALL_T + 2*e, BAL_W, BAL_H]); + + // ── Velcro strap slots (side walls, 2 pairs) ────────────────────── + // Pair A: near front end (−X), Pair B: near rear end (+X) + // Each slot runs through the wall in Y direction + for (sx = [-TRAY_INN_L/2 + STRAP_W*0.5 + 10, + TRAY_INN_L/2 - STRAP_W*0.5 - 10]) + for (sy = [-1, 1]) { + translate([sx - STRAP_W/2, + sy*(TRAY_INN_W/2) - (sy > 0 ? TRAY_WALL_T + e : -e), + TRAY_WALL_H * 0.35]) + cube([STRAP_W, TRAY_WALL_T + 2*e, STRAP_T]); + } + + // ── QR tab finger-loop hole ──────────────────────────────────────── + translate([-TRAY_INN_L/2 - TRAY_WALL_T/2, + 0, front_wall_h + QR_TAB_H * 0.55]) + rotate([0, 90, 0]) + cylinder(d = QR_HOLE_D, h = QR_TAB_T + 2*e, center = true); + + // ── Saddle bolt holes (M3 through floor into saddle boss) ───────── + for (sy = [-RAIL_SPAN/2, RAIL_SPAN/2]) + translate([0, sy, -TRAY_FLOOR_T - e]) + cylinder(d = M3_D, h = TRAY_FLOOR_T + 2*e); + + // ── Floor lightening grid (non-structural area) ─────────────────── + // 2D grid of pockets reduces weight without weakening battery support + for (gx = [-40, 0, 40]) + for (gy = [-12, 12]) + translate([gx, gy, -TRAY_FLOOR_T - e]) + cylinder(d = 14, h = TRAY_FLOOR_T - 1.5 + e); + + // ── Inner corner chamfers (battery slide-in guidance) ───────────── + // 45° chamfers at bottom-front inner corners + translate([-TRAY_INN_L/2, -TRAY_INN_W/2 - e, -e]) + rotate([0, 0, 45]) + cube([4, 4, TRAY_WALL_H * 0.3 + e]); + translate([-TRAY_INN_L/2, TRAY_INN_W/2 + e, -e]) + rotate([0, 0, -45]) + cube([4, 4, TRAY_WALL_H * 0.3 + e]); + } +} + +// ============================================================ +// PART 2 — RAIL SADDLE +// ============================================================ +// T-nut foot that clamps to the top face of a 2020 T-slot rail. +// Battery tray boss drops into saddle socket; M3 bolt through tray +// floor and saddle body locks everything together. +// M3 thumbscrew through side of saddle body grips the rail T-groove +// (same thumbscrew interface as all other SaltyLab rail brackets). +// +// Saddle sits on top of rail (no T-nut tongue needed — saddle clamps +// from the top using a T-nut inserted into the rail T-groove from the +// end). Low profile keeps battery CoG as low as possible. +// +// Print: flat base on bed, PETG, 5 perims, 50% gyroid. +module rail_saddle() { + sock_d = SADDLE_W - 4; // tray boss socket diameter + + difference() { + union() { + // ── Main saddle body ────────────────────────────────────────── + translate([-SADDLE_W/2, -SADDLE_W/2, 0]) + cube([SADDLE_W, SADDLE_W, SADDLE_T]); + + // ── T-nut tongue (enters rail T-groove from above) ──────────── + translate([-TNUT_W/2, -TNUT_L/2, -SLOT_NECK_H]) + cube([TNUT_W, TNUT_L, SLOT_NECK_H + e]); + + // ── T-nut inner body (locks in groove) ──────────────────────── + translate([-TNUT_W/2, -TNUT_L/2, -SLOT_NECK_H - (TNUT_H - SLOT_NECK_H)]) + cube([TNUT_W, TNUT_L, TNUT_H - SLOT_NECK_H + e]); + } + + // ── Rail channel clearance (bottom of saddle straddles rail) ────── + // Saddle body has a channel that sits over the rail top face + translate([-RAIL_W/2 - e, -SADDLE_W/2 - e, -e]) + cube([RAIL_W + 2*e, SADDLE_W + 2*e, 2.0]); + + // ── M3 clamp bolt bore (through saddle body into T-nut) ─────────── + translate([0, 0, -SLOT_NECK_H - TNUT_H - e]) + cylinder(d = TNUT_BOLT_D, h = SADDLE_T + TNUT_H + 2*e); + + // ── M3 hex nut pocket (top face of saddle for thumbscrew) ───────── + translate([0, 0, SADDLE_T - TNUT_NUT_H - 0.5]) + cylinder(d = TNUT_NUT_AF / cos(30), + h = TNUT_NUT_H + 0.6, $fn = 6); + + // ── Tray boss socket (top face of saddle, tray boss nests here) ─── + // Cylindrical socket receives tray underside boss; M3 bolt centres + translate([0, 0, SADDLE_T - 3]) + cylinder(d = sock_d + 0.4, h = 3 + e); + + // ── M3 tray bolt bore (vertical, through saddle top) ────────────── + translate([0, 0, SADDLE_T - 3 - e]) + cylinder(d = M3_D, h = SADDLE_T + e); + + // ── Anti-slip pad recess (bottom face, optional rubber adhesive) ── + translate([0, 0, -e]) + cylinder(d = SADDLE_W - 8, h = SADDLE_PAD_T + e); + + // ── Lightening pockets ───────────────────────────────────────────── + for (lx = [-1, 1], ly = [-1, 1]) + translate([lx*8, ly*8, -e]) + cylinder(d = 5, h = SADDLE_T - 3 - 1 + e); + } +} + +// ============================================================ +// PART 3 — STRAP GUIDE +// ============================================================ +// Snap-on guide that sits on top of tray wall at each strap slot, +// directing the 25 mm Velcro strap from the side slot up and over +// the battery top. Four per tray, one at each slot exit. +// Curved lip prevents strap from cutting into PETG wall edge. +// Push-fit onto tray wall top; no fasteners required. +// +// Print: flat base on bed, PETG, 3 perims, 20% infill. +module strap_guide() { + strap_w_clr = STRAP_W + 0.5; // strap slot with clearance + lip_r = 3.0; // guide lip radius + + difference() { + union() { + // ── Body (sits on tray wall top edge) ───────────────────────── + translate([-GUIDE_OD/2, 0, 0]) + cube([GUIDE_OD, GUIDE_T, GUIDE_BODY_H]); + + // ── Curved guide lip (top of body, strap bends around this) ─── + translate([0, GUIDE_T/2, GUIDE_BODY_H]) + rotate([0, 90, 0]) + cylinder(r = lip_r, h = GUIDE_OD, center = true); + + // ── Wall engagement tabs (snap over tray wall top) ──────────── + for (sy = [0, -(TRAY_WALL_T + GUIDE_T)]) + translate([-strap_w_clr/2 - 3, sy - GUIDE_T, 0]) + cube([strap_w_clr + 6, GUIDE_T, GUIDE_BODY_H * 0.4]); + } + + // ── Strap slot (through body) ────────────────────────────────────── + translate([-strap_w_clr/2, -e, -e]) + cube([strap_w_clr, GUIDE_T + 2*e, GUIDE_BODY_H + 2*e]); + + // ── Wall clearance slot (body slides over tray wall) ────────────── + translate([-strap_w_clr/2 - 3 - e, + -TRAY_WALL_T - GUIDE_T, -e]) + cube([strap_w_clr + 6 + 2*e, + TRAY_WALL_T, GUIDE_BODY_H * 0.4 + 2*e]); + + // ── Lightening pockets on side faces ────────────────────────────── + for (lx = [-GUIDE_OD/4, GUIDE_OD/4]) + translate([lx, GUIDE_T/2, GUIDE_BODY_H/2]) + cube([6, GUIDE_T + 2*e, GUIDE_BODY_H * 0.5], center = true); + } +}