Created
September 11, 2020 17:27
-
-
Save ednisley/ee7c4519d73796762e65a766f455babd to your computer and use it in GitHub Desktop.
OpenSCAD source code: AA alkaline battery holder with ping-pong ball radome
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Astable Multivibrator | |
// Holder for Alkaline cells | |
// Ed Nisley KE4ZNU August 2020 | |
// 2020-09 add LED radome | |
/* [Layout options] */ | |
Layout = "Build"; // [Build,Show,Lid,Spider] | |
/* [Hidden] */ | |
CellName = "AA"; // [AA] -- does not work with anything else | |
NumCells = 2; // [2] -- likewise | |
Struts = -1; // [0:None, -1:Dual, 1:Quad] -- Quad is dead | |
// Extrusion parameters | |
/* [Hidden] */ | |
ThreadThick = 0.25; | |
ThreadWidth = 0.40; | |
HoleWindage = 0.2; | |
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit); | |
function IntegerLessMultiple(Size,Unit) = Unit * floor(Size / Unit); | |
Protrusion = 0.1; // make holes end cleanly | |
inch = 25.4; | |
//- Basic dimensions | |
WallThick = IntegerMultiple(3.0,ThreadWidth); | |
CornerRadius = WallThick/2; | |
FloorThick = IntegerMultiple(3.0,ThreadThick); | |
TopThick = IntegerMultiple(2.0,ThreadThick); | |
WireOD = 1.5; // battery & LED wiring | |
WireOC = 4; | |
Gap = 5.0; | |
// Cylindrical cell sizes | |
// https://en.wikipedia.org/wiki/List_of_battery_sizes#Cylindrical_batteries | |
CELL_NAME = 0; | |
CELL_OD = 1; | |
CELL_OAL = 2; | |
// FIXME search() needs special-casing to properly find AAA and AAAA | |
// Which is why CellName is limited to AA | |
CellData = [ | |
["AAAA",8.3,42.5], | |
["AAA",10.5,44.5], | |
["AA",14.5,50.5], | |
["C",26.2,50], | |
["D",34.2,61.5], | |
["A23",10.3,28.5], | |
["CR123A",17.0,34.5], | |
["18650",18.8,65.2], // bare 18650 with button end | |
["18650Prot",19.0,70.0], // protected 18650 = 19670 plus a bit | |
]; | |
CellIndex = search([CellName],CellData,1,0)[0]; | |
echo(str("Cell index: ",CellIndex," = ",CellData[CellIndex][CELL_NAME])); | |
//- Contact dimensions | |
CONTACT_NAME = 0; | |
CONTACT_WIDE = 1; | |
CONTACT_HIGH = 2; | |
CONTACT_THICK = 3; // plate thickness | |
CONTACT_TIP = 4; // tip to rear face | |
CONTACT_TAB = 5; // solder tab width | |
ContactData = [ | |
["AA+",12.2,12.2,0.3,1.7,3.5], // pos bump | |
["AA-",12.2,12.2,0.3,5.0,3.5], // half-compressed neg spring | |
["AA+-",28.2,12.2,0.3,5.0,0], // pos-neg bridge | |
["Li+",18.5,16.0,0.3,2.8,5.5], | |
["Li-",18.5,16.0,0.3,6.0,5.5], | |
]; | |
function ConDat(name,dim) = ContactData[search([name],ContactData,1,0)[0]][dim]; | |
ContactRecess = 2*ConDat(str(CellName,"+"),CONTACT_THICK); | |
ContactOC = CellData[CellIndex][CELL_OD]; | |
WireBay = 6.0; // room for wiring to contacts | |
//- Wire struts | |
StrutDia = 1.6; // AWG 14 = 1.6 mm | |
StrutSides = 3*4; | |
ID = 0; | |
OD = 1; | |
LENGTH = 2; | |
StrutBase = [StrutDia,StrutDia + 2*5*ThreadWidth, // ID = wire, OD = buildable | |
FloorThick + CellData[CellIndex][CELL_OD]]; // LENGTH = base is flush with cell top | |
//- Holder dimensions | |
BatterySize = [CellData[CellIndex][CELL_OAL] + // cell | |
ConDat(str(CellName,"+"),CONTACT_TIP) + // pos contact | |
ConDat(str(CellName,"-"),CONTACT_TIP) - // neg contact | |
2*ContactRecess, // sink into wall | |
NumCells*CellData[CellIndex][CELL_OD], | |
CellData[CellIndex][CELL_OD] | |
]; | |
echo(str("Battery space: ",BatterySize)); | |
CaseSize = [3*WallThick + // end walls + wiring partition | |
BatterySize.x + // cell | |
WireBay, // wiring bay | |
2*WallThick + BatterySize.y, | |
FloorThick + BatterySize.z | |
]; | |
BatteryOffset = (CaseSize.x - (2*WallThick + | |
CellData[CellIndex][CELL_OAL] + | |
ConDat(str(CellName,"-"),CONTACT_TIP)) | |
) /2 ; | |
ThumbRadius = 0.75 * CaseSize.z; | |
StrutOC = [IntegerLessMultiple(CaseSize.x - 2*CornerRadius -2*StrutBase[OD],5.0), | |
IntegerMultiple(CaseSize.y + StrutBase[OD],5.0)]; | |
StrutAngle = atan(StrutOC.y/StrutOC.x); | |
echo(str("Strut OC: ",StrutOC)); | |
LidSize = [2*WallThick + WireBay + ConDat(str(CellName,"+"),CONTACT_THICK), CaseSize.y, FloorThick/2]; | |
LidScrew = [2.0,3.8,7.0]; // M2 pan head screw (LENGTH = threaded) | |
LidScrewOC = CaseSize.y/2 - CornerRadius - LidScrew[OD]; // allow space around screw head | |
//- Piranha LEDs | |
PiranhaBody = [8.0,8.0,8.0]; // Z = heatsink fins + body + lens height | |
PiranhaPin = 0.0; // trimmed pin length beyond heatsink | |
PiranhaPinsOC = [5.0,5.0]; // pin XY distance | |
PiranhaRecess = PiranhaBody.z + PiranhaPin/2; // minimum LED recess depth | |
BallOD = 40.0; // radome sphere | |
BallSides = 4*StrutSides; // nice smoothness | |
BallPillar = [norm([PiranhaBody.x,PiranhaBody.y]), // ID | |
norm([PiranhaBody.x,PiranhaBody.y]) + 3*WallThick, // OD | |
StrutBase[OD] + PiranhaBody.z]; // height to base of chord | |
echo(str("Pillar OD: ",BallPillar[OD])); | |
BallChordM = BallOD/2 - sqrt(pow(BallOD/2,2) - (pow(BallPillar[OD],2))/4); | |
echo(str("Ball chord depth: ",BallChordM)); | |
//---------------------- | |
// Useful routines | |
module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes | |
Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2); | |
FixDia = Dia / cos(180/Sides); | |
cylinder(r=(FixDia + HoleWindage)/2,h=Height,$fn=Sides); | |
} | |
// Spider for single LED atop struts, with the ball | |
module DualSpider() { | |
difference() { | |
union() { | |
for (j=[-1,1]) { | |
translate([0,j*StrutOC.y/2,StrutBase[OD]/2]) | |
rotate(180/StrutSides) | |
sphere(d=StrutBase[OD]/cos(180/StrutSides),$fn=StrutSides); | |
translate([0,j*StrutOC.y/2,0]) | |
rotate(180/StrutSides) | |
cylinder(d=StrutBase[OD],h=StrutBase[OD]/2,$fn=StrutSides); | |
} | |
translate([0,0,StrutBase[OD]/4]) // connecting bars | |
cube([StrutBase[OD]*cos(180/StrutSides),StrutOC.y,StrutBase[OD]/2],center=true); | |
cylinder(d=BallPillar[OD],h=BallPillar[LENGTH],$fn=BallSides); | |
} | |
for (j=[-1,1]) // strut wires | |
translate([0,j*StrutOC.y/2,-Protrusion]) | |
PolyCyl(StrutBase[ID],StrutBase[OD]/2,6); | |
for (n=[-1,1]) // LED wiring | |
rotate(n*90) | |
translate([StrutOC.x/3,0,-Protrusion]) | |
PolyCyl(StrutBase[ID],StrutBase[OD],6); | |
translate([0,0,BallOD/2 + BallPillar[LENGTH] - BallChordM]) // ball inset | |
sphere(d=BallOD); | |
translate([0,0,BallPillar.z - PiranhaRecess + BallPillar.z/2]) // LED inset | |
cube(PiranhaBody + [HoleWindage,HoleWindage,BallPillar.z],center=true); // XY clearance | |
translate([0,0,StrutBase[OD]/2 + WireOD/2 + 0*Protrusion]) // wire channels | |
cube([WireOD,BallPillar[OD] + 2*WallThick,WireOD],center=true); | |
} | |
} | |
//-- Overall case with origin at battery center | |
module Case() { | |
union() { | |
difference() { | |
union() { | |
hull() | |
for (i=[-1,1], j=[-1,1]) | |
translate([i*(CaseSize.x/2 - CornerRadius), | |
j*(CaseSize.y/2 - CornerRadius), | |
0]) | |
cylinder(r=CornerRadius/cos(180/8),h=CaseSize.z,$fn=8); // cos() fixes undersize spheres! | |
if (Struts) | |
for (i = (Struts == 1) ? [-1,1] : -1) { // strut bases | |
hull() | |
for (j=[-1,1]) | |
translate([i*StrutOC.x/2,j*StrutOC.y/2,0]) | |
rotate(180/StrutSides) | |
cylinder(d=StrutBase[OD],h=StrutBase[LENGTH],$fn=StrutSides); | |
translate([i*StrutOC.x/2,0,StrutBase[LENGTH]/2]) | |
cube([2*StrutBase[OD],StrutOC.y,StrutBase[LENGTH]],center=true); // blocks for fairing | |
for (j=[-1,1]) // hemisphere caps | |
translate([i*StrutOC.x/2, | |
j*StrutOC.y/2, | |
StrutBase[LENGTH]]) | |
rotate(180/StrutSides) | |
sphere(d=StrutBase[OD]/cos(180/StrutSides),$fn=StrutSides); | |
} | |
} | |
translate([BatteryOffset,0,BatterySize.z/2 + FloorThick]) // cells | |
cube(BatterySize + [0,0,Protrusion],center=true); | |
translate([BatterySize.x/2 + BatteryOffset + ContactRecess/2 - Protrusion/2, // contacts | |
0, | |
BatterySize.z/2 + FloorThick]) | |
cube([ContactRecess + Protrusion, | |
ConDat(str(CellName,"+-"),CONTACT_WIDE), | |
ConDat(str(CellName,"+-"),CONTACT_HIGH) | |
],center=true); | |
translate([-(BatterySize.x/2 - BatteryOffset + ContactRecess/2 - Protrusion/2), | |
ContactOC/2, | |
BatterySize.z/2 + FloorThick]) | |
cube([ContactRecess + Protrusion, | |
ConDat(str(CellName,"+"),CONTACT_WIDE), | |
ConDat(str(CellName,"+"),CONTACT_HIGH) | |
],center=true); | |
translate([-(BatterySize.x/2 - BatteryOffset + ContactRecess/2 - Protrusion/2), | |
-ContactOC/2, | |
BatterySize.z/2 + FloorThick]) | |
cube([ContactRecess + Protrusion, | |
ConDat(str(CellName,"-"),CONTACT_WIDE), | |
ConDat(str(CellName,"-"),CONTACT_HIGH) | |
],center=true); | |
translate([-CaseSize.x/2 + WireBay/2 + WallThick, // wire bay with screw bosses | |
0, | |
BatterySize.z/2 + FloorThick + Protrusion/2]) | |
cube([WireBay, | |
2*LidScrewOC - LidScrew[ID] - 2*4*ThreadWidth, | |
BatterySize.z + Protrusion | |
],center=true); | |
for (j=[-1,1]) // screw holes | |
translate([-CaseSize.x/2 + WireBay/2 + WallThick, | |
j*LidScrewOC, | |
CaseSize.z - LidScrew[LENGTH] + Protrusion]) | |
PolyCyl(LidScrew[ID],LidScrew[LENGTH],6); | |
for (j=[-1,1]) | |
translate([-(BatterySize.x/2 - BatteryOffset + WallThick/2), // contact tabs | |
j*ContactOC/2, | |
BatterySize.z + FloorThick - Protrusion]) | |
cube([2*WallThick, | |
ConDat(str(CellName,"+"),CONTACT_TAB), | |
(BatterySize.z - ConDat(str(CellName,"+"),CONTACT_HIGH)) | |
],center=true); | |
if (false) | |
translate([0,0,CaseSize.z]) // finger cutout | |
rotate([90,00,0]) | |
cylinder(r=ThumbRadius,h=2*CaseSize.y,center=true,$fn=22); | |
translate([0,0,ThreadThick - Protrusion]) // recess around name | |
cube([0.6*CaseSize.x,8,2*ThreadThick],center=true); | |
if (Struts) | |
for (i2 = (Struts == 1) ? [-1,1] : -1) { // strut wire holes and fairing | |
for (j=[-1,1]) | |
translate([i2*StrutOC.x/2,j*StrutOC.y/2,FloorThick]) | |
rotate(180/StrutSides) | |
PolyCyl(StrutBase[ID],2*StrutBase[LENGTH],StrutSides); | |
for (i=[-1,1], j=[-1,1]) // fairing cutaways | |
translate([i*StrutBase[OD] + (i2*StrutOC.x/2), | |
j*StrutOC.y/2, | |
-Protrusion]) | |
rotate(180/StrutSides) | |
PolyCyl(StrutBase[OD],StrutBase[LENGTH] + 2*Protrusion,StrutSides); | |
} | |
} | |
translate([0,0,0]) | |
linear_extrude(height=2*ThreadThick + Protrusion,convexity=10) | |
mirror([0,1,0]) | |
text(text="KE4ZNU",size=6,spacing=1.20,font="Arial:style:Bold",halign="center",valign="center"); | |
} | |
} | |
module Lid() { | |
difference() { | |
hull() | |
for (i=[-1,1], j=[-1,1], k=[-1,1]) | |
translate([i*(LidSize.x/2 - CornerRadius), | |
j*(LidSize.y/2 - CornerRadius), | |
k*(LidSize.z - CornerRadius)]) // double thickness for flat bottom | |
sphere(r=CornerRadius/cos(180/8),$fn=8); | |
translate([0,0,-LidSize.z]) // remove bottom | |
cube([(LidSize.x + 2*Protrusion),(LidSize.y + 2*Protrusion),2*LidSize.z],center=true); | |
for (j=[-1,1]) // wire holes | |
translate([0,j*WireOC,-Protrusion]) | |
PolyCyl(WireOD,2*LidSize.z,6); | |
for (j=[-1,1]) | |
translate([0,j*LidScrewOC,-Protrusion]) | |
PolyCyl(LidScrew[ID],2*LidSize.z,6); | |
} | |
} | |
//------------------- | |
// Build it! | |
if (Layout == "Case") | |
Case(); | |
if (Layout == "Lid") | |
Lid(); | |
if (Layout == "Spider") | |
if (Struts == -1) | |
DualSpider(); | |
else | |
cube(10,center=true); | |
if (Layout == "Build") { | |
rotate(90) | |
Case(); | |
translate([0,-(CaseSize.x/2 + LidSize.x/2 + Gap),0]) | |
rotate(90) | |
Lid(); | |
if (Struts == -1) | |
translate([CaseSize.x/2,0,0]) | |
DualSpider(); | |
} | |
if (Layout == "Show") { | |
Case(); | |
translate([-CaseSize.x/2 + LidSize.x/2,0,(CaseSize.z + Gap)]) | |
Lid(); | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
More details on my blog at https://wp.me/poZKh-9mQ