Astable Multivibrator: Dressed-up LED Spider

Adding a bit of trim to the bottom of the LED spider makes it look better and helps keep the strut wires in place:

Astable Multivibrator - Alkaline - Radome trim
Astable Multivibrator – Alkaline – Radome trim

It’s obviously impossible to build like that, so it’s split across the middle of the strut:

Astable Multivibrator - Alkaline - Radome trim
Astable Multivibrator – Alkaline – Radome trim

Glue it together with black adhesive and a couple of clamps:

LED Spider - glue clamping
LED Spider – glue clamping

The aluminum fixtures (jigs?) are epoxied around snippets of strut wire aligning the spider parts:

LED Spider - gluing fixture
LED Spider – gluing fixture

Those grossly oversized holes came pre-drilled in an otherwise suitable aluminum rod from the Little Tray o’ Cutoffs. I faced off the ends, chopped the rod in two, recessed the new ends, and declared victory. Might need better ones at some point, but they’ll do for now.

Next step: wire up an astable with a yellow LED to go with the green and blue boosted LEDs.

Kenmore Progressive Vacuum Cleaner vs. Dust Brush Adapters

Contemporary vacuum cleaner dust brush heads have bristles in some combination of [long | short] with [flexy | stiff]. The long + flexy combination results in the bristles jamming the inlet and the short + stiff combo seems unsuited for complex surfaces. Shaking the Amazonian dice brought a different combination:

Vacuum cleaner dust brush assortment - with adapters
Vacuum cleaner dust brush assortment – with adapters

That’s the new one on the bottom and, contrary to what you might think from the picture, it is not identical to the one just above it.

In particular, the black plastic housing came from a different mold (the seam lines are now top-and-bottom) and required a new adapter for the Kenmore Progressive vacuum cleaner’s complicated wand / hose inlet, with a 3/4 inch PVC pipe reinforcement inside.

Early reports indicate it works fine, so I’ll declare a temporary victory in the war on entropy.

I’m still using the same OpenSCAD source code with minute tweaks to suit the as-measured tapers.

Raspberry Pi HQ Camera Mount

As far as I can tell, Raspberry Pi cases are a solved problem, so 3D printing an intricate widget to stick a Pi on the back of an HQ camera seems unnecessary unless you really, really like solid modeling, which, admittedly, can be a thing. All you really need is a simple adapter between the camera PCB and the case of your choice:

HQ Camera Backplate - OpenSCAD model
HQ Camera Backplate – OpenSCAD model

A quartet of 6 mm M2.5 nylon spacers mount the adapter to the camera PCB:

RPi HQ Camera - nylon standoffs
RPi HQ Camera – nylon standoffs

The plate has recesses to put the screw heads below the surface. I used nylon screws, but it doesn’t really matter.

The case has all the right openings, slots in the bottom for a pair of screws, and costs six bucks. A pair of M3 brass inserts epoxied into the plate capture the screws:

RPi HQ Camera - case adapter plate - screws
RPi HQ Camera – case adapter plate – screws

Thick washers punched from an old credit card go under the screws to compensate for the case’s silicone bump feet. I suppose Doing the Right Thing would involve 3D printed spacers matching the cross-shaped case cutouts.

Not everyone agrees with my choice of retina-burn orange PETG:

RPi HQ Camera - 16 mm lens - case adapter plate
RPi HQ Camera – 16 mm lens – case adapter plate

Yes, that’s a C-mount TV lens lurking in the background, about which more later.

The OpenSCAD source code as a GitHub Gist:

