Tour Easy Running Lights: Firmware

The optoisolator carrying the Bafang controller’s LIGHT signal pulls Pin 2 down to turn the LED on constantly for night riding:

    if (!Morser.continueSending())
        if (digitalRead(PIN_LIGHTMODE) == HIGH)
            Morser.startSending();
        else
            digitalWrite(PIN_OUTPUT,HIGH);      // constantly turn on in headlight mode

That’s the entirety of the program’s loop() function, so there’s not much to the firmware.

Imagine that: a whole computer devoted to sampling an input bit a zillion times a second and persistently setting an output bit:

Tour Easy Running Light - Arduino view
Tour Easy Running Light – Arduino view

The Morse output to the rear is now “s” rather than “i” for more blinkiness, but I doubt anybody will ever notice.

The next time I raise the hood on this thing, I’ll add a digital input to select FRONT or REAR mode to get me out of having to remember which hardware goes where.

The Arduino source code as a GitHub Gist:

// Tour Easy Running Light
// Ed Nisley - KE4ZNU
// September 2021
// 2023-03 preprocessorize for front/rear lights
// https://github.com/markfickett/arduinomorse
#include <morse.h>
// Bafang headlight output pulls pin low
#define PIN_LIGHTMODE 2
#define PIN_OUTPUT 13
#define FRONT
#if defined(FRONT)
#define BLINKS "b e "
#define POLARITY false
#elif defined(REAR)
#define BLINKS "s "
#define POLARITY true
#else
#error "Needs FRONT or REAR"
#endif
// second param: true = active low output
LEDMorseSender Morser(PIN_OUTPUT,POLARITY,(float)10.0);
void setup()
{
pinMode(PIN_LIGHTMODE,INPUT_PULLUP);
Morser.setup();
Morser.setMessage(String("qst de ke4znu "));
Morser.sendBlocking();
Morser.setSpeed(75);
Morser.setMessage(String(BLINKS));
}
void loop()
{
if (!Morser.continueSending())
if (digitalRead(PIN_LIGHTMODE) == HIGH)
Morser.startSending();
else
digitalWrite(PIN_OUTPUT,HIGH); // constantly turn on in headlight mode
}

Tour Easy Running Lights: Mechanics

The running lights have the same general structure as before and fit into the same front and rear holders:

Tour Easy Running Light - rear installed
Tour Easy Running Light – rear installed

I made the recess slightly deeper to provide a bit more protection to the lens:

Tour Easy Running Light - front installed
Tour Easy Running Light – front installed

The lenses have a 10° beam angle, so a few more millimeters of sidewall doesn’t intercept much light.

The layout doodle grew a few more notes:

Tour Easy running light - housing dimensions
Tour Easy running light – housing dimensions

I had the good idea of boring the tube, knurling the rod, then epoxying the two together before cutting the rod:

Tour Easy Running Light - heatsink curing
Tour Easy Running Light – heatsink curing

Which let the lathe hold them in perfect alignment during curing:

Tour Easy Running Light - heatsink plug alignment
Tour Easy Running Light – heatsink plug alignment

The rod fits through the lathe spindle and I intended to use it as an arbor while turning the tube exterior, then cut the finished heatsink off flush.

Which really good idea lasted until the next morning, when I looked at the setup and immediately cut the rod flush with the tube. Because reasons, perhaps excess blood in my caffeine stream.

So I had to finish the heatsink on hard mode right up against the chuck:

Tour Easy Running Light - turning heatsink rebate
Tour Easy Running Light – turning heatsink rebate

Flipping it around and gripping that little rebate to skim the OD down to 25 mm seemed fraught with peril, so I stabilized the open end with a chuck and plenty of oil; the live center was just too big around for the job.

Dang, I hate it when I screw up a nice plan.

Then drill various holes on the Sherline and epoxy the circuit support plate:

Tour Easy Running Light - circuit plate curing
Tour Easy Running Light – circuit plate curing

