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: Improvements

Making the world a better place, one piece at a time

  • Seedling Shelter Frame Deployment

    Seedling Shelter Frame Deployment

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

    Seedling Mesh Shelter - installed
    Seedling Mesh Shelter – installed

    Those will become the next round of lunchtime sandwiches:

    Turkey Sandwich with Excessive Lettuce
    Turkey Sandwich with Excessive Lettuce

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

  • Tour Easy: Amber Running Light

    Tour Easy: Amber Running Light

    Having seen a few bikes with amber “headlights” and being desirous of reducing the number of batteries on Mary’s bike, this seems like an obvious first step:

    Fairing Mounted Side Marker - First Light
    Fairing Mounted Side Marker – First Light

    It descends from the fairing flashlight mount with an entry to suit a 15 mm truck side marker body:

    LightBodies = [
      ["AnkerLC90",26.6,48.0],
      ["AnkerLC40",26.6,55.0],
      ["J5TactV2",25.0,30.0],
      ["InnovaX5",22.0,55.0],
      ["Sidemarker",15.0,20.0],
      ["Laser",10.0,30.0],
    ];
    

    The rest of the code gets a few cleanups you’d expect when you compile code untouched for a few years using the latest OpenSCAD.

    The markers are allegedly DOT rated, which matters not for my use case: SAEP2PCDOT.

    The mount is grossly overqualified for a wide-beam light with little need for aiming:

    Fairing Mounted Side Marker - test light
    Fairing Mounted Side Marker – test light

    Eventually, the marker should slip into a prealigned cylindrical holder, with a dab of epoxy to keep it there.

    The lights are a buck apiece, so there’s no reason to form a deep emotional attachment. They are the usual poorly molded and badly assembled crap, although the next step up from a nominally reputable supplier is a factor of five more expensive.

    It’s generated for the left side of the fairing, although I think having a pair of them would improve conspicuity:

    Fairing Mounted Side Marker - installed
    Fairing Mounted Side Marker – installed

    Being automotive, it runs from a 12 V supply, which comes from a boost converter driven by the Bafang 6 V headlight output. The absurdity of bucking a 48 V lithium battery to a 6V switched headlight output, then boosting it to 12 V to drive a single amber LED with a 1.5 V forward drop does not escape me.

    It’s possible to slice the lens off (using a lathe), remove / replace the resistor, then glue it back together, which would be worthwhile if you were intending to drive it from, say, an Arduino-ish microcontroller to get a unique blink pattern.

    Given the overall lack of build quality, it might make more sense to slap a condenser lens in front of a Piranha LED.

    Bonus: contrary to what you (well, I) might expect, the black lead is positive and the white lead is negative.

  • Bafang BBS02: Assist Power Levels

    Bafang BBS02: Assist Power Levels

    Although Gee’s Terry Symmetry is sized for female bodies, I managed to ride it up and down the driveway while watching the power display:

    Voltage52.5
    Rated Current24
    Max current18
    PowerPower
    PASAssistAmpCalcObservedRatio
    00%0.000~
    14%0.7382669%
    26%1.1575292%
    39%1.6857892%
    413%2.312310485%
    520%3.618918296%
    630%5.428425891%
    750%9.047345396%
    885%15.380367584%
    9100%18.094590095%
    Bafang BBS02 on Terry Symmetry – actual voltage

    The variations in the last column suggest my data-taking is … wobbly, at best.

    I think the displayed power does not come from actual current and voltage measurements, because recalculating the power using the nominal 48 V battery value produces an unnatural agreement:

    Voltage48
    Rated Current24
    Max current18
    PowerPower
    PASAssistAmpCalcObservedRatio
    00%0.000~
    14%0.7352675%
    26%1.15252100%
    39%1.67878100%
    413%2.311210493%
    520%3.6173182105%
    630%5.4259258100%
    750%9.0432453105%
    885%15.373467592%
    9100%18.0864900104%
    Bafang BBS02 on Terry Symmetry – nominal voltage

    The motor controller may measure the actual winding currents while generating the BLDC waveforms, but the values may not be available to the display at the end of the cable. If Bafang documented the commands & responses, we’d know for sure, but they don’t.

    Those assist values come from Mary’s Tour Easy, a much heavier bike than the Symmetry, but the first few levels work well in my limited tests. The highest levels may be too peppy for Gee’s normal routes, but having some serious boost in reserve can defang (hah) the worst hills.

    Terry Symmetry - Tour Easy
    Terry Symmetry – Tour Easy

    IMO, the bike would burn rubber at the motor’s full 24 A current …

  • Bafang BBS02: Terry Symmetry Shift Sensor & Cable Guides

    Bafang BBS02: Terry Symmetry Shift Sensor & Cable Guides

    The Bafang BBS02 came with (because I added it to the order) what looks like a genuine shift (“gear”) sensor made by the original company in the Czech Republic:

    Terry Bafang - shift sensor - installed
    Terry Bafang – shift sensor – installed

    On a typical bike, it mounts against a cable stop with the cable housing holding it in place against its other end:

    Tour Easy Bafang BBS02 - shift sensor - installed
    Tour Easy Bafang BBS02 – shift sensor – installed

    The Terry Symmetry has only two lengths of housing: in front of the adjuster on the downtube and behind the stop brazed to the chainstay. In either position, the sensor would move as the shift cable flexed and (IMO) put unreasonable stress on the electrical cable running to the motor.

    Yes, the Tour Easy has those same two lengths of housing, but the forward one joins a sheaf of wires & cables that barely moves.

    Fortunately, the sensor fits neatly between stations 1 and 2 along the downtube, with a snippet of PTFE lIned housing holding it firmly in place, with the 3D printed battery mounting blocks including paths for both cables:

    Terry - Bafang battery - all stations - solid model
    Terry – Bafang battery – all stations – solid model

    The shift cable originally ran from the adjuster in the front to the guide under the bottom bracket along a slightly diagonal path I could not possibly match. Instead, the path is now parallel to the downtube from the front adjuster:

    Terry Bafang - OEM shift stop
    Terry Bafang – OEM shift stop

    .. to the rear block, where it angles downward over the motor to the bottom bracket:

    Terry Bafang - shift cable clearance
    Terry Bafang – shift cable clearance

    The front block at station 1 has a Delrin / acetal bushing to align the cable with the rest of the blocks:

    Terry shift guide - acetal installed
    Terry shift guide – acetal installed

    Yes, it’s a round peg jammed in a hexagonal hole:

    Terry shift guide - acetal hole
    Terry shift guide – acetal hole

    Turning it from stock is well within the capabilities of Tiny Lathe™:

    Terry shift guide - acetal cutoff
    Terry shift guide – acetal cutoff

    For great slippery, a similar UHMW PE bushing supports the cable bend at the rear of the station 4 block:

    Terry shift guide - UHMWPE installed
    Terry shift guide – UHMWPE installed

    The Basement Laboratory Warehouse Wing disgorged an overly large rod taxing Tiny Lathe™ to its limit:

    Terry shift guide - UHMWPE turning
    Terry shift guide – UHMWPE turning

    Memo to Self: next time, just saw off a stub and move on.

  • Bafang BBS02: Motor Reaction Spacer

    Bafang BBS02: Motor Reaction Spacer

    The Terry Symmetry’s rear shift cable passes along the side of the downtube and through a plastic guide channel under the bottom bracket shell. The Bafang BBS02 motor must press against the bottom of the downtube, so the shift cable rubs against the top of the motor.

    The solution is a small block shaped around the point of contact to cradle the downtube, the bottom bracket shell lug, and the motor case:

    Terry - Bafang motor spacer - solid model
    Terry – Bafang motor spacer – solid model

    A strip of double-sided foam tape holds the block to the motor and the reaction force from the motor’s torque presses the block against the downtube:

    Terry Bafang - motor reaction block
    Terry Bafang – motor reaction block

    Seen from the other side, looking parallel to the shift cable, you can see the tight clearance:

    Terry Bafang - shift cable clearance
    Terry Bafang – shift cable clearance

    The block holds the motor 8 mm from the downtube, just enough to give the cable some breathing room.

    The block is slightly taller on its front end, because the motor doesn’t meet the downtube at a right angle:

    Terry - Bafang motor spacer - tube angle - solid model
    Terry – Bafang motor spacer – tube angle – solid model

    I determined the proper angle by taping waxed paper to the top of the motor, sticking a trial (non-angled) block to the downtube, coating its bottom surface with hot-melt glue, then squishing the motor against the block. The cooled glue was flush with the block on the rear and 1.8 mm thick on the front, a 5° angle over the 20 mm block.

    Definitely easier than correctly figuring the geometry from first principles: tweak the model to include the measured thickness, compute the angle, tilt the tube, and print another block that fits like it grew there.

    With the block in place and the motor held against the downtube, tighten the retaining nut against the “fixing plate” by giving it a few gentle whacks with a hammer, then tighten the jam nut.

    The OpenSCAD source code snippet:

    // Motor Reaction Block
    // Holds motor away from downtube enough to miss rear shift wire
    
    MotorOD = 111;              // motor frame dia
    MotorMountRad = 85;         // BB spindle center to motor center
    Space = 8.0;                // motor to frame space
    
    Spacer = [20.0,DownTube[ID]/2,4*Space];
    SpaceAngle = atan(1.8/Spacer.x);            // tilt due to non-right-angle meeting
    echo(str("Spacer angle: ",SpaceAngle));
    
    module MotorSpacer() {
    
        difference() {
            cube(Spacer,center=true);
            translate([0,0,DownTube[ID]/2])
                rotate([0,90 + SpaceAngle,0]) rotate(180/FrameSides)
                    cylinder(d=DownTube[ID],h=DownTube[LENGTH],$fn=FrameSides,center=true);
            translate([DownTube[LENGTH]/2,0,DownTube[ID]/2 - DownTube[LENGTH]*sin(SpaceAngle)/2])       // concentric with ID
                rotate([0,90 + SpaceAngle,0]) rotate(180/FrameSides)
                    cylinder(d=DownTube[OD],h=DownTube[LENGTH],$fn=FrameSides,center=true);
            translate([0,0,-(MotorOD/2 + Space)])
                rotate([90,0,0]) rotate(180/48)
                    cylinder(d=MotorOD,h=2*Spacer.y,$fn=48,center=true);
        }
    
    }
    

    Mary’s Tour Easy didn’t need this block, because all the cables run elsewhere, but I did capture a piece of closed-cell foam between its vestigial downtube and the motor to prevent chafing.

  • Bafang BBS02: Terry Symmetry Battery Mount Tee Nuts

    Bafang BBS02: Terry Symmetry Battery Mount Tee Nuts

    The two middle mounting blocks under the Bafang battery plate have 5 mm holes for the screws going into the water bottle studs brazed to the frame. The outer blocks clamp around the frame and it seemed like a good idea to secure the plate to them, as well:

    Terry Bafang battery mount - trial installation
    Terry Bafang battery mount – trial installation

    Those two blocks have a recess for an M5 tee nut:

    Terry - Bafang battery - station 2 - bottom view - solid model
    Terry – Bafang battery – station 2 – bottom view – solid model

    I snipped off the prongs with hardened diagonal cutters and filed the stubs to a uniform height:

    Modified M5 tee nuts
    Modified M5 tee nuts

    Applying a hot soldering gun to the plate melted the stubs into the block:

    Terry Bafang battery mount - tee nut in place
    Terry Bafang battery mount – tee nut in place

    An M5 screw with a wingnut atop a big washer kept the tee nut properly aligned while pulling it into the melty plastic; I was pleasantly surprised at the lack of drama.

    A ring of JB Plastic Bonder urethane adhesive glued the nut to the block:

    Terry Bafang battery mount - tee nut gluing
    Terry Bafang battery mount – tee nut gluing

    The adhesive starts out runny and flows under the nut, so there’s more surface in play than meets the eye.

    Clamping the partially cured goo to the frame atop a layer of waxed paper squashes any protruding adhesive lumps flat and prevents them from marring the tube’s paint:

    Terry Bafang battery mount - tee nut adhesive molding
    Terry Bafang battery mount – tee nut adhesive molding

    Anchoring the battery to the bike at four spots makes it utterly immovable, which seems like a good way to ensure longevity:

    Bafang BBS02 - Terry Symmetry full assembly
    Bafang BBS02 – Terry Symmetry full assembly

    That’s a test assembly predating the cable management cleanup …

  • Bafang BBS02: Terry Symmetry Battery Mount

    Bafang BBS02: Terry Symmetry Battery Mount

    The Bafang 48 V 11.6 A·h battery for Gee’s Terry Symmetry mounts on the downtube:

    Bafang BBS02 - Terry Symmetry full assembly
    Bafang BBS02 – Terry Symmetry full assembly

    The battery slides onto a plate screwed to the pair of water bottle studs brazed to the tube:

    Terry Bafang battery mount plate - test install
    Terry Bafang battery mount plate – test install

    Water bottle studs are (nominally) 65 mm on center. One stud normally appears under the plate’s center hole, with the other stud under either the upper or lower slot, depending on whether the battery fits better mounted lower or higher on the downtube.

    However, the Symmetry’s downtube is so short the plate must mount with the lowest slot matching the uppermost stud, putting the lower stud beneath the metal compartment with its complete lack of mounting holes.

    Well, I can fix that:

    Terry Bafang battery mount - internal modifications
    Terry Bafang battery mount – internal modifications

    The upper hole in the metal base is 65 mm from the middle of the lower slot in the plastic baseplate, which will be (approximately) centered on the upper stud inside the black plastic mount. The location of that hole is not a free variable: it requires measuring and marking from the slot with the battery plate assembled.

    The lower hole in the base puts the bottom of its plastic mount just about even with the end of the plate.

    I shortened the battery side of the cable, crimped on (genuine!) 45 A Powerpole pins, and shaped the wiring to put the connector inside the metal compartment, out of harm’s way, and shielded from the weather.

    The small bar of white HDPE serves as a cable clamp, held by a pair of M3 BHCS in the conveniently tapped holes.

    With all that settled, the final iteration of the 3D printed mounting blocks took shape:

    Terry - Bafang battery - all stations - solid model
    Terry – Bafang battery – all stations – solid model

    A station number from 1 through 4 identifies the blocks (station 0 is the blank block shape) and, of course, they’re all different. I refactored the OpenSCAD code used for Mary’s Tour Easy to put the feature selection into vectors, rather than convoluted logic:

    Latches = [false,true,true,false,false];                // clearance for battery latch clips
    Notch = [false,true,true,false,false];                  // notch for battery screw pockets
    Recess = ["None","TeeNut","Bottle","Bottle","TeeNut"];  // stud or nut clearance against frame
    
    HarnessCable = [false,true,true,true,true];             // passage for main harness cable
    
    ShiftWire = [false,true,true,true,true];                //  .. shifter wire through sensor
    Ferrules = ["None","Both","Front","None","Back"];       // ferrule and bushing ssockets
    
    GearCable = [false,false,true,true,true];               //  .. gear sensor cable
    
    

    Producing the features for a specific block is now a straightforward series of obvious choices. For example, adding the channels to clear the battery latches at stations 1 and 2 looks like this:

            if (Latches[BlkNum])
                for (i=[-1,1])
                    translate([0,i*LatchOC/2,BlockMaxZ - LatchThick/2 + Protrusion])
                        cube([BossSlotOAL,LatchWidth,LatchThick + Protrusion],center=true);
    
    

    Both parts of the block show the station number to avoid mixups:

    Terry - Bafang battery - station 2 - solid model
    Terry – Bafang battery – station 2 – solid model

    Each block requires a bit under three hours of printing time, so they’re produced singly:

    Terry - Bafang battery - station 2 build - solid model
    Terry – Bafang battery – station 2 build – solid model

    Building them sideways produces the best surface finish in all the recesses and holes. Small support structures under the rounded corners make them look Good Enough™ for their purpose.

    A test assembly:

    Terry Bafang battery mount - trial installation
    Terry Bafang battery mount – trial installation

    The two middle blocks (stations 3 and 2) sit at the water bottle studs. The rightmost block (station 1) is 130 mm from station 2, with the Bafang gear sensor on the rear derailleur cable.

    An aluminum plate spreads the clamping force from the M4 screws across the bottom, as seen here below the cable stop cap holding the harness cable:

    Terry Bafang - shift stop cap
    Terry Bafang – shift stop cap

    Those 50 mm screws are too long; a soon-to-arrive bag of 45 mm screws should fit perfectly. The final assembly will use nyloc nuts so they won’t vibrate loose.

    The OpenSCAD source code for all the pieces as a GitHub Gist:

    // Terry Symmetry – Bafang e-bike conversion
    // Ed Nisley KE4ZNU 2021-06
    Layout = "BuildClip"; // [Frame,Block,AllBlocks,BuildBlock,DispMount,BrakeMagnet,ShiftCap,BuildShiftCap,Case,NutMold,HeadClip, BuildClip]
    Station = 4; // [0:4]
    Support = false;
    //- 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
    // Bike frame lies along X axis, rear to +X
    FrameTube = [400,28.9 + HoleWindage,28.9 + HoleWindage]; // X = longer than anything else
    FrameSides = 24;
    SpeedOD = 3.5; // speed sensor cable
    PowerOD = 6.7; // power cable
    Harness = [6.0,13.0,30.0]; // main motor-to-handlebar cable
    GearOD = 3.0; // gear sensor cable
    HandlebarMax = 1*inch; // middle handlebar diameter
    HandlebarMin = 24.0; // .. tape section
    HeadTube = [32.0,35.0,8.0]; // ID=tube OD=lug LENGTH=clear between lugs
    BottleStud = [5.0,10.0,IntegerMultiple(1.2,ThreadThick)]; // frame fitting for bottle screws
    BafangClampID = 22.3; // their handlebar clamp diameter
    ShiftOD = 2.0; // rear shifter cable
    ShiftFerrule = [ShiftOD,6.0,10.0];
    ShiftOffset = 7.5; // .. from downtube
    ShiftAngle = -20; // .. from midline
    BatteryBoss = [5.5,16.0,2.5]; // battery mount boss, center boss is round
    BossSlotOAL = 32.0; // .. end bosses are elongated
    BossOC = 65.0; // .. along length of mount
    LatchWidth = 10.0; // battery latches to mount plate
    LatchThick = 1.5;
    LatchOC = 56.0;
    // Per-block features
    // first element is unadorned block
    Latches = [false,true,true,false,false]; // clearance for battery latch clips
    Notch = [false,true,true,false,false]; // notch for battery screw pockets
    Recess = ["None","TeeNut","Bottle","Bottle","TeeNut"]; // stud or nut clearance against frame
    HarnessCable = [false,true,true,true,true]; // passage for main harness cable
    ShiftWire = [false,true,true,true,true]; // .. shifter wire through sensor
    Ferrules = ["None","Both","Front","None","Back"]; // ferrule and bushing ssockets
    GearCable = [false,false,true,true,true]; // .. gear sensor cable
    // M3 SHCS nyloc nut
    Screw3 = [3.0,5.5,35.0]; // OD, LENGTH = head
    Washer3 = [3.7,7.0,0.7];
    Nut3 = [3.0,6.0,4.0];
    // M4 SHCS nyloc nut
    Screw4 = [4.0,7.0,4.0]; // OD, LENGTH = head
    Washer4 = [4.2,8.9,1.0];
    Nut4 = [4.0,7.8,5.0];
    // M5 SHCS nyloc nut
    Screw5 = [5.0,8.5,5.0]; // OD, LENGTH = head
    Washer5 = [5.5,10.1,1.0];
    Nut5 = [5.0,9.0,5.0];
    Teenut5 = [6.5,17.0,8.0,2.0]; // OD, LENGTH+1 = flange
    // 10-32 Philips nyloc nut
    Screw10 = [5.2,9.8,3.6]; // OD, LENGTH = head
    Washer10 = [5.5,11.0,1.0];
    Nut10 = [5.2,10.7,6.2];
    CableTie = [150,5.0,2.0];
    WallThick = 4.0; // thinnest wall
    BlockMinZ = -(FrameTube.z/2 + WallThick);
    BlockMaxZ = FrameTube.z/2 + max(WallThick,Teenut5[LENGTH]) + BatteryBoss[LENGTH];
    Block = [25.0,78.0,BlockMaxZ – BlockMinZ]; // Y = battery width
    echo(str("Block: ",Block));
    Kerf = 0.5; // cut through middle to apply compression
    CornerRadius = 5.0;
    EmbossDepth = 2*ThreadThick; // lettering depth
    //———————-
    // 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);
    }
    // frame downtube
    module Frame() {
    rotate([0,90,0]) rotate(180/FrameSides)
    cylinder(d=FrameTube.z,h=FrameTube.x,center=true,$fn=FrameSides);
    }
    // clamp overall shape
    module ClampBlock(BlkNum = 1) {
    Screw = Screw4;
    Washer = Washer4;
    Nut = Nut4;
    ScrewOC = LatchOC;
    ScrewSides = 8;
    ScrewOrient = 180/ScrewSides;
    ScrewRecess = LatchThick + Screw[LENGTH] + Washer[LENGTH] + 1.0;
    echo(str("Screw length: ",Block.z – ScrewRecess));
    difference() {
    hull()
    for (i=[-1,1], j=[-1,1])
    translate([i*(Block.x/2 – CornerRadius),j*(Block.y/2 – CornerRadius),BlockMinZ])
    cylinder(r=CornerRadius,h=Block.z,$fn=4*3);
    cube([2*Block.x,2*Block.y,Kerf],center=true);
    Frame();
    for (j=[-1,1]) {
    translate([0,j*ScrewOC/2,BlockMinZ – Protrusion])
    rotate(ScrewOrient)
    PolyCyl(Screw[ID],2*Block.z,ScrewSides);
    translate([0,j*ScrewOC/2,BlockMaxZ – ScrewRecess])
    rotate(ScrewOrient)
    PolyCyl(Washer[OD],BlockMaxZ,ScrewSides);
    }
    if (Latches[BlkNum])
    for (i=[-1,1])
    translate([0,i*LatchOC/2,BlockMaxZ – LatchThick/2 + Protrusion])
    cube([BossSlotOAL,LatchWidth,LatchThick + Protrusion],center=true);
    if (Notch[BlkNum])
    translate([0,0,BlockMaxZ – BatteryBoss[LENGTH]/2 + Protrusion])
    cube([BossSlotOAL,BatteryBoss[OD],BatteryBoss[LENGTH] + Protrusion],center=true);
    if (HarnessCable[BlkNum])
    rotate([-155,0,0]) {
    translate([0,FrameTube.y/2 – Harness[ID]/2,0])
    cube([2*Block.x,2*Harness[ID],Harness[ID]],center=true);
    translate([0,FrameTube.y/2 + Harness[ID]/2,0])
    rotate([0,90,0])
    translate([0,0,-Block.x])
    rotate(180/6)
    PolyCyl(Harness[ID],2*Block.x,6);
    }
    if (GearCable[BlkNum])
    rotate([-45,0,0]) {
    translate([0,FrameTube.y/2 – GearOD/2,0])
    cube([2*Block.x,2*GearOD,GearOD],center=true);
    translate([0,FrameTube.y/2 + GearOD/2,0])
    rotate([0,90,0])
    translate([0,0,-Block.x])
    rotate(180/6)
    PolyCyl(GearOD,2*Block.x,6);
    }
    rotate([ShiftAngle,0,0]) {
    if (ShiftWire[BlkNum])
    translate([-Block.x,FrameTube.y/2 + ShiftOffset,0])
    rotate([0,90,0]) rotate(-(90 + ShiftAngle))
    PolyCyl(ShiftOD,2*Block.x,6);
    if (Ferrules[BlkNum] == "Back" || Ferrules[BlkNum] == "Both") {
    i = 1;
    translate([i*(Block.x/2 – ShiftFerrule[LENGTH]),FrameTube.y/2 + ShiftOffset,0])
    rotate([0,i*90,0]) rotate(-i*(90 + ShiftAngle))
    PolyCyl(ShiftFerrule[OD],Block.x,6);
    }
    if (Ferrules[BlkNum] == "Front" || Ferrules[BlkNum] == "Both") {
    i = -1;
    translate([i*(Block.x/2 – ShiftFerrule[LENGTH]),FrameTube.y/2 + ShiftOffset,0])
    rotate([0,i*90,0]) rotate(-i*(90 + ShiftAngle))
    PolyCyl(ShiftFerrule[OD],Block.x,6);
    }
    }
    if (Recess[BlkNum] == "Bottle") {
    rotate(ScrewOrient) {
    PolyCyl(BottleStud[ID],2*Block.z,ScrewSides);
    PolyCyl(BottleStud[OD],FrameTube.z/2 + BottleStud[LENGTH],ScrewSides);
    }
    }
    else if (Recess[BlkNum] == "TeeNut") {
    rotate(ScrewOrient) {
    PolyCyl(Teenut5[ID],2*Block.z,ScrewSides);
    PolyCyl(Teenut5[OD],FrameTube.z/2 + Teenut5[LENGTH+1],ScrewSides);
    }
    }
    translate([0,15,BlockMaxZ – EmbossDepth/2 + Protrusion])
    cube([9.0,8,EmbossDepth],center=true);
    translate([0,17,BlockMinZ + EmbossDepth/2 – Protrusion])
    cube([9.0,8,EmbossDepth],center=true);
    translate([0,-5,BlockMinZ + EmbossDepth/2 – Protrusion])
    cube([9.0,30,EmbossDepth],center=true);
    }
    translate([0,15,BlockMaxZ – EmbossDepth])
    linear_extrude(height=EmbossDepth)
    rotate(90)
    text(text=str(BlkNum),size=5,spacing=1.00,font="Bitstream Vera Sans:style=Bold",
    halign="center",valign="center");
    translate([0,17,BlockMinZ])
    linear_extrude(height=EmbossDepth)
    rotate(-90) mirror([0,1,0])
    text(text=str(BlkNum),size=4.5,spacing=1.00,font="Bitstream Vera Sans:style=Bold",
    halign="center",valign="center");
    translate([0,-5,BlockMinZ])
    linear_extrude(height=EmbossDepth)
    rotate(-90) mirror([0,1,0])
    text(text="KE4ZNU",size=4.5,spacing=1.00,font="Bitstream Vera Sans:style=Bold",
    halign="center",valign="center");
    }
    // complete clamp block
    module Clamp(BlkNum = 1) {
    ClampBlock(BlkNum);
    if (Support)
    color("Yellow") {
    NumRibs = 7;
    RibOC = Block.x/(NumRibs – 1);
    intersection() {
    translate([0,0,BlockMaxZ + Kerf/2])
    cube([2*Block.x,2*Block.y,Block.z],center=true);
    union() {
    translate([0,0,Kerf/2])
    cube([1.1*Block.x,FrameTube.y – 2*ThreadThick,4*ThreadThick],center=true);
    for (i=[-floor(NumRibs/2):floor(NumRibs/2)])
    translate([i*RibOC,0,0])
    rotate([0,90,0]) rotate(180/FrameSides)
    cylinder(d=FrameTube.z – 2*ThreadThick,h=2*ThreadWidth,$fn=FrameSides,center=true);
    /*
    translate([0,FrameTube.y/2 + PowerOD/2,Kerf/2])
    cube([1.1*Block.x,PowerOD – 2*ThreadWidth,4*ThreadThick],center=true);
    for (i=[-floor(NumRibs/2):floor(NumRibs/2)])
    translate([i*RibOC,FrameTube.y/2 + PowerOD/2,PowerOD/4])
    cube([2*ThreadWidth,PowerOD – 2*ThreadWidth,PowerOD/2 – 2*ThreadThick],center=true);
    translate([0,-(FrameTube.y/2 + SpeedOD/2),Kerf/2])
    cube([1.1*Block.x,SpeedOD – 2*ThreadWidth,4*ThreadThick],center=true);
    for (i=[-floor(NumRibs/2):floor(NumRibs/2)])
    translate([i*RibOC,-(FrameTube.y/2 + SpeedOD/2),SpeedOD/4])
    cube([2*ThreadWidth,SpeedOD – 2*ThreadWidth,SpeedOD/2 – 2*ThreadThick],center=true);
    */
    }
    }
    }
    }
    // Half clamp sections for printing
    module HalfClamp(BlkNum = 1, Section = "Upper") {
    render()
    if (Section == "Upper")
    intersection() {
    translate([0,0,BlockMaxZ/2])
    cube([1.1*Block.x,Block.y,BlockMaxZ],center=true);
    translate([0,0,-Kerf/2])
    Clamp(BlkNum);
    }
    else
    intersection() {
    translate([0,0,-BlockMinZ/2])
    cube([1.1*Block.x,Block.y,-BlockMinZ],center=true);
    translate([0,0,-BlockMinZ])
    Clamp(BlkNum);
    }
    }
    // Handlebar mount for controller
    module DispMount() {
    ClampRing = [HandlebarMax,HandlebarMax + 2*WallThick,10.0];
    ClampOffset = (HandlebarMax + BafangClampID)/2 + 6.0;
    DispStudLenth = 16.5;
    NumSides = 24;
    Tilt = 0*atan2((ClampRing[OD] – BafangClampID)/2,ClampOffset);
    echo(str("Tilt: ",Tilt));
    difference() {
    union() {
    hull() {
    cylinder(d=ClampRing[OD],h=ClampRing[LENGTH],$fn=NumSides);
    translate([0,ClampOffset,0])
    cylinder(d=BafangClampID,h=ClampRing[LENGTH],$fn=NumSides);
    }
    translate([0,ClampOffset,0])
    cylinder(d=BafangClampID,h=ClampRing[LENGTH] + DispStudLenth,$fn=NumSides);
    translate([-ClampRing[ID]/4,-(ClampRing[OD]/2),ClampRing[LENGTH]/2])
    rotate([0,90,0]) rotate(180/8)
    cylinder(d=ClampRing[LENGTH]/cos(180/8),h=ClampRing[ID]/2,$fn=8);
    }
    cube([Kerf,4*ClampOffset,4*DispStudLenth],center=true);
    translate([0,0,-Protrusion])
    cylinder(d=ClampRing[ID],h=ClampRing[LENGTH] + 2*Protrusion,$fn=NumSides);
    translate([-ClampRing[ID]/2,-(ClampRing[OD]/2),ClampRing[LENGTH]/2])
    rotate([0,90,0]) rotate(180/8)
    PolyCyl(Screw3[ID],ClampRing[ID],8);
    for (i=[-1,1])
    translate([i*ClampRing[ID]/4,-(ClampRing[OD]/2),ClampRing[LENGTH]/2])
    rotate([0,i*90,0]) rotate(180/8)
    PolyCyl(Washer3[OD],ClampRing[ID],$fn=8);
    translate([-5,25,EmbossDepth/2 – Protrusion/2])
    rotate(Tilt)
    cube([4.5,21.5,EmbossDepth + Protrusion],center=true);
    if (false)
    translate([-6,25,EmbossDepth/2 – Protrusion/2])
    rotate(-Tilt)
    cube([4.0,27,EmbossDepth + Protrusion],center=true);
    }
    translate([-5,25,0])
    linear_extrude(height=EmbossDepth)
    rotate(90 + Tilt) mirror([0,1,0])
    text(text="KE4ZNU",size=3.3,spacing=1.05,font="Bitstream Vera Sans:style=Bold",
    halign="center",valign="center");
    if (false)
    translate([-6,25,0])
    linear_extrude(height=EmbossDepth)
    rotate(90 – Tilt) mirror([0,1,0])
    text(text="softsolder.com",size=2.2,spacing=1.05,font="Bitstream Vera Sans:style=Bold",
    halign="center",valign="center");
    }
    // Mold to reshape speed sensor nut
    SensorNut = [0,14.4,13.0];
    SensorMold = [SensorNut[OD] + 2*WallThick,SensorNut[OD] + 2*WallThick,SensorNut[LENGTH] + WallThick];
    MoldSides = 20;
    RodOD = 1.6;
    module NutMoldBlock() {
    difference() {
    translate([0,0,SensorMold.z/2])
    cube(SensorMold,center=true);
    translate([0,0,WallThick])
    rotate(180/MoldSides)
    PolyCyl(SensorNut[OD],2*SensorNut[LENGTH],MoldSides);
    translate([0,0,-Protrusion])
    rotate(180/8)
    PolyCyl(SpeedOD,2*SensorMold.z,8);
    for (i=[-1,1])
    translate([i*(SensorMold.x/2 – WallThick/2),SensorMold.y,SensorMold.z/2])
    rotate([90,0,0])
    PolyCyl(RodOD,2*SensorMold.y,6);
    }
    }
    module NutMold() {
    gap = 1.0;
    for (j=[-1,1])
    translate([0,j*gap,0])
    intersection() {
    translate([0,j*SensorMold.y,0])
    cube(2*SensorMold,center=true);
    NutMoldBlock();
    }
    }
    // Brake sensor magnet mount
    // Magnetized through thinnest section
    module BrakeMagnet() {
    Magnet = [10.5,3.0,5.5];
    Plate = 2*ThreadThick;
    BrakeRad = 10.0; // brake handle curve Radius
    Holder = [2*BrakeRad,7.0,Magnet.z + Plate];
    difference() {
    intersection() {
    translate([0,-BrakeRad,0])
    rotate(180/24)
    cylinder(r=BrakeRad,h=Holder.z,$fn=24);
    translate([0,BrakeRad – Holder.y,Holder.z/2])
    cube([2*BrakeRad,2*BrakeRad,Holder.z],center=true);
    translate([0,0,-2*BrakeRad/sqrt(2) + Holder.z – 3.0 + BrakeRad])
    rotate([0,45,0])
    cube(2*[BrakeRad,2*BrakeRad,BrakeRad],center=true);
    }
    translate([0,Magnet.y/2 – Holder.y – Protrusion/2,Magnet.z/2 + Plate + Protrusion/2])
    cube(Magnet + [0,Protrusion,Protrusion],center=true);
    }
    }
    // Shift stud cap
    // With passage for harness cable
    CapBlock = [18,18,16.5];
    module ShiftCap() {
    Rounding = 3.5;
    CapM = 3.0;
    StudBase = [12.5,12.5,4.5];
    Stud = [5.0,9.3,15.5];
    difference() {
    hull() {
    translate([0,0,CapBlock.z – 0.5])
    PolyCyl(Washer5[OD],0.5,12);
    for (i=[-1,1], j=[-1,1])
    translate([i*(CapBlock.x/2 – Rounding),j*(CapBlock.y/2 – Rounding),0])
    sphere(r=Rounding,$fn=12);
    translate([-CapBlock.x/2,-Harness[ID]/2 – StudBase.y/2,StudBase.z/2])
    rotate([0,90,0])
    cylinder(d=Harness[ID] + 2*WallThick,h=CapBlock.x,$fn=12);
    }
    translate([0,0,-(FrameTube.z/2 – CapM)])
    Frame();
    PolyCyl(Screw5[ID],2*CapBlock.z,6);
    PolyCyl(Stud[OD],Stud[LENGTH],12);
    translate([0,0,StudBase.z/2])
    cube(StudBase,center=true);
    translate([0,-StudBase.y/2,StudBase.z/2])
    cube(StudBase + [0,-StudBase.y/2,0],center=true);
    translate([-CapBlock.x,-Harness[ID]/2 – StudBase.y/2,StudBase.z/2])
    rotate([0,90,0])
    cylinder(d=1.5*Harness[ID],h=2*CapBlock.x,$fn=12);
    }
    }
    // Head tube clip for harness cable joint
    module HeadClip() {
    CableOD = Harness[OD];
    difference() {
    linear_extrude(height=HeadTube[LENGTH],convexity=10)
    difference() {
    hull() {
    circle(d=HeadTube[ID] + 2*WallThick,$fn=FrameSides);
    translate([0,-(HeadTube[ID] + CableOD)/2])
    rotate(180/(FrameSides/2))
    circle(d=CableOD + 2*WallThick,$fn=FrameSides/2);
    }
    circle(d=HeadTube[ID] + HoleWindage,$fn=FrameSides);
    translate([0,-(HeadTube[ID] + CableOD)/2])
    rotate(180/(FrameSides/2))
    circle(d=CableOD + HoleWindage,$fn=FrameSides/2);
    translate([0,-HeadTube[ID]/2])
    square(0.75*CableOD,center=true);
    translate([0,HeadTube[ID]])
    square(2*HeadTube[ID],center=true);
    }
    translate([0,-(HeadTube[ID]/2 + CableOD + WallThick – CableTie.z/2),HeadTube[LENGTH]/2])
    cube([HeadTube[ID],CableTie.z,CableTie.y],center=true);
    for (i=[-1,1])
    translate([i*(HeadTube[ID]/2 + WallThick – CableTie.z/2),0,HeadTube[LENGTH]/2])
    cube([CableTie.z,HeadTube[ID],CableTie.y],center=true);
    }
    }
    // Programming cable case
    ProgCavity = [60.0,18.0,7.0];
    ProgBlock = [70.0,24.0,13.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);
    translate([0,0,ProgBlock.z/2 + ProgCavity.z/2 – EmbossDepth])
    cube(ProgCavity,center=true);
    translate([0,0,-(ProgBlock.z/2 + ProgCavity.z/2 – EmbossDepth)])
    cube(ProgCavity,center=true);
    }
    translate([0,4,ProgBlock.z/2 – EmbossDepth])
    linear_extrude(height=EmbossDepth)
    text(text="Bafang BBS02",
    size=5,spacing=1.00,font="Bitstream Vera Sans:style=Bold",
    halign="center",valign="center");
    translate([0,-4,ProgBlock.z/2 – EmbossDepth])
    linear_extrude(height=EmbossDepth)
    text(text="Programmer",
    size=5,spacing=1.00,font="Bitstream Vera Sans:style=Bold",
    halign="center",valign="center");
    translate([0,4,-ProgBlock.z/2])
    linear_extrude(height=EmbossDepth)
    mirror([1,0])
    text(text="Ed Nisley",
    size=5,spacing=1.00,font="Bitstream Vera Sans:style=Bold",
    halign="center",valign="center");
    translate([0,-4,-ProgBlock.z/2])
    linear_extrude(height=EmbossDepth)
    mirror([1,0])
    text(text="softsolder.com",
    size=5,spacing=1.00,font="Bitstream Vera Sans:style=Bold",
    halign="center",valign="center");
    }
    // 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")
    ProgrammerCase();
    else
    translate([0,0,ProgBlock.z/2])
    ProgrammerCase();
    }
    }
    //———-
    // Build them
    if (Layout == "Frame")
    Frame();
    if (Layout == "DispMount")
    DispMount();
    if (Layout == "BrakeMagnet")
    BrakeMagnet();
    if (Layout == "ShiftCap")
    ShiftCap();
    if (Layout == "HeadClip")
    HeadClip();
    if (Layout == "BuildClip")
    rotate([-90,0,0])
    HeadClip();
    if (Layout == "BuildShiftCap")
    translate([0,0,CapBlock.z])
    rotate([180,0,0])
    ShiftCap();
    if (Layout == "Case")
    ProgrammerCase();
    if (Layout == "NutMold")
    NutMold();
    if (Layout == "Upper" || Layout == "Lower")
    HalfClamp(Station,Layout);
    if (Layout == "Block") {
    ClampBlock(Station);
    if (false)
    color("Red", 0.3)
    Frame();
    }
    if (Layout == "AllBlocks") {
    gap = 3*Block.x;
    for (i=[0:4])
    translate([i*gap – 2*gap,0,0])
    Clamp(i);
    if (true)
    color("Red", 0.3)
    Frame();
    }
    if (Layout == "BuildBlock") {
    gap = 5.0;
    translate([gap,0,Block.x/2])
    rotate([0,90,0])
    HalfClamp(Station,"Upper");
    translate([-gap – Block.z/2,0,Block.x/2])
    rotate([0,90,0])
    HalfClamp(Station,"Lower");
    }