// Raspberry Pi HQ Camera Backplate
// Ed Nisley KE4ZNU 2020-09
//-- 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;
ID = 0;
OD = 1;
LENGTH = 2;
//- Basic dimensions
CamPCB = [39.0,39.0,1.5]; // Overall PCB size, plus a bit
CornerRound = 3.0; // ... has rounded corners
CamScrewOC = [30.0,30.0,0]; // ... mounting screw layout
CamScrew = [2.5,5.0,2.2]; // ... LENGTH = head thickness
Standoff = [2.5,5.5,6.0]; // nylon standoffs
Insert = [3.0,4.0,4.0];
WallThick = IntegerMultiple(2.0,ThreadWidth);
PlateThick = Insert[LENGTH];
CamBox = [CamPCB.x + 2*WallThick,
CamPCB.y + 2*WallThick,
Standoff.z + PlateThick + CamPCB.z + 1.0];
PiPlate = [90.0,60.0,PlateThick];
PiPlateOffset = [0.0,(PiPlate.y - CamBox.y)/2,0];
PiSlotOC = [0.0,40.0];
PiSlotOffset = [3.5,3.5];
NumSides = 2*3*4;
TextDepth = 2*ThreadThick;
//----------------------
// 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);
}
//----------------------
// Build it
difference() {
union() {
hull() // camera enclosure
for (i=[-1,1], j=[-1,1])
translate([i*(CamBox.x/2 - CornerRound),j*(CamBox.y/2 - CornerRound),0])
cylinder(r=CornerRound,h=CamBox.z,$fn=NumSides);
translate(PiPlateOffset)
hull()
for (i=[-1,1], j=[-1,1]) // Pi case plate
translate([i*(PiPlate.x/2 - CornerRound),j*(PiPlate.y/2 - CornerRound),0])
cylinder(r=CornerRound,h=PiPlate.z,$fn=NumSides);
}
hull() // camera PCB space
for (i=[-1,1], j=[-1,1])
translate([i*(CamPCB.x/2 - CornerRound),j*(CamPCB.y/2 - CornerRound),PlateThick])
cylinder(r=CornerRound,h=CamBox.z,$fn=NumSides);
translate([0,-CamBox.y/2,PlateThick + CamBox.z/2])
cube([CamScrewOC.x - Standoff[OD],CamBox.y,CamBox.z],center=true);
for (i=[-1,1], j=[-1,1]) // camera screws with head recesses
translate([i*CamScrewOC.x/2,j*CamScrewOC.y/2,-Protrusion]) {
PolyCyl(CamScrew[ID],2*CamBox.z,6);
PolyCyl(CamScrew[OD],CamScrew[LENGTH] + Protrusion,6);
}
for (j=[-1,1]) // Pi case screw inserts
translate([0,j*PiSlotOC.y/2 + PiSlotOffset.y,-Protrusion] + PiPlateOffset)
PolyCyl(Insert[OD],2*PiPlate.z,6);
translate([-PiPlate.x/2 + (PiPlate.x - CamBox.x)/4,0,PlateThick - TextDepth/2] + PiPlateOffset)
cube([15.0,30.0,TextDepth + Protrusion],center=true);
}
translate([-PiPlate.x/2 + (PiPlate.x - CamBox.x)/4 + 3,0,PlateThick - TextDepth - Protrusion] + PiPlateOffset)
linear_extrude(height=TextDepth + Protrusion,convexity=2)
rotate(-90)
text("Ed Nisley",font="Arial:style=Bold",halign="center",valign="center",size=4,spacing=1.05);
translate([-PiPlate.x/2 + (PiPlate.x - CamBox.x)/4 - 3,0,PlateThick - TextDepth - Protrusion] + PiPlateOffset)
linear_extrude(height=TextDepth + Protrusion,convexity=2)
rotate(-90)
text("KE4ZNU",font="Arial:style=Bold",halign="center",valign="center",size=4,spacing=1.05);

Bike Helmet Mirror: Brasswork Clamp

A bit of Quality Shop Time produced a slight improvement to the clamp holding the mirror to the stalk:

Helmet Mirror Ball Mount - mirror joint brasswork
Helmet Mirror Ball Mount – mirror joint brasswork

