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.

Category: Recumbent Bicycling

Cruisin’ the streets

  • Sharing the Road on Raymond Avenue: Zero Clearance

    Sharing the Road on Raymond Avenue: Zero Clearance

    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:

    Zero Clearance - Ed Front - 2021-09-07 - 0497
    Zero Clearance – Ed Front – 2021-09-07 – 0497

    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:

    Zero Clearance - Ed Front - 2021-09-07 - 0593
    Zero Clearance – Ed Front – 2021-09-07 – 0593

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

    Zero Clearance - Mary - 2021-09-07 - 0797
    Zero Clearance – Mary – 2021-09-07 – 0797

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

    Zero Clearance - Ed Rear - 2021-09-07 - 1140
    Zero Clearance – Ed Rear – 2021-09-07 – 1140

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

    Zero Clearance - Ed Rear - 2021-09-07 - 1522
    Zero Clearance – Ed Rear – 2021-09-07 – 1522

    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:

    Zero Clearance - Mary - 2021-09-07 - 0957
    Zero Clearance – Mary – 2021-09-07 – 0957

    Two seconds later:

    Zero Clearance - Mary - 2021-09-07 - 1078
    Zero Clearance – Mary – 2021-09-07 – 1078

    One second:

    Zero Clearance - Mary - 2021-09-07 - 1110
    Zero Clearance – Mary – 2021-09-07 – 1110

    Two more seconds:

    Zero Clearance - Mary - 2021-09-07 - 1182
    Zero Clearance – Mary – 2021-09-07 – 1182

    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:

    Zero Clearance - Mary - 2021-09-07 - 1270
    Zero Clearance – Mary – 2021-09-07 – 1270

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

    Zero Clearance - Mary - 2021-09-07 - 1308
    Zero Clearance – Mary – 2021-09-07 – 1308

    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:

    Zero Clearance - Ed Rear - 2021-09-07 - 1542
    Zero Clearance – Ed Rear – 2021-09-07 – 1542

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

    Zero Clearance - Ed Rear - 2021-09-07 - 1634
    Zero Clearance – Ed Rear – 2021-09-07 – 1634

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

    Zero Clearance - Ed Rear - 2021-09-07 - 1773
    Zero Clearance – Ed Rear – 2021-09-07 – 1773

    And the driver continues moving:

    Zero Clearance - Mary - 2021-09-07 - 1333
    Zero Clearance – Mary – 2021-09-07 – 1333

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

    Zero Clearance - Ed Rear - 2021-09-07 - 1934
    Zero Clearance – Ed Rear – 2021-09-07 – 1934

    A closer look:

    Zero Clearance - Ed Rear - 2021-09-07 - 1946 detail
    Zero Clearance – Ed Rear – 2021-09-07 – 1946 detail

    She yanks her foot away:

    Zero Clearance - Ed Rear - 2021-09-07 - 1953
    Zero Clearance – Ed Rear – 2021-09-07 – 1953

    While the driver continues to creep forward:

    Zero Clearance - Mary - 2021-09-07 - 1397
    Zero Clearance – Mary – 2021-09-07 – 1397

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

    Zero Clearance - Ed Rear - 2021-09-07 - 2026
    Zero Clearance – Ed Rear – 2021-09-07 – 2026

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

    Zero Clearance - Ed Rear - 2021-09-07 - 2152
    Zero Clearance – Ed Rear – 2021-09-07 – 2152

    Mary regains her balance as the driver backs cautiously away:

    Zero Clearance - Mary - 2021-09-07 - 1546
    Zero Clearance – Mary – 2021-09-07 – 1546

    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:

    Zero Clearance - Ed Rear - 2021-09-07 - 2479
    Zero Clearance – Ed Rear – 2021-09-07 – 2479

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

    Zero Clearance - Mary - 2021-09-07 - 1627
    Zero Clearance – Mary – 2021-09-07 – 1627

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

    Zero Clearance - Ed Rear - 2021-09-07 - 2761
    Zero Clearance – Ed Rear – 2021-09-07 – 2761

    Now the driver can pass her again with more clearance:

    Zero Clearance - Mary - 2021-09-07 - 1891
    Zero Clearance – Mary – 2021-09-07 – 1891

    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:

    Zero Clearance - Ed Front - 2021-09-07 - 2540
    Zero Clearance – Ed Front – 2021-09-07 – 2540

    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.

  • Tour Easy: Bafang BBS02 Lower Power

    Tour Easy: Bafang BBS02 Lower Power

    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:

    Tour Easy Bafang - display 26 mi
    Tour Easy Bafang – display 26 mi

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

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

    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

    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 …

  • Tour Easy 1 W Amber Running Light: End Cap

    Tour Easy 1 W Amber Running Light: End Cap

    My initial doodles suggested an end cap with an opening for the Arduino’s USB port and something for the power cable from the Bafang controller:

    1 W LED Running Light - internal assembly
    1 W LED Running Light – internal assembly

    Common sense finally broke out and I made a simple disk cover held in place with an M3 screw:

    1 W Amber Running Light - bench test
    1 W Amber Running Light – bench test

    Unfortunately, I cut the PVC shell flush with the USB port, which meant the cap couldn’t have a little shoulder to stabilize it on the shell. Maybe next time?

    Machining the disk required using the scrap of aluminum rod left over from the heatsink as a fixture with a piece of sandpaper stuck to the front surface:

    1 W Amber Running Light - end cap setup
    1 W Amber Running Light – end cap setup

    The live center presses the bandsawed + disk sanded cap against the sandpaper, providing barely enough traction for sissy cuts reducing the disk to the proper diameter:

    1 W Amber Running Light - end cap turning
    1 W Amber Running Light – end cap turning

    It actually worked pretty well, although next time I’ll skip the sandpaper, affix the disk directly to the double sided duct tape, and be done with it.

    Line up the center punch dimple and drill a hole for the M3 screw:

    1 W Amber Running Light - end cap drilling
    1 W Amber Running Light – end cap drilling

    The power cable port turned into a little slot bandsawed into the edge of the disk with the sharp edges filed off.

    Basically, the thing needs some road testing before I build one for real …

  • Tour Easy 1 W Amber Running Light: Circuitry

    Tour Easy 1 W Amber Running Light: Circuitry

    With the internal slab attached to the 1 W LED heatsink, some double-sided foam tape affixes an Arduino Nano to one side of the slab:

    1 W LED Running Light - Arduino side
    1 W LED Running Light – Arduino side

    The MP1584 regulator and its 2.2 Ω current-sensing resistor (tacked down with acrylic adhesive) go on the other side:

    1 W LED Running Light - Regulator side
    1 W LED Running Light – Regulator side

    The Arduino and regulator draw power from the Bafang motor controller’s 6.3 V headlight circuit. The 2.2 Ω resistor sets the LED current to 360 mA = 900 mW. The blue wire connects the Arduino’s default LED output pin (D13) to the regulator’s Enable input (pin 2) to allow programmatic blinkiness.

    The end view shows everything Barely Fits™:

    1 W LED Running Light - internal assembly
    1 W LED Running Light – internal assembly

    All it needs is a rear cover of some sort …

  • Tour Easy 1 W Amber Running Light: Internal Plate

    Tour Easy 1 W Amber Running Light: Internal Plate

    A semi-scaled doodle laying out an Arduino Nano and the MP1584 regulator board suggested they might fit behind the heatsink with the 1 W LED:

    Amber running light - board layout doodle - side
    Amber running light – board layout doodle – side

    A somewhat more detailed doodle of the end view prompted me to bore the PVC pipe out to 23 mm:

    Amber running light - board layout doodle - end
    Amber running light – board layout doodle – end

    The prospect of designing a 3D printed holder for the boards suggested Quality Shop Time combined with double-stick foam tape would ensure a better outcome.

    So I bandsawed the remains of a chunky angle bracket into a pair of rectangles, flycut All The Sides to square them up, and tapped a pair of M3 holes along one edge of each:

    1 W LED Running Light - baseplate tapping
    1 W LED Running Light – baseplate tapping

    The other long edges got the V groove that killed the Sherline’s Y axis nut:

    Sherline Y-Axis Nut Mishap - setup
    Sherline Y-Axis Nut Mishap – setup

    The groove holds a length of 4 mm OD (actually 5/32 inch, but don’t tell anybody) brass tubing:

    1 W LED Running Light - baseplate trial fit
    1 W LED Running Light – baseplate trial fit

    The M3 button head screws are an admission of defeat, as I could see no way of controlling the width + thickness of the aluminum slabs to get a firm push fit in the PVC tube. The screws let me tune for best picture after everything else settled out.

    A little more machining opened up the top of the groove:

    1 W LED Running Light - baseplate dry assembly
    1 W LED Running Light – baseplate dry assembly

    A short M3 button head screw (with its head turned down to 4 mm) drops into the slot and holds the slab to the threaded hole in the LED heatsink. The long screw is holding the threaded insert in place for this dry fit.

    I doodled a single long screw through the whole thing, but having it fall off the heatsink when taking the rear cover off seemed like a Bad Idea™. An M3 button head screw uses a 2 mm hex key that fits neatly through the threaded insert, thereby making it work.

    Butter it up with epoxy, scrape off the excess, and let things cure:

    1 W LED Running Light - baseplate curing
    1 W LED Running Light – baseplate curing

    This was obviously made up as I went along …