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

  • 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!

  • 1-by-One Folding Bluetooth Keyboard: Flex Cable Fix

    1-by-One Folding Bluetooth Keyboard: Flex Cable Fix

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

    Folding keyboard - front
    Folding keyboard – front

    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:

    Folding keyboard - acute cable fold edge
    Folding keyboard – acute cable fold edge

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

    Folding keyboard - failed cable - borrom view
    Folding keyboard – failed cable – borrom view

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

    Folding keyboard - cracked flex conductor
    Folding keyboard – cracked flex conductor

    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:

    Folding keyboard - patched flex conductors
    Folding keyboard – patched flex conductors

    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:

    Folding keyboard - reinforced cable section
    Folding keyboard – reinforced cable section

    Apply more reinforcing tape and button it up again:

    Folding keyboard - reinforced cable flex edge
    Folding keyboard – reinforced cable flex edge

    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.

  • 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 …

  • Red Oaks Mill Dam: Flood Stage

    Red Oaks Mill Dam: Flood Stage

    The remnants of Hurricane Ida dropped half a foot of rain in our area, so we walked to the remains of the Red Oaks Mill Dam to see the water:

    Red Oaks Mill Dam - 2021-09-02
    Red Oaks Mill Dam – 2021-09-02

    The white water crests stand in place over rocks in the stream bed, with hypnotic flowlines.

    The concrete abutment over on the left is now completely submerged. It was more conspicuous in May:

    Red Oaks Mill Dam - 2021-05-17
    Red Oaks Mill Dam – 2021-05-17

    Surprisingly, most of the tree trunks and debris collecting over on the right remain jammed in place, as seen in March:

    Red Oaks Mill Dam - 2021-03-19
    Red Oaks Mill Dam – 2021-03-19

    For completeness, the scene in February:

    Red Oaks Mill Dam - 2021-02-25
    Red Oaks Mill Dam – 2021-02-25

    The USGS has a hydrology station just downstream that reported about 10 feet of water, the “moderate” flood stage, around the time I took the first picture. The normal level is 3 feet.

    The “major” flood stage is 14 feet and, back in 2007, this is what it looked like at 15 feet:

    Red Oaks Mill Dam - 2007-04-17
    Red Oaks Mill Dam – 2007-04-17

    Our reference point is a drain pipe on the retaining wall behind the hotel: when the Mighty Wappingers Creek covers the pipe, it’s well and truly flooding.

    Searching for “red oaks mill dam” will surface more pix and stories.

  • No Dumbing

    No Dumbing

    Spotted on a walk around the neighborhood:

    Private Property - No Dumbing
    Private Property – No Dumbing

    If only it was a sign of the times …