Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.
This is the simple height-map Tux image I’d been using for the chocolate molds:
Tux_Hi_Profile
But the poor critter looks a bit flattened:
Tux_Hi_Profile – solid model
The final result is tastier, but gives off a roadkill vibe:
Tux chocolates – detail
After a few tweaks to the image, now he has a radial gradient on his tummy, his right flipper extends forward, his feet have webs, and his smile looks radiant. The gray levels now extend over a larger range with a bit more separation, with the intent that he’ll now be 5 mm thick:
A rough estimate of the volume and measurement thereof:
Assume 1 cm slab thickness for mold cavities 4 or 5 mm deep
Measure size of base plate in cm (given by OpenSCAD script in mm)
Compute slab volume in cubic cm = millliliters (ignoring mold cavity volumes)
Divide by 2 to find volume of each silicone component
Mark that volume on the side of two sacrificial containers
Pour silicone components into those containers
Pour one into the other, mix twice as long as you think you should
Scrupulously avoid cross-contaminating the original containers!
Fast-forward overnight, cut the tape, and peel the silicone negative off the positive:
Tux 2×2 mold – opened
The top surface of the 3D printed positive wasn’t completely water silicone-tight, so the silicone leaked through the top and filled part of the interior. No harm done, but I wasn’t expecting that. The interior of the silicone negative came out pretty well, although you can see some small bubble cavities that may be due to air leaking out through the top of the positive:
Tux 2×2 mold – negative detail
The hand-knitted texture of the 3D printing process comes through very well, which is a Good Thing in this application. If you don’t like that, you can devote considerable time & attention to removing all traces of the production process.
As a proof of concept, I melted and tempered four Dove Dark Chocolate Promises, then poured the chocolate into the cavities:
Tux 2×2 mold – filled
The tempering followed a fairly simple process that worked reasonably well, but the chocolate obviously wasn’t liquid when I poured it. The results looked pretty good, in a textured sort of way:
Tux chocolates – silicone mold
Flushed with success, I tweaked the mold to eliminate the raised lip around the edge, printed another positive plate, mixed up more silicone rubber, paid more attention to getting rid of the bubbles, and got this result:
Tux 2×2 mold 2 – opened
The printed surface still isn’t silicone-tight, which began to puzzle me, but the result looked pretty good.
After some fiddling around, though, I think printing the entire mold array isn’t the way to go. OpenSCAD can handle these 2×2 arrays, but a slightly tweaked Tux model (about which, more later) grossly increased the processing time and memory usage; OpenSCAD (and its CGAL geometry back end) filled all 4 GB of RAM, then blotted up 5 GB of swap space, ran for well over half an hour, and totally locked up the desktop UI for the duration.
It’s certainly infeasible to print the larger array on a sizable base plate that you’d need for a real project. I think printing multiple copies of a single model (duplicating them in the slicer, which is fast & easy), then attaching them to a plain base will work better. There’s no need to print the base plate, either, as a serrated top surface doesn’t buy anything; acrylic (or some such) sheet is cheap, flat, and readily available.
The Bash scripts and OpenSCAD programs below don’t produce exactly the same results you see above, mostly because I screwed around with them while discovering the reasons why doing it this way doesn’t make sense, but they can serve as a starting point if you must convince yourself, too.
This Bash script produces a single positive mold item from a height map image:
// Mold positive pattern from grayscale height map
// Ed Nisley KE4ZNU - March 2014 - adapted from cookie press, added alignment pins
//-----------------
// Mold files
fnMap = "Tux_map.dat"; // override with -D 'fnMap="whatever.dat"'
fnPlate = "Tux_plate.dat"; // override with -D 'fnPlate="whatever.dat"'
DotsPerMM = 3.0; // overrride with -D DotsPerMM=number
MapHeight = 4.0; // overrride with -D MapHeight=number
ImageX = 100; // overrride with -D ImageX=whatever
ImageY = 100;
UsePins = true;
MapScaleXYZ = [1/DotsPerMM,1/DotsPerMM,MapHeight/255];
PlateScaleXYZ = [1/DotsPerMM,1/DotsPerMM,1.0];
echo("Press File: ",fnMap);
echo("Plate File: ",fnPlate);
echo(str("ImageX:",ImageX," ImageY: ", ImageY));
echo(str("Map Height: ",MapHeight));
echo(str("Dots/mm: ",DotsPerMM));
echo(str("Scale Map: ",MapScaleXYZ," Plate: ",PlateScaleXYZ));
//- Extrusion parameters - must match reality!
ThreadThick = 0.25;
ThreadWidth = 2.0 * ThreadThick;
//- Buid parameters
PlateThick = IntegerMultiple(1.0,ThreadThick); // solid plate under press relief
PinOD = 1.75; // locating pin diameter
PinDepth = PlateThick; // ... depth into bottom surface = total length/2
PinOC = 20.0; // spacing within mold item
echo(str("Pin depth: ",PinDepth," spacing: ",PinOC));
//- Useful info
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
HoleWindage = 0.2;
Protrusion = 0.1; // make holes & unions work correctly
MaxConvexity = 5; // used for F5 previews in OpenSCAD GUI
ZFuzz = 0.2; // numeric chaff just above height map Z=0 plane
//-----------------
// Import plate height map, slice off a slab to define outline
module Slab(Thick=1.0) {
intersection() {
translate([0,0,Thick/2])
cube([2*ImageX,2*ImageY,Thick],center=true);
scale(PlateScaleXYZ)
difference() {
translate([0,0,-ZFuzz])
surface(fnPlate,center=true,convexity=MaxConvexity);
translate([0,0,-1])
cube([2*ImageX,2*ImageY,2],center=true);
}
}
}
//- Put peg grid on build surface
module ShowPegGrid(Space = 10.0,Size = 1.0) {
Range = floor(50 / Space);
for (x=[-Range:Range])
for (y=[-Range:Range])
translate([x*Space,y*Space,Size/2])
%cube(Size,center=true);
}
//-- convert cylinder to low-count polygon
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);
}
//-- Locating pin hole with glue recess
// Default length is two pin diameters on each side of the split
module LocatingPin(Dia=PinOD,Len=0.0) {
PinLen = (Len != 0.0) ? Len : (4*Dia);
translate([0,0,-ThreadThick])
PolyCyl((Dia + 2*ThreadWidth),2*ThreadThick,4);
translate([0,0,-2*ThreadThick])
PolyCyl((Dia + 1*ThreadWidth),4*ThreadThick,4);
translate([0,0,-(Len/2 + ThreadThick)])
PolyCyl(Dia,(Len + 2*ThreadThick),4);
}
//- Build it
//ShowPegGrid();
echo("Building mold");
union() {
difference() {
Slab(PlateThick + Protrusion);
if (UsePins)
for (i=[-1,1])
translate([0,i*PinOC/2,0])
rotate(180/4) LocatingPin(Len=2*PinDepth);
}
translate([0,0,PlateThick]) // cookie press height map
scale(MapScaleXYZ)
difference() {
translate([0,0,-ZFuzz])
surface(fnMap,center=true,convexity=MaxConvexity);
translate([0,0,-1])
cube([2*ImageX,2*ImageY,2],center=true);
}
}
This OpenSCAD source code slides a base plate under an array of those mold items, with options for a separate plate using alignment pins or the combined plate-with-molds shown above:
// Positive mold framework for chocolate slabs
// Ed Nisley - KE4ZNU - March 2014
Layout = "FrameMolds"; // FramePins FrameMolds Pin
//- Extrusion parameters must match reality!
// Print with 2 shells and 3 solid layers
ThreadThick = 0.20;
ThreadWidth = 0.40;
Protrusion = 0.1; // make holes end cleanly
HoleWindage = 0.2;
//----------------------
// Dimensions
FileName = "Tux_Hi_Profile-positive.stl"; // overrride with -D
Molds = [2,2]; // count of molds within framework
MoldOC = [45.0,50.0]; // on-center spacing of molds
MoldSlab = 1.0; // thickness of slab under molds
BaseThick = 3.0;
BaseSize = [(Molds[0]*MoldOC[0] + 0),(Molds[1]*MoldOC[1] + 0),BaseThick];
echo(str("Overall base: ",BaseSize));
PinOD = 1.75; // locating pin diameter
PinLength = 2.0; // ... total length
PinOC = 20.0; // spacing within mold item
//----------------------
// Useful routines
//- Put peg grid on build surface
module ShowPegGrid(Space = 10.0,Size = 1.0) {
RangeX = floor(100 / Space);
RangeY = floor(125 / Space);
for (x=[-RangeX:RangeX])
for (y=[-RangeY:RangeY])
translate([x*Space,y*Space,Size/2])
%cube(Size,center=true);
}
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);
}
// Locating pin hole with glue recess
// Default length is two pin diameters on each side of the split
module LocatingPin(Dia=PinOD,Len=0.0) {
PinLen = (Len != 0.0) ? Len : (4*Dia);
translate([0,0,-ThreadThick])
PolyCyl((Dia + 2*ThreadWidth),2*ThreadThick,4);
translate([0,0,-2*ThreadThick])
PolyCyl((Dia + 1*ThreadWidth),4*ThreadThick,4);
translate([0,0,-(Len/2 + ThreadThick)])
PolyCyl(Dia,(Len + 2*ThreadThick),4);
}
module LocatingPins(Length) {
for (i=[-1,1])
translate([0,i*PinOC/2,0])
rotate(180/4)
LocatingPin(Len=Length);
}
//-- import a single mold item
module MoldItem() {
// intersection() {
import(FileName,convexity=10);
// cube([100,100,3],center=true);
// }
}
//-- Overall frame shape
module Frame() {
// translate([0,0,BaseSize[2]/2]) // platform under molds
// cube(BaseSize,center=true);
difference() {
hull()
for (i=[-1,1], j=[-1,1])
translate([i*BaseSize[0]/2,j*BaseSize[1]/2,0])
sphere(r=BaseThick);
translate([0,0,-BaseThick])
cube(2*BaseSize,center=true);
}
}
//- Build it
ShowPegGrid();
if (Layout == "Pin")
LocatingPin(Len=PinLength);
if (Layout == "Frame")
Frame();
if (Layout == "FramePins")
difference() {
Frame();
translate([-MoldOC[0]*(Molds[0] - 1)/2,-MoldOC[1]*(Molds[1] - 1)/2,0])
for (i=[0:Molds[0]-1],j=[0:Molds[1]-1])
translate([i*MoldOC[0],j*MoldOC[1],BaseSize[2]])
LocatingPins(BaseThick);
}
if (Layout == "FrameMolds") {
Frame();
translate([-MoldOC[0]*(Molds[0] - 1)/2,-MoldOC[1]*(Molds[1] - 1)/2,0])
for (i=[0:Molds[0]-1],j=[0:Molds[1]-1])
translate([i*MoldOC[0],j*MoldOC[1],BaseThick - MoldSlab + Protrusion])
MoldItem();
}
Natural PLA provides a nice, crystalline appearance:
Kenmore 158 Sewing Machine – Cool white LEDs – rear no flash
Cool white LEDs have somewhat higher lumen/watt efficiency, but the real gain came from doubling the number of LEDs:
Kenmore 158 Sewing Machine – Cool white LEDs – front flash
I overvolted the warm white LEDs to 14 V to get closer to 20 mA/segment, but the cool white ones run pretty close to 20 mA at 12 V, so I didn’t bother.
Commercial versions of this hack secure the wiring with little white clips and foam tape, so I should conjure up something like that. Mary specifically did not want the lights affixed under the arm, though, so those things weren’t even in the running.
The OpenSCAD source code widens the mount and moves the wiring conduit a little bit, to simplify the connections to both strips, but is otherwise identical to the earlier version:
// LED Strip Lighting Brackets for Kenmore Model 158 Sewing Machine
// Ed Nisley - KE4ZNU - March 2014
Layout = "Build"; // Build Show Channels Strip
//- Extrusion parameters must match reality!
// Print with 2 shells and 3 solid layers
ThreadThick = 0.20;
ThreadWidth = 0.40;
HoleWindage = 0.2; // extra clearance
Protrusion = 0.1; // make holes end cleanly
AlignPinOD = 1.70; // assembly alignment pins: filament dia
inch = 25.4;
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
//----------------------
// Dimensions
Segment = [25.0,10.0,3.0]; // size of each LED segment
SEGLENGTH = 0;
SEGWIDTH = 1;
SEGHEIGHT = 2;
WireChannel = 3.0; // wire routing channel
StripHeight = 12.0; // sticky tape width
StripSides = 8*4;
DefaultLayout = [1,2,"Wire","NoWire"];
NUMSEGS = 0;
NUMSTRIPS = 1;
WIRELEFT = 2;
WIRERIGHT = 3;
EndCapSides = StripSides;
CapSpace = 2.0; // build spacing for endcaps
BuildSpace = 3.0; // spacing between objects on platform
//----------------------
// 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);
}
module ShowPegGrid(Space = 10.0,Size = 1.0) {
RangeX = floor(100 / Space);
RangeY = floor(125 / Space);
for (x=[-RangeX:RangeX])
for (y=[-RangeY:RangeY])
translate([x*Space,y*Space,Size/2])
%cube(Size,center=true);
}
//-- The negative space used to thread wires into the endcap
module MakeWireChannel(Layout = DefaultLayout,Which = "Left") {
EndCap = [(2*WireChannel + 1.0),Layout[NUMSTRIPS]*Segment[SEGWIDTH],StripHeight]; // radii of end cap spheroid
HalfSpace = EndCap[0] * ((Which == "Left") ? 1 : -1);
render(convexity=2)
translate([0,Segment[SEGWIDTH]/2,0])
intersection() {
union() {
cube([2*WireChannel,WireChannel,EndCap[2]],center=true);
translate([-2*EndCap[0],0,EndCap[2]/2])
rotate([0,90,0]) rotate(180/6)
PolyCyl(WireChannel,4*EndCap[0],6);
}
translate([HalfSpace,0,(EndCap[2] - Protrusion)]) {
cube(2*EndCap,center=true);
}
}
}
//-- The whole strip, minus wiring channels
module MakeStrip(Layout = DefaultLayout) {
EndCap = [(2*WireChannel + 1.0),Layout[NUMSTRIPS]*Segment[SEGWIDTH],StripHeight]; // radii of end cap spheroid
BarLength = Layout[NUMSEGS] * Segment[SEGLENGTH]; // central bar length
hull()
difference() {
for (x = [-1,1]) // endcaps as spheroids
translate([x*BarLength/2,0,0])
resize(2*EndCap) rotate([0,90,0]) sphere(1.0,$fn=EndCapSides);
translate([0,0,-EndCap[2]])
cube([2*BarLength,3*EndCap[1],2*EndCap[2]],center=true);
translate([0,-EndCap[1],0])
cube([2*BarLength,2*EndCap[1],3*EndCap[2]],center=true);
}
}
//-- Cut wiring channels out of strip
module MakeMount(Layout = DefaultLayout) {
BarLength = Layout[NUMSEGS] * Segment[SEGLENGTH];
difference() {
MakeStrip(Layout);
if (Layout[WIRELEFT] == "Wire")
translate([BarLength/2,0,0])
MakeWireChannel(Layout,"Left");
if (Layout[WIRERIGHT] == "Wire")
translate([-BarLength/2,0,0])
MakeWireChannel(Layout,"Right");
}
}
//- Build it
ShowPegGrid();
if (Layout == "Channels") {
translate([ (2*WireChannel + 1.0),0,0]) MakeWireChannel(DefaultLayout,"Left");
translate([-(2*WireChannel + 1.0),0,0]) MakeWireChannel(DefaultLayout,"Right");
}
if (Layout == "Strip") {
MakeStrip(DefaultLayout);
}
if (Layout == "Show") {
MakeMount(DefaultLayout);
}
if (Layout == "Build") {
translate([0,(3*Segment[SEGWIDTH]),0]) MakeMount([1,2,"Wire","Wire"]); // rear left side, vertical
translate([0,0,0]) MakeMount([5,2,"Wire","NoWire"]); // rear top, across arm
translate([0,-(3*Segment[SEGWIDTH]),0]) MakeMount([6,2,"NoWire","Wire"]); // front top, across arm
}
The game plan: drop a small object through a laser beam that shines on a photodiode, thus causing an electrical signal that triggers various flashes and cameras and so forth and so on. This fixture holds the laser and photodiode in the proper orientation, with enough stability that you (well, I) can worry about other things:
Laser-photodiode fixture – on blade
It’s mounted on the blade of a dirt-cheap 2 foot machinist’s square clamped to the bench which will probably get a few holes drilled in its baseplate for more permanent mounting.
The solid model looks about like you’d expect:
Laser-photodiode fixture – solid model
There’s a small hole in the back for an 8-32 setscrew that locks it to the blade; the fit turned out snug enough to render the screw superfluous. I added those two square blocks with the holes after I taped the wires to the one in the picture.
The two semicircular (well, half-octagonal) trenches have slightly different diameters to suit the heatshrink tubing around the photodiode (a.k.a., IR LED) and brass laser housing. A dab of fabric adhesive holds the tubes in place, in addition to the Gorilla Tape on the ends.
The laser came focused at infinity, of course. Unscrewing the lens almost all the way put the focus about 3/4 of the way across the ring; call it 40 mm. The beam is rectangular, about 1×2 mm, at the center of the ring, and I rotated the body to make the short axis vertical; that’s good enough for my purposes.
The cable came from a pair of cheap earbuds with separate Left/Right pairs all the way from the plug.
The model builds in one piece, of course, and pops off the platform ready to use:
Laser-photodiode fixture – on platform
If you were doing this for an analytic project, you’d want a marker for the beam centerline on the vertical scale, but that’s in the nature of fine tuning. As it stands, the beam sits 8 mm above the base and flush with the top surface of the ring; if that were 10 mm, it’d be easier to remember.
The OpenSCAD source code has a few tweaks and improvements:
// Laser and LED-photodiode break-beam sensor
// Ed Nisley - KE4ZNU - March 2014
Layout = "Show"; // Build Show Ring Mount Guide
//- Extrusion parameters must match reality!
// Print with 2 shells and 3 solid layers
ThreadThick = 0.20;
ThreadWidth = 0.40;
HoleWindage = 0.2; // extra clearance
Protrusion = 0.1; // make holes end cleanly
AlignPinOD = 1.70; // assembly alignment pins: filament dia
inch = 25.4;
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
//----------------------
// Dimensions
LaserOD = 6.0; // brass focus tube
LaserLength = 20.0; // ... wire clearance
SensorOD = 6.5; // including light shield
SensorLength = 20.0; // ... wire clearance
RingSize = [50.0,70.0,8.0,8*4]; // support ring dimensions
RING_ID = 0;
RING_OD = 1;
RING_THICK = 2;
RING_SIDES = 3;
StrutWidth = 2.5; // strut supporting this thing
StrutLength = 26.5;
StrutBlock = [10.0,35.0,20.0]; // block around the clearance slot
BLOCK_WIDTH = 0;
BLOCK_LENGTH = 1;
BLOCK_HEIGHT = 2;
StrutScrewTap = 2.7; // 6-32 SHCS
GuideID = 4.0; // guide for cables
GuideOD = 3*GuideID;
BuildSpace = 3.0; // spacing between objects on platform
//----------------------
// 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);
}
module ShowPegGrid(Space = 10.0,Size = 1.0) {
RangeX = floor(100 / Space);
RangeY = floor(125 / Space);
for (x=[-RangeX:RangeX])
for (y=[-RangeY:RangeY])
translate([x*Space,y*Space,Size/2])
%cube(Size,center=true);
}
module Ring() {
difference() {
union() {
rotate(180/RingSize[RING_SIDES])
cylinder(d=RingSize[RING_OD],h=RingSize[RING_THICK],
$fn=RingSize[RING_SIDES]);
translate([-LaserOD,(-LaserLength - RingSize[RING_ID]/2),0])
cube([2*LaserOD,LaserLength,RingSize[RING_THICK]],center=false);
translate([-SensorOD,(-0*SensorLength + RingSize[RING_ID]/2),0])
cube([2*SensorOD,SensorLength,RingSize[RING_THICK]],center=false);
}
rotate(180/RingSize[RING_SIDES])
translate([0,0,-Protrusion])
cylinder(d=RingSize[RING_ID],h=(RingSize[RING_THICK] + 2*Protrusion),
$fn=RingSize[RING_SIDES]);
translate([0,0,RingSize[RING_THICK]])
rotate([90,0,0]) rotate(180/8)
PolyCyl(LaserOD,3*LaserLength,8);
translate([0,0,RingSize[RING_THICK]])
rotate([-90,0,0]) rotate(180/8)
PolyCyl(SensorOD,3*SensorLength,8);
}
}
module Mount() {
translate([0,0,StrutBlock[2]/2])
difference() {
cube(StrutBlock,center=true);
cube([StrutWidth,StrutLength,2*StrutBlock[2]],center=true);
translate([0,-StrutLength/3,0])
rotate([90,0,0])
PolyCyl(StrutScrewTap,StrutLength/2,6);
}
}
module Guide() {
difference() {
translate([0,0,RingSize[RING_THICK]/2])
cube([GuideOD,GuideOD,RingSize[RING_THICK]],center=true);
translate([0,0,-Protrusion]) rotate(180/8)
PolyCyl(GuideID,(RingSize[RING_THICK] + 2*Protrusion),8);
}
}
module Assembly() {
Ring();
translate([(RingSize[RING_OD]/2 + StrutBlock[BLOCK_LENGTH]/2
- (StrutBlock[BLOCK_LENGTH] - StrutLength)/2) + Protrusion,0,0])
rotate(90)
Mount();
for (i=[-1,1])
translate([(RingSize[RING_OD]/2 + GuideID/2),
i*(StrutBlock[BLOCK_WIDTH]/2 + GuideID),
0])
Guide();
}
//- Build it
ShowPegGrid();
if (Layout == "Ring") {
Ring();
}
if (Layout == "Mount") {
Mount();
}
if (Layout == "Guide") {
Guide();
}
if (Layout == "Show") {
Assembly();
}
if (Layout == "Build") {
translate([-5/2,-5/2,0])
cube(5);
}
Quite a while ago, I rebuilt a gooseneck shop lamp with an LED floodlight module, the light from which appears in many pictures of the Sherline mill. That module has a sibling that I just combined with a defunct halogen desk lamp to produce a better task light for the bench; the original 12 VAC 50 W transformer now loafs along at 4 W and ballasts the lamp base against tipping.
My initial idea, of course, was a 3D printed adapter from the existing arm hardware to the LED module, but PLA gets droopy at normal high-intensity LED heatsink temperatures. That led to doodling a metal bracket around the LED module flange, which led to pondering how annoying that would be to make, which led to the discovery that the screws holding the LED plug to the heatsink were ordinary M2x0.4 Philips head, which suggested I could just screw a bracket to the back of the module, which brought a recently harvested aluminum heatsink to hand, which led to the discovery that the tip of the pivot screw fit perfectly between the fins, which …
Shortly thereafter, I milled off the central fins to fit the shaft of the pivot screw, introduced the heatsink to Mr. Disk Sander to bevel the bottom, sawed the threads off the pivot, press-fit the two together, drilled a 2 mm cross-hole into the pivot, buttered it all up with epoxy, jammed a short M2 screw into the cross hole, and let the whole mess cure:
Desk Lamp LED Adapter – top view
The lamp modules were a surplus find, with one pin clipped nearly flush to the insulator. I soldered a pair of the same male pins as in the battery holders, with the matching female pins as a crude connector. The unshrunk heatstink tubing isn’t lovely, but got us to First Light:
Desk Lamp LED Adapter – front view
The original counterweight is, of course, much too heavy for the dinky LED module, so I’ll drill the mounting hole for the vertical arm further back on the beam to get another foot of reach. That will require more wire between the transformer to the lamp, soooo the connectors might just become soldered joints.
As you can tell from the background, Mary snatched the lamp from my hands and put it to immediate use in The Quilting Room.
The original doodles bear no resemblance to the final product, but do have some key dimensions that (having discarded the unused hardware) I’ll likely never need again.
The pivot between the arm and the lamp housing, with an idea for the LED holder:
Desk Lamp Bracket Dimensions – doodle
Details of the repurposed heatsink and the pivot bolt, with a block that never got built:
Although commenting out an undesired variable isn’t fashionable, OpenSCAD doesn’t have a practical mechanism to set specific values based on a control variable:
if-then-else deals with geometric objects
(boolean)?when_true:when_false (the ternary operator) doesn’t scale well
You could, of course, depend on OpenSCAD’s behavior of using the last (in syntactic order) instance of a “variable”, but IMHO that’s like depending on semantic whitespace.
In any event, the rest of the block builds itself around those three values by recomputing all of its dimensions.
The Browning OEM block looks like this:
Browning Hi-Power Magazine Block – solid model – BHP OEM
The Generic floorplate has a much larger spring retaining crimp, so the block has far more overhang:
Browning Hi-Power Magazine Block – solid model – Generic 1
As before, the yellow widgets are built-in support structures separated from the main object by one thread thickness and width. That seems to maintain good vertical tolerance and allow easy removal; the structures snap free with minimal force. A closeup look shows the gaps:
Browning Hi-Power Magazine Block – solid model – Generic 1 – support detail
The main shape now has a 2 mm taper to ease the magazine spring past the upper edge of the block. The horn remains slightly inset from the side walls to ensure that the whole thing remains manifold:
Browning Hi-Power Magazine Block – solid model – Generic 1 – whole end
The whole object looks about the same, though:
Browning Hi-Power Magazine Block – solid model – Generic 1 – whole side
The shape descends from the geometry I used for the stainless steel block, with the additional internal channel (on the right in the models) to be filled with steel-loaded epoxy during assembly. That should make the whole block sufficiently robust that you must destroy the floorplate and distort the spring to get it out; wrecking the magazine’s innards should count as not “readily” modifiable.
Some destructive testing seems to be in order…
The OpenSCAD source code:
// Browning Hi-Power Magazine Plug
// Ed Nisley KE4ZNU December 2013
// February 2014 - easier customization for different magazine measurements
Layout = "Whole"; // Whole Show Split
// Whole = upright for steel or plastic
// Show = section view for demo, not for building
// Split = laid flat for plastic show-n-tell assembly
AlignPins = true && (Layout == "Split"); // pins only for split show-n-tell
Support = true && (Layout != "Split"); // no support for split, optional otherwise
// Define magazine measurements
//BlockData = [-0.5, 1.5, 11.5]; // Browning OEM
BlockData = [-1.5, 2.0, 9.0]; // Generic 1
SCREWOFFSET = 0;
CRIMPHEIGHT = 1;
CRIMPDISTANCE = 2;
//- Extrusion parameters must match reality!
// Print with 2 shells and 3 solid layers
ThreadThick = 0.20;
ThreadWidth = 0.40;
HoleWindage = 0.2;
Protrusion = 0.1; // make holes end cleanly
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
//----------------------
// Dimensions
Angle = 12.5; // from vertical
SpringID = 10.3; // magazine spring curvature (measure with drill shank)
SpringRadius = SpringID / 2;
Taper = 2.0; // total taper toward top
Length = 24.5; // front-to-back perpendicular to magazine shaft
Height = 17.0; // bottom-to-top, parallel to magazine shaft
RectLength = Length - SpringID; // block length between end radii
HornBaseOD = 8.0; // fits between follower pegs to prevent shortening
HornTipOD = 5.0;
HornAddTip = (HornTipOD/2)*tan(Angle);
HornAddBase = (HornBaseOD/2)*tan(Angle);
HornAddLength = HornAddTip + HornAddBase + 2*Protrusion;
HornLength = 12.0; // should recompute ODs, but *eh*
ScrewOD = 3.0 - 0.25; // screw hole dia - minimal thread engagement
ScrewLength = Height - 5.0;
ScrewOffset = BlockData[SCREWOFFSET]; // ... from centerline on XY plane
NutOD = 5.8; // hex nut dia across flats
NutThick = 2.4; // ... generous allowance for nut
NutTrapLength = 1.5*NutThick; // allow for epoxy buildup
NutTrapBaseHeight = 5.0; // ... base height from floor plate
CrimpHeight = IntegerMultiple(BlockData[CRIMPHEIGHT],ThreadThick); // vertical clearance for spring crimp tab on base plate
CrimpDistance = BlockData[CRIMPDISTANCE]; // ... clip to screw hole center
CrimpOffset = -(CrimpDistance - ScrewOffset); // ... horizontal from centerline
SupportLength = 4.0; // length of support struts under Trim
SupportWidth = IntegerMultiple(0.9*SpringID,4*ThreadWidth); // ... size needed for platform adhesion
SupportThick = CrimpHeight - ThreadThick; // ... clearance for EZ removal
VentDia = 2.5; // air vent from back of screw recess
//VentOffset = CrimpOffset + VentDia/2 + 5*ThreadWidth;
VentOffset = -(NutOD + 4*ThreadWidth);
VentLength = ScrewLength + VentDia;
RecessDia = 3.5; // additional air vent + weight reduction
RecessLength = ScrewLength + RecessDia/2; // ... internal length
RecessOffset = Length/2 - RecessDia/2 - 5*ThreadWidth; // ... offset from centerline
PinOD = 1.72; // alignment pins
PinLength = 4.0;
PinInset = 0.6*SpringRadius; // from outside edges
echo(str("Alignment pin length: ",PinLength));
NumSides = 8*4; // default cylinder sides
Offset = 5.0/2; // from centerline for build layout
//----------------------
// Useful routines
function Delta(a,l) = l*tan(a); // incremental length due to angle
// Locating pin hole with glue recess
// Default length is two pin diameters on each side of the split
module LocatingPin(Dia=PinOD,Len=0.0) {
PinLen = (Len != 0.0) ? Len : (4*Dia);
translate([0,0,-ThreadThick])
PolyCyl((Dia + 2*ThreadWidth),2*ThreadThick,4);
translate([0,0,-2*ThreadThick])
PolyCyl((Dia + 1*ThreadWidth),4*ThreadThick,4);
translate([0,0,-(Len/2 + ThreadThick)])
PolyCyl(Dia,(Len + 2*ThreadThick),4);
}
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);
}
module ShowPegGrid(Space = 10.0,Size = 1.0) {
Range = floor(50 / Space);
for (x=[-Range:Range])
for (y=[-Range:Range])
translate([x*Space,y*Space,Size/2])
%cube(Size,center=true);
}
//----------------------
// The magazine block
module Block(SectionSelect = 0) {
CropHeight = Height*cos(Angle); // block height perpendicular to base
echo(str("Perpendicular height: ",CropHeight));
difference() {
union() {
intersection() {
rotate([Angle,0,0])
hull() {
for (i=[-1,1])
translate([0,i*RectLength/2,-((Length/2)*sin(Angle) + Protrusion)])
cylinder(r1=SpringRadius,r2=(SpringRadius - Taper/2),
h=(Height + 2*(Length/2)*sin(Angle) + 2*Protrusion),
$fn=NumSides);
}
translate([0,0,CropHeight/2])
cube([2*SpringID,3*Length,CropHeight],center=true);
}
translate([0,-Height*sin(Angle),Height*cos(Angle)])
resize([(SpringID - Taper),0,0])
intersection() {
rotate([Angle,0,0])
translate([0,0,-(HornAddBase + Protrusion)])
cylinder(r1=HornBaseOD/2,
r2=HornTipOD/2,
h=(HornLength + HornAddLength + Protrusion),
$fn=NumSides);
cube([2*SpringID,Length,2*(HornLength*cos(Angle) + Protrusion)],center=true);
}
}
translate([0,ScrewOffset,-Protrusion]) // screw
rotate(180/6)
PolyCyl(ScrewOD,(ScrewLength + Protrusion),6);
translate([0,ScrewOffset,NutTrapBaseHeight]) // nut trap in center
rotate(180/6)
PolyCyl(NutOD,NutTrapLength,6);
translate([0,ScrewOffset,-Protrusion]) // nut clearance at base
rotate(180/6)
PolyCyl(NutOD,(1.1*NutThick + Protrusion),6);
translate([SpringID/2,CrimpOffset,-Protrusion])
rotate(180)
cube([SpringID,Length,(CrimpHeight + Protrusion)],center=false);
if (AlignPins) // alignment pins
if (true)
translate([0,-CropHeight*tan(Angle),CropHeight])
rotate([0,90,0]) rotate(45 + Angle)
LocatingPin(PinOD,PinLength);
else
for (i=[-1,1]) // cannot use these with additional vents * channels
rotate([Angle,0,0])
translate([0,
(i*((Length/2)*cos(Angle) - PinInset)),
(CropHeight/2 - i*2*PinInset)])
rotate([0,90,0]) rotate(45 - Angle)
LocatingPin(PinOD,PinLength);
translate([0,(ScrewOffset + 1.25*NutOD),ScrewLength]) // air vent
rotate([90,0,0]) rotate(180/8)
PolyCyl(VentDia,3*NutOD,8);
translate([0,VentOffset,-(VentDia/2)*tan(Angle)])
rotate([Angle,0,0]) rotate(180/8)
PolyCyl(VentDia,VentLength,8);
translate([0,RecessOffset,0]) // weight reduction recess
rotate([Angle,0,0]) rotate(180/8)
translate([0,0,-((RecessDia/2)*tan(Angle))])
PolyCyl(RecessDia,(RecessLength + (RecessDia/2)*tan(Angle)),8);
if (SectionSelect == 1)
translate([0*SpringID,-2*Length,-Protrusion])
cube([2*SpringID,4*Length,(Height + HornLength + 2*Protrusion)],center=false);
else if (SectionSelect == -1)
translate([-2*SpringID,-2*Length,-Protrusion])
cube([2*SpringID,4*Length,(Height + HornLength + 2*Protrusion)],center=false);
}
SupportSlots = (SupportWidth / (4*ThreadWidth)) / 2; // SupportWidth is multiple of 4*ThreadWidth
if (Support)
color("Yellow") {
translate([0,(CrimpOffset - SupportLength/2),SupportThick/2])
difference() {
translate([0,-ThreadWidth,0])
cube([(SupportWidth - Protrusion),SupportLength,SupportThick],center=true);
for (i=[-SupportSlots:SupportSlots])
translate([i*4*ThreadWidth + 0*ThreadWidth,ThreadWidth,0])
cube([(2*ThreadWidth),SupportLength,(SupportThick + 2*Protrusion)],center=true);
}
translate([0,ScrewOffset,0])
for (j=[0:5]) {
rotate(30 + 360*j/6)
translate([(NutOD/2 - ThreadWidth)/2,0,(1.1*NutThick - ThreadThick)/2])
color("Yellow")
cube([(NutOD/2 - ThreadWidth),
(2*ThreadWidth),
(1.1*NutThick - ThreadThick)],
center=true);
}
}
}
//-------------------
// Build it...
ShowPegGrid();
if (Layout == "Show")
Block(1);
if (Layout == "Whole")
Block(0);
if (Layout == "Split") {
translate([(Offset + Length/2),Height/2,0])
rotate(90) rotate([0,-90,-Angle])
Block(-1);
translate([-(Offset + Length/2),Height/2,0])
rotate(-90) rotate([0,90,Angle])
Block(1);
}
Some trial fitting with the prototype showed that there’s no possible way to route the connections through the socket, no matter how much I wanted that to happen, so I rotated the body to align the LEDs with the socket pin slots:
Sears Lamp LED Adapter – Show view
The body now builds with the flat end down, so the overall finish should be better:
Sears Lamp LED Adapter – Build view
A test run shows why I really, really wanted cool white LEDs in the strips over the arm:
Kenmore 158 Sewing Machine – mixed LED lighting
The LED mount doesn’t have quite enough room inside the end cap for the holder to tilt as I wanted; the two 10 mm LEDs can be about 10 mm lower and slightly closer to the shaft driving the needle, which is what this rapid prototyping stuff is all about. Scrapping the existing lamp socket and (120 VAC!) wiring seems the best way to make this more useful.
Early reports on the arm LEDs indicate a requirement for more light, so the next iteration of those mounts will put two strips side-by-side…