Merge pull request 'feat: Battery holder bracket (Issue #588)' (#589) from sl-mechanical/issue-588-battery-holder into main

This commit is contained in:
sl-jetson 2026-03-14 13:32:16 -04:00
commit ddf8332cd7

410
chassis/battery_holder.scad Normal file
View File

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