After boring the PVC pipe to 23 mm ID, I made a pair of Delrin fixtures to simplify turning the exterior to 25 mm before parting it off:

Tour Easy Running Light - turning body OD
Tour Easy Running Light – turning body OD

The PVC is so thin the Arduino’s LEDs shine right through:

Tour Easy Running Light - installed top view
Tour Easy Running Light – installed top view

The radioactive green endcap is ordinary laser-cut fluorescent edge-lit acrylic with sunlight through the garage door on the left. I used red acrylic for the taillight to encourage their separate identities.

The knockoff Arduino Nano fits on one side of the support plate:

Tour Easy Running Light - Arduino view
Tour Easy Running Light – Arduino view

And the current regulator on the other:

Tour Easy Running Light - current regulator
Tour Easy Running Light – current regulator

Because these run from a dedicated 6.3 V step-down regulator, rather than the Bafang controller’s headlight output, the 2.0 Ω sense resistor sets the LED current to 0.8 V / 2.0 Ω = 400 mA, which is pretty close to the LED 1 W spec.

The white blob at the end of the two ribbon cable wires is the optoisolator pulling down a pin when the LIGHT signal is active, telling the firmware to stop the normal blink pattern and just turn the LED on all the time. This will come in handy if I ever do any night riding.

The LED is epoxied to the aluminum shell (with metal-filled JB Weld) and the whole affair never gets more than comfortably warm even with the LED running constantly.

I think they came out All Good™, despite various blunders along the way.

Bafang Headlight Circuit Current Limit

Having just replaced Rev 1 of the amber running light with Rev 3 (about which, more later) on Mary’s Tour Easy, both the front and rear lights began blinking erratically. Given that they have completely independent circuitry, this strongly suggests a power problem.

Herewith, the headlight circuit voltage:

Bafang headlight voltage - two 1 W running lights
Bafang headlight voltage – two 1 W running lights

The voltage should be a constant 6 or 6.3 V, depending on which description you most recently read. That is the case with only one light attached, so the problem occurs only when running both lights.

The four pulses come from the amber LED’s Morse code “b” (dah-dit-dit-dit) with a 85 ms dits; the first dah pulse should be three times longer than the dits and definitely isn’t. The rear light’s red LED stays on continuously, except for two dark dits, so it draws a constant current and does not produce any changes in this trace.

Both lights have 2.0 Ω sense resistors setting the LED current to 400 mA, which corresponds to 250 mA each from the Bafang controller’s 6.3 V headlight circuit. The headlight circuit’s total of 500 mA should work fine, although the “spec” seems to be basically whatever the OEM headlight requires.

The Rev 1 amber light ran the LED at 360 mA with a supply current around 450 mA. That light and the rear light on the back ran fine, so the supply seems to have a hard maximum current limit at (a bit less than?) 500 mA.

The least-awful solution seems to be backing off both LED currents to 360 mA to keep the total supply current well under 500 mA.

Running Light Waveforms: A Closer Look

A test setup on the bench allows a bit more room for probes:

1 W Amber LED - MP1584 pulse setup
1 W Amber LED – MP1584 pulse setup

Some heatsink tape holds the LED to the far side of that oversize heatsink.

The input signal (top trace) arrives from a function generator set to blip the MP1584 regulator’s Enable input at 4 Hz with a 7 ms pulse:

Amber 1 w LED - pulse 200 mA-div
Amber 1 w LED – pulse 200 mA-div

The purple trace is the voltage across the 2 Ω sense resistor. The MP1584 datasheet says the regulator soft-starts for (typically) 1.5 ms, during which the output ramps upward at 600 mV/ms to 800 mV , whereupon the actual regulation commences. The amber LED forward drop adds 2.5 V to the sense voltage, so the regulator produces 3.3 V from the 6.3 V bench supply input.

The cyan trace is the output current through the LED and sense resistor, also ramping up to 800 mV/2 Ω = 400 mA to drive the LED at 1 W.

