The beaver family living in their pond along the Dutchess Rail Trail near Titusville Rd is doing so well they’ve erected a second lodge:

It’s 500 feet (-ish) upstream from the first lodge and seems somewhat smaller, so perhaps it’s for the kids.
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.

The beaver family living in their pond along the Dutchess Rail Trail near Titusville Rd is doing so well they’ve erected a second lodge:

It’s 500 feet (-ish) upstream from the first lodge and seems somewhat smaller, so perhaps it’s for the kids.

Spotted over a fast food emporium’s parking lot:

It’s hard to be sure, but I think there’s a paper wasp nest around the bundle of wires just above the transformer / ballast / whatever. Perhaps the repair tech departed with the job unfinished?
As with traffic signals, flashlights, and automotive lighting, the LEDs surely work long after the driver circuitry has given up.

Building the circuit support plate for the amber front running light was entirely too fiddly:

This was definitely easier:

Two pins fit in the small holes to align it with the LED heatsink, with an M3 stud and brass insert holding it in place:

The rectangular hole around the insert let me glop urethane adhesive over it to lock it into the plate, with more goop on the screw and pins to unify heatsink and plate.
The LED wires now emerge from the heatsink on the same side of the plate, simplifying the connections to the MP1584 regulator and current-sense resistor:

The paralleled 5.1 Ω and 3.3 Ω resistors form a 2.0 Ω resistor setting the LED current to 400 mA = 1 W at 2.6 V forward drop. They’re 1 W resistors dissipating a total of 320 mW and get barely warm.
The resistors and wires are stuck in place with clear adhesive, so things shouldn’t rattle around too much.
The OpenSCAD source code as a GitHub Gist:
| // Circuit plate for Tour Easy running lights | |
| // Ed Nisley – KE4ZNU – 2021-09 | |
| /* [Hidden] */ | |
| ThreadThick = 0.25; | |
| ThreadWidth = 0.40; | |
| HoleWindage = 0.2; | |
| Protrusion = 0.1; // make holes end cleanly | |
| function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit); | |
| ID = 0; | |
| OD = 1; | |
| LENGTH = 2; | |
| inch = 25.4; | |
| //———————- | |
| // Dimensions | |
| // Light case along X axis | |
| LightID = 23.0; | |
| WallThick = 2.0; | |
| Screw = [3.0,6.8,4.0]; // M3 OD=washer, length=nut + washers | |
| Insert = [3.0,4.2,8.0]; // splined brass insert, minus splines | |
| InsertOffset = 10.0; // insert from heatsink end | |
| PinOD = 1.6; // alignment pins | |
| PinOC = 14.0; | |
| PinDepth = 5.0; | |
| Plate = [50.0,LightID,Insert[OD] + 4*ThreadThick]; // overall plate size | |
| WirePort = [10.0,3.0,2*Plate.z]; | |
| NumSides = 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); | |
| } | |
| // Circuit plate | |
| module Plate() { | |
| difference() { | |
| intersection() { | |
| cube(Plate,center=true); | |
| rotate([0,90,0]) | |
| cylinder(d=LightID,h=2*Plate.x,$fn=NumSides,center=true); | |
| } | |
| rotate([0,90,0]) rotate(180/6) | |
| translate([0,0,-Plate.x]) | |
| PolyCyl(Screw[ID],2*Plate.x,6); | |
| rotate([0,90,0]) rotate(180/6) | |
| translate([0,0,-Plate.x/2 – Protrusion]) | |
| PolyCyl(Insert[OD],Insert[LENGTH] + InsertOffset + Protrusion,6); | |
| translate([-Plate.x/2 + InsertOffset + Insert[LENGTH]/2,0,Plate.z/2]) | |
| cube([Insert[LENGTH],Insert[OD],Plate.z],center=true); | |
| for (j=[-1,1]) | |
| translate([-Plate.x/2,j*PinOC/2,0]) | |
| rotate([0,90,0]) rotate(180/6) | |
| translate([0,0,-PinDepth]) | |
| PolyCyl(PinOD,2*PinDepth,6); | |
| for (j=[-1,1]) | |
| translate([0,j*(Plate.y/2 – WirePort.y/2),0]) | |
| cube(WirePort,center=true); | |
| } | |
| } | |
| //- Build it | |
| Plate(); | |

Having discovered the need for careful alignment of the LED PCB with the lens, I paid more attention to detail this time around.
The LEDs arrive soldered to PCBs atop aluminum star heat spreaders, but the one I picked out of the bag looked slightly misaligned. Unsoldering it showed a smear of solder paste had melted across the central pad:

