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