We found this critter while checking for water after Hurricane Ida drenched the area:

It’s on the outside of the vent screen and we have no objection.
We should put up a bat house to encourage more of its friends to hang out with us …
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.

We found this critter while checking for water after Hurricane Ida drenched the area:

It’s on the outside of the vent screen and we have no objection.
We should put up a bat house to encourage more of its friends to hang out with us …

Found behind a store in Red Oaks Mill, overlooking the Mighty Wappingers Creek:

The old rail fell off its (long gone) post before the tree grew around it and the newer rail (upper right) definitely isn’t fresh from the factory, so this tableau has been on display for quite a while.
The tree’s growth rings have pretty much weathered away.

We’re bicycling on Collegeview Avenue, approaching the eastern traffic circle (of three) along Raymond Avenue. I’m in the lead, hauling a trailer with the week’s groceries:

The four digit frame numbers tick along at 60 fps for my helmet camera and 30 fps for the rear cameras.
Note the “splitter” (a.k.a. “pedestrian refuge”) on the left, intended to separate Collegeview’s incoming and outgoing traffic. It formerly had one non-reflective black bollard on each side of the ladder crosswalk, but errant drivers destroyed so many bollards along Raymond that they’re now WONTFIX remnants. The flush concrete disk in the lower left of this picture will become relevant in a few seconds of real time:

Collegeview has the same deteriorating pavement as found along Raymond Avenue, so we must maneuver beside the potholes:

The potholes make maintaining a safe-ish distance from the parked cars somewhat difficult:

All of us are slowing to stop at the traffic circle, with Mary behind the car that will eventually stop beside me:

Mary could see the car behind her in her helmet mirror, but she’s slowing to stall speed with no time for sightseeing and no room for maneuvering. The view from the camera on the seat frame behind her left shoulder:

Two seconds later:

One second:

Two more seconds:

Mary has stopped, as shown by the parked car’s unchanging position in the frame over on the left in the next images. The driver, however, continues creeping slowly forward; there can be no doubt she sees Mary at this distance.
After three more seconds:

One second later, the front wheel is exactly at Mary’s left foot:

The same events, viewed from the camera on my bike, start less than one second from the 1522 image above. I’m stopped, while the driver next to me continues to roll forward.
Mary is extending her left leg in preparation for a complete stop, at about the same time as the 1078 image:

Three seconds later her toe touches the pavement, while both she and the driver continue moving forward very slowly:

Five seconds later, she is stopped with her foot firmly planted:

And the driver continues moving:

Another five seconds and the sidewall bulge of the car’s radial tire is pressing her foot to the pavement:

A closer look:

She yanks her foot away:

While the driver continues to creep forward:

Sometimes, it’s the only way to get some attention:

Mary is now off-balance, leaning on the car door, explaining what just happened:

Mary regains her balance as the driver backs cautiously away:

Were the bollard still atop that sad concrete foundation, the driver might not have driven up on the splitter to get around Mary, if only to avoid scuffing a fender:

Compare this clearance with what you saw earlier in the 0957 image:

Mary can’t get far enough away, but this must suffice:

Now the driver can pass her again with more clearance:

I pointed to the car, then to the circle, and shouted “GO!” because neither of us wanted to be in front of that particular driver:

We’ll surely meet her again, ideally with more clearance.
Henceforth, we will take the middle of the lane into splitters, as cyclists should do on a “shared” roadway. I was assured by the DOT engineer who designed Raymond Avenue that it’s all “standards compliant”, so this is what NYS DOT regards as “making their highway systems safe and functional for all users”.
Having amateur radio HTs on the bikes lets us talk with each other in real time, which is a definite asset when stuff like this happens.
Not to mention having cameras here, there, and everywhere.
Elapsed time from the first to the last picture: 33 s.
For the record: blue Ford (although the ersatz fender vents seem reminiscent of an old Buick), license ANC-4273.

It turns out Mary rarely used assist level 6 and had no use for levels 7 and 8 of my derated BBS02 configuration:
LC=15
ALC0=0
ALC1=5
ALC2=7
ALC3=16
ALC4=25
ALC5=37
ALC6=51
ALC7=67
ALC8=85
ALC9=100
Level 9 must be 100% of the maximum motor current so the throttle can apply full power to get out of the way in a hurry.
The new and even more derated configuration allows small-step assist level selection for our usual riding, at the cost of an unused huge step to level 9 for the throttle:
[Basic]
LBP=42
LC=18
ALC0=0
ALC1=4
ALC2=6
ALC3=9
ALC4=15
ALC5=20
ALC6=25
ALC7=30
ALC8=40
ALC9=100
ALBP0=0
ALBP1=100
ALBP2=100
ALBP3=100
ALBP4=100
ALBP5=100
ALBP6=100
ALBP7=100
ALBP8=100
ALBP9=100
WD=12
SMM=0
SMS=1
[Pedal Assist]
PT=3
DA=0
SL=0
SSM=4
WM=0
SC=20
SDN=4
TS=15
CD=8
SD=5
KC=100
[Throttle Handle]
SV=11
EV=42
MODE=1
DA=10
SL=0
SC=5
The LC=18 line limits the maximum motor current to 18 A, rather than the rated 24 A, which may improve controller MOSFET longevity; reliable evidence is hard to come by. Controller failures seem to happen more often to riders who value jackrabbit acceleration on harsh terrain, so it may make little difference for road cyclists.
So level 5 now selects 75% × 20% = 15% of the motor’s nominal 750 W:

Call it 115 W: we’re both getting plenty of exercise!

Four years ago I got a folding Bluetooth keyboard for my then-newish Pixel phone:

A few days ago, the 2 W S X Win keys stopped working, suggesting a problem with the matrix scan of that column.
The trim cover over the fold on the back of the keyboard disengages from the hinge with gentle prying at the obvious places, exposing a flex cable pressed against a disturbingly right-angled edge:

Unfolding the keyboard makes the acute bend against the case obvious, even though it’s hidden under the cable:

Some tedious poking around with a continuity meter revealed not only a broken trace, but a crack in the flex cable:

Protip: when you have nothing to lose, poke a pin through the flex cable into the trace to localize the break. The point leaves little holes, but so what?
I scraped off the black coating and the insulation over the traces with an Xacto knife under the microscope, which definitely reveals my need for a tiny Waldo manipulator.
Coating the exposed copper with solder and bridging the crack with one strand of the finest wire in my collection produced a truly horrific scene:

The glop on the left is flux applied before soldering. The rugged terrain on the right is the exceedingly gummy adhesive holding the cable to the keyboard, which turned out to be surprisingly heat-sensitive.
Fairly obviously, those patches will not survive much more flexing, so wrap the cable with Kapton tape and apply a stiffening layer of thick plastic tape:

Apply more reinforcing tape and button it up again:

I stuck the flex cable down with the repaired joint about a millimeter under that sharp edge, with double-sided sticky tape underneath to help immobilize the bruised area.
While I had the covers off, I also reinforced the same section of the cable on the other side of the keyboard, in the hopes of preventing a crack.
I have little faith in the long-term survival of this repair. Similar keyboards routinely emerge from the quantum froth of randomly named Amazon sellers, most of which have negative reviews reporting the failure of entire key columns; there’s no indication of any design improvement.
The alert reader will have noted the cable has eight traces, enough for a 3×5 matrix of 15 keys, but the folding wing has 16 keys: the second row has four keys. I have no idea how they made that work, other than perhaps resistive coding for some of the keys.

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

Which looks like this in real life:

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:

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:

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

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.

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 …