The LED has a die contact slug on the bottom which, I suppose, could be directly soldered to the spreader. For my simple needs, removing the errant solder, plunking the LED atop a layer of heatsink compound, and resoldering the leads should suffice:

The LED holder has a pair of slots aligning it with the LED leads on the PCB. The base of the holder sits flush against the PCB, so the wires must attach directly to the LED pads.
I ran the wires for the amber light through holes close to the pads:

Which required chewing two passages in the base of the holder:

It turns out the 5° and 10° lenses are strongly conical and leave plenty of room around the LED to run a wire around the inside of the holder, so I drilled a pair of holes to put both wires on the same side of the circuit plate:

The holder required minor surgery to let the wire double back on itself over the LED pad:

The wires thread through two holes drilled in the plastic holder:

More urethane adhesive glues the PCB to the LED holder, with the clamp applying pressure to the lens to ensure the lens seats properly around the LED. It turned out that worked well and the light has a nicely rounded beam.
With the optics bonded together, metal-filled JB Weld epoxy attaches the heat spreader to the heatsink with good thermal conductivity:

The LED holder is a slide fit in the heatsink, so the clamps can keep the PCB flat on the bottom of the recess while the epoxy gets a good grip on all parts.
Now it’s just a matter of wiring everything up!

Because the rear running light will have a higher duty cycle than the front light, I made the (admittedly too small) heatsink slightly longer, with a deeper recess to protect the lens from cargo on the rear rack:

Boring that nice flat bottom is tedious; I must lay in a stock of aluminum tubing to simplify the process.
Drilling the holes went smoothly:

Those two holes fit a pair of pins aligning the circuit plate, with a screw and brass insert holding it to the heatsink. Scuffing a strip across the aluminum might give the urethane adhesive (you can see uncured globs on the pins) a better grip:

The screw / insert /pins are glued into the plate to permanently bond it to the heatsink. The screw occupies only half of the insert, with the longer screw from the end cap pulling the whole affair together.
The two holes on the left pass both LED leads to one side of the circuit plate, where they connect to the current regulator and its sense resistor.

The same lathe fixture and double-sided duct tape trick I used for the amber running light’s end cap should have worked for this one, but only after I re-learned the lesson about taking sissy cuts:

Yet another snippet of tape and sissy cuts produced a better result:

Protip: when you affix an aluminum disk bandsawed from a scrap of nonstick griddle to a lathe fixture, the adhesive will grip the disk in only one orientation.

With the amber front running light blinking away, it’s time to replace the decade-old Planet Bike Superflash behind the seat:

The new mount descends directly from the clamps holding the fairing strut on the handlebars and various hose clamps:

The central block has two quartets of brass inserts epoxied inside:

That means I can install the light, then mount the whole affair on the bike, without holding everything together while fiddling with overly long screws.
A trial fit with the not-yet-cut-to-length 25.3 (-ish) PVC pipe body tube:

