Dripworks Mainline Pipe Clamp

This is laid in against a need I hope never occurs:

Dripworks 0.75 inch pipe clamp
Dripworks 0.75 inch pipe clamp

It’s intended to clamp around one of the Dripworks mainline pipes carrying water from the pressure regulator to the driplines in the raised beds, should an errant shovel or fork find the pipe.

It descends from a long line of soaker hose clamps, with a 25 mm ID allowing for a silicone tape wrap as a water barrier.

The solid model has no surprises:

Dripworks Mainline Clamp - build view
Dripworks Mainline Clamp – build view

The OpenSCAD source code as a GitHub Gist:

// Dripworks 3/4 inch mainline clamp
// Ed Nisley KE4ZNU 2021-06
Layout = "Build"; // [Hose,Block,Show,Build]
HoseOD = 25.0;
TestFit = false; // true to build test fit slice from center
//- 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
// Hose lies along X axis
Hose = [200,HoseOD,HoseOD]; // X = longer than anything else
NumScrews = 2; // screws along each side of cable
WallThick = 3.0; // Thinnest printed wall
PlateThick = 1.5; // Stiffening plate thickness
// 8-32 stainless screws
Screw = [4.1,8.0,50.0]; // OD = head LENGTH = thread length
Washer = [4.4,9.5,1.0];
Nut = [4.1,9.7,3.3];
Block = [30.0,Hose.y + 2*Washer[OD],HoseOD + 2*WallThick]; // overall splice block size
echo(str("Block: ",Block));
ScrewMinLength = Block.z + 2*PlateThick + 2*Washer.z + Nut.z; // minimum screw length
echo(str("Screw min length: ",ScrewMinLength));
Kerf = 1.0; // cut through middle to apply compression
CornerRadius = Washer[OD]/2;
ScrewOC = [(Block.x - 2*CornerRadius) / (NumScrews - 1),
Block.y - 2*CornerRadius,
2*Block.z // ensure complete holes
];
echo(str("Screw OC: x=",ScrewOC.x," y=",ScrewOC.y));
//----------------------
// 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);
}
// Hose shape
// This includes magic numbers measured from reality
module HoseProfile() {
NumSides = 12*4;
rotate([0,-90,0])
translate([0,0,-Hose.x/2])
resize([Hose.z,Hose.y,0])
cylinder(d=Hose.z,h=Hose.x,$fn=NumSides);
}
// Outside shape of splice Block
// Z centered on hose rim circles, not overall thickness through center ridge
module SpliceBlock() {
difference() {
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=4*8);
for (i = [0:NumScrews - 1], j=[-1,1]) // screw holes
translate([-(Block.x/2 - CornerRadius) + i*ScrewOC.x,
j*ScrewOC.y/2,
-(Block.z/2 + Protrusion)])
PolyCyl(Screw[ID],Block.z + 2*Protrusion,6);
cube([2*Block.x,2*Block.y,Kerf],center=true); // slice through center
}
}
// Splice block less hose
module ShapedBlock() {
difference() {
SpliceBlock();
HoseProfile();
}
}
//----------
// Build them
if (Layout == "Hose")
HoseProfile();
if (Layout == "Block")
SpliceBlock();
if (Layout == "Show") {
difference() {
SpliceBlock();
HoseProfile();
}
color("Green",0.25)
HoseProfile();
}
if (Layout == "Build") {
SliceOffset = TestFit && !(NumScrews % 2) ? ScrewOC.x/2 : 0;
intersection() {
translate([SliceOffset,0,Block.z/4])
if (TestFit)
cube([ScrewOC.x/2,4*Block.y,Block.z/2],center=true);
else
cube([4*Block.x,4*Block.y,Block.z/2],center=true);
union() {
translate([0,0.6*Block.y,Block.z/2])
ShapedBlock();
translate([0,-0.6*Block.y,Block.z/2])
rotate([0,180,0])
ShapedBlock();
}
}
}

Sticky Trap Screen Frames

The objective being to reduce the number of onion maggots in Mary’s Vassar Farm plot without chemical agents, I conjured sticky trap screen frames from the vasty digital deep:

Sticky Trap - first production run
Sticky Trap – first production run

Each one contains half a sheet of yellow sticky plastic, which is easy enough to cut before peeling off the protective covering sheets. The cage is half-inch galvanized hardware cloth snipped with hardened diagonal cutters. A bead of acrylic adhesive around the base holds the cage in place

Although you can deploy sticky sheets without cages, they tend to attract and affix beneficial critters: butterflies, small birds, furry critters, toads, gardeners, and the like. We don’t know how effective the cages will be, but they seemed better than nothing.

They mount on ski poles cut in half:

