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.

Tag: M2

Using and tweaking a Makergear M2 3D printer

  • Alpha Geek Clock: Radome Update

    Alpha Geek Clock: Radome Update

    There being nothing like a new problem to take one’s mind off all one’s old problems:

    C-Max CMMR-60 WWVB receiver - D cell display holder
    C-Max CMMR-60 WWVB receiver – D cell display holder

    It’s a variation on the camera battery and AA alkaline holders for various blinky LEDs:

    Astable Multivibrator - D cell WWVB
    Astable Multivibrator – D cell WWVB

    The little flag holding the C-Max CMMR-60 receiver PCB gets glued to the copper upright to keep it from swiveling in the breeze.

    The conical caps on the ferrite bar antenna are glued to the uprights and the antenna, in the expectation this is a one-off build-only project.

    Rather than buy specialized D-cell contacts, I used 18650 lithium cell contacts and conjured the bridge by soldering two together:

    D cell bridge contact from 18650 contacts
    D cell bridge contact from 18650 contacts

    It sits on the windowsill, blinks quietly in the dark, and flickers invisibly during the daytime.

    Those D cells came from the same batch that powered the previous version for the last five years, so they probably won’t last that long, even with a Nov 2024 date code.

    C-Max is apparently out of the WWVB biz, but you can get a similar Canaduino AM WWVB receiver.

    The far more complex EverSet ES100-MOD WWVB receiver requires a microcontroller with an I²C interface and very careful power management.

    The OpenSCAD source code as a GitHub Gist:

    // Astable Multivibrator
    // Holder for Alkaline cells
    // Ed Nisley KE4ZNU August 2020
    // 2020-09 add LED radome
    // 2020-11 add radome trim
    // 2021-11 D cells and WWVB receiver
    /* [Layout options] */
    Layout = "Build"; // [Build,Show,Lid,Spider,AntCap,RecFlag]
    CellName = "AA"; // [AA, D]
    Struts = -1; // [0:None, -1:Dual, 1:Quad]
    WWVB = true;
    /* [Hidden] */
    NumCells = 2; // [2]
    // Extrusion parameters
    /* [Hidden] */
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    function IntegerLessMultiple(Size,Unit) = Unit * floor(Size / Unit);
    Protrusion = 0.1; // make holes end cleanly
    inch = 25.4;
    //- Basic dimensions
    WallThick = IntegerMultiple(3.0,ThreadWidth);
    CornerRadius = WallThick/2;
    FloorThick = IntegerMultiple(3.0,ThreadThick);
    TopThick = IntegerMultiple(2.0,ThreadThick);
    WireOD = 1.5; // battery & LED wiring
    WireOC = 8.0; // hole spacing in lid
    Gap = 5.0;
    // Cylindrical cell sizes
    // https://en.wikipedia.org/wiki/List_of_battery_sizes#Cylindrical_batteries
    CELL_NAME = 0;
    CELL_OD = 1;
    CELL_OAL = 2;
    // FIXME search() needs special-casing to properly find AAA and AAAA
    // Which is why CellName is limited to AA
    CellData = [
    ["AAAA",8.3,42.5],
    ["AAA",10.5,44.5],
    ["AA",14.5,50.5],
    ["C",26.2,50],
    ["D",34.2,61.5],
    ["A23",10.3,28.5],
    ["CR123A",17.0,34.5],
    ["18650",18.8,65.2], // bare 18650 with button end
    ["18650Prot",19.0,70.0], // protected 18650 = 19670 plus a bit
    ];
    CellIndex = search([CellName],CellData,1,0)[0];
    echo(str("Cell index: ",CellIndex," = ",CellData[CellIndex][CELL_NAME]));
    //- Contact dimensions
    CONTACT_NAME = 0;
    CONTACT_WIDE = 1;
    CONTACT_HIGH = 2;
    CONTACT_THICK = 3; // plate thickness
    CONTACT_TIP = 4; // tip to rear face
    CONTACT_TAB = 5; // solder tab width
    ContactData = [
    ["AA+",12.2,12.2,0.3,1.7,3.5], // pos bump
    ["AA-",12.2,12.2,0.3,5.0,3.5], // half-compressed neg spring
    ["AA+-",28.2,12.2,0.3,5.0,0], // pos-neg bridge
    ["D+",18.5,16.0,0.3,2.8,5.5],
    ["D-",18.5,16.0,0.3,6.0,5.5],
    ["D+-",50.0,19.0,0.3,7.0,0], // solder +/- tabs together
    ["Li+",18.5,16.0,0.3,2.8,5.5],
    ["Li-",18.5,16.0,0.3,6.0,5.5],
    ];
    function ConDat(name,dim) = ContactData[search([name],ContactData,1,0)[0]][dim];
    ContactRecess = 2*ConDat(str(CellName,"+"),CONTACT_THICK);
    ContactOC = CellData[CellIndex][CELL_OD];
    WireBay = 6.0; // room for wiring to contacts
    //- Wire struts
    StrutDia = 1.6; // AWG 14 = 1.6 mm
    StrutSides = 3*4;
    ID = 0;
    OD = 1;
    LENGTH = 2;
    StrutBase = [StrutDia,StrutDia + 2*5*ThreadWidth, // ID = wire, OD = buildable
    FloorThick + CellData[CellIndex][CELL_OD]]; // LENGTH = base is flush with cell top
    //- Holder dimensions
    BatterySize = [CellData[CellIndex][CELL_OAL] + // cell
    ConDat(str(CellName,"+"),CONTACT_TIP) + // pos contact
    ConDat(str(CellName,"-"),CONTACT_TIP) – // neg contact
    2*ContactRecess, // sink into wall
    NumCells*CellData[CellIndex][CELL_OD],
    CellData[CellIndex][CELL_OD]
    ];
    echo(str("Battery space: ",BatterySize));
    CaseSize = [3*WallThick + // end walls + wiring partition
    BatterySize.x + // cell
    WireBay, // wiring bay
    2*WallThick + BatterySize.y,
    FloorThick + BatterySize.z
    ];
    echo(str("CaseSize: ",CaseSize));
    BatteryOffset = (CaseSize.x – (2*WallThick +
    CellData[CellIndex][CELL_OAL] +
    ConDat(str(CellName,"-"),CONTACT_TIP))
    ) /2 ;
    ThumbRadius = 0.75 * CaseSize.z;
    StrutOC = [IntegerLessMultiple(CaseSize.x – 2*CornerRadius -2*StrutBase[OD],5.0),
    IntegerMultiple(CaseSize.y + StrutBase[OD],5.0)];
    StrutAngle = atan(StrutOC.y/StrutOC.x);
    echo(str("Strut OC: ",StrutOC));
    LidSize = [2*WallThick + WireBay + ConDat(str(CellName,"+"),CONTACT_THICK), CaseSize.y, FloorThick/2];
    LidScrew = [2.0,3.8,7.0]; // M2 pan head screw (LENGTH = threaded)
    LidScrewOC = CaseSize.y/2 – CornerRadius – LidScrew[OD]; // allow space around screw head
    //- Piranha LEDs
    PiranhaBody = [8.0,8.0,8.0]; // Z = heatsink fins + plastic body + lens
    PiranhaPin = 0.0; // trimmed pin length beyond heatsink
    PiranhaPinsOC = [5.0,5.0]; // pin XY distance
    PiranhaRecess = PiranhaBody.z + PiranhaPin/2; // minimum LED recess depth
    BallOD = 40.0; // radome sphere
    BallSides = 4*3*4; // nice smoothness
    PillarOD = norm([PiranhaBody.x,PiranhaBody.y]) + 2*WallThick;
    BallChordM = BallOD/2 – sqrt(pow(BallOD/2,2) – (pow(PillarOD,2))/4);
    echo(str("Ball chord depth: ",BallChordM));
    RadomePillar = [norm([PiranhaBody.x,PiranhaBody.y]), // ID = LED diagonal
    PillarOD,
    FloorThick + PiranhaRecess + BallChordM]; // height to top of ball chord
    echo(str("Pillar: ",RadomePillar));
    RadomeBar = [StrutBase[OD]*cos(180/StrutSides),StrutOC.y,StrutBase[OD]/2];
    Tape = [RadomePillar[ID],16.0,1.0]; // sticky tape disk, OD to match hole punch
    //- WWVB receiver hardware
    Antenna = [10.0 + 0.5,14.0,60.0 + 2.0]; // ferrite antenna bar with clearance
    AntCapSize = [Antenna[ID] + 1.0,Antenna[OD],5.0]; // LENGTH=insertion
    RecPCB = [24.0,16.0,5.0];
    //———————-
    // Useful routines
    module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
    Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    FixDia = Dia / cos(180/Sides);
    cylinder(r=(FixDia + HoleWindage)/2,h=Height,$fn=Sides);
    }
    // Spider for single LED atop struts, with the ball
    module DualSpider() {
    difference() {
    union() {
    for (j=[-1,1]) {
    for (k=[-1,1])
    translate([0,j*StrutOC.y/2,k*RadomeBar.z])
    rotate(180/StrutSides)
    sphere(d=StrutBase[OD]/cos(180/StrutSides),$fn=StrutSides);
    translate([0,j*StrutOC.y/2,0])
    rotate(180/StrutSides)
    cylinder(d=StrutBase[OD],h=2*RadomeBar.z,center=true,$fn=StrutSides);
    }
    cube(RadomeBar,center=true); // connecting bar
    cylinder(d=RadomePillar[OD],h=RadomePillar[LENGTH],$fn=BallSides);
    translate([0,0,-RadomeBar.z/2])
    cylinder(d1=0.9*RadomePillar[OD],d2=RadomePillar[OD],h=RadomeBar.z/2,$fn=BallSides);
    }
    for (j=[-1,1]) // strut wires
    translate([0,j*StrutOC.y/2,-3*StrutBase[OD]/2])
    rotate(180/StrutSides)
    PolyCyl(StrutBase[ID],2*StrutBase[OD],StrutSides);
    for (k=[-1,1]) // LED wiring through bar
    translate([0,k*(StrutOC.x/2 – 2*RadomeBar.x),-RadomeBar.z])
    rotate(180/6)
    PolyCyl(StrutBase[ID],2*RadomeBar.z,6);
    translate([0,0,BallOD/2 + RadomePillar[LENGTH] – BallChordM]) // ball inset
    sphere(d=BallOD);
    translate([0,0,BallOD/2 + RadomePillar[LENGTH] – BallChordM – Tape[LENGTH]/2]) // tape inset
    intersection() {
    sphere(d=BallOD);
    cylinder(d=Tape[OD],h=2*BallOD,center=true);
    }
    translate([0,0,RadomePillar.z – PiranhaRecess + RadomePillar.z/2]) // LED inset
    cube(PiranhaBody + [HoleWindage,HoleWindage,RadomePillar.z],center=true); // XY clearance
    translate([0,0,StrutBase[OD]/4 + WireOD/2 + 0*Protrusion]) // wire channels
    cube([WireOD,RadomePillar[OD] + 2*WallThick,WireOD],center=true);
    }
    }
    //– WWVB antenna support cap
    module AntennaBar() {
    rotate([90,0,0])
    union() {
    cylinder(d=Antenna[ID],h=Antenna[LENGTH],$fn=BallSides,center=true);
    cylinder(d=2*Antenna[OD],h=Antenna[LENGTH] – 2*AntCapSize[LENGTH],$fn=BallSides,center=true);
    }
    }
    module AntennaCap() {
    rotate([90,0,0])
    intersection() {
    translate([0,-Antenna[LENGTH]/2 + AntCapSize[LENGTH],0])
    difference() {
    hull() {
    rotate([90,0,0])
    cylinder(d=AntCapSize[OD],h=Antenna[LENGTH],$fn=BallSides,center=true);
    for (j=[-1,1])
    translate([0,j*StrutOC.y/2,0])
    rotate(180/StrutSides)
    cylinder(d=StrutBase[OD],h=1*StrutBase[OD],$fn=StrutSides,center=true);
    }
    for (j=[-1,1])
    translate([0,j*StrutOC.y/2,-Antenna[OD]/2])
    rotate(180/StrutSides)
    PolyCyl(StrutBase[ID],Antenna[OD],StrutSides);
    AntennaBar();
    }
    rotate([-90,0,0])
    cylinder(d=Antenna[OD],h=Antenna[LENGTH],center=false);
    }
    }
    //– WWVB PCB support flag
    module RecFlag() {
    difference() {
    hull() {
    rotate(180/StrutSides)
    cylinder(d=StrutBase[OD],h=RecPCB.x,$fn=StrutSides);
    translate([0,RecPCB.y,0])
    rotate(180/StrutSides)
    cylinder(d=StrutBase[OD],h=RecPCB.x,$fn=StrutSides);
    }
    translate([0,0,-Protrusion])
    rotate(180/StrutSides)
    PolyCyl(StrutBase[ID],2*RecPCB.x,StrutSides);
    translate([0,StrutBase[OD]/2,-Protrusion])
    cube([StrutBase[OD],RecPCB.y,2*RecPCB.x],center=false);
    }
    }
    //– Overall case with origin at battery center
    module Case() {
    union() {
    difference() {
    union() {
    hull()
    for (i=[-1,1], j=[-1,1])
    translate([i*(CaseSize.x/2 – CornerRadius),
    j*(CaseSize.y/2 – CornerRadius),
    0])
    cylinder(r=CornerRadius/cos(180/8),h=CaseSize.z,$fn=8); // cos() fixes undersize spheres!
    if (Struts)
    for (i = (Struts == 1) ? [-1,1] : -1) { // strut bases
    hull()
    for (j=[-1,1])
    translate([i*StrutOC.x/2,j*StrutOC.y/2,0])
    rotate(180/StrutSides)
    cylinder(d=StrutBase[OD],h=StrutBase[LENGTH],$fn=StrutSides);
    translate([i*StrutOC.x/2,0,StrutBase[LENGTH]/2])
    cube([2*StrutBase[OD],StrutOC.y,StrutBase[LENGTH]],center=true); // blocks for fairing
    for (j=[-1,1]) // hemisphere caps
    translate([i*StrutOC.x/2,
    j*StrutOC.y/2,
    StrutBase[LENGTH]])
    rotate(180/StrutSides)
    sphere(d=StrutBase[OD]/cos(180/StrutSides),$fn=StrutSides);
    }
    }
    translate([BatteryOffset,0,BatterySize.z/2 + FloorThick]) // cells
    cube(BatterySize + [0,0,Protrusion],center=true);
    translate([BatterySize.x/2 + BatteryOffset + ContactRecess/2 – Protrusion/2, // contacts
    0,
    BatterySize.z/2 + FloorThick])
    cube([ContactRecess + Protrusion,
    ConDat(str(CellName,"+-"),CONTACT_WIDE),
    ConDat(str(CellName,"+-"),CONTACT_HIGH)
    ],center=true);
    translate([-(BatterySize.x/2 – BatteryOffset + ContactRecess/2 – Protrusion/2),
    ContactOC/2,
    BatterySize.z/2 + FloorThick])
    cube([ContactRecess + Protrusion,
    ConDat(str(CellName,"+"),CONTACT_WIDE),
    ConDat(str(CellName,"+"),CONTACT_HIGH)
    ],center=true);
    translate([-(BatterySize.x/2 – BatteryOffset + ContactRecess/2 – Protrusion/2),
    -ContactOC/2,
    BatterySize.z/2 + FloorThick])
    cube([ContactRecess + Protrusion,
    ConDat(str(CellName,"-"),CONTACT_WIDE),
    ConDat(str(CellName,"-"),CONTACT_HIGH)
    ],center=true);
    translate([-CaseSize.x/2 + WireBay/2 + WallThick, // wire bay with screw bosses
    0,
    BatterySize.z/2 + FloorThick + Protrusion/2])
    cube([WireBay,
    2*LidScrewOC – LidScrew[ID] – 2*4*ThreadWidth,
    BatterySize.z + Protrusion
    ],center=true);
    for (j=[-1,1]) // screw holes
    translate([-CaseSize.x/2 + WireBay/2 + WallThick,
    j*LidScrewOC,
    CaseSize.z – LidScrew[LENGTH] + Protrusion])
    PolyCyl(LidScrew[ID],LidScrew[LENGTH],6);
    for (j=[-1,1])
    translate([-(BatterySize.x/2 – BatteryOffset + WallThick/2), // contact tabs
    j*ContactOC/2,
    BatterySize.z + FloorThick – Protrusion])
    cube([2*WallThick,
    ConDat(str(CellName,"+"),CONTACT_TAB),
    (BatterySize.z – ConDat(str(CellName,"+"),CONTACT_HIGH))
    ],center=true);
    if (false)
    translate([0,0,CaseSize.z]) // finger cutout
    rotate([90,00,0])
    cylinder(r=ThumbRadius,h=2*CaseSize.y,center=true,$fn=22);
    if (Struts)
    for (i2 = (Struts == 1) ? [-1,1] : -1) { // strut wire holes and fairing
    for (j=[-1,1])
    translate([i2*StrutOC.x/2,j*StrutOC.y/2,FloorThick])
    rotate(180/StrutSides)
    PolyCyl(StrutBase[ID],2*StrutBase[LENGTH],StrutSides);
    for (i=[-1,1], j=[-1,1]) // fairing cutaways
    translate([i*StrutBase[OD] + (i2*StrutOC.x/2),
    j*StrutOC.y/2,
    -Protrusion])
    rotate(180/StrutSides)
    PolyCyl(StrutBase[OD],StrutBase[LENGTH] + 2*Protrusion,StrutSides);
    }
    translate([0,0,ThreadThick – Protrusion]) // recess around name
    cube([51.0,15,2*ThreadThick],center=true);
    }
    linear_extrude(height=2*ThreadThick + Protrusion,convexity=10) {
    translate([0,-3.5,0])
    mirror([0,1,0])
    text(text="softsolder",size=6,spacing=1.20,font="Arial:style:Bold",halign="center",valign="center");
    translate([0,3.5,0])
    mirror([0,1,0])
    text(text=".com",size=6,spacing=1.20,font="Arial:style:Bold",halign="center",valign="center");
    }
    }
    }
    module Lid() {
    difference() {
    hull()
    for (i=[-1,1], j=[-1,1], k=[-1,1])
    translate([i*(LidSize.x/2 – CornerRadius),
    j*(LidSize.y/2 – CornerRadius),
    k*(LidSize.z – CornerRadius)]) // double thickness for flat bottom
    sphere(r=CornerRadius/cos(180/8),$fn=8);
    translate([0,0,-LidSize.z]) // remove bottom
    cube([(LidSize.x + 2*Protrusion),(LidSize.y + 2*Protrusion),2*LidSize.z],center=true);
    for (j=[-1,1]) // wire holes
    translate([0,j*WireOC/2,-Protrusion])
    PolyCyl(WireOD,2*LidSize.z,6);
    for (j=[-1,1])
    translate([0,j*LidScrewOC,-Protrusion])
    PolyCyl(LidScrew[ID],2*LidSize.z,6);
    }
    }
    //——————-
    // Show & build stuff
    if (Layout == "Case")
    Case();
    if (Layout == "Lid")
    Lid();
    if (Layout == "AntCap")
    AntennaCap();
    if (Layout == "RecFlag")
    RecFlag();
    if (Layout == "Spider")
    if (Struts == -1)
    DualSpider();
    else
    cube(10,center=true);
    if (Layout == "Build") {
    rotate(90)
    Case();
    translate([0,-(CaseSize.x/2 + LidSize.x/2 + Gap),0])
    rotate(90)
    Lid();
    if (Struts == -1) {
    difference() {
    union() {
    translate([CaseSize.x/2 + RadomePillar[OD],0,0])
    DualSpider();
    translate([-(CaseSize.x/2 + RadomePillar[OD]),0,0])
    rotate([180,0,0])
    DualSpider();
    }
    translate([0,0,-2*CaseSize.z])
    rotate(90)
    cube(4*CaseSize,center=true);
    }
    }
    if (WWVB) {
    for (i=[-1,1])
    translate([i*(Antenna[LENGTH]/2 – AntCapSize[LENGTH]),CaseSize.x/2 + Antenna[OD],0])
    AntennaCap();
    translate([0,CaseSize.x/2 + Antenna[OD],0])
    RecFlag();
    }
    }
    if (Layout == "Show") {
    Case();
    for (j=[-1,1])
    color("Brown",0.3)
    translate([-StrutOC.x/2,j*StrutOC.y/2,Protrusion])
    cylinder(d=StrutDia[ID],h=3*CaseSize.z,$fn=StrutSides);
    translate([-(CaseSize.x/2 – LidSize.x/2),0,(CaseSize.z + Gap)])
    Lid();
    if (Struts == -1)
    translate([-StrutOC.x/2,0,3*CaseSize.z])
    DualSpider();
    if (WWVB) {
    for (j=[-1,1])
    translate([-StrutOC.x/2,,j*(Antenna[LENGTH]/2 – AntCapSize[LENGTH]),1.5*CaseSize.z])
    rotate([-j*90,0,0])
    AntennaCap();
    translate([-StrutOC.x/2,,-(StrutOC.y/2),2*CaseSize.z])
    RecFlag();
    }
    }

  • Tube Turning Adapters

    Tube Turning Adapters

    Finishing the PVC tubes reinforcing the vacuum cleaner adapters required fixtures on each end:

    Dirt Devil adapter - pipe turning
    Dirt Devil adapter – pipe turning

    Because the tubes get epoxied into the adapters, there’s no particular need for a smooth surface finish and, in fact, some surface roughness makes for a good epoxy bond. The interior of a 3D printed adapter is nothing if not rough; the epoxy in between will be perfectly happy.

    Turning the tubes started by just grabbing the conduit in the chuck and peeling the end that stuck out down to the finished diameter, because the conduit was thick-walled enough to let that work.

    The remaining wall was so thin that the chuck would crunch it into a three-lobed shape, so the white ring in the chuck is a scrap of PVC pipe turned to fit the tube ID and provide enough reinforcement to keep the tube round.

    The conduit ID isn’t a controlled dimension and was, in point of fact, not particularly round. It was, however, smooth, which counts for more than anything inside a tube carrying airborne fuzzy debris; polishing the interior of a lathe-bored pipe simply wasn’t going to happen.

    The fixture on the other end started as a scrap of polycarbonate bandsawed into a disk with a hole center-drilled in the middle:

    Pipe end lathe fixture - center drilling
    Pipe end lathe fixture – center drilling

    Stick it onto a disk turning fixture and sissy-cut the OD down a little smaller than the eventual tube OD:

    Pipe end lathe fixture - turning OD
    Pipe end lathe fixture – turning OD

    Turn the end down to fit the tube ID, flip it around to center-drill the other side, stick it into the tube, and finally finish the job:

    Dirt Devil adapter - pipe fixture
    Dirt Devil adapter – pipe fixture

    The nice layering effect along the tube probably comes from molding the conduit from recycled PVC with no particular concern for color matching.

    A family portrait of the fixtures with a finished adapter:

    Dirt Devil adapter - fixtures
    Dirt Devil adapter – fixtures

    A fine chunk of Quality Shop Time: solid modeling, 3D printing, mini-lathe turning, and even some coordinate drilling on the Sherline.

  • Dirt Devil Vacuum Tool Adapters

    Dirt Devil Vacuum Tool Adapters

    Being the domain expert for adapters between a new vacuum cleaner and old tools, this made sense (even though it’s not our vacuum):

    Dirt Devil Nozzle Bushing - solid model
    Dirt Devil Nozzle Bushing – solid model

    The notch snaps into a Dirt Devil Power Stick vacuum cleaner and the tapered end fits a variety of old tools for other vacuum cleaners:

    Dirt Devil Nozzle Bushing top view - solid model
    Dirt Devil Nozzle Bushing top view – solid model

    Having some experience breaking thin-walled adapters, these have reinforcement from a PVC tube:

    Dirt Devil adapter - parts
    Dirt Devil adapter – parts

    A smear of epoxy around the interior holds the tube in place:

    Dirt Devil adapters - assembled
    Dirt Devil adapters – assembled

    Building the critical dimensions with a 3D printed part simplified the project, because I could (and did!) tweak the OpenSCAD code to match the tapers to the tools. Turning four of those tubes from a chunk of PVC conduit, however, makes a story for another day.

    The OpenSCAD source code as a GitHub Gist:

    // Dirt Devil nozzle adapter
    // Ed Nisley KE4ZNU 2021-10
    // Tool taper shift
    Finesse = -0.1; // [-0.5:0.1:0.5]
    // PVC pipe liner
    PipeOD = 28.5;
    /* [Hidden] */
    //- Extrusion parameters
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    Protrusion = 0.1; // make holes end cleanly
    //———————-
    // Dimensions
    TAPER_MIN = 0;
    TAPER_MAX = 1;
    TAPER_LENGTH = 2;
    Socket = [36.0,37.0,40.0];
    LockringDia = 33.5;
    LockringWidth = 4.5;
    LockringOffset = 2.5;
    Tool = [Finesse,Finesse,0] + [30.0,31.1,30.0];
    AdapterOAL = Socket[TAPER_LENGTH] + Tool[TAPER_LENGTH];
    NumSides = 36;
    $fn = NumSides;
    //———————-
    // Useful routines
    module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
    Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    FixDia = Dia / cos(180/Sides);
    cylinder(r=(FixDia + HoleWindage)/2,h=Height,$fn=Sides);
    }
    //——————-
    // Define it!
    module Adapter() {
    difference() {
    union() {
    difference() {
    cylinder(d1=Socket[TAPER_MIN],d2=Socket[TAPER_MAX],h=Socket[TAPER_LENGTH]);
    translate([0,0,LockringOffset])
    cylinder(d=2*Socket[TAPER_MAX],h=LockringWidth);
    }
    cylinder(d=LockringDia,h=Socket[TAPER_LENGTH]);
    translate([0,0,LockringOffset + 0.75*LockringWidth])
    cylinder(d1=LockringDia,d2=Socket[TAPER_MIN],h=0.25*LockringWidth);
    translate([0,0,Socket[TAPER_LENGTH]])
    cylinder(d1=Tool[TAPER_MAX],d2=Tool[TAPER_MIN],h=Tool[TAPER_LENGTH]);
    }
    translate([0,0,-Protrusion])
    PolyCyl(PipeOD,AdapterOAL + 2*Protrusion,NumSides);
    }
    }
    //———————-
    // Build it!
    Adapter();

    The taper in the code almost certainly won’t fit whatever tool you have: measure thrice, print twice, and maybe fit once …

  • Tour Easy Rear Running Light: Circuit Support Plate

    Tour Easy Rear Running Light: Circuit Support Plate

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

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

    This was definitely easier:

    Running Light Circuit Plate - solid model
    Running Light Circuit Plate – solid model

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

    Tour Easy Rear Running Light - circuit plate attachment
    Tour Easy Rear Running Light – circuit plate attachment

    The rectangular hole around the insert let me glop urethane adhesive over it to lock it into the plate, with more goop on the screw and pins to unify heatsink and plate.

    The LED wires now emerge from the heatsink on the same side of the plate, simplifying the connections to the MP1584 regulator and current-sense resistor:

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

    The paralleled 5.1 Ω and 3.3 Ω resistors form a 2.0 Ω resistor setting the LED current to 400 mA = 1 W at 2.6 V forward drop. They’re 1 W resistors dissipating a total of 320 mW and get barely warm.

    The resistors and wires are stuck in place with clear adhesive, so things shouldn’t rattle around too much.

    The OpenSCAD source code as a GitHub Gist:

    // Circuit plate for Tour Easy running lights
    // Ed Nisley – KE4ZNU – 2021-09
    /* [Hidden] */
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    Protrusion = 0.1; // make holes end cleanly
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    ID = 0;
    OD = 1;
    LENGTH = 2;
    inch = 25.4;
    //———————-
    // Dimensions
    // Light case along X axis
    LightID = 23.0;
    WallThick = 2.0;
    Screw = [3.0,6.8,4.0]; // M3 OD=washer, length=nut + washers
    Insert = [3.0,4.2,8.0]; // splined brass insert, minus splines
    InsertOffset = 10.0; // insert from heatsink end
    PinOD = 1.6; // alignment pins
    PinOC = 14.0;
    PinDepth = 5.0;
    Plate = [50.0,LightID,Insert[OD] + 4*ThreadThick]; // overall plate size
    WirePort = [10.0,3.0,2*Plate.z];
    NumSides = 2*3*4;
    //———————-
    // Useful routines
    module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
    Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    FixDia = Dia / cos(180/Sides);
    cylinder(r=(FixDia + HoleWindage)/2,
    h=Height,
    $fn=Sides);
    }
    // Circuit plate
    module Plate() {
    difference() {
    intersection() {
    cube(Plate,center=true);
    rotate([0,90,0])
    cylinder(d=LightID,h=2*Plate.x,$fn=NumSides,center=true);
    }
    rotate([0,90,0]) rotate(180/6)
    translate([0,0,-Plate.x])
    PolyCyl(Screw[ID],2*Plate.x,6);
    rotate([0,90,0]) rotate(180/6)
    translate([0,0,-Plate.x/2 – Protrusion])
    PolyCyl(Insert[OD],Insert[LENGTH] + InsertOffset + Protrusion,6);
    translate([-Plate.x/2 + InsertOffset + Insert[LENGTH]/2,0,Plate.z/2])
    cube([Insert[LENGTH],Insert[OD],Plate.z],center=true);
    for (j=[-1,1])
    translate([-Plate.x/2,j*PinOC/2,0])
    rotate([0,90,0]) rotate(180/6)
    translate([0,0,-PinDepth])
    PolyCyl(PinOD,2*PinDepth,6);
    for (j=[-1,1])
    translate([0,j*(Plate.y/2 – WirePort.y/2),0])
    cube(WirePort,center=true);
    }
    }
    //- Build it
    Plate();

  • Rear Running Light: Tour Easy Seat Clamp

    Rear Running Light: Tour Easy Seat Clamp

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

    Superflash on Tour Easy
    Superflash on Tour Easy

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

    Rear Running Light Seat Clamp - solid model
    Rear Running Light Seat Clamp – solid model

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

    Rear Running Light Seat Clamp - sectioned - solid model
    Rear Running Light Seat Clamp – sectioned – solid model

    That means I can install the light, then mount the whole affair on the bike, without holding everything together while fiddling with overly long screws.

    A trial fit with the not-yet-cut-to-length 25.3 (-ish) PVC pipe body tube:

    Rear Running Light - Tour Easy seat clamp trial fit
    Rear Running Light – Tour Easy seat clamp trial fit

    The aluminum plates have the standard used-car finish: nice polish over deep scratches.

    Although I’ve been thinking of mounting the light below the seat rail, as shown, it can also sit above the rail.

    Mary hauls seedlings and suchlike to the garden in a plastic drawer bungied to the rack, with the SuperFlash serving as an anchor point; this light may need fine tuning for that purpose.

    The OpenSCAD source code as a GitHub Gist:

    // Rear running light clamp for Tour Easy seat strut
    // Ed Nisley – KE4ZNU – 2021-09
    Layout = "Show"; // [Show,Build,Block]
    Section = true;
    /* [Hidden] */
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    Protrusion = 0.1; // make holes end cleanly
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    ID = 0;
    OD = 1;
    LENGTH = 2;
    inch = 25.4;
    //———————-
    // Dimensions
    // Light case along X axis, seat strut along Y, Z=0 at strut centerline
    LightOD = 25.4 + HoleWindage;
    StrutOD = 5/8 * inch + HoleWindage;
    PlateThick = 1/16 * inch;
    WallThick = 2.0;
    Kerf = ThreadThick;
    Screw = [3.0,6.8,4.0]; // M3 OD=washer, length=nut + washers
    Insert = [3.0,5.4,8.0 + 1.0]; // splined brass insert
    RoundRadius = IntegerMultiple(Screw[OD]/2,0.5); // corner rounding
    ScrewOC = [IntegerMultiple(StrutOD + 2*WallThick + Screw[ID],1.0),
    IntegerMultiple(LightOD + 2*WallThick + Screw[ID],1.0)];
    echo(str("Screw OC: ",ScrewOC));
    BlockSize = [ScrewOC.x + Insert[OD] + 2*WallThick,
    ScrewOC.y + Insert[OD] + 2*WallThick,
    LightOD + StrutOD + 3*WallThick];
    echo(str("Block: ",BlockSize));
    BaseOffset = -(WallThick + LightOD/2); // block bottom to centerline
    StrutOffset = LightOD/2 + WallThick + StrutOD/2; // light centerline to strut centerline
    echo(str("Strut screw min: ",IntegerMultiple(PlateThick + WallThick + StrutOD/2 + Insert[LENGTH]/2,1.0)));
    echo(str("Light screw min: ",IntegerMultiple(PlateThick + WallThick + LightOD/2 + Insert[LENGTH]/2,1.0)));
    NumSides = 2*3*4;
    //———————-
    // Useful routines
    module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
    Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    FixDia = Dia / cos(180/Sides);
    cylinder(r=(FixDia + HoleWindage)/2,
    h=Height,
    $fn=Sides);
    }
    // Block with light along X axis
    module Block() {
    difference() {
    hull()
    for (i=[-1,1], j=[-1,1])
    translate([i*(BlockSize.x/2 – RoundRadius),j*(BlockSize.y/2 – RoundRadius),BaseOffset])
    cylinder(r=RoundRadius,h=BlockSize.z,$fn=NumSides);
    for (i=[-1,1], j=[-1,1])
    translate([i*ScrewOC.x/2,j*ScrewOC.y/2,BaseOffset – Protrusion])
    rotate(180/8)
    PolyCyl(Screw[ID],BlockSize.z + 2*Protrusion,8);
    for (i=[-1,1], j=[-1,1])
    translate([i*ScrewOC.x/2,j*ScrewOC.y/2,0]) {
    translate([0,0,-Protrusion])
    rotate(180/8)
    PolyCyl(Insert[OD],Insert[LENGTH] + 1*Protrusion,8);
    translate([0,0,(StrutOffset – Insert[LENGTH] – Kerf/2 + Protrusion)])
    rotate(180/8)
    PolyCyl(Insert[OD],Insert[LENGTH] + 1*Protrusion,8);
    }
    translate([-BlockSize.x,0,0])
    rotate([0,90,0])
    cylinder(d=LightOD,h=2*BlockSize.x,$fn=NumSides);
    translate([0,BlockSize.y,StrutOffset])
    rotate([90,0,0])
    cylinder(d=StrutOD,h=2*BlockSize.y,$fn=NumSides);
    translate([0,0,StrutOffset])
    cube([2*BlockSize.x,2*BlockSize.y,Kerf],center=true);
    cube([2*BlockSize.x,2*BlockSize.y,Kerf],center=true);
    }
    }
    //- Build it
    if (Layout == "Block")
    if (Section)
    difference() {
    Block();
    rotate(atan(ScrewOC.y/ScrewOC.x))
    translate([0,BlockSize.y,0])
    cube(2*BlockSize,center=true);
    }
    else
    Block();
    if (Layout == "Show") {
    Block();
    color("Green",0.25)
    translate([-BlockSize.x,0,0])
    rotate([0,90,0])
    cylinder(d=LightOD,h=2*BlockSize.x,$fn=NumSides);
    color("Green",0.25)
    translate([0,BlockSize.y,StrutOffset])
    rotate([90,0,0])
    cylinder(d=StrutOD,h=2*BlockSize.y,$fn=NumSides);
    }
    if (Layout == "Build") {
    translate([-1.2*BlockSize.x,0,-BaseOffset])
    difference() {
    Block();
    translate([0,0,BlockSize.z])
    cube(2*BlockSize,center=true);
    }
    translate([1.2*BlockSize.x,0,StrutOD/2 + WallThick])
    difference() {
    rotate([180,0,0])
    translate([0,0,-StrutOffset])
    Block();
    translate([0,0,BlockSize.z])
    cube(2*BlockSize,center=true);
    }
    translate([0,0,StrutOffset – Kerf/2])
    rotate([180,0,0])
    intersection() {
    Block();
    translate([0,0,StrutOffset/2])
    cube([2*BlockSize.x,2*BlockSize.y,StrutOffset],center=true);
    }
    }

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

  • Seedling Shelter Frame Deployment

    Seedling Shelter Frame Deployment

    Mary bound up a mesh cover for the shelter frame and deployed it to protect some yummy seedlings:

    Seedling Mesh Shelter - installed
    Seedling Mesh Shelter – installed

    Those will become the next round of lunchtime sandwiches:

    Turkey Sandwich with Excessive Lettuce
    Turkey Sandwich with Excessive Lettuce

    It’s a quarter-pounder: 4 oz of turkey, 4 oz of lettuce, and a layer of Swiss and good stinky Provolone cheese. Yum!