The aluminum plates have the standard used-car finish: nice polish over deep scratches.
Although I’ve been thinking of mounting the light below the seat rail, as shown, it can also sit above the rail.
Mary hauls seedlings and suchlike to the garden in a plastic drawer bungied to the rack, with the SuperFlash serving as an anchor point; this light may need fine tuning for that purpose.
The OpenSCAD source code as a GitHub Gist:
| // Rear running light clamp for Tour Easy seat strut | |
| // Ed Nisley – KE4ZNU – 2021-09 | |
| Layout = "Show"; // [Show,Build,Block] | |
| Section = true; | |
| /* [Hidden] */ | |
| ThreadThick = 0.25; | |
| ThreadWidth = 0.40; | |
| HoleWindage = 0.2; | |
| Protrusion = 0.1; // make holes end cleanly | |
| function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit); | |
| ID = 0; | |
| OD = 1; | |
| LENGTH = 2; | |
| inch = 25.4; | |
| //———————- | |
| // Dimensions | |
| // Light case along X axis, seat strut along Y, Z=0 at strut centerline | |
| LightOD = 25.4 + HoleWindage; | |
| StrutOD = 5/8 * inch + HoleWindage; | |
| PlateThick = 1/16 * inch; | |
| WallThick = 2.0; | |
| Kerf = ThreadThick; | |
| Screw = [3.0,6.8,4.0]; // M3 OD=washer, length=nut + washers | |
| Insert = [3.0,5.4,8.0 + 1.0]; // splined brass insert | |
| RoundRadius = IntegerMultiple(Screw[OD]/2,0.5); // corner rounding | |
| ScrewOC = [IntegerMultiple(StrutOD + 2*WallThick + Screw[ID],1.0), | |
| IntegerMultiple(LightOD + 2*WallThick + Screw[ID],1.0)]; | |
| echo(str("Screw OC: ",ScrewOC)); | |
| BlockSize = [ScrewOC.x + Insert[OD] + 2*WallThick, | |
| ScrewOC.y + Insert[OD] + 2*WallThick, | |
| LightOD + StrutOD + 3*WallThick]; | |
| echo(str("Block: ",BlockSize)); | |
| BaseOffset = -(WallThick + LightOD/2); // block bottom to centerline | |
| StrutOffset = LightOD/2 + WallThick + StrutOD/2; // light centerline to strut centerline | |
| echo(str("Strut screw min: ",IntegerMultiple(PlateThick + WallThick + StrutOD/2 + Insert[LENGTH]/2,1.0))); | |
| echo(str("Light screw min: ",IntegerMultiple(PlateThick + WallThick + LightOD/2 + Insert[LENGTH]/2,1.0))); | |
| NumSides = 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); | |
| } | |
| // Block with light along X axis | |
| module Block() { | |
| difference() { | |
| hull() | |
| for (i=[-1,1], j=[-1,1]) | |
| translate([i*(BlockSize.x/2 – RoundRadius),j*(BlockSize.y/2 – RoundRadius),BaseOffset]) | |
| cylinder(r=RoundRadius,h=BlockSize.z,$fn=NumSides); | |
| for (i=[-1,1], j=[-1,1]) | |
| translate([i*ScrewOC.x/2,j*ScrewOC.y/2,BaseOffset – Protrusion]) | |
| rotate(180/8) | |
| PolyCyl(Screw[ID],BlockSize.z + 2*Protrusion,8); | |
| for (i=[-1,1], j=[-1,1]) | |
| translate([i*ScrewOC.x/2,j*ScrewOC.y/2,0]) { | |
| translate([0,0,-Protrusion]) | |
| rotate(180/8) | |
| PolyCyl(Insert[OD],Insert[LENGTH] + 1*Protrusion,8); | |
| translate([0,0,(StrutOffset – Insert[LENGTH] – Kerf/2 + Protrusion)]) | |
| rotate(180/8) | |
| PolyCyl(Insert[OD],Insert[LENGTH] + 1*Protrusion,8); | |
| } | |
| translate([-BlockSize.x,0,0]) | |
| rotate([0,90,0]) | |
| cylinder(d=LightOD,h=2*BlockSize.x,$fn=NumSides); | |
| translate([0,BlockSize.y,StrutOffset]) | |
| rotate([90,0,0]) | |
| cylinder(d=StrutOD,h=2*BlockSize.y,$fn=NumSides); | |
| translate([0,0,StrutOffset]) | |
| cube([2*BlockSize.x,2*BlockSize.y,Kerf],center=true); | |
| cube([2*BlockSize.x,2*BlockSize.y,Kerf],center=true); | |
| } | |
| } | |
| //- Build it | |
| if (Layout == "Block") | |
| if (Section) | |
| difference() { | |
| Block(); | |
| rotate(atan(ScrewOC.y/ScrewOC.x)) | |
| translate([0,BlockSize.y,0]) | |
| cube(2*BlockSize,center=true); | |
| } | |
| else | |
| Block(); | |
| if (Layout == "Show") { | |
| Block(); | |
| color("Green",0.25) | |
| translate([-BlockSize.x,0,0]) | |
| rotate([0,90,0]) | |
| cylinder(d=LightOD,h=2*BlockSize.x,$fn=NumSides); | |
| color("Green",0.25) | |
| translate([0,BlockSize.y,StrutOffset]) | |
| rotate([90,0,0]) | |
| cylinder(d=StrutOD,h=2*BlockSize.y,$fn=NumSides); | |
| } | |
| if (Layout == "Build") { | |
| translate([-1.2*BlockSize.x,0,-BaseOffset]) | |
| difference() { | |
| Block(); | |
| translate([0,0,BlockSize.z]) | |
| cube(2*BlockSize,center=true); | |
| } | |
| translate([1.2*BlockSize.x,0,StrutOD/2 + WallThick]) | |
| difference() { | |
| rotate([180,0,0]) | |
| translate([0,0,-StrutOffset]) | |
| Block(); | |
| translate([0,0,BlockSize.z]) | |
| cube(2*BlockSize,center=true); | |
| } | |
| translate([0,0,StrutOffset – Kerf/2]) | |
| rotate([180,0,0]) | |
| intersection() { | |
| Block(); | |
| translate([0,0,StrutOffset/2]) | |
| cube([2*BlockSize.x,2*BlockSize.y,StrutOffset],center=true); | |
| } | |
| } |