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.

Category: Recumbent Bicycling

Cruisin’ the streets

  • Planet Bike Superflash: Tour Easy Mount

    Having not yet gotten around to building better taillights for our bikes, we picked up some Planet Bike Superflash lights on sale. I don’t like single-LED lights, because the optics produce a concentrated beam (which is how they get such high lumen ratings) that’s essentially invisible anywhere off-axis; a taillight that requires careful alignment for maximum effect is a Bad Thing. But, eh, they were on sale…

    The graceful OEM seatpost mount, done in engineering plastic with smooth curves and something of a reputation for fragility, doesn’t work on a recumbent, so I build a butt-ugly mount that should last forever. It clamps firmly around a length of grippy silicone tape on the top seat frame rail:

    Superflash on Tour Easy
    Superflash on Tour Easy

    The reviews also complain that normal road vibrations transmitted through the somewhat whippy OEM mount pop the case apart, depositing the lens and electronics on the road behind you. Hence the black tape across the case joint.

    Here’s the whole affair on the bench:

    Superflash on mount
    Superflash on mount

    The weird color line comes from white plastic left in the extruder that covers the bottom layer or two of each part. I’m not fussy about the first pass of any new gadget, because I know I’ll build at least one more to get everything right.

    This is the first build arrangement; note the huge white teardrop blob at the start of the Skirt outline on the left. Obviously I didn’t have the initial retraction under control:

    Superflash mount on build platform
    Superflash mount on build platform

    The screw recesses built over the plate and got cute little support spiders to keep their interiors from sagging:

    Superflash mount - bolt support
    Superflash mount – bolt support

    After doing it that way, I flipped the top piece over so it builds with the screw head recesses upward to get a better finish on those nice curves. That means the arch needs support, which almost worked, although some of the fins fell over:

    Superflash mount - failed arch support
    Superflash mount – failed arch support

    The solid model now adds a two-layer-thick flat plate joining the fins that should hold them firmly to the build plate.

    Clamp Support - Solid Model
    Clamp Support – Solid Model

    I also added an option to build the flash mounting shoe separately:

    Superflash mount - solid model
    Superflash mount – solid model

    That gives better control over the flange thickness, which turns out to be critical parameter requiring a bit of adjustment with a file in the first version. Of course, the shoe needs an alignment pin and another assembly step to glue it in place:

    Superflash mount - gluing shoe
    Superflash mount – gluing shoe

    A 4-40 setscrew jams into the latch recess in the Superflash case, thus preventing it from walking off the shoe. You don’t need any particular pressure here, just enough protrusion to engage the case:

    Superflash mount - setscrew
    Superflash mount – setscrew

    The first pass at hex nut recesses were exactly cos(30) too large, as I forgot my Useful Sizes file has the across-the-points diameter, so I added a dab of epoxy to each recess before gluing the halves together with solvent:

    Superflash mount - glue clamping
    Superflash mount – glue clamping

    And then it’s all good.

    The OpenSCAD source code:

    // Planet Bike Superflash mount for Tour Easy seatback
    // Ed Nisley KE4ZNU - Dec 2011
    
    Layout = "Show";            // Assembly: Show
                                // Parts: Clamp Base Shoe Mount
                                // Build Plate: Build
    
    SeparateShoe = true;        // true = print mounting shoe separately
                                // false = join shoe to Mount block
    
    Support = true;             // true = include support
    
    Gap = 8;                    // between "Show" objects
    
    include </home/ed/Thing-O-Matic/lib/MCAD/units.scad>
    include </home/ed/Thing-O-Matic/Useful Sizes.scad>
    include </home/ed/Thing-O-Matic/lib/visibone_colors.scad>
    
    //-------
    //- Extrusion parameters must match reality!
    //  Print with +1 shells, 3 solid layers, 0.2 infill
    
    ThreadThick = 0.25;
    ThreadWidth = 2.0 * ThreadThick;
    
    HoleFinagle = 0.1;
    HoleFudge = 1.00;
    
    function HoleAdjust(Diameter) = HoleFudge*Diameter + HoleFinagle;
    
    Protrusion = 0.1;           // make holes end cleanly
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    function IntegerMultipleMin(Size,Unit) = Unit * floor(Size / Unit);
    
    //-------
    // Dimensions
    
    BarDia = (5/8) * inch;              // seat back rail diameter
    BarRad = BarDia/2;
    
    TapeThick = 0.3;                    // grippy tape around bar
    
    HoleDia = BarDia + 2*TapeThick;     // total hole dia
    HoleRad = HoleDia/2;
    HoleSides = 4*5;
    
    echo("Bar hole dia: ",HoleDia);
    
    TightSpace = 1.0;                   // space for tightening screws
    
    PlateWidth = 20.0;                  // mounting plate across flanges
    PlateLength = 20.0;                 //  ... parallel to flanges
    PlateThick = IntegerMultipleMin(1.96,ThreadThick);          //  ... thickness
    FlangeThick = IntegerMultiple(1.40,ThreadThick);            // lamp flange thickness
    FlangeWidth = 2.0;                  //  ... width
    
    ShoeThick = PlateThick + FlangeThick;    // dingus protruding from main block
    ShoeOffset = 1.0;                   // offset due to end wall
    
    echo("Shoe thickness: ",ShoeThick," = ",PlateThick," + ",FlangeThick);
    
    LockOffset = -5.0;                  // offset of locking setscrew
    
    TopRoundRad = 1.5*Head10_32/2;      // tidy rounding on top edge of clamp
    echo("Top rounding radius: ",TopRoundRad);
    
    NutDia = Nut10_32Dia*cos(30);       // adjust from across-points to across-flats dia
    NutPart = IntegerMultiple(0.5*Nut10_32Thick,ThreadThick);  // part of nut in each half
    
    BoltOffset = HoleRad + max(Head10_32,NutDia);
    BoltClear = Clear10_32;
    BoltHeadDia = Head10_32;
    BoltHeadThick = Head10_32Thick;
    
    MountWidth = PlateLength + ShoeOffset;         // side-to-side
    MountLength = HoleDia + 3.5*max(BoltHeadDia,NutDia);
    
    ClampHeight = TopRoundRad + HoleRad;            // includes gap/2 for simplicity
    BaseHeight = NutPart + HoleRad;                 //  ... likewise
    MountHeight = PlateWidth;
    
    echo("Mount width: ",MountWidth," length: ",MountLength);
    echo("Height of clamp: ",ClampHeight," base: ",BaseHeight," mount: ",MountHeight);
    echo(" total: ",ClampHeight+BaseHeight+MountHeight);
    
    AlignPegDia = 2.9;                  // shoe alignment peg
    AlignPegLength = ShoeThick;
    
    echo("Alignment peg length: ",AlignPegLength);
    
    //-------
    
    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=HoleAdjust(FixDia)/2,h=Height,$fn=Sides);
    }
    
    module ShowPegGrid(Space = 10.0,Size = 1.0) {
    
      Range = floor(50 / Space);
    
        for (x=[-Range:Range])
          for (y=[-Range:Range])
            translate([x*Space,y*Space,Size/2])
              %cube(Size,center=true);
    }
    
    //-------
    // Upper clamp half
    
    module Clamp() {
    
      difference() {
        translate([0,0,-TightSpace/2]) {
          difference() {
            translate([0,0,ClampHeight/2]) {
              intersection() {
                translate([0,0,-TopRoundRad])
                  minkowski() {
                    cube([(MountLength - 2*TopRoundRad),
                          (MountWidth - 2*Protrusion),
                          ClampHeight],center=true);
                    rotate([90,0,0])
                      cylinder(r=TopRoundRad,h=Protrusion,$fn=4*8);
    
                  }
                cube([MountLength,MountWidth,ClampHeight],center=true);
              }
            }
            translate([0,(MountWidth/2 + Protrusion)])
              rotate([90,0,0])
                PolyCyl(HoleDia,(MountWidth + 2*Protrusion),HoleSides);
            for (Index=[-1,1])
              translate([(Index*BoltOffset),0,0]) {
                translate([0,0,-Protrusion])
                  PolyCyl(BoltClear,(ClampHeight + Protrusion));
                translate([0,0,(ClampHeight - BoltHeadThick)])
                  PolyCyl(BoltHeadDia,(BoltHeadThick + Protrusion));
              }
          }
    
        }
        translate([0,0,-TightSpace/2])
          cube([(MountLength + 2*Protrusion),
               (MountWidth + 2*Protrusion),
               TightSpace],center=true);
      }
    
        if (Support)                    // choose support to suit printing orientation
          union() {
            translate([0,0,1.5*ThreadThick])
              cube([0.75*HoleDia,(MountWidth + 2*ThreadWidth),3*ThreadThick],center=true);
            intersection() {
              for (Index=[-3:3])
                translate([0,Index*(MountWidth/6),-TightSpace/2])
                  rotate([90,0,0])
                    cylinder(r=(HoleRad - 0.25*ThreadThick),
                            h=2*ThreadWidth,center=true,$fn=HoleSides);
              translate([-HoleRad,-MountWidth,0])
                cube([HoleDia,2*MountWidth,HoleRad]);
            }
          }
    }
    
    //-------
    // Lower clamp half = base
    
    module Base() {
    
      difference() {
        translate([0,0,-TightSpace/2])
          difference() {
            translate([0,0,BaseHeight/2])
              cube([MountLength,MountWidth,BaseHeight],center=true);
            translate([0,(MountWidth/2 + Protrusion)])
              rotate([90,0,0])
                PolyCyl(HoleDia,(MountWidth + 2*Protrusion),HoleSides);
            for (Index=[-1,1])
              translate([(Index*BoltOffset),0,0]) {
                translate([0,0,-Protrusion])
                  PolyCyl(BoltClear,(BaseHeight + Protrusion));
                translate([0,0,(BaseHeight - NutPart)])
                  rotate(30)
                    PolyCyl(NutDia,(NutPart + Protrusion),6);
    //              cylinder(r=NutDia/2,h=(NutPart + Protrusion),$fn=6);
              }
          }
        translate([0,0,-TightSpace/2])
          cube([(MountLength + 2*Protrusion),
               (MountWidth + 2*Protrusion),
               TightSpace],center=true);
    
      }
    
      if (Support)
        for (Index=[-1,1])                  // support inside nut openings
          translate([(Index*BoltOffset),
                    0,
                    (BaseHeight - (NutPart - ThreadThick) - TightSpace/2)]) {
            translate([0,0,0])
              for (Seg=[0:5]) {
                rotate(30 + 360*Seg/6)
                  cube([NutDia/2,2*ThreadWidth,NutPart - ThreadThick],center=false);
              }
          }
    
    }
    
    //-------
    // Superflash mounting shoe
    // Offset by -ShoeOffset/2 in Y to align on Mount (half of total offset on each side)
    
    module Shoe() {
    
        difference() {
          translate([-ShoeThick/2,-ShoeOffset/2,PlateWidth/2])
            if (SeparateShoe)
              cube([ShoeThick,PlateLength,PlateWidth],center=true);
            else
              cube([(ShoeThick + Protrusion),PlateLength,PlateWidth],center=true);
    
          translate([-(FlangeThick - Protrusion),
                    -(PlateLength/2 + ShoeOffset/2 + Protrusion),
                    (MountHeight - FlangeWidth)])
            cube([FlangeThick,(PlateLength + 2*Protrusion),(FlangeWidth + Protrusion)]);
    
          translate([-(FlangeThick - Protrusion),
                    -(PlateLength/2 + ShoeOffset/2 + Protrusion),
                    -Protrusion])
            cube([FlangeThick,(PlateLength + 2*Protrusion),(FlangeWidth + Protrusion)]);
    
          translate([-(ShoeThick + Protrusion),LockOffset,MountHeight/2])
            rotate([0,90,0])
              rotate(0)                 // align to match Mount hole orientation
                PolyCyl(Tap4_40,(ShoeThick + 2*Protrusion));
    
          if (SeparateShoe)
            translate([-(ShoeThick - AlignPegLength/2),0,MountHeight/2])
              rotate([0,90,0])
                PolyCyl(AlignPegDia,AlignPegLength);
        }
    }
    
    //-------
    // Bottom block for Superflash mount
    
    module Mount() {
    
      translate([0,0,MountHeight/2])
        union() {
          difference() {
            union() {
              translate([-MountLength/4,0,0])
                cube([MountLength/2,MountWidth,MountHeight],center=true);
              translate([((MountLength/2 - MountHeight)/2 + Protrusion),0,0])
                cube([(MountLength/2 - MountHeight + 2*Protrusion),
                     MountWidth,
                     MountHeight],center=true);
              translate([(MountLength/2 - MountHeight),0,0])
                intersection() {
                  translate([MountLength/4,0,0])
                    cube([MountLength/2,MountWidth,MountHeight],center=true);
                  translate([0,0,MountHeight/2])
                    rotate([90,0,0])
                      cylinder(r=MountHeight,h=MountWidth,center=true,$fn=4*16);
                }
            }
    
            translate([-(MountLength/2 + Protrusion),LockOffset,0])
              rotate([0,90,0])
                rotate(0)       // align through hole sides with point upward
                  PolyCyl(Clear4_40,(MountLength + 2*Protrusion));
    
            for (Index=[-1,1])
              translate([(Index*BoltOffset),0,0]) {
                translate([0,0,BaseHeight/2])
                  PolyCyl(BoltClear,(BaseHeight/2 + Protrusion));
                translate([0,0,(BaseHeight - NutPart)])
                  rotate(30)
                    PolyCyl(NutDia,(NutPart + Protrusion),6);
              }
    
            if (SeparateShoe)
              translate([-(MountLength/2 + AlignPegLength/2),0,0])
                rotate([0,90,0])
                  PolyCyl(AlignPegDia,AlignPegLength);
          }
    
          if (Support)
            for (Index=[-1,1])            // support inside nut openings
              translate([(Index*BoltOffset),0,(MountHeight/2 - (NutPart - ThreadThick))]) {
                translate([0,0,0])
                  for (Seg=[0:5]) {
                    rotate(30 + 360*Seg/6)
                      cube([NutDia/2,
                          2*ThreadWidth,
                          (NutPart - ThreadThick)],center=false);
                  }
              }
    
          if (!SeparateShoe)
            translate([-MountLength/2,0,-MountHeight/2])
              Shoe();
        }
    }
    
    //-------
    
    ShowPegGrid();
    
    if (Layout == "Clamp")
      Clamp();
    
    if (Layout == "Base")
      Base();
    
    if (Layout == "Shoe")
      Shoe();
    
    if (Layout == "Mount")
      Mount();
    
    if (Layout == "Show") {
      translate([0,0,(BaseHeight + MountHeight + Gap)]) {
        translate([0,0,TightSpace/2 + Gap])
          color(MFG) Clamp();
        translate([0,0,-TightSpace/2])
          rotate([180,0,0])
            color(DHC) Base();
      }
      translate([0,0,0])
        color(LDM) render(convexity=3) Mount();
    
      if (SeparateShoe)
        translate([-(MountLength/2 + Gap),0,0])
          color(DDM) Shoe();
    }
    
    if (Layout == "Build") {
      translate([-15,30,(BaseHeight - TightSpace/2)]) rotate([180,0,0])
        Base();
      translate([-15,00,0]) rotate([0,0,0])
        Clamp();
      if (SeparateShoe)
        translate([20,30,ShoeThick]) rotate([0,-90,180])
          Shoe();
      if (SeparateShoe)
        translate([-15,-30,MountHeight]) rotate([180,0,180])
          Mount();
      else
        translate([-15,-40,MountWidth/2]) rotate([90,0,180])
          Mount();
    
    }
    

    The original doodles, done on a retina-burning yellow scratchpad:

    Superflash Mount Doodles
    Superflash Mount Doodles
  • GPS+Voice Interface for Wouxun KG-UV3D: PCB in a Box!

    It always feels good when the parts fit together, even if they don’t actually do anything yet…

    Bare PCB in Wouxun HT battery case
    Bare PCB in Wouxun HT battery case

    That’s the bare PCB in the first-pass 3D-printed battery case adapter, both of which need quite a bit more work. In particular, the case desperately needs some sort of latch to hold the yet-to-be-built contacts against the HT’s battery terminals.

    Amazingly, all the holes lined up spot on, although I think the lower battery contact could move half a millimeter closer to the base of the radio. The battery case contacts are large enough to work as-is and, for what it’s worth, the Wouxun battery cases seem to differ slightly among themselves, too.

    The PCB itself came out about as well as any homebrew PCB I’ve ever made, after getting the Logitech Joggy Thing working again to line the Sherline up for hole drilling:

    Wouxun HT GPS-Audio PCB - copper
    Wouxun HT GPS-Audio PCB – copper

    The circuit has provision for pairs of SMD caps on all the inputs, with which I hope to squash RFI from both the VHF and UHF amateur bands by choosing their self-resonant frequencies appropriately.

  • Presta Valve: Proper Pump Attachment Thereto

    All our bikes have Presta valves, which seem better suited for bike rims than the larger and more common automotive Schraeder valves:

    Presta valve stem
    Presta valve stem

    For all these years, I’d been attaching the pump head so the obvious sealing ring near the nozzle opening lined up with the flat section adjacent to the valve core stem. The pump head never seemed stable on the stem, often leaked, and generally had a precarious hold:

    Incorrect Presta pump head attachment
    Incorrect Presta pump head attachment

    Come to find out, more by accident than intention, that the correct way to attach the pump head involves ramming it all the way down onto the stem so that it can seal along the entire length of the threads. That’s nice and secure, doesn’t leak, and even looks like it should work perfectly:

    Correct Presta pump head attachment
    Correct Presta pump head attachment

    I’d feel even more like a doof if I hadn’t learned to do it wrong by watching somebody else back in the day or if I haven’t observed many other people making exactly the same mistake. I think the fact that the short nozzles on the old-school Zéfal pumps I swore by back in my wedgie-bike days never got a good grip on Presta stems got me off to a bad start, but … dang do I feel stupid.

    FWIW, the little tab sticking out under the latch handle makes up for a bit of slop in the valve head. When I got the pump, the Schraeder nozzle didn’t seal very well, either, and taking up a few mils of slack helped immeasurably. We don’t need that nozzle very often, but our bicycle touring guests frequently do; they know that they can top off a Schraeder-valved tube at any gas station or with any pump anywhere around the world.

    [Update: I hate it when I misspell a word in the title…]

  • Self-resonant Frequencies of Some Ceramic Capacitors

    In that version of the GPS+voice interface, I sprinkled 100 nF and 100 pF SMD caps across the input lines in the hope that they’d reduce EMI on the audio board. The board worked fine for years, but now that it’s time to build another board & box, I figured it’d be good to know a bit more about their actual response.

    So I cobbled up a test fixture with a 3 dB pad from the tracking generator output and a 20 dB pad to the spectrum analyzer input (both of those are bogus, because the cap impedance varies wildly, but work with me on this):

    Ceramic 100 nF cap on copper
    Ceramic 100 nF cap on copper

    Pulled an assortment of 100 nF ceramic caps from the stockpile:

    100 nF ceramic capacitor assortment
    100 nF ceramic capacitor assortment

    And rubbed them against the HP8591 spectrum analyzer & tracking generator:

    Cap Comparison - Detail
    Cap Comparison – Detail

    Their self-resonant frequencies are much lower than I expected:

    Cap Comparison
    Cap Comparison

    The attenuators produce about 17 dB of loss with no cap in the circuit, so the disk caps are pretty much asleep at the switch from VHF on up. The small bypass cap in the top photo is OK and the SMD cap is pretty good, but they’re all well past their self-resonant frequency and acting like inductors.

    The relevant equations:

    • FR = 1/(2π √(LC))
    • XC = 1/(2π f C)
    • Q = FR / BW
    • ESR = XC / Q

    The drill goes a little something like this:

    • Find resonant frequency FR and 3 db bandwidth BW
    • Knowing FR and C, find parasitic L
    • Knowing FR and BW, find Q
    • Knowing XC and Q, find ESR

    In round numbers, the 100 nF SMD cap has L=2 nH and ESR=60 mΩ.

    Now, it turns out a 100 pF SMD cap resonates up at 300 MHz, between the VHF and UHF amateur bands:

    SMD - 100 pF Bandwidth
    SMD – 100 pF Bandwidth

    So I think the way to do this is to pick the capacitance to put the self-resonant frequency in the VHF band, parallel another cap to put a second dip in the UHF band, and run with it. A back of the envelope calculation suggests 470 pF and 47 pF, but that obviously depends on a bunch of other imponderables and I’ll just interrogate the heap until the right ones step forward.

    Just to show the test fixture isn’t a complete piece of crap, here’s a 12 pF cap resonating up around 850 MHz:

    SMD - 12 pF Bandwidth
    SMD – 12 pF Bandwidth

    For the combination of components, sweep speeds, bandwidths, and suchlike in effect, the spectrum analyzer’s noise floor is down around -75 dBm. I think the 12 pF cap is actually better than it looks, but I didn’t fiddle around with a narrower resolution bandwidth.

  • Glass Chip Flat

    Having had trouble with tire liners eroding the rear tube, I went with just a tube and a Kevlar belted Marathon tire. Somewhat to my surprise, that lasted for most of the riding season, but a recent trip had a protracted rest stop:

    Glass chip in tire
    Glass chip in tire

    The puncture came from a knife-edged glass chip that avoided most of the Kevlar belt inside the tire:

    Glass chip - detail
    Glass chip – detail

    I think even a tire liner wouldn’t help with this one.

    Other than that, the tube was in fine shape, so I’ll probably patch it and toss it back in the bike pack. Tire liners prevent most flats from gashes along the midline of the tire, but …

  • Praying Mantis

    Praying Mantis on bike rack
    Praying Mantis on bike rack

    We met this Praying Mantis on the bike rack outside Skinner Hall at Vassar College. Even knowing they’re harmless, I’d have trouble picking it up; we parked on the other end of the rack.

    If these things were any bigger, they’d be terrifying…

  • Flex-fatigued Helmet Cable

    I cable-tied the mic/earphone cable on Mary’s bike helmet to a rib on the fancy air vents near the back end, hoping that would reduce the inevitable flexing. Alas, it didn’t work out that way and the cable lasted only two seasons. This cut-away view shows the pulverized shield braid inside the jacket:

    Fatigue-failed helmet cable
    Fatigue-failed helmet cable

    The symptoms were totally baffling: the mic worked perfectly, but the earphones cut out for at most a few syllables. Of course, I can’t wear her helmet and it only failed occasionally while riding. I barked up several wrong trees, until it got so bad that I could make it fail in the garage while listening to the local NWS weather radio station.

    I spliced in a new USB male-A connector and (re-)discovered that the braid seems to be aluminum, rather than tinned copper. In any event, the wire is completely unsolderable; I crimped the braid from the new connector to a clean section of the old braid. The braid serves only as an electrostatic shield, as it’s not connected to anything on the helmet end. That should suffice until I rebuild the headsets this winter.