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

  • ShopVac Nozzle Caddy

    ShopVac Nozzle Caddy

    Shortly after acquiring the Greatest ShopVac, I zip-tied half a foot of cardboard tube to the handle to corral the nozzle and keep the ungainly hose from sprawling across the floor. While disembowling the Ottlite into a mini-lathe light, the plastic trim joining the baseplate to the vertical tube cried out to become a nozzle caddy:

    ShopVac Nozzle Caddy - front view
    ShopVac Nozzle Caddy – front view

    It was exactly the right size and shape (by my admittedly slack standards) to hold the nozzle, plus being destined for the trash, so all it needed was a pair of clamp brackets conjured from the vasty digital deep:

    ShopVac Nozzle Caddy - solid model
    ShopVac Nozzle Caddy – solid model

    The bosses fit into a tapered slot along what was the rear side, with a pair of 4 mm holes at each end for screws into threaded brass inserts epoxied into the brackets:

    ShopVac Nozzle Caddy - clamps mounted
    ShopVac Nozzle Caddy – clamps mounted

    They obviously descend from the many clamp mounts I’ve made for everything from garden hoses to bike running lights. A pair of 4 mm SHCS squish the clamp around the handle, with a strip of electrical tape improving plastic-to-metal griptivity:

    ShopVac Nozzle Caddy - side view
    ShopVac Nozzle Caddy – side view

    The clearance just barely allows a nylock nut atop a washer and you’ll want to trim those 40 mm screws to an exact fit, but it came out pretty well.

    The original dimension doodle with some modeling ideas that didn’t survive more thinking:

    ShopVac Nozzle Caddy - Dimension Doodle 1
    ShopVac Nozzle Caddy – Dimension Doodle 1

    A more detailed doodle with brass inserts instead of the nylock nuts and an aluminum spreader plate that was obviously not necessary:

    ShopVac Nozzle Caddy - Dimension Doodle 2
    ShopVac Nozzle Caddy – Dimension Doodle 2

    In retrospect, the inserts would make more sense.

    The angle doodles convinced me not to bother modeling either the slot’s taper along its length or its mold draft.

    Kinda looks like it grew there and makes one wonder why they don’t include a caddy as a standard option.

    The OpenSCAD source code as a GitHub Gist:

    // ShopVac Nozzle Caddy
    // Ed Nisley KE4ZNU 2022-02
    Layout = "Show"; // [Handle,Block,Show,Build]
    HandleOD = 20.0;
    //- Extrusion parameters must match reality!
    /* [Hidden] */
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    Protrusion = 0.1; // make holes end cleanly
    inch = 25.4;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    ID = 0;
    OD = 1;
    LENGTH = 2;
    //———-
    // Dimensions
    // Handle lies along X axis
    Handle = [200,HandleOD,HandleOD]; // X = longer than anything else
    WallThick = 5.0; // Thinnest printed wall
    Screw = [4.0,7.0,25.0]; // M4 socket head cap screw
    Washer = [4.5,9.0,0.8]; // M4 washer
    Insert = [4.0,5.9,10.0]; // M4 brass insert
    Block = [15.0,Handle.y + 4*WallThick + 2*Screw[ID],HandleOD + 2*WallThick]; // overall clamp block
    echo(str("Block: ",Block));
    Bosses = [[Block.x,9.5,13.0],[Block.x,15.0,9.0]];
    ScrewOC = Handle.y + 2*WallThick + Screw[ID];
    Kerf = 1.0; // cut through middle to apply compression
    Gap = 1.25;
    CornerRadius = Washer[OD]/2;
    //———————-
    // 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(d=(FixDia + HoleWindage),h=Height,$fn=Sides);
    }
    // Shopvac handle
    module Handle() {
    rotate([0,90,0])
    translate([0,0,-Handle.x/2])
    rotate(180/(4*8))
    PolyCyl(Handle.y,Handle.x,4*8);
    }
    // Clamp block
    module ClampBlock(BossID=0) {
    difference() {
    union() {
    hull()
    for (i=[-1,1], j=[-1,1]) // rounded block
    translate([i*(Block.x/2 – CornerRadius),j*(Block.y/2 – CornerRadius),-Block.z/2])
    cylinder(r=CornerRadius,h=Block.z,$fn=8);
    translate([0,0,-(Block.z/2 + Bosses[BossID].z/2 – Protrusion)])
    cube(Bosses[BossID],center=true);
    }
    for (j = [-1,1]) // screw holes
    translate([0,j*ScrewOC/2,-(Block.z/2 + Protrusion)])
    rotate(180/6)
    PolyCyl(Screw[ID],Block.z + 2*Protrusion,6);
    cube([2*Block.x,2*Block.y,Kerf],center=true);
    Handle();
    translate([0,0,-Block.z])
    rotate(180/6)
    PolyCyl(Screw[ID],Block.z,6);
    translate([0,0,-(Handle.z/2 + Insert[LENGTH])])
    rotate(180/6)
    PolyCyl(Insert[OD],Handle.y,6);
    }
    }
    // Splice block less handle bore
    module ShapedBlock() {
    difference() {
    ClampBlock();
    Handle();
    }
    }
    //———-
    // Build them
    if (Layout == "Handle")
    Handle();
    if (Layout == "Block")
    ClampBlock(BossID=0);
    if (Layout == "Show") {
    color("Green",0.25)
    Handle();
    xofs = -((len(Bosses) – 1)/2 * Gap*Block.x);
    for (i=[0:len(Bosses) – 1])
    translate([xofs + i*Gap*Block.x,0,0])
    ClampBlock(i);
    }
    if (Layout == "Build") {
    yofs = -((len(Bosses) – 1)/2 * Gap*Block.y);
    for (j=[0:len(Bosses) – 1])
    translate([0,yofs + j*Gap*Block.y,0])
    translate([0,0,Block.x/2])
    rotate([0,90,0])
    ClampBlock(j);
    }
  • CNC-3018XL: Improved X-Axis Home Switch Mount

    CNC-3018XL: Improved X-Axis Home Switch Mount

    A few months of inactivity left the CNC-3018XL table parked in its homed position where the gentle-but-inexorable pressure of the switch lever displaced the foam holding the plastic actuator tab on the X-axis bearing enough that it would no longer operate reliably:

    3018 CNC - Y axis endstop
    3018 CNC – Y axis endstop

    Putting foam tape in a highly leveraged position produces the same poor results as in finance.

    The fix requires reorienting the switch so a solid block on the bearing can push directly on the actuator lever:

    CNC-3018 X Home Switch - bottom view
    CNC-3018 X Home Switch – bottom view

    The block must curve around the bearing to give the tape enough surface area for a good grip:

    CNC-3018 X Home Switch - oblique view
    CNC-3018 X Home Switch – oblique view

    The solid model for the new X-axis mount looks about like you’d expect:

    CNC-3018 X Home Switch Mount - solid model
    CNC-3018 X Home Switch Mount – solid model

    I increased the home switch pulloff to 2 mm, although it’s not clear that will make any difference in the current orientation.

    The OpenSCAD source code as a GitHub Gist:

    // 3018-Pro Mount for Makerbot Endstop PCB
    // Ed Nisley KE4ZNU – 2019-07 (using OEM machine axes)
    // 2022-02-02 rotate X block (after renaming axes to match new layout)
    /* [Build Options] */
    Layout = "Show"; // [Build, Show]
    /* [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;
    ID = 0;
    OD = 1;
    LENGTH = 2;
    //- Shapes
    // Basic PCB with hole for switch pins
    // origin at switch actuator corner, as seen looking at component side
    SwitchClear = [15.0,5.0,2.0]; // clearance around switch pins
    SwitchOffset = [12.5,9.0,0.0]; // center of switch pins from actuator corner
    PCB = [26.0,16.4,2*SwitchClear.z]; // switch PCB beyond connector, pin height
    //XBlock = [PCB.x + 10.0,PCB.y,20.0];
    XBlock = [PCB.x,PCB.y,10.0];
    XBearing = [10.0,26.5,28.5];
    XPin = [10.0,20.0,10.0];
    module XMount() {
    if (false) // side-push switch tended to slip
    difference() {
    translate([-10.0,0,0])
    cube(XBlock,center=false);
    translate([0,-Protrusion,10.0])
    cube(XBlock + [0,2*Protrusion,0],center=false);
    translate(SwitchOffset + [0,0,10.0 – SwitchClear.z/2])
    cube(SwitchClear + [0,0,Protrusion],center=true);
    }
    else {
    difference() {
    cube(XBlock,center=false);
    translate(SwitchOffset + [0,0,XBlock.z – SwitchClear.z/2])
    cube(SwitchClear + [0,0,Protrusion],center=true);
    }
    translate([1.25*XBlock.x,0,0])
    difference() {
    cube(XPin + [0,0,XBearing[OD]/4],center=false);
    translate([-Protrusion,XPin.y/2,XPin.z + XBearing[OD]/2])
    rotate([0,90,0])
    cylinder(d=XBearing[OD],h=XPin.x + 2*Protrusion,center=false);
    translate([-Protrusion,-XPin.y/2,XPin.z])
    cube(XPin + [2*Protrusion,0,0],center=false);
    }
    }
    }
    YBlock = [PCB.x,PCB.y,5.0];
    module YMount() {
    difference() {
    cube(YBlock,center=false);
    translate(SwitchOffset + [0,0,YBlock.z – SwitchClear.z/2])
    cube(SwitchClear + [0,0,Protrusion],center=true);
    }
    }
    ZBlock = [PCB.x,PCB.y,6.0];
    ZPin = [20.0,10.0,5.5];
    module ZMount() {
    difference() {
    cube(ZBlock,center=false);
    translate(SwitchOffset + [0,0,ZBlock.z – SwitchClear.z/2])
    cube(SwitchClear + [0,0,Protrusion],center=true);
    }
    translate([1.25*ZBlock.x,0,0])
    difference() {
    cube(ZPin,center=false);
    translate([ZPin.x/2,-Protrusion,4.0])
    cube(ZPin + [0,2*Protrusion,0],center=false);
    }
    }
    //- Build things
    if (Layout == "Show") {
    translate([0,XBlock.y,0])
    YMount();
    translate([0,-XBlock.y/2])
    XMount();
    translate([0,-(ZBlock.y + XBlock.y)])
    ZMount();
    }
  • MicroMark Bandsaw Cover Screw Knobs

    MicroMark Bandsaw Cover Screw Knobs

    These descend directly from the LMS Mini-Lathe cover knobs:

    Micro-Mark bandsaw cover screw knob
    Micro-Mark bandsaw cover screw knob

    The top pair of screw heads aren’t quite flush with the cover, so the knobs have 1 mm extensions:

    Micromark Bandsaw - cover screw knobs - upper
    Micromark Bandsaw – cover screw knobs – upper

    The bottom pair sit inside 4 mm recesses, so those knobs get matching extensions:

    Micromark Bandsaw - cover screw knobs - lower
    Micromark Bandsaw – cover screw knobs – lower

    Attacking an anonymous 5 mm hex wrench with a Dremel cutoff wheel produced a quartet of 12 mm shafts and reduced drawer clutter by one unit.

    In retrospect, I should have dismantled the cover, grabbed the screws in a vise with their shafts vertical, and epoxied all the knobs with perfect alignment. Next time, maybe.

  • Bafang Battery Charge Port: Battery Reset Tool

    Bafang Battery Charge Port: Battery Reset Tool

    A lithium battery management system can (and should!) disable the battery output to prevent damage from overcurrent or undervoltage, after which it must be reset. The inadvertent charge port short may have damaged the BMS PCB, but did not shut down the battery’s motor output, which means the BMS will not should not require resetting. However, because all this will happen remotely, it pays to be prepared.

    A description of how to reset the BMS in a similar battery involves poking bare hot wires into the battery terminals, which IMO is akin to Tickling The Dragon’s Tail. The alert reader will note that the “Shark” battery shown on that page has its terminal polarity exactly opposite of the “Ultra Slim Shark” battery on our bikes. Given the energies involved, eliminating any possible errors makes plenty of sense.

    The battery connector looks like this:

    Bafang battery - Ultra-Slim Shark connector
    Bafang battery – Ultra-Slim Shark connector

    For this battery, the positive terminal is on the right, as shown by the molded legend and verified by measurement.

    A doodle with various dimensions, most of which are pretty close:

    Bafang battery - connector dimension doodle
    Bafang battery – connector dimension doodle

    Further doodling produced a BMS reset adapter keyed to fit the battery connector in only one way:

    Bafang battery - adapter doodle
    Bafang battery – adapter doodle

    Which turned into the rectangular lump at the top of the tool kit, along with the various shell drills and suchlike discussed earlier:

    Bafang battery tools
    Bafang battery tools

    Looking into the solid model from the battery connector shows the notches and projections that prevent it from making incorrect contact:

    Battery Reset Adapter - show front
    Battery Reset Adapter – show front

    The pin dimensions on the right, along with a mysterious doodle that must have meant something at the time :

    Bafang battery - adapter pin doodle
    Bafang battery – adapter pin doodle

    The pins emerged from 3/16 inch brass rod, with pockets for the soldered wires:

    Bafang battery - reset tool - pins
    Bafang battery – reset tool – pins

    The wires go into a coaxial breakout connector that’s hot-melt glued into the recess. The coaxial connectors are rated for 12 V and intended for CCTV cameras, LED strings, and suchlike, but I think they’re good for momentary use at 48 V with minimal current.

    I printed the block with the battery connector end on top for the best dimensional accuracy and the other end of the pin holes held in place by a single layer of filament bridging the rectangular opening:

    Bafang battery - reset tool - hole support layer
    Bafang battery – reset tool – hole support layer

    I made a hollow punch to cut the bridge filaments:

    Bafang battery - reset tool - pin hole punch
    Bafang battery – reset tool – pin hole punch

    The holes extend along the rectangular cutout for the coaxial connector, so pressing the punch against the notch lines it up neatly with the hole:

    Bafang battery - reset tool - hole punching
    Bafang battery – reset tool – hole punching

    Whereupon a sharp rap with a hammer clears the hole:

    Bafang battery - reset tool - hole cleared
    Bafang battery – reset tool – hole cleared

    A dollop of urethane adhesive followed the pins into their holes to lock them in place. I plugged the block and pins into the battery to align the pins as the adhesive cured, with the wire ends carefully taped apart.

    After curing: unplug the adapter, screw wires into coaxial connector, slobber hot melt glue into the recess, squish into place, align, dribble more glue into all the gaps and over the screw terminals, then declare victory.

    It may never be needed, but that’s fine with me.

    [Update: A few more doodles with better dimensions and fewer malfeatures appeared from the back of the bench.]

    Bafang battery - adapter better doodle
    Bafang battery – adapter better doodle
    Bafang battery - adapter dimension doodle
    Bafang battery – adapter dimension doodle
    Bafang battery - connector key doodle
    Bafang battery – connector key doodle

    The OpenSCAD source code as a GitHub Gist:

    // Adapter to reset Bafang battery management system
    // Ed Nisley KE4ZNU Dec 2021
    Layout = "Block"; // [Show, Build, Pins, Block, CoaxAdapter, Key]
    Gap = 4.0;
    /* [Hidden] */
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    Protrusion = 0.1; // make holes end cleanly
    inch = 25.4;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
    Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    FixDia = Dia / cos(180/Sides);
    cylinder(d=(FixDia + HoleWindage),h=Height,$fn=Sides);
    }
    ID = 0;
    OD = 1;
    LENGTH = 2;
    //———————-
    // Dimensions
    WallThick = 3.0;
    PinSize = [3.5,4.75,9.0 + WallThick]; // LENGTH = exposed + wall
    PinFerrule = [3.5,4.75,10.0]; // larger section for soldering
    PinOC = 18.0;
    PinOffset = [-9.0,0,9.0];
    Keybase = 4.0; // key bottom plate thickness
    KeyBlockSize = [15.0,50.0,15.0];
    CoaxSize = [35.0,15.0,11.0];
    CoaxGlue = [0,2*2,1];
    // without key X section
    BlockSize = [CoaxSize.x + WallThick + PinFerrule[LENGTH],KeyBlockSize.y,KeyBlockSize.z + WallThick];
    echo(BlockSize=BlockSize);
    //———————-
    // Battery connection pin
    // Used to carve out space for real brass pin
    // Long enough to slide ferrule through block
    module Pins() {
    for (j=[-1,1])
    translate(PinOffset + [0,j*PinOC/2,0])
    rotate([0,90,0])
    rotate(180/6) {
    PolyCyl(PinSize[ID],BlockSize.x,6);
    translate([0,0,PinSize[LENGTH]])
    PolyCyl(PinSize[OD],BlockSize.x,6);
    }
    }
    //———————-
    // Coaxial socket adapter nest
    // X=0 at left end of block, Z=0 at bottom
    // includes glue, extends rightward to ensure clearance
    module CoaxAdapter() {
    translate([0,0,CoaxSize.z])
    cube(CoaxSize + CoaxGlue + [CoaxSize.x,0,CoaxSize.z],center=true);
    }
    //———————-
    // Block without key
    // X=0 at connector face, Z=0 at bottom of block
    module BareBlock() {
    difference() {
    translate([BlockSize.x/2,0,BlockSize.z/2])
    cube(BlockSize,center=true);
    Pins();
    translate([BlockSize.x,0,Keybase])
    CoaxAdapter();
    }
    translate([BlockSize.x – CoaxSize.x,0,BlockSize.z/2]) // bridging layer
    cube([ThreadThick,BlockSize.y,BlockSize.z],center=true);
    }
    //———————-
    // Complete block
    module Block() {
    BareBlock();
    BatteryKey();
    }
    //———————-
    // Battery connector key shape
    // Chock full of magic sizes
    // Polygons start at upper left corner
    module BatteryKey() {
    // base outline
    kb = [[-15,KeyBlockSize.y/2],[0,KeyBlockSize.y/2],[0,-KeyBlockSize.y/2],[-15,-KeyBlockSize.y/2]];
    // flange cutout
    kf = [[kb[0].x,20],[-3,20],[-3,15],[-8,15],[-8,-15],[-3,-15],[-3,-20],[kb[0].x,-20]];
    // sidewalls
    kw = [[-15,KeyBlockSize.y/2],[0,KeyBlockSize.y/2],[0,20],kf[0]];
    linear_extrude(height=Keybase)
    difference() {
    polygon(kb);
    polygon(kf);
    }
    linear_extrude(height=KeyBlockSize.z)
    polygon(kw);
    mirror([0,1,0])
    linear_extrude(height=KeyBlockSize.z)
    polygon(kw);
    translate([0,0,KeyBlockSize.z])
    linear_extrude(height=BlockSize.z – KeyBlockSize.z)
    polygon(kb);
    }
    //———————-
    // Build it
    if (Layout == "Block") {
    BareBlock();
    }
    if (Layout == "Pins") {
    Pins();
    }
    if (Layout == "Key") {
    BatteryKey();
    }
    if (Layout == "CoaxAdapter") {
    CoaxAdapter();
    }
    if (Layout == "Show") {
    Block();
    color("Brown",0.3)
    Pins();
    }
    if (Layout == "Build") {
    rotate([0,90,0])
    translate([-BlockSize.x,0,0])
    Block();
    }
  • Dirt Devil Vacuum: Stuck Adapter

    Dirt Devil Vacuum: Stuck Adapter

    My tool adapters for the Dirt Devil stick vacuum cleaner worked fine when inserted into the power unit, but got stuck in the floor brush extension tube:

    Dirt Devil Floor Tube - stuck adapter
    Dirt Devil Floor Tube – stuck adapter

    The adapter rotated freely inside the socket, so its diameter was correct and it wasn’t jammed, but pushing the latch button (at the depression on the right) didn’t release the adapter.

    Popping the latch out of the tube let the adapter slide easily out of the socket and exposed the innards:

    Dirt Devil Floor Tube - latch internals
    Dirt Devil Floor Tube – latch internals

    The two bosses inside the latch originally captured a nice conical spring:

    Dirt Devil Floor Tube - conical latch spring
    Dirt Devil Floor Tube – conical latch spring

    The tab on the left side of the latch button engages a slot in the OEM brush head and the recessed ring around my adapters:

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

    It turns out the molded tab was slightly too long, so pushing the latch button all the way down didn’t retract the tab out of the bore, so it remained engaged in the adapter’s ring.

    The conical spring also didn’t seem to collapse completely flat, so the bosses inside the latch button couldn’t quite bottom out, leaving the tab protruding even further inside the bore. It also required an inordinate amount of force to push the latch all the way down.

    While fiddling with all this, I noticed that the OEM floor brush would sometimes hang up on the tab, so the operation wasn’t all that smooth even with the original equipment.

    So I trimmed maybe half a millimeter off the tab, just enough to release the adapter with the button fully pressed and without the conical spring, then replaced the conical spring with a tiny spring (from the Big Box o’ Random Springs) trimmed to allow the full range of travel. This not only released the adapter, it also let the OEM floor brush pop out more easily.

    A zero-dollar repair, although with considerable annoyance.

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