The Smell of Molten Projects in the Morning

Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.

Author: Ed

  • The First Annual Dutchess Rail Trail Muster of Recumbent Riders

    Did a 38 mile end-to-end ride on the Dutchess Rail Trail yesterday in the company of a dozen recumbents drawn to The First Annual Dutchess Rail Trail Muster of Recumbent Riders

    Some of the group in the parking lot of the Hopewell Junction Depot:

    Recumbent Riders - Hopewell Junction Depot - 2014-04-05
    Recumbent Riders – Hopewell Junction Depot – 2014-04-05

    Rolling out:

    Recumbent Riders - Leaving Hopewell Junction Depot - 2014-04-05
    Recumbent Riders – Leaving Hopewell Junction Depot – 2014-04-05

    The ADA nubbly strips at North Grand Avenue remain missing in action:

    Recumbent Riders - North Grand Avenue Crossing - 2014-04-05
    Recumbent Riders – North Grand Avenue Crossing – 2014-04-05

    A highracer gingerly navigates the low-speed gauntlet to the Walkway Over the Hudson:

    Recumbent Riders - Walkway Over the Hudson Entrance - 2014-04-05
    Recumbent Riders – Walkway Over the Hudson Entrance – 2014-04-05

    The pix come from the Sony HDR-AS30V helmet camera video, set to 1920×1080 / 120° @ 60 frame/s. They’ve had a bit of refocusing and color adjustment, but nothing can dramatically improve them. The video looks better only because eyes aren’t as fussy with moving images. I’m not red-hot pleased with the resolution & compression, but the camera is what it is.

    The lens carried a smear on the upper-right quadrant that shows the sensitivity of the optical path.

    Memo to Self: Clean the lens and keep fingers off!

    That’s harder than it may seem. The Start button is on the back of the body, recessed far enough into the skeleton frame to require an index finger rather than a thumb, and it’s remarkably easy to touch the bulging fisheye lens with your (well, my) thumb; a touch is all it takes to create a nice smear.

    I started and ended at home, rather than at the Hopewell Junction end, but you get the general idea:

    DCRT Muster - GPS Track - 2014-04-05
    DCRT Muster – GPS Track – 2014-04-05
  • Improved Packing Tape Dispenser Spool

    As I recall, a few weeks after I bought this packing tape dispenser, I dropped it with the nut downward, whereupon all six of the little tabs that were supposed to hold the tape roll in place broke off, allowing the roll to walk off the holder. Having put up with that for far too long (I don’t do a lot of shipping these days), I finally drilled and tapped three 4-40 holes and ran a trio of setscrews against the inside of the roll core:

    Packing tape dispenser - improved spool holder
    Packing tape dispenser – improved spool holder

    The holes are angled so that the setscrews bite into the core just enough to prevent it from walking away, but I can still pull the roll off when it’s empty.

    That should last pretty nearly forever…

  • Chocolate Molds: 3D Printed Positive Plate vs. Pourable Silicone

    Based on the results of the Squidwrench molds, I tweaked Tux’s height-map image a bit:

    Tux_Hi_Profile
    Tux_Hi_Profile

    A Bash script driving an OpenSCAD program turns that into a single positive mold model:

    Tux_Hi_Profile - solid model
    Tux_Hi_Profile – solid model

    Another OpenSCAD program arranges them in an array atop a baseplate:

    Tux Hi Profile - Framework 2x2
    Tux Hi Profile – Framework 2×2

    Pause an hour or so for 3D printing, tape a cardboard frame around it, then pour on a dollop of OOMOO 30 Tin-cure Silicone Rubber:

    Tux 2x2 mold - curing
    Tux 2×2 mold – curing

    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 2x2 mold - opened
    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 2x2 mold - negative detail
    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 2x2 mold - filled
    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
    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 2x2 mold 2 - opened
    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:

    #!/bin/bash
    DotsPerMM=3.0
    MapHeight=4
    ImageName="${1%%.*}"
    rm ${ImageName}_* ${ImageName}-positive.stl
    echo Prepare grayscale image...
    #convert $1 -type Grayscale -depth 8 -trim +repage -flip +set comment ${ImageName}_prep.png
    convert $1 -flip +set comment ${ImageName}_prep.png
    echo Create PGM files...
    convert ${ImageName}_prep.png -compress none ${ImageName}_map.pgm
    convert ${ImageName}_prep.png -white-threshold 1 -compress none ${ImageName}_plate.pgm
    echo Create height map data files...
    ImageX=`identify -format '%[fx:w]' ${ImageName}_map.pgm`
    ImageY=`identify -format '%[fx:h]' ${ImageName}_map.pgm`
    echo Width: ${ImageX} x Height: ${ImageY}
    cat ${ImageName}_map.pgm   | tr -s ' \012' '\012' | tail -n +5 | column -x -c $((8*$ImageX)) > ${ImageName}_map.dat
    cat ${ImageName}_plate.pgm | tr -s ' \012' '\012' | tail -n +5 | column -x -c $((8*$ImageX)) > ${ImageName}_plate.dat
    echo Create mold positive...
    time openscad -D fnPlate=\"${ImageName}_plate.dat\" \
    -D fnMap=\"${ImageName}_map.dat\" -D Height=$MapHeight \
    -D ImageX=$ImageX -D ImageY=$ImageY -D DotsPerMM=$DotsPerMM \
    -o ${ImageName}-positive.stl MoldPositive.scad
    
    

    That script drives this OpenSCAD source code:

    // 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();
    }
    
    
  • Long-duration Exposure: Neutral Density Filters vs. Color Shift

    The view through the front window (1950-vintage glass + storm window) at 1/500 s, f/4, ISO 100, auto-exposed to EV 0:

    Front yard - normal exposure
    Front yard – normal exposure

    The same view, using stacked 2 + 4 + 8 ND filters, at 1/3 s, f/4, ISO 100, auto-exposed to EV 0:

    Front yard - ND2-4-8 filters
    Front yard – ND2-4-8 filters

    That blur to the left of the mailbox is a passing car, which is the whole point of heavy ND filtering: you can use absurdly long shutter times and still get a decent exposure.

    I cannot explain the fact that the ND filters allegedly reduce the light by 14 stops, but the actual (auto) exposure increases by about 7 stops. They’re cheap non-coated K&F Concept Digital filters, of course, but …

    There’s an obvious color shift toward red / magenta.

    This is a placeholder so I can pick up the thought later on…

  • Kenmore Model 158 Sewing Machine: Cool White LED Strip Lights

    The reel of cool-white LEDs finally arrived from halfway around the planet and, based on the ahem customer feedback concerning the earlier prototype warm white LED strip lights for Mary’s sewing machine, I went with two parallel strips on each mount:

    Strip Light Mount - 2 wide - build layout
    Strip Light Mount – 2 wide – build layout

    Natural PLA provides a nice, crystalline appearance:

    Kenmore 158 Sewing Machine - Cool white LEDs - rear no flash
    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
    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.

    That black Gorilla Tape remains butt-ugly:

    Kenmore 158 Sewing Machine - Cool white LEDs - rear flash
    Kenmore 158 Sewing Machine – Cool white LEDs – rear flash

    Obviously, I must find some white duct tape…

    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
    }
    
  • Monthly Science: Early Lithium Cell Failures

    During my monthly data logging, I replace any weak CR2032 cells in the Hobo data loggers and, being that type of guy, I write the current date and the elapsed time since the last replacement on the top of the cells. This month I had to replace two cells:

    Energizer CR2023 - early failures
    Energizer CR2023 – early failures

    Huh.

    It seems the previous Energizer CR2023 cells in those loggers lasted for more than the usual year, but these cells from the same lot with the same date code failed in two weeks (my last monthly science was unusually late, because distraction). The YA date code (printed on the other side of the cell) isn’t helpful (that Q&A  list shows the problem), but they’re supposed to have an eight year shelf life. As nearly as I can tell, these are getting on toward five years on my shelf, so maybe they spent a bit more time on somebody else’s shelf than the seller claimed.

    I’d previously slandered one of the Hobo loggers by accusing it of high current drain, but it turned out to be different crappy batteries.

    I know they encrypt the date codes so we can’t buy the freshest retail batteries, but I don’t have to like it…

  • Strobe Photography: Control Program

    A solderless breadboard sufficed for the simple circuitry behind the strobe controller:

    Strobe Photography - control breadboard
    Strobe Photography – control breadboard

    I used a separate 7.5 V supply for the Arduino Pro Mini to keep the relay noise out of the VCC circuit, but that’s probably not really necessary; you could back-drive the Pro Mini’s regulator with +5 V and it’d be perfectly happy. There’s a +5 V wall wart for the relay, LEDs, and so forth.

    Protip: you do not want to drive all the other circuitry through the Pro Mini’s tiny little regulator. Work out the power dissipation in the regulator caused by a 130 Ω relay, about 10 mA for the laser, 100 mA for the white LED, and whatever the Pro Mini draws. Yeah, some of those are intermittent loads, but work it out anyway.

    A 1.5 V bench supply powers the Xenon strobe in place of the AA alkaline cell I used at first. The boost circuit pins the supply at 3 A for a few seconds, then settles at about 350 mA (!) while idling; no wonder the poor little AA cells don’t last very long!

    The control program is also dead simple; it’s mostly a state machine that notices when the photocurrent drops to zero, then steps through a series of fixed delays while turning the laser, LED, and strobe outputs on and off.

    The default values highlight a falling object about 200 mm below the laser beam-break sensor, assuming you release the object just above the beam:

    Ball at 200 mm - detail
    Ball at 200 mm – detail

    The laser beam is at the 200 mm mark, so that ball passing 400 mm has dropped 200 mm.

    The quadrature encoder knob recycles the same interrupt handler I used earlier, with the shaft button selecting either the LED delay (pushed) or the Xenon strobe delay (released). There’s precious little error checking, as befits a quick hack job, so use at your own risk…

    The Arduino source code:

    // Optical flash triggering
    // Ed Nisley - KE4ANU - March 2014
    
    //----------
    // Pin assignments
    
    const byte PIN_KNOB_A = 2;			// knob A switch - must be on ext interrupt 2
    const byte PIN_KNOB_B = 4;			//  .. B switch
    const byte PIN_KNOB_SWITCH = A3;	//  .. shaft push switch
    
    const byte PIN_PHOTOCURRENT = A0;	// photodiode current input
    
    const byte PIN_LASER = 8;   		// laser drive -active
    const byte PIN_LED = 7;		   		// LED drive -active
    const byte PIN_FLASH = 12;			// Xenon flash relay -active
    
    const byte PIN_SYNC = 13;			// scope sync - and Arduino LED
    
    //----------
    // Constants
    
    enum FALLING_STATES {F_IDLE,F_WAIT,F_DETECT,F_PREFALL,F_LED,F_MD,F_FLASH,F_CLEAR};
    
    enum KNOB_STATES {KNOB_CLICK_0,KNOB_CLICK_1};
    
    //----------
    // Globals
    
    const unsigned long UPDATEMS = 250;	// update displays only this many ms apart
    
    volatile char KnobCounter = 0;
    volatile byte KnobState;
    
    byte Button, PrevButton;
    
    byte Falling = F_IDLE;				// cold start the detection state machine
    
    unsigned long FallStart;			// when we we detected the falling object
    
    unsigned int DetectLevel = 200;		// ADC reading for object detection
    
    unsigned int DelayLED = 1;			// ms from trigger detect to LED preflash
    
    unsigned int DelayFlash = 180;		//  ... to Xenon flash
    
    unsigned int DelayClear = 6000;		//  ... after impact to allow camera restart
    
    const byte PulseLED = 50;			// ms LED on to pass motion detection threshold
    const byte PulseFlash = 20;			// ms Xenon flash relay on
    
    const unsigned int RelayAdvance = 3;	// ms relay activation to Xenon flash
    
    unsigned long MillisNow;
    unsigned long MillisThen;
    
    //-- Helper routine for printf()
    
    int s_putc(char c, FILE *t) {
      Serial.write(c);
    }
    
    //-- Knob interrupt handler
    
    void KnobHandler(void)
    {
    	byte Inputs;
    	Inputs = digitalRead(PIN_KNOB_B) << 1 | digitalRead(PIN_KNOB_A);  // align raw inputs
    //	Inputs ^= 0x02;                             // fix direction
    
    	switch (KnobState << 2 | Inputs) {
    	case 0x00 : 				// 0 00 - glitch
            break;
    	case 0x01 : 				 // 0 01 - UP to 1
            KnobCounter++;
    		KnobState = KNOB_CLICK_1;
    		break;
    	case 0x03 : 				 // 0 11 - DOWN to 1
            KnobCounter--;
    		KnobState = KNOB_CLICK_1;
    		break;
    	case 0x02 : 				 // 0 10 - glitch
            break;
    	case 0x04 : 				 // 1 00 - DOWN to 0
            KnobCounter--;
    		KnobState = KNOB_CLICK_0;
    		break;
    	case 0x05 : 				 // 1 01 - glitch
            break;
    	case 0x07 : 				 // 1 11 - glitch
            break;
    	case 0x06 : 				 // 1 10 - UP to 0
            KnobCounter++;
    		KnobState = KNOB_CLICK_0;
    		break;
    	default :  					// something is broken!
            KnobCounter = 0;
    		KnobState = KNOB_CLICK_0;
    	}
    }
    
    //------------------
    // Set things up
    
    void setup() {
    
    	pinMode(PIN_SYNC,OUTPUT);
    	digitalWrite(PIN_SYNC,LOW);			// show we arrived
    
    	pinMode(PIN_KNOB_B,INPUT_PULLUP);
    	pinMode(PIN_KNOB_A,INPUT_PULLUP);
    	pinMode(PIN_KNOB_SWITCH,INPUT_PULLUP);
    
        pinMode(PIN_LASER,OUTPUT);
        digitalWrite(PIN_LASER,HIGH);
    
        pinMode(PIN_LED,OUTPUT);
        digitalWrite(PIN_LED,HIGH);
    
        pinMode(PIN_FLASH,OUTPUT);
        digitalWrite(PIN_FLASH,HIGH);
    
    	KnobState = digitalRead(PIN_KNOB_A);
    	Button = PrevButton = !digitalRead(PIN_KNOB_SWITCH);
    
    	attachInterrupt((PIN_KNOB_A - 2),KnobHandler,CHANGE);
    
    	Falling = F_IDLE;
    
    	Serial.begin(9600);
    	fdevopen(&s_putc,0);				// set up serial output for printf()
    
    	printf("Xenon Flash Trigger\r\nEd Nisley - KE4ZNU - March 2014\r\n");
    
    	MillisThen = millis();
    
    }
    
    //------------------
    // Go flash!
    
    void loop() {
    
    	MillisNow = millis();
    
    	if (KnobCounter) {
    		Button = !digitalRead(PIN_KNOB_SWITCH);
    		if (Button)
    			DelayLED += KnobCounter;
    		else
    			DelayFlash += KnobCounter;
    
    		DelayLED = min(DelayLED,DelayFlash - PulseLED);
    		printf("Knob: %d, LED: %d, Flash: %d\n",KnobCounter,DelayLED,DelayFlash);
    		KnobCounter = 0;
    	}
    
    	digitalWrite(PIN_SYNC,HIGH);
    
    	switch (Falling) {
    	case F_IDLE :								// turn on laser for object detection
    		digitalWrite(PIN_LASER,LOW);
    		printf("Laser on, stabilizing... ");
    		while (analogRead(PIN_PHOTOCURRENT) <= DetectLevel) {
    			printf("*");
    		}
    		printf("\nReady!\n");
    		Falling = F_WAIT;
    		break;
    	case F_WAIT :								// record starting time of beam break
    		if (analogRead(PIN_PHOTOCURRENT) < DetectLevel) {
    			FallStart = millis();
    			Falling = F_DETECT;
    		}
    		break;
    	case F_DETECT :								// turn off laser to signal detection
    		digitalWrite(PIN_LASER,HIGH);
    		Falling = F_PREFALL;
    		break;
    	case F_PREFALL :							// turn on LED to trigger camera motion detection
    		if ((millis() - FallStart) >= DelayLED) {
    			digitalWrite(PIN_LED,LOW);
    			Falling = F_LED;
    		}
    		break;
    	case F_LED : 								// turn off LED
    		if ((millis() - FallStart) >= (DelayLED + PulseLED)) {
    			digitalWrite(PIN_LED,HIGH);
    			Falling = F_MD;
    		}
    		break;
    	case F_MD :									// fire the strobe to take picture
    		if ((millis() - FallStart) >= (DelayFlash - RelayAdvance)) {
    			digitalWrite(PIN_FLASH,LOW);
    			Falling = F_FLASH;
    		}
    		break;
    	case F_FLASH :								// turn off strobe relay
    		if ((millis() - FallStart) >= (DelayFlash - RelayAdvance + PulseFlash)) {
    			digitalWrite(PIN_FLASH,HIGH);
        		printf("Flash with LED delay: %d, Xenon delay: %d ...",DelayLED,DelayFlash);
    			Falling = F_CLEAR;
    		}
    		break;
    	case F_CLEAR :								// wait for camera to recycle
    		if ((millis() - FallStart) >= DelayClear) {
    			printf("done\n");
    			Falling = F_IDLE;
    		}
    		break;
    	default :
    		printf("** Bad Falling state: %02X",Falling);
    		Falling = F_IDLE;
    	}
    
    	digitalWrite(PIN_SYNC,LOW);
    
    	if ((MillisNow - MillisThen) > UPDATEMS) {
    //		printf("State: %02X\n",Falling);
    		MillisThen = MillisNow;
    	}
    
    }