The furry section shows when the regulator is actively regulating, with the output voltage rising and falling over a small range to maintain the average current (via the sense voltage). Successive Enable pulses may have longer, shorter, or completely missing fur, with no predictable pattern. Increasing the duty cycle doesn’t affect the results, with the fur sometimes extending for the entire pulse and sometimes being completely missing.

I think the regulator can settle in one of two metastable states. The best case has a constant voltage producing a constant LED current, with the sense voltage remaining within whatever deadband keeps the error amplifier happy. When something knocks the sense voltage out of the deadband, the error amp starts the usual regulation cycle, which will stop when the minimum or maximum voltage of a cycle remains within the deadband:

Amber 1 w LED - pulse - detail - 200 mA-div
Amber 1 w LED – pulse – detail – 200 mA-div

The ripple shows the regulator running at three cycles per 20 µs division = 150 kHz, far lower then the MP1584 datasheet’s maximum 1.5 MHz and the typical 500 kHz in the test circuits. Perhaps a low frequency lets the designers use a cheap PCB and not worry about pesky EMI issues.

In any event, during this pulse the ripple amplitude gradually decreased as the output voltage settled at the point where the error voltage variation stayed within the deadband. The typical amp gain is only 200 V/V, so it’s definitely less fussy than something build around an op amp.

For whatever it’s worth, a 7 ms flash from a 1 W amber LED at 4 Hz is way attention-getting in a dim Basement Laboratory. You wouldn’t need an Arduino to produce that signal, even though I like the Morse capability.

Tour Easy Rear Running Light: Current Waveforms

There’s just enough slack in the LED wiring to clip a Tek current probe in there:

Tour Easy Rear Running Light - regulator wiring
Tour Easy Rear Running Light – regulator wiring

Which reveals the LED current waveform:

Red LED - LED current - 100 mA-div
Red LED – LED current – 100 mA-div

The LED is on continuously, except for the two 75 ms Morse code dits in the upper trace.

The lower trace shows the current ramping up at the end of the first dit, from zero to 400 mA in 1.5 ms.

Clamping the probe around the 6.3 V power supply lead:

Red LED - power supply - 100 mA-div
Red LED – power supply – 100 mA-div

The supply current includes maybe 20 mA for the Arduino running the Morse code program and the current ramps up from there to about 250 mA when the LED is on.

The LED drops 2.6 V at 400 mA, so it dissipates a smidge over 1 W. The 2.0 Ω current sense resistor (3.3 Ω in parallel with 5.1 Ω) dissipates 800 mV × 400 mA = 320 mW.

The dissipation from the Bafang headlight output, including the Arduino, is 1.6 W.

The running light ticks along at the hot side of comfortably warm on the Electronics Workbench and runs barely warm in free air out on the bike, so I’ll define it to be Good Enough™.

Tour Easy 1 W Amber Running Light: Holder and First Light

Wrapping a left-side ball mount around the PVC case produced a holder:

Fairing 1 W LED Mount - Left side - show view
Fairing 1 W LED Mount – Left side – show view

Which looks like this in real life:

1 W Amber Running Light - installed front
1 W Amber Running Light – installed front

The support structure under the arch required a bit more cleanup than it got, so the clamp didn’t quite close around the ball on the first full test:

1 W Amber Running Light - installed side
1 W Amber Running Light – installed side

Both the phone camera and the eyeballometer report the 1 W amber LED isn’t quite as bright as the 400 lumen Anker flashlight on its low setting:

1 W Amber Running Light - First Light
1 W Amber Running Light – First Light

Stir the unusual (for a bike) amber color together with some blinkiness, though, and it’s definitely attention-getting.

The OpenSCAD source code as a GitHub Gist:

