Dropping a simplified ping-pong ball radome for a Piranha RGB LED atop a discrete LM3909 on the AA alkaline cell holder:

The solid model has screw holes for the lid and the revised LED spider:

The RGB LED needs only two wires, as the LM3909 circuit can blink only one LED. I tried all three colors, but only blue and green justify the LM3909 hairball; red can get along with the astable circuit.
The LED wires connect across a 1 MΩ resistor serving as a mechanical strut between the 9.1 kΩ resistor on the left and the 10 Ω ballast resistor on the right.
Fresh alkaline cells at 3.0 V put 3.3 V across the blue LED with a 37 mA peak current. Older cells at 2.3 V produce 2.9 V at 15 mA. Dead cells at 1.9 V still fire the LED with 2.7 V at 4.2 mA, although the flash is barely visible in ordinary room light.
The lovely blue ball looks better in person!
The OpenSCAD source code as a GitHub Gist:
// 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(); | |
} | |
2 thoughts on “Discrete LM3909: Blue LED Radome”
Comments are closed.