The general idea is to hold the wave washer (it’s mashed under the flat washer, honest) above those bumps on the plate holding the mirror and stalk balls. It’s a few millimeters from the end of a ¼ inch brass rod, drilled for the M3 screw, and reduced to 4.5 mm with a parting tool to clear the bumps.

While I was at it, I made two spare mirrors, just to have ’em around:

Helmet Mirror Ball Mount - new vs old
Helmet Mirror Ball Mount – new vs old

The new ball mount looks downright svelte compared to the old Az-El mount, doesn’t it?

I should replace the steel clamp plates with a stainless-steel doodad of some sort to eliminate the unsightly rust, but that’s definitely in the nature of fine tuning.

More AAA-to-AA Alkaline Adapters

Having a handful of not-dead-yet AAA alkalines and a bunch of LED blinkies built for AA alkalines, a pair of adapters seemed in order:

AAA-to-AA Alkaline Adapters - installed
AAA-to-AA Alkaline Adapters – installed

The blinkies need a somewhat wider base than they’d get from a pair of AAA alkalines, so it’s not quite as dumb as it may seem.

In any event, the positive terminal comes from a brass rod:

AAA-to-AA Alkaline Adapters - brass terminal
AAA-to-AA Alkaline Adapters – brass terminal

Nobody will ever see the fancy Hilbert Curve infill around the brass:

AAA-to-AA Alkaline Adapters - end view
AAA-to-AA Alkaline Adapters – end view

In this application, they’ll go from not-dead-yet to oh-it’s-dead faster than AA cells, so I can watch how the blinkies work with lower voltages.

Bike Helmet Mirror: Ball Mount

Nine years ago, I didn’t know how enough to design a bike helmet mirror with a ball mount, but even an old dog can learn a new trick:

Helmet Mirror Ball Mount - on helmet
Helmet Mirror Ball Mount – on helmet

However, it’s worth noting my original, butt-ugly Az-El mounts lasted for all of those nine years, admittedly with adjustments along the way, which is far more than the commercial mounts making me unhappy enough to scratch my itch.

The mount adapts the split spherical clamp from the daytime running light:

Helmet Mirror Mount - Ball
Helmet Mirror Mount – Ball

Scaling it down for a 10 mm polypropylene ball around the base of the 30 mm inspection mirror’s shaft simplified everything:

Helmet Mirror Ball Mount - drilled ball test
Helmet Mirror Ball Mount – drilled ball test

I’m reasonably sure I couldn’t have bought 100 polypro balls for eight bucks a decade ago, but we’ll never know. Drilling the hole was a complete botch job, about which more later. The shaft came from a spare mirror mount I made up a while ago; a new shaft appears below.

The solid model, like Gaul, is in three parts divided:

Helmet Mirror Ball Mount - Slic3r
Helmet Mirror Ball Mount – Slic3r

The helmet plate (on the right) has a slight indent more-or-less matching the helmet curvature and gets a layer of good double-stick foam tape. The clamp base (on the left) has a pair of brass inserts epoxied into matching recesses below the M3 clearance holes:

Helmet Mirror Ball Mount - inserts
Helmet Mirror Ball Mount – inserts

A layer of epoxy then sticks the helmet plate in place, with the inserts providing positive alignment:

Helmet Mirror Ball Mount - plates
Helmet Mirror Ball Mount – plates

The clamp screws pull the inserts against the plastic in the clamp base, so they can’t pull out or through, and the plates give the epoxy enough bonding surface that (I’m pretty sure) they won’t ever come apart.

I turned down a 2 mm brass insert to fit inside the butt end of the mirror shaft and topped it off with a random screw harvested from a dead hard drive:

Helmet Mirror Ball Mount - assembled - rear view
Helmet Mirror Ball Mount – assembled – rear view

At the start, it wasn’t obvious the shaft would stay stuck in the ball, so I figured making it impossible to pull out would eliminate the need to find it by the side of the road. As things turned out, the clamp exerts enough force to ensure the shaft ain’t goin’ nowhere, so I’ll plug future shafts with epoxy.