// Tour Easy Fairing Flashlight Mount
// Ed Nisley KE4ZNU - July 2017
// August 2017 -
// August 2020 - add reinforcing columns under mount cradle
// August 2021 - 1 W Amber LED
/* [Build Options] */
FlashName = "1WLED"; // [AnkerLC40,AnkerLC90,J5TactV2,InnovaX5,Sidemarker,Clearance,Laser,1WLED]
Component = "BallClamp"; // [Ball, BallClamp, Mount, Plates, Bracket, Complete]
Layout = "Build"; // [Build, Show]
Support = true;
MountSupport = true;
/* [Hidden] */
ThreadThick = 0.25; // [0.20, 0.25]
ThreadWidth = 0.40; // [0.40]
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
Protrusion = 0.01; // [0.01, 0.1]
HoleWindage = 0.2;
/* [Fairing Mount] */
Side = "Right"; // [Right,Left]
ToeIn = -10; // inward from ahead
Tilt = 20; // upward from forward (M=20 E=10)
Roll = 0; // outward from top
//- Screws and inserts
/* [Hidden] */
ID = 0;
OD = 1;
LENGTH = 2;
/* [Hidden] */
ClampInsert = [3.0,4.2,8.0];
ClampScrew = [3.0,5.9,35.0]; // thread dia, head OD, screw length
ClampScrewWasher = [3.0,6.75,0.5];
ClampScrewNut = [3.0,6.1,4.0]; // nyloc nut
/* [Hidden] */
F_NAME = 0;
F_GRIPOD = 1;
F_GRIPLEN = 2;
LightBodies = [
["AnkerLC90",26.6,48.0],
["AnkerLC40",26.6,55.0],
["J5TactV2",25.0,30.0],
["InnovaX5",22.0,55.0],
["Sidemarker",15.0,20.0],
["Clearance",50.0,20.0],
["Laser",10.0,30.0],
["1WLED",25.4,40.0],
];
//- Fairing Bracket
// Magic numbers taken from the actual fairing mount
/* [Hidden] */
inch = 25.4;
BracketHoleOD = 0.25 * inch; // 1/4-20 bolt holes
BracketHoleOC = 1.0 * inch; // fairing hole spacing
// usually 1 inch, but 15/16 on one fairing
Bracket = [48.0,16.3,3.6 - 0.6]; // fairing bracket end plate overall size
BracketHoleOffset = (3/8) * inch; // end to hole center
BracketM = 3.0; // endcap arc height
BracketR = (pow(BracketM,2) + pow(Bracket[1],2)/4) / (2*BracketM); // ... radius
//- Base plate dimensions
Plate = [100.0,30.0,6*ThreadThick + Bracket[2]];
PlateRad = Plate[1]/4;
RoundEnds = true;
echo(str("Base plate thick: ",Plate[2]));
//- Select flashlight data from table
echo(str("Flashlight: ",FlashName));
FlashIndex = search([FlashName],LightBodies,1,0)[F_NAME];
//- Set ball dimensions
BallWall = 5.0; // max ball wall thickness
echo(str("Ball wall: ",BallWall));
BallOD = IntegerMultiple(LightBodies[FlashIndex][F_GRIPOD] + 2*BallWall,1.0);
echo(str(" OD: ",BallOD));
BallLength = IntegerMultiple(min(sqrt(pow(BallOD,2) - pow(LightBodies[FlashIndex][F_GRIPOD],2)) - 2*4*ThreadThick,
LightBodies[FlashIndex][F_GRIPLEN]),1.0);
echo(str(" length: ",BallLength));
BallSides = 8*4;
//- Set clamp ring dimensions
//ClampOD = 50;
ClampOD = BallOD + 2*5;
echo(str("Clamp OD: ",ClampOD));
ClampLength = min(20.0,0.75*BallLength);
echo(str(" length: ",ClampLength));
ClampScrewOC = IntegerMultiple((ClampOD + BallOD)/2,1);
echo(str(" screw OC: ",ClampScrewOC));
TiltMirror = (Side == "Right") ? [0,0,0] : [0,1,0];
//- Adjust hole diameter to make the size come out right
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);
}
//- Fairing Bracket
// This part of the fairing mount supports the whole flashlight mount
// Centered on screw hole
module Bracket() {
linear_extrude(height=Bracket[2],convexity=2)
difference() {
translate([(Bracket[0]/2 - BracketHoleOffset),0,0])
offset(delta=ThreadWidth)
intersection() {
square([Bracket[0],Bracket[1]],center=true);
union() {
for (i=[-1,0,1]) // middle circle fills gap
translate([i*(Bracket[0]/2 - BracketR),0])
circle(r=BracketR);
}
}
circle(d=BracketHoleOD/cos(180/8),$fn=8); // dead center at the origin
}
}
//- General plate shape
// Centered in the middle of the plate
module PlateBlank() {
difference() {
intersection() {
translate([0,0,Plate[2]/2]) // select upper half of spheres
cube(Plate,center=true);
hull()
if (RoundEnds)
for (i=[-1,1])
translate([i*(Plate[0]/2 - PlateRad),0,0])
resize([Plate[1]/2,Plate[1],2*Plate[2]])
sphere(r=PlateRad); // nice round ends!
else
for (i=[-1,1], j=[-1,1])
translate([i*(Plate[0]/2 - PlateRad),j*(Plate[1]/2 - PlateRad),0])
resize([2*PlateRad,2*PlateRad,2*Plate[2]])
sphere(r=PlateRad); // nice round corners!
}
translate([BracketHoleOC,0,-Protrusion]) // punch screw holes
PolyCyl(BracketHoleOD,2*Plate[2],8);
translate([-BracketHoleOC,0,-Protrusion])
PolyCyl(BracketHoleOD,2*Plate[2],8);
}
}
//- Inner plate
module InnerPlate() {
difference() {
PlateBlank();
translate([-BracketHoleOC,0,Plate[2] - Bracket[2] + Protrusion]) // punch fairing bracket
Bracket();
}
}
//- Outer plate
// With optional legend for orientation and parameters
module OuterPlate(Legend = true) {
TextRotate = (Side == "Left") ? 0 : 180;
difference() {
PlateBlank();
if (Legend)
mirror([0,1,0])
translate([0,0,-Protrusion])
linear_extrude(height=3*ThreadThick + Protrusion) {
translate([BracketHoleOC + 15,0,0])
text(text=">>>",size=5,spacing=1.20,font="Arial",halign="center",valign="center");
translate([-BracketHoleOC,8,0]) rotate(TextRotate)
text(text=str("Toe ",ToeIn),size=5,spacing=1.20,font="Arial",halign="center",valign="center");
translate([-BracketHoleOC,-8,0]) rotate(TextRotate)
text(text=str("Tilt ",Tilt),size=5,spacing=1.20,font="Arial",halign="center",valign="center");
translate([BracketHoleOC,-8,0]) rotate(TextRotate)
text(text=Side,size=5,spacing=1.20,font="Arial",halign="center",valign="center");
translate([BracketHoleOC,8,0]) rotate(TextRotate)
text(text=str("Roll ",Roll),size=5,spacing=1.20,font="Arial",halign="center",valign="center");
translate([0,0,0])
rotate(90)
text(text="KE4ZNU",size=4,spacing=1.20,font="Arial",halign="center",valign="center");
}
}
}
//- Slotted ball around flashlight
// Print with brim to ensure adhesion!
module SlotBall() {
NumSlots = 8*2; // must be even, half cut from each end
SlotWidth = 2*ThreadWidth;
SlotBaseThick = 10*ThreadThick; // enough to hold finger ends together
RibLength = (BallOD - LightBodies[FlashIndex][F_GRIPOD])/2;
translate([0,0,(Layout == "Build") ? BallLength/2 : 0])
rotate([0,(Layout == "Show") ? 90 : 0,0])
difference() {
intersection() {
sphere(d=BallOD,$fn=2*BallSides); // basic ball
cube([2*BallOD,2*BallOD,BallLength],center=true); // trim to length
}
translate([0,0,-LightBodies[FlashIndex][F_GRIPOD]])
rotate(180/BallSides)
PolyCyl(LightBodies[FlashIndex][F_GRIPOD],2*BallOD,BallSides); // remove flashlight body
for (i=[0:NumSlots/2 - 1]) { // cut slots
a=i*(2*360/NumSlots);
SlotCutterLength = LightBodies[FlashIndex][F_GRIPOD];
rotate(a)
translate([SlotCutterLength/2,0,SlotBaseThick])
cube([SlotCutterLength,SlotWidth,BallLength],center=true);
rotate(a + 360/NumSlots)
translate([SlotCutterLength/2,0,-SlotBaseThick])
cube([SlotCutterLength,SlotWidth,BallLength],center=true);
}
}
color("Yellow")
if (Support && (Layout == "Build")) {
for (i=[0:NumSlots-1]) {
a = i*360/NumSlots;
rotate(a + 180/NumSlots)
translate([(LightBodies[FlashIndex][F_GRIPOD] + RibLength)/2 + ThreadWidth,0,BallLength/(2*4)])
cube([RibLength,2*ThreadWidth,BallLength/4],center=true);
}
}
}
//- Clamp around flashlight ball
BossLength = ClampScrew[LENGTH] - 1*ClampScrewWasher[LENGTH];
BossOD = ClampInsert[OD] + 2*(6*ThreadWidth);
module BallClamp(Section="All") {
difference() {
union() {
intersection() {
sphere(d=ClampOD,$fn=BallSides); // exterior ball clamp
cube([ClampLength,2*ClampOD,2*ClampOD],center=true); // aiming allowance
}
hull()
for (j=[-1,1])
translate([0,j*ClampScrewOC/2,-BossLength/2])
cylinder(d=BossOD,h=BossLength,$fn=6);
}
sphere(d=(BallOD + 1*ThreadThick),$fn=BallSides); // interior ball with minimal clearance
for (j=[-1,1]) {
translate([0,j*ClampScrewOC/2,-ClampOD]) // screw clearance
PolyCyl(ClampScrew[ID],2*ClampOD,6);
translate([0,j*ClampScrewOC/2, // insert clearance
-0*(BossLength/2 - ClampInsert[LENGTH] - 3*ThreadThick) + Protrusion])
rotate([0,180,0])
PolyCyl(ClampInsert[OD],2*ClampOD,6);
translate([0,j*ClampScrewOC/2, // insert transition
-(BossLength/2 - ClampInsert[LENGTH] - 3*ThreadThick)])
cylinder(d1=ClampInsert[OD]/cos(180/6),d2=ClampScrew[ID],h=6*ThreadThick,$fn=6);
}
if (Section == "Top")
translate([0,0,-ClampOD/2])
cube([2*ClampOD,2*ClampOD,ClampOD],center=true);
else if (Section == "Bottom")
translate([0,0,ClampOD/2])
cube([2*ClampOD,2*ClampOD,ClampOD],center=true);
}
color("Yellow")
if (Support) { // ad-hoc supports
NumRibs = 6;
RibLength = 0.5 * BallOD;
RibWidth = 1.9*ThreadWidth;
SupportOC = ClampLength / NumRibs;
if (Section == "Top") // base plate for adhesion
translate([0,0,ThreadThick])
cube([ClampLength + 6*ThreadWidth,RibLength,2*ThreadThick],center=true);
else if (Section == "Bottom")
translate([0,0,-ThreadThick])
cube([ClampLength + 6*ThreadWidth,RibLength,2*ThreadThick],center=true);
render(convexity=2*NumRibs)
intersection() {
sphere(d=BallOD - 0*ThreadWidth); // cut at inner sphere OD
cube([ClampLength + 2*ThreadWidth,RibLength,BallOD],center=true);
if (Section == "Top") // select only desired section
translate([0,0,ClampOD/2])
cube([2*ClampOD,2*ClampOD,ClampOD],center=true);
else if (Section == "Bottom")
translate([0,0,-ClampOD/2])
cube([2*ClampOD,2*ClampOD,ClampOD],center=true);
union() { // ribs for E-Z build
for (j=[-1,0,1])
translate([0,j*SupportOC,0])
cube([ClampLength,RibWidth,1.0*BallOD],center=true);
for (i=[0:NumRibs]) // allow NumRibs + 1 to fill the far end
translate([i*SupportOC - ClampLength/2,0,0])
rotate([0,90,0])
cylinder(d=BallOD - 2*ThreadThick,
h=RibWidth,$fn=BallSides,center=true);
}
}
}
}
//- Mount between fairing plate and flashlight ball
// Build with support for bottom of clamp screws!
module Mount() {
MountShift = [ClampOD*sin(ToeIn/2),0,ClampOD/2];
OuterPlate();
mirror(TiltMirror) {
intersection() {
translate(MountShift)
rotate([-Roll,ToeIn,Tilt])
BallClamp("Bottom");
translate([0,0,Plate.x/2 + 3*ThreadThick])
cube(Plate.x,center=true);
}
if (MountSupport) // anchor outer corners at worst overhang
color("Yellow") {
RibWidth = 1.9*ThreadWidth;
SupportOC = 0.1 * ClampLength;
intersection() {
difference() {
rotate([0,0,Tilt])
translate([(ClampOD - BallOD)*sin(ToeIn/2),0,3*ThreadThick]) // Z = avoid legends
for (i=[-4.5,-2.5,0,2.0,4.5])
translate([i*SupportOC - 0.0,0,(5 + Plate[2])/2])
cube([RibWidth,0.7*ClampOD,(5 + Plate[2])],center=true);
translate(MountShift)
rotate([-Roll,ToeIn,Tilt])
sphere(d=ClampOD - 2*ThreadWidth,$fn=BallSides);
}
translate([0,0,ClampOD/2])
cube([Plate.x,Plate.y,ClampOD],center=true);
}
}
}
}
//- Build things
if (Component == "Bracket")
Bracket();
if (Component == "Ball")
SlotBall();
if (Component == "BallClamp")
if (Layout == "Show")
BallClamp("All");
else if (Layout == "Build")
BallClamp("Top");
if (Component == "Mount")
Mount();
if (Component == "Plates") {
translate([0,0.7*Plate[1],0])
InnerPlate();
translate([0,-0.7*Plate[1],0])
OuterPlate(Legend = false);
}
if (Component == "Complete") {
OuterPlate();
mirror(TiltMirror) {
translate([0,0,ClampOD/2 + BossOD*abs(sin(ToeIn))]) {
rotate([-Roll,ToeIn,Tilt])
SlotBall();
rotate([-Roll,ToeIn,Tilt])
BallClamp();
}
}
}

