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

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

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

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

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

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

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