The front side of the clamp looks downright sleek:

Helmet Mirror Ball Mount - assembled - front view
Helmet Mirror Ball Mount – assembled – front view

Well, how about “chunky”?

The weird gray-black highlights are optical effects from clear / natural PETG, rather than embedded grunge; it looks better in person. I should have used retina-burn orange or stylin’ black.

This mount is much smaller than the old one and should, in the event of a crash, not cause much injury. Based on how the running light clamp fractures, I expect the clamp will simply tear out of the base on impact. In the last decade, neither of us has crashed, so I don’t know what the old mount would do.

The clamp is 7 mm thick (front-to-back), set by the M3 washer diameter, with 1.5 mm of ball sticking out on each side. The model has a kerf one thread high (0.25 mm) between the pieces to add clamping force and, with the screws tightened down, moving the ball requires a disturbingly large effort. I added a touch of rosin and that ball straight-up won’t move, which probably means the shaft will bend upon droppage; I have several spare mirrors in stock.

On the other paw, the ball turns smoothly in the clamp and it’s easy to position the shaft as needed: much better than the old Az-El mount!

The inspection mirror hangs from a double ball joint which arrives with a crappy screw + nut. I epoxied the old mirror mount nut in place, but this time around I drilled the plates for a 3 mm stainless SHCS, used a wave washer for a bit of flexible force, and topped it off with a nyloc nut:

Helmet Mirror Ball Mount - mirror joint
Helmet Mirror Ball Mount – mirror joint

I’m unhappy with how it looks and don’t like how the washer hangs in free space between those bumps, so I may eventually turn little brass fittings to even things out. It’s either that or more epoxy.

So far, though, it’s working pretty well and both units meet customer requirements.

The OpenSCAD source code as a GitHub Gist:

// Bike helmet mirror mount - ball joint
// Ed Nisley KE4ZNU 2020-09
/* [Layout options] */
Layout = "Build"; // [Build, Show, Plate, Base, Clamp]
//-- Extrusion parameters
// 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;
ID = 0;
OD = 1;
LENGTH = 2;
//- Basic dimensions
MountDia = 30.0; // footprint on helmet
BallDia = 10.0;
BallRad = BallDia / 2;
WallThick = IntegerMultiple(2.0,ThreadWidth);
FloorThick = IntegerMultiple(2.0,ThreadThick);
CornerRound = 2.0;
Insert = [3.0,4.0,4.0]; // threaded brass insert
Screw = [3.0,5.5,25.0]; // clamp screw
Washer = [3.7,7.0,0.7]; // washer
ShowGap = 2.0;
BuildGap = 5.0;
//-- Helmet Interface Plate
ScrewOC = BallDia + 2*WallThick + Screw[ID];
echo(str("Screw OC: ",ScrewOC));
Clamp = [ceil(Washer[OD]), // barely holds washer under screw
ScrewOC + Washer[OD], // minimal clearance for washer
BallDia +2*FloorThick // screw fits through insert
];
Kerf = ThreadThick;
echo(str("Clamp: ",Clamp));
HelmetCX = 60.0; // empirical helmet side curve
HelmetMX = 3.0;
HelmetRX = (pow(HelmetMX,2) + pow(HelmetCX,2)/4)/(2*HelmetMX);
HelmetPlateC = MountDia;
HelmetPlateTheta = atan(HelmetPlateC/HelmetRX);
HelmetPlateM = 2*HelmetRX*pow(sin(HelmetPlateTheta/4),2);
echo(str("Plate indent: ",HelmetPlateM));
HelmetPlateThick = max(FloorThick,0.6*Insert[LENGTH]) + HelmetPlateM;
echo(str("Screw length: ",Clamp.z + Insert[LENGTH]));
MountSides = 2*3*4;
//----------------------
// 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);
}
//----------------------
// Clamp frame around ball
module ClampFrame() {
difference() {
union() {
hull()
for (i=[-1,1], j=[-1,1]) {
translate([i*(Clamp.x/2 - CornerRound),j*(Clamp.y/2 - CornerRound),Clamp.z/2 - CornerRound])
sphere(r=CornerRound,$fn=24);
translate([i*(Clamp.x/2 - CornerRound),j*(Clamp.y/2 - CornerRound),-Clamp.z/2])
cylinder(r=CornerRound,$fn=24);
}
for (j=[-1,1])
translate([0,j*ScrewOC/2,0])
rotate(180/12)
cylinder(d=Washer[OD],h=Clamp.z/2,$fn=12);
}
sphere(d=BallDia + HoleWindage,$fn=48);
cube([2*MountDia,2*MountDia,Kerf],center=true);
for (j=[-1,1])
translate([0,j*ScrewOC/2,-Screw[LENGTH]])
rotate(180/6)
PolyCyl(Screw[ID],2*Screw[LENGTH],6);
}
}
module ClampSelect(Section) {
XlateZ = (Section == "Top") ? Clamp.z/2 :
(Section == "Bottom") ? -Clamp.z/2 :
0;
intersection(convexity=5) {
ClampFrame();
translate([0,0,XlateZ])
cube([2*Clamp.x,2*Clamp.y,Clamp.z + 2*Protrusion],center=true);
}
}
//----------------------
// Concave plate fitting helmet shell
module HelmetPlate() {
render()
difference() {
cylinder(d=MountDia,h=HelmetPlateThick,$fn=MountSides);
translate([0,0,HelmetPlateThick - HelmetPlateM + HelmetRX])
sphere(r=HelmetRX,$fn=128);
for (j=[-1,1])
translate([0,j*ScrewOC/2,-Protrusion]) {
PolyCyl(Insert[OD],0.6*Insert[LENGTH] + Protrusion,6);
PolyCyl(Screw[ID],2*HelmetPlateThick,6);
}
}
}
//----------------------
// Base of clamp ring
module MountBase() {
difference() {
union() {
cylinder(d=MountDia,h=FloorThick,$fn=MountSides);
translate([0,0,FloorThick + Clamp.z/2])
ClampSelect("Bottom");
}
for (j=[-1,1])
translate([0,j*ScrewOC/2,-Protrusion])
rotate(180/6)
PolyCyl(Insert[OD],0.6*Insert[LENGTH] + Protrusion,6);
}
}
//----------------------
// Lash it together
if (Layout == "Plate") {
HelmetPlate();
}
if (Layout == "Base") {
MountBase();
}
if (Layout == "Clamp") {
ClampFrame();
}
if (Layout == "Show") {
rotate([180,0,0])
HelmetPlate();
translate([0,0,ShowGap]) {
MountBase();
color("Ivory",0.3)
translate([0,0,Clamp.z/2 + FloorThick + ShowGap/2])
sphere(d=BallDia);
translate([0,0,Clamp.z/2 + FloorThick + ShowGap])
ClampSelect("Top");
}
}
if (Layout == "Build") {
translate([MountDia/2 + BuildGap,0,0])
HelmetPlate();
translate([-(MountDia/2 + BuildGap),0,0])
MountBase();
translate([0,MountDia/2 + BuildGap,Clamp.z/2])
rotate([0,180,0])
rotate(90)
ClampSelect("Top");
}

The original doodles include a bit of dress-up fairing that didn’t make the cut:

Helmet Mirror Ball Mount - doodles
Helmet Mirror Ball Mount – doodles

Discrete LM3909: Blue LED Radome

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

Discrete LM3909 Radome - AA alkaline
Discrete LM3909 Radome – AA alkaline

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

Astable Multivibrator - Alkaline AA Base - radome - solid model
Astable Multivibrator – Alkaline AA Base – radome – solid model

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