Sticky Trap - ski pole installed
Sticky Trap – ski pole installed

And on fence posts around the perimeter:

Sticky Trap - angle bracket installed
Sticky Trap – angle bracket installed

To my untrained eye, some of those doomed critters are, indeed, onion maggot flies. The rest seem to be gnats and other nuisances, so IMO we’re applying population pressure in the right direction.

Each base-and-cap frame takes about three hours to print, so I did them one at a time over the course of a few days while applying continuous product improvement.

The sheets rest on small V blocks intended to keep them centered within the cage:

Sticky Sheet Cage - angle bracket - solid model
Sticky Sheet Cage – angle bracket – solid model

The ski pole attachment must build with the cap on top, but it bridges well enough for the purpose:

Sticky Sheet Cage - ski pole - solid model
Sticky Sheet Cage – ski pole – solid model

The overhanging hooks on the blocks (just barely) engage the grid to keep the lid in place, while remaining short enough to not droop too badly. You could probably delete the hooks from the bottom plate, but they align the cage while the adhesive cures.

The sheets tend to bend in the middle, so I’ll stick a thin slat or two vertically to keep them straight.

Deer Fence Hangers

For what should be obvious reasons, we armored Mary’s “kitchen garden” with buried concrete blocks and deer fence. I secured the fence to 7 foot plastic-coated steel-core posts strapped to shorter stakes supporting the lower wire fence, using cable ties we both knew wouldn’t survive exposure to the sun.

As part of the spring garden prep, I summoned proper supports from the vasty digital deep:

Deer Fence Hanger - Build view
Deer Fence Hanger – Build view

The general idea is to plunk one atop each post and tangle wrap the netting through the hooks, thusly:

Deer Fence Hanger - installed
Deer Fence Hanger – installed

The garden looks like we killed an entire chess set and impaled their carcasses as a warning to others of their kind, but the fence now hangs neatly from the top of the posts rather than drooping sadly.

Each one of those things takes nigh onto two hours to emerge from the M2, so I printed them one by one over the course of a few days while making continuous product improvements.

The “natural” PETG isn’t UV stabilized, either, but it ought to last longer than those little bitty nylon cable ties. We shall see.

The OpenSCAD source code as a GitHub Gist:

// Deer Fence Hangers
// Ed Nisley KE4ZNU May 2021
Layout = "Show"; // [Build, Show, Cap, Hook]
// net grid spacing
NetOC = 55.0; // [40.0:5.0:70.0]
// stake OD
PoleDia = 23.0; // [20.0:30.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;
ID = 0;
OD = 1;
LENGTH = 2;
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
//----------------------
// Dimensions
Notch = 5.0; // hook engagement
WallThick = 3.0; // min wall and end thickness
Shell = [PoleDia,PoleDia + 2*WallThick,NetOC + 2*Notch];
HookBlock = [10.0,Shell.y/4,2*Notch]; // hanger inside length
LegendBlock = [0.7*Shell.z,Shell.y/2,2*ThreadThick]; // legend size
//----------------------
// 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);
}
//----------------------
// Pieces
module Hook() {
//%Cap();
translate([Shell[OD]/2 - Protrusion,HookBlock.y/2,0])
rotate([90,0,0])
linear_extrude(height=HookBlock.y)
difference() {
scale([1,2])
intersection() {
circle(r=HookBlock.x);
square(HookBlock.x,center=false);
}
square(Notch,center=false);
}
}
module Cap() {
difference() {
rotate(180/6)
PolyCyl(Shell[OD],Shell[LENGTH],6);
translate([0,0,-WallThick])
rotate(180/24)
PolyCyl(Shell[ID],Shell[LENGTH],24);
translate([-Shell[OD]/2,0,Shell[LENGTH]/2])
rotate([0,90,0])
cube(LegendBlock,center=true);
}
translate([-(Shell[OD]/2 - LegendBlock.z/2),0,Shell[LENGTH]/2])
rotate([0,-90,0])
resize(0.8*LegendBlock,auto=[true,true,false])
linear_extrude(height=LegendBlock.z)
text(text=str(NetOC," ",PoleDia),
size=6,spacing=1.00,font="Bitstream Vera Sans:style=Bold",
halign="center",valign="center");
}
module Hanger() {
Cap();
for (k=[0,1])
translate([0,0,k*Shell.z])
for (a=[-1:1])
rotate([k*180,0,a*60])
Hook();
}
//----------------------
// Build it
if (Layout == "Cap")
Cap();
if (Layout == "Hook")
Hook();
if (Layout == "Show")
Hanger();
if (Layout == "Build")
translate([0,0,Shell[LENGTH]])
rotate([180,0,0])
Hanger();

