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(); | |
| } | |
Comments
2 responses to “Discrete LM3909: Blue LED Radome”
[…] step: wire up an astable with a yellow LED to go with the green and blue boosted […]
[…] a yellow / amber LED runs at a lower voltage than blue and green LEDs, it sits atop an astable multivibrator, rather than a discrete LM3909. The battery […]