Files
busbar-designer/scad/hex_cell.scad
T
wenil 6bc922cabf Add hex holder designer page (/holder)
Server-side OpenSCAD renders STL from bundled hex_cell.scad with parameter
overrides via -D. Frontend is a Three.js viewer with auto-form generated
from /api/holder/params. 'Design busbars →' button posts the computed
cell coordinates to /api/projects and redirects to the busbar editor with
the holder cells pre-loaded.

  - holder.py:                openscad subprocess wrapper + compute_cells()
                              (Python mirror of get_hex_center_points_*)
  - scad/hex_cell.scad:       verbatim copy of Addy/Hex-Cell-Holder source
  - app.py:                   /holder route + /api/holder/{params,render,cells}
  - static/holder.html etc:   parameter form + Three.js STL viewer
  - Dockerfile / install.sh:  apt install openscad
  - static/index.html:        nav link Holder ↔ Busbars in topbar
2026-05-24 19:27:50 +03:00

1498 lines
48 KiB
OpenSCAD

// https://github.com/Addy771/Hex-Cell-Holder
// This script generates models of cell holders and caps for
// building battery packs using cylindrical cells.
// Original concept by ES user "SpinningMagnets"
// More info can be found here:
// https://endless-sphere.com/forums/viewtopic.php?f=3&t=90058
//
// This file was created by Addy and is released as public domain
// Contributors
// Albert Phan - Added boxes, stacking, and optimizations
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// BASIC CONFIGURATION
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
cell_dia = 21.2; // Cell diameter default = 18.4 for 18650s **PRINT OUT TEST FIT PIECE STL FIRST**
cell_height = 70; // Cell height default = 65 for 18650s
wall = 0.8; // Wall thickness around a single cell. Make as a multiple of the nozzle diameter. Spacing between cells is twice this amount. default = 1.2
// If using bought injection molded hexes and printing out the boxes, take the distance between the centers of 2 cells and divide by two for the wall thickness (((((pitch - diameter)/2). Add space for the protuding interlocking tabs in the cap or box clearances.
num_rows = 6;
num_cols = 12;
holder_height = 10; // Height of cell holder default = 10 (not including slot_height)
slot_height = 3; // Height of all slots default = 3 mm (set to 0 for no slots but that allows you to print without support)
col_slot_width = 8; // Width of slots between rows default = 8
row_slot_width = 8; // Width of slots along rows default = 8
pack_style = "rect"; // "rect" for rectangular pack, "para" for parallelogram, "tria" for triangle shaped pack (number of rows define the amount of rows at the bottom of the triangle. Columns get ignored)
wire_style = "strip"; // "strip" to make space to run nickel strips between cells. Default usage
// "bus" to make space for bus wires between rows
box_style = "both"; // "bolt" for bolting the box pack together
// "ziptie" for using zipties to fasten the box together. (ziptie heads will stick out),
// "both" default: uses bolts for the 4 corners and zipties inbetween. Useful for mounting the pack to something with zipties but while still using bolts to hold it together
part_type = "normal"; // "normal","mirrored", or "both". "assembled" is used for debugging. You'll want a mirrored piece if the tops and bottom are different ( ie. When there are even rows in rectangular style or any number of rows in parallelogram. The Console will tell you if you need a mirrored piece).
part = "box bottom"; // "holder" to generate cell holders,
// "cap" to generate pack end caps,
// "box lid" to generate box lid
// "box bottom" for box bottom
// "wire clamp" for strain relief clamp
// "insulator" for insulator piece to fit over the nickel strips
// "vertical box section" for vertical battery stacking boxes (print 1 section for every additional stacked pack)
box_lip = true; // Adds a lip to the box pieces. default = true.
wire_clamp_add = true; // Adds a wire exit hole out the side of the box lid.
insulator_as_support = true; // Print the insulator as a part of the holder support material.
cap_wall = 1.2; // Cap wall thickness (default = 1.2 recommend to make a multiple of nozzle dia)
cap_clearance = 0.2; // Clearance between holder and caps default = 0.2
box_wall = 2; // Box wall thickness (default = 2.0 recommend to make at least 4 * multiple of nozzle dia)
box_clearance = 0.2; // Clearance between holder and box default = 0.2
// Box clearances for wires
bms_clearance = 10; // Vertical space for the battery management system (bms) on top of holders, set to 0 for no extra space
box_bottom_clearance = 0; // Vertical space for wires on bottom of box
box_wire_side_clearance = 3; // Horizontal space from right side (side with wire hole opening) to the box wall for wires
box_nonwire_side_clearance = 0; // Horizontal space from left side (opposite of wire hole) to the box wall for wires
support_z_gap = 0.3; // Insulator gap to holder. default 0.3
insulator_tolerance = 1.5; // How much smaller to make the width of the insulator default 1.5
insulator_thickness = (slot_height-support_z_gap); // Thickness of insulator
wire_diameter = 5; // Diameter of 1 power wire used in the strain relief clamps default = 5 for 10 awg stranded silicon wire
wire_clamp_bolt_dia = 3; // Bolt dia used for clamping wire default = 3 for M3 bolt
clamp_factor = 0.7; // Factor of wire diameter to be clamped. Higher number is less clamping force (default=0.7 max=1.0)
bolt_dia = 3; // Actual dia of bolt default = 3 for M3 bolt
bolt_head_dia = 6; // Actual dia of bolt head default = 6 for M3 socket head bolt
bolt_head_thickness = 3; // Thickness (height) of bolt head default = 3 for M3 Socket head
ziptie_width = 8;
ziptie_thickness = 2.5;
////////////////////////////////////////////////////////////////////////////////////
// EXPERIMENTAL Vertical Holder Stacking
// Use at own risk - Stacking holders vertically requires more thought into electrical short prevention.
// For best use, col and row slots sizes should be the same so the pin is centered
// Rectangular packs only
// Using stacking pins or bolts require you to cut holes out for the kapton tape/fishpaper for the insulation.
// If you don't use stacking pins or bolts, you can print out insulators (use part = insulator) and add kapton/fishpaper without cutting holes in it on top.
// The fishpaper is a last line of protection to prevent shorts in case the plastic insulation melts due to a malfunction.
///////////////////////////////////////////////////////////////////////////////////
stacking_pins = false; // Adds pins and holes for stacking holders vertically. Make sure col and row slots are the same width. You'll have to think about how to insulate the strips properly. Maybe precut kapton/fishpaper?
stacking_pin_dia = 3; // Default 3 mm. Smaller than 3 not recommended.
stacking_pin_alt_style = false; // Alternate style of pins that are longer and go into the holder deeper. (Used when the triangle islands are too small for a hole)
stacking_bolts = false; // Adds holes through the holders to bolt them (if not using box to bolt them together).
// !!!!!!MAKE SURE BOLTS DO NOT SHORT NICKEL STRIPS!!!!
// Don't use with stacking pins. You'll need mirrored pieces.
stacking_bolt_dia = 4.5; // Bolt dia. Make slightly bigger for bolt fit. Watch out for too large bolts that cut too much out of the holder.
num_pack_stacks = 1; // How many additional packs you will stack vertically. Affects part = vertical box section. (set to 1 if you just want to print single sections and glue them together. 1 section for every additional pack stack)
///////////////////////////////////////////////////////////////////////////////////
// ADVANCED CONFIGURATION for users that need to customize everything
//////////////////////////////////////////////////////////////////////////////////
cell_top_overlap = 3; // How big the opening overlaps the cell default = 3
opening_dia = cell_dia-cell_top_overlap*2; // Circular opening to expose cell
separation = 1; // Separation between cell top and wire slots (aka tab thickness) default = 1
wire_hole_width = 15; // Width of wire hole default = 15
wire_hole_length = 10; // Length of the wireclamp that sticks out default = 10
wire_top_wall = 4; // Thickness of top wire wall default = 4mm
clamp_plate_height = 4; // default = 4
bolt_dia_clearance = 1; // Amount of extra diameter for bolt holes default = 1
box_lip_height = box_wall * 0.75; // Height of lip default = box_wall * 0.75
box_lip_width = box_wall * 0.5; // Width of lip default = box_wall * 0.5
stacking_pins_tolerance = 0.5; // How much larger for the stacking pin hole compared to it's pin diameter
spacer_overhang = box_clearance + 3; // Amount of spacer overhang to hold the holders default = 3
flip_holders = false; // Mostly used for taking pngs
// cell_tab_width = 5; // Width of tab that keeps the cell in the holder default = 5
// cell_tab_length = 3; // Approx Length of tab that keeps the cell in the holder default = 3
//////////////////////////////////////////////////////
// Don't forget to do a test fit print
//////////////////////////////////////////////////////
// END OF CONFIGURATION
////////////////////////////////////////////////////////////////////////
// TODO:
// [x] Vertical Stacking Pins
// [x] Vertical Stacking Bolts
// [x] Add insulators
// [x] rect support
// [x] other styles support
// [x] Fixed boxes spacers
// [x] Add vertical stacking boxes
// [x] Add insulator to bat file
// [] Add some more echo helper messages for mirrored pieces wrt to stacking pins/bolt
// [x] Add insulation to "both" part types
// [x] Don't do mirrored versions for Both parts unless needed.
// [x] Double check box_lip_height usage versus box_wall/2 in some cases with the lid and bottom lips
///////////////////////////////////////////////////////////////////////////
// NON-Configurable helper variables
///////////////////////////////////////////////////////////////////////////
$fn = 50; // Number of facets for circular parts.
hextra = 0.0001; // enlarge hexes by this to make them overlap
extra = 1; // for proper differences()
spacing = 4; // Spacing between top and bottom pieces
box_total_height = get_mock_pack_height() + 2 * (box_wall + box_clearance) + bms_clearance + box_bottom_clearance;
box_lid_height = (holder_height + slot_height)/2 + (box_clearance + box_wall) + bms_clearance; // box lid to middle of holder
box_bottom_height = box_total_height - box_lid_height;
vertical_box_section_height = get_mock_pack_height();
hex_w = (cell_dia + 2*wall); // Width of one hex cell
hex_pt = (hex_w/2) / cos(30); // Half the distance of point to point of a hex aka radius
cell_radius = cell_dia/2;
box_clearance_x = box_clearance * cos(30); // Used whenever we are translating in the x direction from the hexes
box_wall_x = box_wall * cos(30); // Used whenever we are translating in the x direction from the hexes
wire_clamp_support = hex_pt + box_clearance + box_wall - wire_hole_width/2 ; // Place for strain relief clamp to screw into
wire_clamp_nib_dia = 5;
if (part_type == "mirrored")
{
if (part == "cap")
{
mirror([0,1,0])
cap(cap_wall,cap_clearance);
}
else if (part == "holder")
{
mirror([0,1,0])
rotate([0,180,0])
holders();
}
}
else if(part_type == "both")
{
if (part == "cap")
{
cap(cap_wall,cap_clearance);
mirror([0,1,0])
translate([0,2*hex_pt + 2 * (cap_wall + cap_clearance) + spacing,0])
cap(cap_wall,cap_clearance);
}
else if (part == "holder")
{
//rotate([0, 180, 0]) //flips around all the holders
{
// First holder
holders();
// Second holder
if(pack_style == "para")
{
mirror([0,1,0])
translate([hex_w*0.5,1.5*hex_pt + spacing,0])
holders();
}
else if(pack_style == "rect")
{
if(num_rows % 2 == 1) // If odd pack move pack over to nest properly
{
if(stacking_bolts)
{
mirror([1,0,0]) // mirrored for bolt stacking holes
rotate([0,0,180])
translate([-(hex_w*0.5), (1.5*(hex_pt) + spacing),0])
holders();
}
else // not mirrored for anything but bolt stacking holes
{
translate([-(hex_w*0.5), -(1.5*(hex_pt) * num_rows+ spacing),0])
holders();
}
}
else // if even pack
{
mirror([0,1,0]) // mirrored for bolt stacking holes
translate([hex_w*0.5,1.5*hex_pt + spacing,0])
holders();
}
}
}
}
else if (part == "box lid" || part == "box bottom")
{
box_bottom();
translate([0,-(hex_pt * 2 + 2 * (box_wall + box_clearance) + spacing), 0])
mirror([0,0,1])
rotate([180,0,0])
box_lid();
}
}
else if(part_type == "assembled")
{
// TESTING difference section analysis
difference()
{
union()
{
mock_pack(); // for debugging for now
if(part == "vertical box section")
{
// iterate through packs
for(i = [1:num_pack_stacks])
{
translate([0,0,get_mock_pack_height()*i])
{
mock_pack();
// vertical section box
color("darkgreen", alpha =0.8)
translate([0,0,box_bottom_height -(box_clearance+box_wall)-get_mock_pack_height()])
vertical_box_section();
if(i == num_pack_stacks)
{
// lid
translate([0,0,box_bottom_height + box_lid_height - 2 * (box_wall + box_clearance) - box_bottom_clearance])
mirror([0,0,1])
{
color("green", alpha = 0.7)
box_lid();
if(wire_clamp_add)
{
color("orange", alpha = 0.7)
translate([(get_hex_center_x_length(num_cols + 1) + box_wire_side_clearance + box_wall + box_clearance + wire_hole_length/2),0,box_lid_height-box_wall-box_clearance])
mirror([0,0,1])
wire_clamp();
}
}
}
}
}
}
else
{
//normal non stacking assembly
// lid
translate([0,0,box_bottom_height-(box_wall +box_clearance)*2+box_lid_height])
mirror([0,0,1])
{
color("green", alpha = 0.7)
box_lid();
if(wire_clamp_add)
{
color("orange", alpha = 0.7)
translate([(get_hex_center_x_length(num_cols + 1) + box_wire_side_clearance + box_wall + box_clearance + wire_hole_length/2),0,box_lid_height-box_wall-box_clearance])
mirror([0,0,1])
wire_clamp();
}
}
}
// bottom
color("lightgreen", alpha = 0.7)
translate([0,0,0])
box_bottom();
}
// // sectional analysis testing cutout
// translate([0,50,50])
// cube([150,100,2000], center = true);
}
// Additional assembled packs
// translate([(get_hex_center_x_length(num_rows+2)+ 2*(box_wall + box_clearance) + box_nonwire_side_clearance + box_wire_side_clearance)*2,get_hex_center_y_length(num_cols+2) + 2*(box_wall + box_clearance),0])
// {
// mock_pack(); // for debugging for now - 2 * (box_wall + box_clearance) - bms_clearance
// translate([0,0,get_mock_pack_height() + bms_clearance])
// mirror([0,0,1])
// {
// color("green", alpha = 0.7)
// box_lid();
// if(wire_clamp_add)
// {
// color("orange", alpha = 0.7)
// translate([(get_hex_center_x_length(num_cols + 1) + box_wire_side_clearance + box_wall + box_clearance + wire_hole_length/2),0,box_lid_height-box_wall-box_clearance])
// mirror([0,0,1])
// wire_clamp();
// }
// }
// }
// translate([(get_hex_center_x_length(num_rows+2)+ 2*(box_wall + box_clearance) + box_nonwire_side_clearance + box_wire_side_clearance)*4,(get_hex_center_y_length(num_cols+2) + 2*(box_wall + box_clearance)) * 3,0])
// {
// mock_pack(); // for debugging for now
// color("lightgreen", alpha = 0.7)
// translate([0,0,-(box_bottom_clearance)])
// box_bottom();
// }
}
else if(part_type == "mock pack")
{
mock_pack();
}
else // if Normal
{
if (part == "cap")
{
cap(cap_wall,cap_clearance);
}
else if (part == "holder")
rotate([0,0,0])
{
holders();
}
else if (part == "box lid")
{
translate([0,get_hex_center_y_length(num_cols),0])
mirror([0,0,1])
rotate([180,0,0])
box_lid();
}
else if (part == "box bottom")
{
box_bottom();
}
else if (part == "wire clamp")
wire_clamp();
else if(part == "insulator")
{
holder_insulators();
}
else if(part == "vertical box section")
{
vertical_box_section(num_pack_stacks);
}
else if(part == "flipped holder png")
{
rotate([0,180,0])
holders();
}
else if(part == "testing")
{
translate([0,0,0])
{
box_lid();
}
// Testing
translate([0,-get_hex_center_y_length(num_rows+2) - (box_wall + box_clearance)*2,0])
box_bottom();
translate([0,-get_hex_center_y_length(num_rows+2)*2 - (box_wall + box_clearance)*4,0])
vertical_box_section();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// echos and info
echo(hex_cell_center_to_center_distance = get_hex_center_x_length(2));
echo(pack_height_holder = get_mock_pack_height());
echo(box_lid_height = box_lid_height);
echo(box_bottom_height = box_bottom_height);
echo(box_total_height = box_total_height);
echo(box_total_length = get_box_total_length());
echo(box_total_width = get_box_total_width());
echo(vertical_box_section_height = vertical_box_section_height * (num_pack_stacks) + box_lip_height);
echo(box_total_height_with_vertical_stacking = box_bottom_height + box_lid_height + vertical_box_section_height * num_pack_stacks - box_lip_height);
echo(total_width_holder = get_hex_center_y_length(num_rows)+hex_pt*2);
if (pack_style == "rect")
{
// Rectangular style
echo(total_length_holder=get_holder_rect_length());
if((num_rows % 2) == 0) // Even?
echo("\n******************************************************* \n Top and bottom are different. Don't forget to do a mirrored holder\n*******************************************************");
}
else if(pack_style == "para")
{
// Parallelogram style
echo(total_length_holder=hex_w*(num_cols+0.5*(num_rows-1)));
echo("\n******************************************************* \n Top and bottom are different. Don't forget to do a mirrored holder\n*******************************************************");
}
else if(pack_style == "tria")
{
// Triangle style
echo(total_length_holder=hex_w*(num_rows-1));
echo("\n******************************************************* \n Top and bottom are not different. But don't forget to print twice ;)\n*******************************************************");
}
if (part_type == "mirrored" && (part == "box lid" || part == "box bottom"))
echo("\n******************************************************* \n Please choose Normal for box lid or box bottom as there aren't mirrored versions of them.\n*******************************************************");
if (pack_style == "tria" && (part == "box lid" || part == "box bottom" || part == "cap"))
echo("\n******************************************************* \n There are currently no boxes and caps for triangle style\n*******************************************************");
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
module cap(cap_wall,cap_clearance,cap_height = holder_height+slot_height,positive_x = 0, negative_x = 0, positive_y = 0, negative_y = 0)
{
difference()
{
// Positive Hull
cap_positive(cap_wall,cap_clearance,cap_height,positive_x,negative_x,positive_y,negative_y);
// Negative Hull
cap_negative(cap_wall,cap_clearance,cap_height,positive_x,negative_x,positive_y,negative_y);
}
}
// Generates the rectangular cap positive piece used in caps and boxes. Default height is holder height
// height includes wall thickness and clearance
// Z Origin is -(cap_wall + cap_clearance)
// Positive_x = amount of clearance between the positive x box wall and the holder.
// Negative_x = amount of clearance between the negative x box wall and the holder.
// Same goes for y
module cap_positive(cap_wall,cap_clearance,cap_height = holder_height,positive_x = 0, negative_x = 0, positive_y = 0, negative_y = 0)
{
if(pack_style=="rect")
{
translate([0,0,-(cap_wall + cap_clearance)])
hull()
{
// Generate 4 hexes in each corner and hull them together
// [0,0] Bottom left
translate([-negative_x,-negative_y,0])
hex(cap_height,hex_pt + cap_wall + cap_clearance);
// [1,0] Bottom right
translate([get_hex_center_x_length(num_cols + 0.5)+ positive_x,-(negative_y),0])
hex(cap_height,hex_pt + cap_wall + cap_clearance);
// [0,1] Top left
translate([-(negative_x),get_hex_center_y_length(num_rows) + positive_y,0])
hex(cap_height,hex_pt + cap_wall + cap_clearance);
// [1,1] Top right
translate([get_hex_center_x_length(num_cols + 0.5) + positive_x,get_hex_center_y_length(num_rows) + positive_y,0])
hex(cap_height,hex_pt + cap_wall + cap_clearance);
}
}
else if(pack_style=="para")
{
translate([0,0,-(cap_wall + cap_clearance)])
hull()
{
// Generate 4 hexes in each corner and hull them together
// [0,0] Bottom left
translate([-negative_x,-negative_y,0])
hex(cap_height,hex_pt + cap_wall + cap_clearance);
// [1,0] Bottom right
translate([get_hex_center_x_length(num_cols)+ positive_x,-(negative_y),0])
hex(cap_height,hex_pt + cap_wall + cap_clearance);
// [0,1] Top left
translate([get_hex_center_x_length(num_rows/2 +0.5)-(negative_x),get_hex_center_y_length(num_rows) + positive_y,0])
hex(cap_height,hex_pt + cap_wall + cap_clearance);
// [1,1] Top right
translate([get_hex_center_x_length(num_cols + num_rows/2-0.5) + positive_x,get_hex_center_y_length(num_rows) + positive_y,0])
hex(cap_height,hex_pt + cap_wall + cap_clearance);
}
}
}
// Generates the cap negative piece (as a positive to be cut out using difference) used in cap and box. This is basically the same as cap_positive but the hexes are smaller by cap_wall and
// z origin = -cap_clearance
module cap_negative(cap_wall,cap_clearance,cap_height = holder_height,positive_x = 0, negative_x = 0, positive_y = 0, negative_y = 0)
{
translate([0,0,-cap_clearance])
{
if(pack_style=="rect")
{
hull()
{
// Generate 4 hexes in each corner and hull them together
// [0,0] Bottom left
translate([-negative_x,-negative_y,0])
hex(cap_height,hex_pt + cap_clearance);
// [1,0] Bottom right
translate([get_hex_center_x_length(num_cols + 0.5)+ positive_x,-(negative_y),0])
hex(cap_height,hex_pt + cap_clearance);
// [0,1] Top left
translate([-(negative_x),get_hex_center_y_length(num_rows) + positive_y,0])
hex(cap_height,hex_pt + cap_clearance);
// [1,1] Top right
translate([get_hex_center_x_length(num_cols + 0.5) + positive_x,get_hex_center_y_length(num_rows) + positive_y,0])
hex(cap_height,hex_pt + cap_clearance);
}
}
else if(pack_style=="para")
{
hull()
{
// Generate 4 hexes in each corner and hull them together
// [0,0] Bottom left
translate([-negative_x,-negative_y,0])
hex(cap_height,hex_pt + cap_clearance);
// [1,0] Bottom right
translate([get_hex_center_x_length(num_cols)+ positive_x,-(negative_y),0])
hex(cap_height,hex_pt + cap_clearance);
// [0,1] Top left
translate([get_hex_center_x_length(num_rows/2 +0.5)-(negative_x),get_hex_center_y_length(num_rows) + positive_y,0])
hex(cap_height,hex_pt + cap_clearance);
// [1,1] Top right
translate([get_hex_center_x_length(num_cols + num_rows/2-0.5) + positive_x,get_hex_center_y_length(num_rows) + positive_y,0])
hex(cap_height,hex_pt + cap_clearance);
}
}
}
}
module box_lid()
{
difference()
{
union()
{
difference()
{
union()
{
// Positive
cap_positive(box_wall,box_clearance,box_lid_height,box_wire_side_clearance, box_nonwire_side_clearance);
// Lip Positive ( lip is added to box_lid and subtracted from box_bottom)
if(box_lip)
{
translate([0,0,box_lid_height-box_lip_width])
cap_positive(box_lip_width,box_clearance,box_lip_height,box_wire_side_clearance,box_nonwire_side_clearance);
}
// Wire support hole
if(wire_clamp_add)
{
translate([(num_cols * hex_w + box_wire_side_clearance + box_clearance_x + box_wall_x) - (box_wall_x + box_clearance_x + num_cols/2 * hex_w/2 + extra),-(wire_hole_width + 2 * (wire_clamp_support)) /2,-(box_wall + box_clearance)])
cube([wire_hole_length + box_wall_x + box_clearance_x + num_cols/2 * hex_w/2 + extra,wire_hole_width + 2 * (wire_clamp_support),box_lid_height]);
}
}
// Negatives
cap_negative(box_wall,box_clearance,box_lid_height*2,box_wire_side_clearance,box_nonwire_side_clearance);
if(wire_clamp_add)
{
// Wire hole cutout
translate([(num_cols)*hex_w+box_clearance_x + box_wall_x +box_wire_side_clearance,0,wire_top_wall - box_wall + box_lid_height/2])
cube([(wire_hole_length + box_wall_x + box_clearance_x + num_cols/2 * hex_w/2) * 2 + box_wall *3,wire_hole_width,box_lid_height], center = true);
// Strain relief bolt cutouts
translate([(num_cols)*hex_w+box_clearance_x + box_wall_x + box_wire_side_clearance + wire_hole_length/2,(wire_hole_width + wire_clamp_support)/2,0])
cylinder(d = wire_clamp_bolt_dia * 0.9, h=wire_top_wall + box_lid_height);
translate([(num_cols)*hex_w+box_clearance_x + box_wall_x + box_wire_side_clearance + wire_hole_length/2,-(wire_hole_width + wire_clamp_support)/2,0])
cylinder(d = wire_clamp_bolt_dia * 0.9, h=wire_top_wall + box_lid_height);
}
}
// Lid supports
if(box_lip)
both_box_holder_supports(box_lid_height + box_lip_height, bms_clearance);
else
both_box_holder_supports(box_lid_height, bms_clearance);
}
// Other cutouts of entire box lid
generate_box_lid_holes(lid = true);
}
}
module box_bottom()
{
difference()
{
union()
{
cap(box_wall,box_clearance,box_bottom_height,box_wire_side_clearance,box_nonwire_side_clearance);
both_box_holder_supports(box_bottom_height,box_bottom_clearance);
}
// Other cutouts of entire box bottom
// Lip cutout
if(box_lip)
{
translate([0,0,box_bottom_height-box_lip_width-box_lip_height])
{
cap_negative(box_wall,box_clearance + box_lip_width,box_lid_height,box_wire_side_clearance,box_nonwire_side_clearance); // z origin -(box_clearance + box_lip_width)
}
}
generate_box_lid_holes();
}
}
// Creates a mock pack for debugging
// Origin is the bottom of the center of the first hex cell
module mock_pack()
{
color("blue") holders();
// add 18650s
if(pack_style == "rect")
{
for(hex_list = get_hex_center_points_rect(num_rows,num_cols))
{
// Iterate through each hex center and place a cell
translate([hex_list.x,hex_list.y,slot_height + separation])
color("CornflowerBlue")mock_cell();
}
}
else if(pack_style == "para")
{
for(hex_list = get_hex_center_points_para(num_rows,num_cols))
{
// Iterate through each hex center and place a cell
translate([hex_list.x,hex_list.y,slot_height + separation])
color("CornflowerBlue")mock_cell();
}
}
color("blue")
translate([0,0,slot_height + separation + cell_height + slot_height + separation])
mirror([0,0,1])
holders();
}
// Creates a mock cell. Origin is bottom of 1st hex cell holder.
module mock_cell()
{
cylinder(d = cell_dia, h = cell_height);
}
// Generates the holders
module holders()
{
if(insulator_as_support)
{
translate([0,0,-holder_height+slot_height])
holder_insulators();
}
translate([0, 0, holder_height])
{
difference()
{
union()
{
if(pack_style == "rect")
{
for(hex_list = get_hex_center_points_rect(num_rows,num_cols))
{
// Iterate through each hex center and place a hex cell
translate([hex_list.x,hex_list.y,0])
pick_hex();
}
}
else if(pack_style == "para")
{
for(hex_list = get_hex_center_points_para(num_rows,num_cols))
{
// Iterate through each hex center and place a hex cell
translate([hex_list.x,hex_list.y,0])
pick_hex();
}
}
else if(pack_style == "tria")
{
for(hex_list = get_hex_center_points_tria(num_rows,num_cols))
{
// Iterate through each hex center and place a hex cell
translate([hex_list.x,hex_list.y,0])
pick_hex();
}
}
// Other unions
// Stacking Pins here
if(stacking_pins)
{
for(hex_list = get_pin_list_rect(num_rows,num_cols))
{
// add pins
if(stacking_pin_alt_style)
{
translate([hex_list.x,hex_list.y,-(holder_height + slot_height)])
{
// increase triangle island height by one slot height
linear_extrude(height = slot_height)
{
polygon(points=[
// generate points
for(a = [30,150,270])[(hex_pt-(0.5*col_slot_width)/cos(60))*cos(a),(hex_pt-(0.5*col_slot_width)/cos(60))*sin(a)],
]);
}
// add pin
translate([0,0,-slot_height*0.5])
cylinder(d = stacking_pin_dia, h = slot_height);
}
}
else
{
translate([hex_list.x,hex_list.y,-(holder_height + slot_height*0.5)])
cylinder(d = stacking_pin_dia, h = slot_height);
}
}
}
}
// Cutouts
// Stacking Pins
if(stacking_pins)
{
// do pin holes
for(hex_list = get_pin_holes_list_rect(num_rows,num_cols))
{
if(stacking_pin_alt_style)
{
// delete triangle
translate([hex_list.x,hex_list.y,-(holder_height + extra)])
linear_extrude(height = slot_height + extra)
{
polygon(points=[
// generate points
for(a = [90,210,330])[hex_pt*cos(a),hex_pt*sin(a)]
]);
}
}
// add hole
translate([hex_list.x,hex_list.y,-holder_height-extra])
cylinder(d = stacking_pin_dia + stacking_pins_tolerance, h = slot_height *2 + extra);
}
}
// Stacking Bolts
if(stacking_bolts)
{
for(hex_list = get_pin_holes_list_rect(num_rows,num_cols))
{
// delete triangle
translate([hex_list.x,hex_list.y,-(holder_height + extra)])
linear_extrude(height = slot_height + extra)
{
polygon(points=[
// generate points
for(a = [90,210,330])[hex_pt*cos(a),hex_pt*sin(a)]
]);
}
// add hole
translate([hex_list.x,hex_list.y,-(holder_height + extra)])
cylinder(d = stacking_bolt_dia, h = holder_height*2);
}
}
}
}
}
module pick_hex()
{
if (wire_style == "strip")
strip_hex();
else if (wire_style == "bus")
{
bus_hex();
}
else
strip_hex();
}
module strip_hex()
{
mirror([0,0,1])
{
difference()
{
union()
{
// Hex block
hex(holder_height, hex_pt + hextra);
}
// Top opening
translate([0,0,-1])
cylinder(h=holder_height+2,d=opening_dia);
// Cell space
cylinder(h=2 *(holder_height-slot_height-separation) ,d=cell_dia, center=true);
// 1st column slot
rotate([0,0,60])
translate([0,0,holder_height + slot_height])
cube([hex_w+1,col_slot_width,4*slot_height], center=true);
// 2nd column slot
rotate([0,0,-60])
translate([0,0,holder_height + slot_height])
cube([hex_w+1,col_slot_width,4*slot_height], center=true);
// Row slot
translate([0,0,holder_height + slot_height])
cube([hex_w+1,row_slot_width,4*slot_height], center=true);
}
//cell_tabs(); // older style of cell tabs. Not used anymore
}
}
module bus_hex()
{
mirror([0,0,1])
{
difference()
{
// Hex block
hex(holder_height, hex_pt + hextra);
// Top opening
translate([0,0,-1])
cylinder(h=holder_height+2,d=opening_dia);
// Cell space
cylinder(h=2 *(holder_height-slot_height-separation) ,d=cell_dia, center=true);
// 1st column slot
rotate([0,0,60])
translate([0,0,holder_height])
cube([hex_w+1,col_slot_width,2*slot_height], center=true);
// 2nd column slot
rotate([0,0,-60])
translate([0,0,holder_height])
cube([hex_w+1,col_slot_width,2*slot_height], center=true);
// Row slot A
translate([0,(hex_pt*cos(60) + hex_pt)/2,holder_height])
cube([hex_w + extra,row_slot_width,2*slot_height], center=true);
// Row slot B
translate([0,-(hex_pt*cos(60) + hex_pt)/2,holder_height])
cube([hex_w + extra,row_slot_width,2*slot_height], center=true);
}
//cell_tabs(); // older style of cell tabs. Not used anymore
}
}
// 3 tabs in the holder that keep the cell in. Not used anymore.
module cell_tabs()
{
// Tabs
if(wire_style == "strip")
{
for(a = [1,3,5])
{
difference()
{
intersection()
{
translate([ cell_radius*sin(a*60),cell_radius*cos(a*60),holder_height-(separation/2 + slot_height)])
rotate([0,0,a*30])cube([cell_tab_length * 2 + wall, cell_tab_width, separation],center=true);
hex(holder_height, hex_pt + hextra);
}
// Difference with strip cutouts
union()
{
// 1st column slot
rotate([0,0,60])
translate([0,0,holder_height/2])
cube([hex_w+1,col_slot_width,holder_height*2], center=true);
// 2nd column slot
rotate([0,0,-60])
translate([0,0,holder_height/2])
cube([hex_w+1,col_slot_width,holder_height*2], center=true);
// Row slot
translate([0,0,holder_height/2])
cube([hex_w+1,row_slot_width,holder_height*2], center=true);
}
}
}
}
else if(wire_style == "bus")
{
for(a = [1,3])
{
intersection()
{
translate([cell_radius*sin(a*90),cell_radius*cos(a*90),holder_height-(separation/2 + slot_height)])
rotate([0,0,a*180])cube([cell_tab_length * 2 + wall, cell_tab_width, separation],center=true);
hex(holder_height, hex_pt + hextra);
}
}
}
}
// Generates a insulator to fit inside the nickel strip channels, also doubles as a spacer
// TODO: support para too
// Add cut outs for stacking bolts
module holder_insulators()
{
// TESTING
// mirror([0,0,1])
// translate([0,0,-holder_height])
// holders();
// Use difference code from hex cells
// intersection it with hexes
if(pack_style == "rect")
{
single_insulator(get_hex_center_points_rect(num_rows,num_cols));
}
else if(pack_style == "para")
{
single_insulator(get_hex_center_points_para(num_rows,num_cols));
}
else if(pack_style == "tria")
{
single_insulator(get_hex_center_points_tria(num_rows,num_cols));
}
}
module single_insulator(hex_list)
{
difference()
{
// sorry
for(hex_list = hex_list)
{
// Iterate through each hex center and place a hex cell
translate([hex_list.x,hex_list.y,0])
{
intersection()
{
union()
{
// Hex block
hex(holder_height-slot_height + insulator_thickness, hex_pt + hextra);
}
union()
{
if(wire_style == "strip")
{
// 1st column slot
rotate([0,0,60])
translate([0,0,holder_height + slot_height])
cube([hex_w+1,col_slot_width-insulator_tolerance,4*slot_height], center=true);
// 2nd column slot
rotate([0,0,-60])
translate([0,0,holder_height + slot_height])
cube([hex_w+1,col_slot_width-insulator_tolerance,4*slot_height], center=true);
// Row slot
translate([0,0,holder_height + slot_height])
cube([hex_w+1,row_slot_width-insulator_tolerance,4*slot_height], center=true);
}
else if(wire_style == "bus")
{
// 1st column slot
rotate([0,0,60])
translate([0,0,holder_height])
cube([hex_w+1,col_slot_width-insulator_tolerance,2*slot_height], center=true);
// 2nd column slot
rotate([0,0,-60])
translate([0,0,holder_height])
cube([hex_w+1,col_slot_width-insulator_tolerance,2*slot_height], center=true);
// Row slot A
translate([0,(hex_pt*cos(60) + hex_pt)/2,holder_height])
cube([hex_w + extra,row_slot_width-insulator_tolerance,2*slot_height], center=true);
// Row slot B
translate([0,-(hex_pt*cos(60) + hex_pt)/2,holder_height])
cube([hex_w + extra,row_slot_width-insulator_tolerance,2*slot_height], center=true);
// Cell opening
translate([0,0,holder_height-slot_height])
cylinder(h=slot_height,d=opening_dia-insulator_tolerance);
}
}
}
}
}
// cut out stacking bolt holes
if(stacking_bolts)
{
for(bolt_holes_list = concat(get_pin_holes_list_rect(num_rows,num_cols), get_pin_list_rect(num_rows,num_cols)))
{
// add hole
translate([bolt_holes_list.x,bolt_holes_list.y,0])
cylinder(d = stacking_bolt_dia, h = holder_height*2);
}
}
}
}
// Generates a box section designed for one addition vertical pack stack. Can make sections larger if printer is able to print higher
module vertical_box_section(num_stacks = 1)
{
// Create a box section with lips (if enabled) on both sides
difference()
{
union()
{
// main box
difference()
{
union()
{
translate([0,0,0])
cap_positive(box_wall,box_clearance,vertical_box_section_height*num_stacks,box_wire_side_clearance,box_nonwire_side_clearance);
// lip addition on bottom
if(box_lip)
{
translate([0,0,-(box_lip_width + box_lip_height)])
cap_positive(box_lip_width,box_clearance,box_lip_height,box_wire_side_clearance,box_nonwire_side_clearance);
}
}
// hollow out
translate([0,0,-vertical_box_section_height*num_stacks])
cap_negative(box_wall,box_clearance,vertical_box_section_height*num_stacks*100,box_wire_side_clearance,box_nonwire_side_clearance);
}
if(box_lip)
{
translate([0,0,-box_lip_height])
both_box_holder_supports(vertical_box_section_height*num_stacks,0);
}
else
{
translate([0,0,0])
both_box_holder_supports(vertical_box_section_height*num_stacks,0);
}
//%cylinder(d=50,h = vertical_box_section_height*num_stacks); // helper cylinder
}
// Other cutouts of entire box bottom
// Lip cutout
if(box_lip)
{
// top lip cut
translate([0,0,vertical_box_section_height*num_stacks])
cap_negative(box_wall,box_clearance + box_lip_width,box_lid_height,box_wire_side_clearance,box_nonwire_side_clearance);
}
generate_box_lid_holes(make_bolt_head_holes = false);
}
}
// Generates support for the box for bolts and zipties. spacer parameter addes a spacer incase there is extra space on the boxes for wires.
module both_box_holder_supports(lid_support_height = box_lid_height, spacer = 0)
{
intersection()
{
union()
{
// Generate +y side of supports
box_holder_support(lid_support_height,spacer);
// Generate -y side of supports
// if even, translate over half a hex
if(num_rows % 2 == 0)
{
if(pack_style == "rect")
{
translate([get_hex_center_x_length(num_cols + 0.5),get_hex_center_y_length(num_rows),0])
rotate([0,0,180])
box_holder_support(lid_support_height,spacer);
}
else if(pack_style == "para")
{
translate([get_hex_center_x_length(num_cols+num_rows/2 - 0.5),get_hex_center_y_length(num_rows),0])
rotate([0,0,180])
box_holder_support(lid_support_height,spacer);
}
}
else // if odd
{
if(pack_style =="rect")
{
translate([0,get_hex_center_y_length(num_rows),0])
mirror([0,1,0])
box_holder_support(lid_support_height,spacer);
}
else if(pack_style == "para")
{
translate([get_hex_center_x_length(num_rows/2+0.5),get_hex_center_y_length(num_rows),0])
mirror([0,1,0])
box_holder_support(lid_support_height,spacer);
}
}
}
translate([0,0,-(box_wall/2 + extra)])
cap_positive(box_wall/2,box_clearance,lid_support_height*2 ,box_wire_side_clearance,box_nonwire_side_clearance);
}
}
// Generates the support to hold the holders in the box lid and bottom.
// spacer sets height for bms clearance or bottom_clearance
// mirrored is used for the -y box_holder_support which will have different sizes due to different side clearances
module box_holder_support(lid_support_height = box_lid_height,spacer = 0)
{
difference()
{
for(col = [1:num_cols])
{
// iterate on one side
// add support in the shape of a hex inbetween cols
union()
{
translate([get_hex_center_x_length(col+0.5),-get_hex_center_y_length(2)-box_clearance,-(box_wall+box_clearance)])
hex(lid_support_height);
if(spacer)
{
translate([get_hex_center_x_length(col)+hex_w/2,-(get_hex_center_y_length(2) + box_clearance - spacer_overhang),-box_clearance-box_wall])
{
intersection()
{
translate([0,0,0])
cube([hex_w/2,hex_pt * 2 + spacer_overhang,(spacer+box_wall)*3],center = true);
hex(spacer+box_wall);
}
}
}
}
}
// Cutouts
// TESTING: flatten the lid support tips for injection molded holders with tabs
// translate([get_hex_center_x_length(0.5),-(hex_pt- cos(60) * hex_pt + box_clearance),0])
// cube([1000,5,100]);
}
}
module generate_box_lid_holes(lid = false,make_bolt_head_holes = true)
{
list = get_holes_list(num_rows,num_cols);
// create holes
for(range = list)
{
if(box_style == "bolt")
{
// Bolt hole
translate([range.x,range.y,box_lid_height/2-box_wall-box_clearance])
{
generate_bolt_hole(lid,make_bolt_head_holes);
}
}
else if(box_style == "ziptie")
{
// Ziptie holes
translate([range.x,range.y,box_bottom_height/2-box_wall-box_clearance])
{
generate_ziptie_hole();
}
}
}
if(box_style == "both")
{
// these indexes set where the bolt holes should go for rect and para styles
last = len(list)-1;
row_end = (pack_style == "rect")
? num_cols - 1
: num_cols - 2; // if para style
row_start = (pack_style == "rect")
? num_cols
: num_cols -1; // if para style
// Bolt holes on ends, zipties in middle
for(i = [list[0],list[row_end],list[row_start],list[last]])
{
translate([i.x,i.y,box_lid_height/2-box_wall-box_clearance])
{
generate_bolt_hole(lid,make_bolt_head_holes);
}
}
// iterate on list but not the bolt holes
for(index = [0:len(list)-1])
{
// if not where the bolt holes are
if (!(index == 0 || index == row_end || index == row_start || index == last ))
{
// Do zipties
// Ziptie holes
translate([list[index].x,list[index].y,box_bottom_height/2-box_wall-box_clearance])
{
generate_ziptie_hole();
}
}
}
}
}
module generate_bolt_hole(lid = false,make_bolt_head_holes = true)
{
if(lid)
cylinder(d = bolt_dia, h = box_bottom_height*100,center = true); // Bolt size
else
cylinder(d = bolt_dia*0.9, h = box_bottom_height*100,center = true); // Tap size
translate([0,0,-(box_lid_height/2-box_wall-box_clearance)- (box_wall + box_clearance) - extra])
{
// Bolt Head Hole
if(make_bolt_head_holes)
cylinder(d = bolt_head_dia, h = bolt_head_thickness + extra);
}
}
module generate_ziptie_hole()
{
cube([ziptie_width,ziptie_thickness,box_bottom_height*100],center = true);
}
// returns a list of all the positions of the holes for box and lid
function get_holes_list(num_rows,num_cols)
= [
// Iterate through rows/cols
for(row = [0,num_rows],col = [1:pack_style == "rect" ? num_cols: num_cols-1])
[ // X Component of list member
row == 0 // if bottom row
? get_hex_center_x_length(pack_style == "rect" ? col + 0.5: col + 0.5 + row/2)
: row % 2 == 0
? get_hex_center_x_length(pack_style == "rect" ? col: col + row/2) // if top row even
: get_hex_center_x_length(pack_style == "rect" ? col + 0.5: col + row/2) // else top row odd
,
// Y component of list member
row == 0 // if bottom row
? -hex_pt + cos(60) * hex_pt - box_clearance - (cos(60) * hex_pt + box_wall)/2 // Messy but spaces the hole half way between the lid support tip and box wall
: get_hex_center_y_length(num_rows) + hex_pt - cos(60) * hex_pt + box_clearance + (cos(60) * hex_pt + box_wall)/2 // else top row
]
];
// Part which clamps down the wires for strain relief
// Z origin is top of the mounting plate
module wire_clamp()
{
translate([0,0,-clamp_plate_height/2])
{
difference()
{
union()
{
clamp_height = box_lid_height - wire_top_wall - box_clearance - wire_diameter * clamp_factor;
cube([wire_hole_length,wire_hole_width + 2 * wire_clamp_support,clamp_plate_height],center = true);
// To top of plate
translate([0,0,wire_top_wall/2])
{
translate([0,0,(clamp_height - wire_clamp_nib_dia/2)/2])
cube([wire_hole_length,wire_hole_width - extra, clamp_height - wire_clamp_nib_dia/2], true);
translate([0,(wire_hole_width - extra)/2,(clamp_height) - wire_clamp_nib_dia/2])
rotate([90,0,0])
cylinder(d = wire_clamp_nib_dia, h = wire_hole_width - extra);
}
}
// Bolt hole cutout
translate([0,(wire_hole_width + wire_clamp_support)/2,-wire_top_wall])
cylinder(d = wire_clamp_bolt_dia + bolt_dia_clearance, h = wire_top_wall * 2);
translate([0,-(wire_hole_width + wire_clamp_support)/2,-wire_top_wall])
cylinder(d = wire_clamp_bolt_dia + bolt_dia_clearance, h = wire_top_wall * 2);
}
}
}
// Generates a hex of cap_height tall and hex_pt radius by default.
module hex(cap_height = holder_height,hex_pt = hex_pt)
{
linear_extrude(height=cap_height, convexity = 10)
polygon([ for (a=[0:5])[hex_pt*sin(a*60),hex_pt*cos(a*60)]]);
}
// returns total box width
function get_box_total_width()
= 2 * (box_clearance + box_wall) + get_holder_width();
// returns holder width
function get_holder_width()
= get_hex_center_y_length(num_rows) + hex_pt * 2;
// returns length of the longest part of box (from side of lid to wireclamp)
// Reminder: Rect box only.
function get_box_total_length()
= 2 * (box_clearance + box_wall) + get_holder_rect_length() + box_wire_side_clearance + box_nonwire_side_clearance + wire_hole_length;
// returns length of rect holders
function get_holder_rect_length()
= hex_w*(num_cols+0.5);
// returns height of the mock pack
function get_mock_pack_height()
= 2 * (slot_height + separation) + cell_height;
// returns the length of the center of one hex cell on a row to number to hexes passed to function
function get_hex_center_x_length(num_cell)
= (num_cell-1) * hex_w;
// returns the length of the center of vertical(columns) hex cells to number to hexes passed to function
function get_hex_center_y_length(num_cell)
= (num_cell-1) * hex_pt*1.5;
// returns a list of the hex cell centers of a given num of rows and columns for para packs.
function get_hex_center_points_para(num_rows, num_cols)
= [
for(row = [0:num_rows-1],col = [0:num_cols-1])
[ // X component of list member
row*(0.5 * hex_w) + hex_w * col
,
// Y component of list member
row * 1.5 * (hex_pt)
]
];
// returns a list of the hex cell centers of a given num of rows and columns for rect packs.
function get_hex_center_points_rect(num_rows, num_cols)
= [
// Iterate through num of rows and cols
for(row = [0:num_rows-1],col = [0:num_cols-1])
[ // X component of list member
row % 2 == 0 ? // if even
hex_w * col
://else
0.5 * hex_w + hex_w * col
,
// Y component of list member
row * 1.5 * (hex_pt)
]
]; // Closing function bracket
// returns a list of the hex cell centers of a given num of rows and columns for rect packs.
function get_hex_center_points_tria(num_rows, num_cols)
= [
// Iterate through num of rows and cols
for(row = [0:num_rows-1],col = [0:row])
[ // X component of list member
row*(0.5 * hex_w) + hex_w*(-col)
,
// Y component of list member
row * 1.5 * (hex_pt)
]
]; // Closing function bracket
// code from tria for
// else if(pack_style == "tria")
// {
// for(row = [0:num_rows-1])
// {
// translate([row*(0.5 * hex_w),1.5*(hex_pt)*row,0])
// for(col = [0:row])
// {
// translate([hex_w*(-col),0,0])
// pick_hex();
// }
// }
// }
// returns a list of all the positions of the pin holes for stacking pins
function get_pin_holes_list_rect(num_rows,num_cols)
= [
// Iterate through rows/cols and ignore last row and last column
for(row = [0:num_rows-2],col = [0:num_cols-2])
[ // X Component of list member
row % 2 == 0? // if even
hex_w * col + hex_pt*cos(30)
: // else odd
0.5 * hex_w +hex_w * col + hex_pt*cos(30)
,
// Y component of list member
row * 1.5 * hex_pt + hex_pt*sin(30)
]
];
// returns a list of all the positions of the pins for stacking pins
function get_pin_list_rect(num_rows,num_cols)
= [
// Iterate through rows/cols and ignore last row and last column
for(row = [0:num_rows-2],col = [1:num_cols-1])
[ // X Component of list member
row % 2 == 0? // if even
hex_w * col + hex_pt*cos(90)
: // else odd
col == num_cols - 1? // if also last column then just put pin in first column hex (this nicely works out)
0.5 * hex_w
: // else
0.5 * hex_w +hex_w * col + hex_pt*cos(90)
,
// Y component of list member
row * 1.5 * hex_pt + hex_pt*sin(90)
]
];