Tour Easy: Bafang Mid-drive vs. Cateye Cadence Sensor

For inscrutable reasons, the Bafang 500C display includes all stopped time in its average trip speed. While that is, in fact, the average speed over the entire trip, the Cateye cyclocomputers we’ve been using forever stop averaging after a few seconds at 0 mph.

Bonus: Although the Bafang BBS02 motor knows the pedal cadence, it’s not part of the display.

The Bafang BBS02 bottom bracket shaft put its pedal cranks much farther from the Tour Easy’s frame than the Shimano cranks, to the extent that the existing Cateye cadence sensor position just wasn’t going to work, so I printed a simple clip to fit over the motor’s “fixing plate”:

Tour Easy Bafang BBS02 motor
Tour Easy Bafang BBS02 motor

It turns out putting a magnetic sensor immediately next to the winding end of a high-current three-phase motor isn’t the brightest idea I’ve ever had. The Cateye cadence display spent most of its time maxed out at 199 rpm, far faster than Mary can spin for, well, a single revolution.

A somewhat more complex mount put the sensor roughly where it used to be:

Cateye Cadence Sensor mount - installed
Cateye Cadence Sensor mount – installed

It looks precarious, but it spent nigh onto two decades there without incident, so we have precedent.

Those are the original 165 mm Shimano cranks, because the 170 mm Bafung cranks threatened to lock out her knees. More on this in a while, as it’s a more complex issue than it may appear.

The solid model looks about like you’d expect:

Cateye Cadence Sensor mount - solid model
Cateye Cadence Sensor mount – solid model

The OpenSCAD code replaces the simple clip in the original GitHub Gist:

// Cateye cadence sensor bracket

LockRingDia = [44.0,46.0];
LockRingLen = [4.0,6.5];
LockRingOAD = LockRingDia[1] + 2*WallThick;
LockRingOAL = LockRingLen[0] + LockRingLen[1];

Notches = 16;
SensorAngle = 3*360/Notches;
SensorBase = 10.0;

module Cateye() {

    difference() {
        union() {
            cylinder(d=LockRingOAD,h=LockRingOAL,$fn=Notches);
            translate([LockRingOAD/2 + LockRingOAL/2 - WallThick/2,0,LockRingOAL/2])
                cube([LockRingOAL + WallThick,2*WallThick + Kerf,LockRingOAL],center=true);
      rotate(SensorAngle)
                translate([LockRingOAD/2 + SensorBase - WallThick/2,0,LockRingOAL/2])
                    cube([2*SensorBase + WallThick,2*WallThick,LockRingOAL],center=true);
        }
        translate([0,0,LockRingLen[0]])
            PolyCyl(LockRingDia[1],LockRingOAL,Notches);
        translate([0,0,-Protrusion])
            PolyCyl(LockRingDia[0],2*LockRingOAL,Notches);

        translate([LockRingDia[0],0,0])
            cube([2*LockRingDia[0],Kerf,4*LockRingOAL],center=true);
        translate([LockRingOAD/2 + LockRingOAL/2,2*WallThick,LockRingOAL/2])
            rotate([90,0,0])
                PolyCyl(3.0,4*WallThick,6);

        rotate(SensorAngle)
            translate([LockRingOAD/2 + 2*SensorBase - SensorBase/2,2*WallThick,LockRingOAL/2])
                rotate([90,0,0])
                    PolyCyl(3.0,4*WallThick,6);
    }

}

Bafang USB Programming Adapter

Changing (“programming”) the Bafang BBS02 motor controller parameters requires a USB-to-serial adapter with a connector matching the end of the cable from the motor to the display. While you can buy such things directly from the usual randomly named Amazon sellers, I happen to have a wide variety of bare adapter boards, so I just bought a display extender cable and cut it in half to get the connector; you can apparently buy pigtailed connectors (for more than the price of an extender) if you dislike cutting cables in half.

Various documents provide versions of the canonical illustration of the motor end of the display cable, as ripped from Penoff’s original documentation:

Bafang BBS02 display cable pinout
Bafang BBS02 display cable pinout

The pin colors correspond to the wiring inside the motor cable, but the extender uses different colors, because nobody will ever know:

Bafang programmer - wire colors
Bafang programmer – wire colors

A bit of work with a continuity meter gave the pinout:

Bafang BBS02 display extender - wire colors
Bafang BBS02 display extender – wire colors

Don’t trust stuff you read on the Intertubes: make your own measurements and draw your own diagrams!

You want the cable end carrying the sockets to mate with the pins on the motor cable (coming in from the left):

Bafang programmer - cable ends
Bafang programmer – cable ends

Soldering the cable to a known-counterfeit FTDI USB adapter went swimmingly:

Bafang programmer - USB adapter wiring
Bafang programmer – USB adapter wiring

Note that the yellow-blue connection carries the full 48 V from the battery and may or may not have any current limiting / fusing / protection, so be a little more careful than usual in your wiring layout.

The red jumper from DTR to CTS, shown in all the Amazon and eBay listIngs, turns out to be unnecessary.

A quick and dirty case (eventually held together with generous hot-melt glue blobs) protects the PCB and armors the cables:

Bafang USB-serial adapter interior
Bafang USB-serial adapter interior

The solid model over on the right looks about like you’d expect:

Bafang Battery Mount - complete build view
Bafang Battery Mount – complete build view

Most of the instructions will tell you to hot-plug the cable to the motor with the battery connected, which strikes me as foolhardy; not all of those pins make contact in the right order, which means you will slap 50-odd volts across the wrong parts of the circuitry.

Instead:

  • Disconnect the battery
  • Unplug the display
  • Plug the adapter cable into the motor connector
  • Plug the USB cable into the Token Windows Laptop
  • Reconnect the battery
  • Fire up the “programming” routine
  • Send the new configuration to the motor controller
  • Disconnect the battery
  • Unplug the adapter cable
  • Reconnect the display cable
  • Reconnect the battery

Makes more sense to me, even if it’s more tedious.

Tuck this OpenSCAD source code for the case into the original program that produces the battery mounts:

Layout = "Build";               // [Frame,Block,Show,Build,Bushing,Cateye,Case]

… snippage …

// Programming cable case

ProgCavity = [70.0,19.0,10.0];
ProgBlock = [85.0,25.0,15.0];
ProgCableOD = 4.0;

module ProgrammerCase() {

    difference() {
        hull() {
            for (i=[-1,1], j=[-1,1])
                translate([i*(ProgBlock.x/2 - CornerRadius),j*i*(ProgBlock.y/2 - CornerRadius),-ProgBlock.z/2])
                    cylinder(r=CornerRadius,h=ProgBlock.z,$fn=12);
            }
        translate([-ProgBlock.x,0,0])
            rotate([0,90,0])
                PolyCyl(ProgCableOD,3*ProgBlock.x,6);
        cube(ProgCavity,center=true);
    }
}

// Half case sections for printing

module HalfCase(Section = "Upper") {

    intersection() {
       translate([0,0,ProgBlock.z/4])
            cube([2*ProgBlock.x,2*ProgBlock.y,ProgBlock.z/2],center=true);
        if (Section == "Upper")
            translate([0,0,-Kerf/2])
                ProgrammerCase();
        else
            translate([0,0,ProgBlock.z/2])
                ProgrammerCase();
    }
}

… snippage …

// tuck this into the Build conditional

    translate([0,3*Block.x,0]) {

        translate([gap*ProgBlock.x/2,0,ProgBlock.z/2])
            rotate([180,0,0])
                HalfCase("Upper");
        translate([-gap*ProgBlock.x/2,0,0])
            HalfCase("Lower");

Sherline CNC Driver Step Pulse Width Puzzle

Long long ago, as part of tidying up the power distribution inside the Sherline CNC controller PCB, I wrote a cleanroom reimplementation of its PIC firmware and settled on a 25 µs Step pulse width with a minimum 50 µs period:

[PARPORT]
ADDRESS = 0x378
RESET_TIME = 10000
STEPLEN = 25000
STEPSPACE = 25000
DIRSETUP = 50000
DIRHOLD = 50000

Even shorter values for the Direction signal worked with the initial pncconf setup for the Mesa 5I25 FPGA card:

DIRSETUP   = 25000
DIRHOLD    = 25000
STEPLEN    = 25000
STEPSPACE  = 25000

After thrashing through enough of the Kicad-to-HAL converter to get a HAL file sufficiently tasty to prevent LinuxCNC from spitting it out, the X and A axes moved with a gritty sound and the two other axes were pretty much inert.

After eliminating everything else, including having Tiny Scope™ confirm the pulses were exactly the right duration, I increased them by 10 µs:

DIRSETUP   = 35000
DIRHOLD    = 35000
STEPLEN    = 35000
STEPSPACE  = 35000

After which, all the axes suddenly worked perfectly.

At some point along the way, I (re)discovered that Sherline Step pulses are active-low, although in practical terms getting the pulse upside-down just delays the active edge by its width. Given that the Sherline’s top speed is 24 inch/min = 0.4 inch/s, the minimum step period is 156 µs and even a wrong-polarity step should work fine.

For the record, here’s a perfectly good Step pulse:

Mesa 5I25 35us active-low Step pulse
Mesa 5I25 35us active-low Step pulse

Gotta wipe off that screen more often …