Tour Easy 1 W Amber Running Light: Firmware

Rather than conjure a domain specific language to blink an LED, it’s easier to use Morse code:

Herewith, Arduino source code using Mark Fickett’s Morse library to blink an amber running light:

// Tour Easy Running Light
// Ed Nisley - KE4ZNU
// September 2021

#include <morse.h>

#define PIN_OUTPUT	13

LEDMorseSender Morser(PIN_OUTPUT,(float)10.0);

void setup()
{
	Morser.setup();

    Morser.setMessage(String("qst de ke4znu "));
    Morser.sendBlocking();

//    Morser.setWPM((float)3.0);
    Morser.setSpeed(50);
	Morser.setMessage(String("s   "));
}

void loop()
{
	if (!Morser.continueSending())
		Morser.startSending();

}

Bonus: a trivially easy ID string.

A dit time of 50 ms produces a brief flash that’s probably about as fast as it can be, given that the regulator must ramp the LED current up from zero after its Enable input goes high. In round numbers, a 50ms dit corresponds to 24 WPM Morse.

Each of the three blanks after the “s” produces a seven element word space to keep the blinks from running together.

Sending “b ” (two blanks) with a 75 ms dit time may be more noticeable. You should tune for maximum conspicuity on your rides.

1 W Amber Running Light - installed front
1 W Amber Running Light – installed front

On our first ride, Mary got a friendly wave from a motorcyclist, an approving toot from a driver, and several “you go first” gestures at intersections.

Works for us …