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

General-purpose computers doing something specific

  • Raspberry Pi: Nominal vs. Actual I2C Speeds

    Two lines in /boot/config.txt enable the I2C hardware and set the I2C bus speed:

    dtparam=i2c_arm=on
    dtparam=i2c_arm_baudrate=200000
    

    However, the actual SCL frequency comes from dividing the CPU’s core clock by an even integer, so you can’t always get what you want. The Pi 3 ticks along at 1.2 GHz (actually 1.1 GHz, because marketing) from a core clock of 550 MHz, so a 200 kHz clock calls for a 2750 divider: 550 MHz / 2750 = 200 kHz.

    Actually measuring the SCL frequencies suggests something else is going on:

    I2C 200kHz - actual 125kHz
    I2C 200kHz – actual 125kHz

    D0, the bottom trace, is SCL, D1 is SDA, and D2 is a trigger output not used in this setup. The yellow analog trace is the current in the SCL line between the Pi and the BNO055, about which more later.

    So a 200 kHz nominal frequency produces a 125 kHz actual frequency.

    The BNO055 pulls the clock low (“clock stretching”), which can (and does) cause problems, but it’s not active during the main part of the transfer where the Pi determines the SCL frequency.

    More measurement along those lines produces a table:

    CPU Core Clock: 550 MHz
    I2C SCL kHz
    Nominal Ratio Actual Ratio
    250 2200 156.20 3521
    200 2750 125.00 4400
    150 3667 92.59 5940
    125 4400 78.12 7040
    100 5500 62.50 8800
    50 11000 31.25 17600
    25 22000 15.63 35189
    10 55000 6.25 88000

    Apparently, the code converting the nominal I2C rate in config.txt uses a table of divider values intended for another CPU core clock. AFAICT, the boot code could divide the actual core clock by the desired I2C frequency to produce the appropriate value.

    I have no particular desire to Use The Source to figure out what’s going on …

    [Update: Perhaps this comes along with CPU clock throttling due to temperature. For completeness, I should dump the temperature and actual clock speed.]

  • Rubber Soaker Hose Repair

    A soaker hose leaped under a descending garden fork and accumulated a nasty gash:

    Soaker Hose Splice - gashed
    Soaker Hose Splice – gashed

    Mary deployed a spare and continued the mission, while I pondered how to fix such an odd shape.

    For lack of anything smarter, I decided to put a form-fitting clamp around the hose, with silicone caulk buttered around the gash to (ideally) slow down any leakage:

    Soaker Hose Splice - Solid Model - Assembled
    Soaker Hose Splice – Solid Model – Assembled

    As usual, some doodling got the solid model started:

    Soaker Hose Splice - Dimension doodle 1
    Soaker Hose Splice – Dimension doodle 1

    A hose formed from chopped rubber doesn’t really have consistent dimensions, so I set up the model to spit out small test pieces:

    Soaker Hose Splice - Test Fit - Slic3r
    Soaker Hose Splice – Test Fit – Slic3r

    Lots and lots of test pieces:

    Soaker Hose Splice - test pieces
    Soaker Hose Splice – test pieces

    Each iteration produced a better fit, although the dimensions never really converged:

    Soaker Hose Splice - Dimension doodle 2
    Soaker Hose Splice – Dimension doodle 2

    The overall model looks about like you’d expect:

    Soaker Hose Splice - Complete - Slic3r
    Soaker Hose Splice – Complete – Slic3r

    The clamp must hold its shape around a hose carrying 100 psi (for real!) water, so I put 100 mil aluminum backing plates on either side. Were you doing this for real, you’d shape the plates with a CNC mill, but I just bandsawed them to about the right size and transfer-punched the hole positions:

    Soaker Hose Splice - plate transfer punch
    Soaker Hose Splice – plate transfer punch

    Some drill press action with a slightly oversize drill compensated for any misalignment and Mr Disk Sander rounded the corners to match the plastic block:

    Soaker Hose Splice - plate corner rounding
    Soaker Hose Splice – plate corner rounding

    A handful of stainless steel 8-32 screws holds the whole mess together:

    Soaker Hose Splice - installed
    Soaker Hose Splice – installed

    These hoses spend their lives at rest under a layer of mulch, so I’m ignoring the entire problem of stress relief at those sharp block edges. We’ll see how this plays out in real life, probably next year.

    I haven’t tested it under pressure, but it sure looks capable!

    The OpenSCAD source code as a GitHub Gist:

    // Rubber Soaker Hose Splice
    // Ed Nisley KE4ZNU July 2018
    Layout = "Build"; // Hose Block Show Build
    TestFit = false; // true to build test fit slice from center
    //- Extrusion parameters must match reality!
    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);
    //———-
    // Dimensions
    // Hose lies along X axis
    Hose = [200,27.0,12.0]; // X = longer than anything else
    Block = [80.0,50.0,4.0 + Hose.z]; // overall splice block size
    echo(str("Block: ",Block));
    Kerf = 0.1; // cut through middle to apply compression
    ID = 0;
    OD = 1;
    LENGTH = 2;
    // 8-32 stainless screws
    Screw = [4.1,8.0,3.0]; // OD = head LENGTH = head thickness
    Washer = [4.4,9.5,1.0];
    Nut = [4.1,9.7,6.0];
    CornerRadius = Washer[OD]/2;
    NumScrews = 3; // screws along each side of cable
    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() {
    RimThick = 10.0; // outer sections
    RimOD = RimThick;
    RimFlatRecess = -0.7; // recess to front flat surface
    OuterOC = Hose.y – RimOD; // outer tube centers
    RecessM = 1.5; // back recess chord
    RecessC = OuterOC;
    RecessR = (pow(RecessM,2) + pow(RecessC,2)/4) / (2*RecessM);
    RidgeM = 1.0; // front ridge chord
    RidgeC = 8.0;
    RidgeR = (pow(RidgeM,2) + pow(RidgeC,2)/4) / (2*RidgeM);
    NumSides = 12*4;
    rotate([0,-90,0])
    translate([0,0,-Hose.x/2])
    linear_extrude(height=Hose.x,convexity=4)
    difference() {
    union() {
    for (j=[-1,1]) // outer channels
    translate([0,j*OuterOC/2])
    circle(d=RimOD,$fn=NumSides);
    translate([-RimOD/4,0]) // rear flat fill
    square([RimOD/2,OuterOC],center=true);
    translate([(RimOD/4 + RimFlatRecess),0]) // front flat fill
    square([RimOD/2,OuterOC],center=true);
    intersection() {
    translate([Hose.z/2,0])
    square([Hose.z,OuterOC],center=true);
    translate([-RidgeR + RimOD/2 + RimFlatRecess + RidgeM,0])
    circle(r=RidgeR,$fn=NumSides);
    }
    }
    translate([-(RecessR + RimOD/2 – RecessM),0])
    circle(r=RecessR,$fn=2*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 == "Bottom")
    BottomPlate();
    if (Layout == "Top")
    TopPlate();
    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();
    }
    }
    }
  • Tour Easy Front Fender Clip: Longer and Stronger

    We negotiated the Belmar Bridge connection stairway from the Allegheny River Trail to the Sandy Creek trail:

    Belmar Bridge Stairs - Overview
    Belmar Bridge Stairs – Overview

    We’re maneuvering Mary’s bike, but you get the general idea. Our bikes aren’t built for stairways, particularly ones with low overheads:

    Belmar Bridge Stairs - Low Overhead
    Belmar Bridge Stairs – Low Overhead

    The front fender clip on my Tour Easy snapped (at the expected spots) when the mudflap snagged on one of the angles:

    Belmar Bridge Stairs - First Turn
    Belmar Bridge Stairs – First Turn

    For some inexplicable reason, I didn’t have a roll of duct tape in my packs, so the temporary repair required a strip of tape from a battery pack, two snippets of hook-and-loop tape, and considerable muttering:

    Tour Easy front fender clip - expedient repair
    Tour Easy front fender clip – expedient repair

    It was good for two dozen more miles to the end of our vacation, so I’d say that was Good Enough.

    The new version has holes in the ferrules ten stay diameters deep, instead of six, which might eliminate the need for heatstink tubing. I added a small hole at the joint between the curved hooks and the ferrules to force more plastic into those spots:

    Front Fender Clip - Slic3r
    Front Fender Clip – Slic3r

    I also bent the hanger extension to put the fender’s neutral position closer to the wheel.

    We’ll see how long this one lasts. By now, I now have black double-sticky foam tape!

    The OpenSCAD source code as a GitHub Gist:

    // Tour Easy front fender clip
    // Ed Nisley KE4ZNU July 2017
    Layout = "Build"; // Build Profile Ferrule Clip
    //- Extrusion parameters must match reality!
    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);
    //———————-
    // Dimensions
    // special case: fender is exactly half a circle!
    FenderC = 51.0; // fender outside width = chord
    FenderM = 21.0; // height of chord
    FenderR = (pow(FenderM,2) + pow(FenderC,2)/4) / (2 * FenderM); // radius
    echo(str("Fender radius: ", FenderR));
    FenderD = 2*FenderR;
    FenderA = 2 * asin(FenderC / (2*FenderR));
    echo(str(" … Arc: ",FenderA," deg"));
    FenderThick = 2.5; // fender thickness, assume dia of edge
    ClipHeight = 15.0; // top to bottom, ignoring rakish tilt
    ClipThick = IntegerMultiple(2.5,ThreadWidth); // thickness of clip around fender
    ClipD = FenderD; // ID of clip against fender
    ClipSides = 4 * 8; // polygon sides around clip circle
    BendReliefD = 2.5; // bend arch diameter
    BendReliefA = 2/3 * FenderA/2; // … angle from dead ahead
    BendReliefCut = 1.5; // factor to thin outside of bend
    ID = 0;
    OD = 1;
    LENGTH = 2;
    StayDia = 3.3; // fender stay rod diameter
    StayOffset = 15.0; // stay-to-fender distance
    StayPitch = -5; // angle from stay to fender arch
    DropoutSpace = 120; // stay spacing at wheel hub
    StayLength = 235; // stay length: hub to fender
    StaySplay = asin((DropoutSpace – FenderC)/(2*StayLength)); // outward angle to hub
    echo(str(" … Pitch: ",StayPitch," deg"));
    echo(str(" … Splay: ",StaySplay," deg"));
    FerruleSides = 2*4;
    Ferrule = [StayDia,3*FenderThick/cos(180/FerruleSides),10*StayDia + StayOffset]; // ID = stay rod OD
    FerruleHoleD = 0.1; // small hole to create solid plastic at ferrule joint
    //———————-
    // 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);
    }
    //———————-
    // Clip profile around fender
    // Centered on fender arc
    module Profile(HeightScale = 1) {
    linear_extrude(height=HeightScale*ClipHeight,convexity=5) {
    difference() {
    offset(r=ClipThick) // outside of clip
    union() {
    circle(d=ClipD,$fn=ClipSides);
    for (i=[-1,1])
    rotate(i*BendReliefA) {
    translate([ClipD/2 + BendReliefD/2,0,0])
    circle(d=BendReliefD,$fn=6);
    }
    }
    union() { // inside of clip
    circle(d=ClipD,$fn=ClipSides);
    for (i=[-1,1])
    rotate(i*BendReliefA) {
    translate([ClipD/2 + BendReliefCut*BendReliefD/2,0,0])
    circle(d=BendReliefD/cos(180/6),$fn=6);
    translate([ClipD/2,0,0])
    square([BendReliefCut*BendReliefD,BendReliefD],center=true);
    }
    }
    translate([(FenderR – FenderM – FenderD/2),0]) // trim ends
    square([FenderD,2*FenderD],center=true);
    }
    for (a=[-1,1]) // hooks around fender
    rotate(a*(FenderA/2))
    translate([FenderR – FenderThick/2,0]) {
    difference() {
    rotate(1*180/12)
    circle(d=FenderThick + 2*ClipThick,$fn=12);
    rotate(1*180/8)
    circle(d=FenderThick,$fn=8);
    rotate(a * -90)
    translate([0,-2*FenderThick,0])
    square(4*FenderThick,center=false);
    }
    }
    }
    }
    //———————-
    // Ferrule body
    module FerruleBody() {
    translate([0,0,Ferrule[OD]/2 * cos(180/FerruleSides)])
    rotate([0,-90,0]) rotate(180/FerruleSides)
    difference() {
    cylinder(d=Ferrule[OD],h=Ferrule[LENGTH],$fn=FerruleSides,center=false);
    translate([0,0,StayOffset + Protrusion])
    PolyCyl(Ferrule[ID],Ferrule[LENGTH] – StayOffset + Protrusion,FerruleSides);
    }
    }
    //———————-
    // Generate entire clip at mounting angle
    module FenderClip() {
    difference() {
    union() {
    translate([FenderR,0,0])
    difference() { // angle and trim clip
    rotate([0,StayPitch,0])
    translate([-(FenderR + ClipThick),0,0])
    Profile(2); // scale upward for trimming
    translate([0,0,-ClipHeight]) // trim bottom
    cube(2*[FenderD,FenderD,ClipHeight],center=true);
    translate([0,0,ClipHeight*cos(StayPitch)+ClipHeight]) // trim top
    cube(2*[FenderD,FenderD,ClipHeight],center=true);
    }
    for (j = [-1,1]) // place ferrules
    translate([Ferrule[OD]*sin(StayPitch) + (Ferrule[OD]/2)*sin(StaySplay),j*(FenderR – FenderThick/2),0])
    rotate(-j*StaySplay)
    FerruleBody();
    }
    for (i=[-1,1]) // punch stiffening holes
    translate([FenderThick/2,-i*(FenderR – FenderThick/2),Ferrule[OD]/2])
    rotate([0,-90,i*StaySplay])
    PolyCyl(FerruleHoleD,Ferrule[OD],FerruleSides);
    }
    }
    //———————-
    // Build it
    if (Layout == "Profile") {
    Profile();
    }
    if (Layout == "Ferrule") {
    FerruleBody();
    }
    if (Layout == "Clip") {
    FenderClip();
    }
    if (Layout == "Build") {
    FenderClip();
    }

    As a bonus for paging all the way to the end, here’s the descent on the same stairway:

    Belmar Bridge Stairs - Descent
    Belmar Bridge Stairs – Descent

    No, I wasn’t even tempted …

  • Copying Action Camera Video: Now With UUIDs

    Having tired of manually decoding UDEV’s essentially random device names produced for the various USB action cameras and card readers, I put the device UUIDs in /etc/fstab and let the device names fall where they may:

    UUID=B40C6DD40C6D9262	/mnt/video	ntfs	noauto,uid=ed 0 0
    UUID=0FC4-01AB	/mnt/Fly6	vfat	noauto,nodiratime,uid=ed	0	0
    UUID=0000-0001	/mnt/M20	vfat	noauto,nodiratime,uid=ed	0	0
    LABEL=AS30V	/mnt/AS30V	exfat	noauto,nodiratime,uid=ed	0	0
    

    You get those by plugging everything in, running blkid, and sorting out the results.

    The 64 GB MicroSD card from the Sony AS30V camera uses Microsoft’s proprietary exfat file system, which apparently doesn’t associate a UUID/GUID with the entire device, so you must use a partition label. The Official SD Card Formatter doesn’t (let you) set one, so:

    exfatlabel /dev/sdd1 AS30V
    

    It turns out you can include spaces in the partition label, but there’s no way to escape them (that I know of) in /etc/fstab, so being succinct counts for more than being explanatory.

    One could name the partition in the Windows device properties pane, which would make sense if one knew it was necessary while the Token Windows Laptop was booted with the card in place.

    I think this is easier then trying to persuade UDEV to create known device names based on the USB hardware characteristics, because those will depend on which USB card / device / reader I use. I can force the UUIDs to be whatever I want, because they’re just bits in the disk image.

    With all that in place, you plug in All. The. Gadgets. and run the script (as seen below). The general idea is to verify the bulk video drive mounted OK, attempt to mount each memory card and fire off a corresponding rsync copy, wait until they’re all done, tidy the target filenames, then delete all the source files to get ready for the next ride.

    Funneling all three copies to a single USB hard drive probably isn’t the smartest thing, but the overall write ticks along at 18 MB/s, which is Good Enough for my simple needs. If the drive thrashes itself to death, I won’t do it again; I expect it won’t fail until well outside the 1 year limited warranty.

    If any of the rsync copies fail, then nothing gets deleted. I’m a little queasy about automagically deleting files, but it’s really just video with very little value. Should something horrible happen, I’d do the copies by hand, taking great care to not screw up.

    After all, how many pictures like this do we need?

    Ed signalling on Raymond
    Ed signalling on Raymond

    The Bash script as a GitHub Gist:

    UUID=B40C6DD40C6D9262 /mnt/video ntfs noauto,uid=ed 0 0
    UUID=0FC4-01AB /mnt/Fly6 vfat noauto,nodiratime,uid=ed 0 0
    UUID=0000-0001 /mnt/M20 vfat noauto,nodiratime,uid=ed 0 0
    LABEL=AS30V /mnt/AS30V exfat noauto,nodiratime,uid=ed 0 0
    view raw fstab hosted with ❤ by GitHub
    #!/bin/bash
    # This uses too many bashisms for dash
    thisdate=$(date –rfc-3339=date)
    echo Date is $thisdate
    date
    # MicroSD / readers / USB drive defined in fstab
    # … with UUID or PARTID as appropriate
    echo Mounting bulk video drive
    sudo mount /mnt/video
    if [ $? -ne 0 ]; then
    echo '** Cannot mount video storage drive'
    exit
    fi
    # Show starting space available
    df -h /mnt/video
    #– Sony AS30V
    printf "\n— AS30V\n"
    as30v=/mnt/AS30V
    sudo mount $as30v
    if [ $? -eq 0 ]; then
    echo " start AS30V transfer on $as30v"
    mkdir /mnt/video/AS30V/$thisdate
    rsync -ahu –progress –exclude "*THM" $as30v/MP_ROOT/100ANV01/ /mnt/video/AS30V/$thisdate &
    pid1=$!
    echo " PID is $pid1"
    else
    echo " skipping"
    as30v=
    fi
    #– Cycliq Fly6
    printf "\n— Fly6\n"
    fly6=/mnt/Fly6
    sudo mount $fly6
    if [ $? -eq 0 ]; then
    echo " start Fly6 transfer on $fly6"
    rsync -ahu –progress $fly6 /mnt/video &
    pid2=$!
    echo " PID is $pid2"
    else
    echo " skipping"
    fly6=
    fi
    #– SJCAM M20
    printf "\n— M20\n"
    m20=/mnt/M20
    sudo mount $m20
    if [ $? -eq 0 ]; then
    echo " start M20 transfer on $m20"
    mkdir /mnt/video/M20/$thisdate
    # See if any still images exist to avoid error messages
    n=$( ls $m20/DCIM/Photo/* 2> /dev/null | wc -l )
    if [ $n -gt 0 ] ; then
    echo " copy M20 photos first"
    rsync -ahu –progress $m20/DCIM/Photo/ /mnt/video/M20/$thisdate
    fi
    rsync -ahu –progress $m20/DCIM/Movie/ /mnt/video/M20/$thisdate &
    pid3=$!
    echo " PID is $pid3"
    else
    echo " skipping"
    m20=
    fi
    printf "\n—– Waiting for all rsync terminations\n"
    rc=0
    for p in $pid1 $pid2 $pid3 ; do
    wait -n
    rc=$(( rc+$? ))
    echo RC so far: $rc
    done
    date
    if [ $rc -eq 0 ] ; then
    echo '—– Final cleanups'
    echo Fix capitalized extensions
    find /mnt/video -name \*AVI -print0 | xargs -0 rename -v -f 's/AVI/avi/'
    find /mnt/video -name \*MP4 -print0 | xargs -0 rename -v -f 's/MP4/mp4/'
    if [ "$as30v" ]; then
    echo Remove files on $as30v
    rm $as30v/MP_ROOT/100ANV01/*
    sudo umount $as30v
    fi
    if [ "$fly6" ]; then
    echo Remove files on $fly6
    rm -rf $fly6/DCIM/*
    sudo umount $fly6
    fi
    if [ "$m20" ]; then
    echo Remove files on $m20
    rm $m20/DCIM/Movie/* $m20/DCIM/Photo/*
    sudo umount $m20
    fi
    echo '—– Space remaining on video drive'
    df -h /mnt/video
    sudo umount /mnt/video
    echo Done!
    else
    echo Whoopsie! Total RC: $rc
    fi
    view raw savevideo.sh hosted with ❤ by GitHub
  • Xubuntu 18.04 vs. VNC

    For unknown reasons, the Gnome-ish vino-server package for Xubuntu 18.04 no longer installs vino-preferences, so it’s not obvious how to configure the server.

    After considerable flailing, I installed good old x11vnc, set up a password, then started it in .xprofile:

    x11vnc -forever -find -no6 -avahi -usepw
    

    I don’t mind having programs change, but it’d be nice if features like, say, configuration wouldn’t just vanish.

  • Tour Easy Front Derailleur Cable Clamp

    In addition to sawing through the side of the cable ferrule, the front derailleur cable began breaking at the edge of the derailleur arm:

    Tour Easy Front Derailleur Cable - frayed
    Tour Easy Front Derailleur Cable – frayed

    It wouldn’t have survived another ride!

    Dan pointed out CNC machined aluminum cable clamps are a thing, but those are sized for larger frame tubes than the 1.0 inch steel used on our Tour Easy ‘bents and, although I’ve shimmed everything else on the frame, I wanted to tweak the cable angle to match the arm on the derailleur.

    A bit of OpenSCAD wrangling produces a likely candidate:

    Front Derailleur Cable Clamp - Slic3r
    Front Derailleur Cable Clamp – Slic3r

    That’s a bulked-up revision of the prototype:

    Tour Easy Front Derailleur Cable Clamp - installed
    Tour Easy Front Derailleur Cable Clamp – installed

    Done up in orange PETG, it demonstrated the idea worked, but two perimeter threads wrapped around 15% infill isn’t quite up to the task. Note the split along the screw on the far half and various irregularities around the ferrule.

    The cable angle isn’t quite right, either, as the proper compound angle would, alas, aim the cable into the pedal crank. The bulky bushings get in the way of putting the ferrule where it should be with the screws aligned in a tidy manner, so I must get used to the jaunty angle.

    The bulkier version, done with 50% infill and four perimeter threads, has the same tilt angle, but the ferrule sits further from the screws:

    Tour Easy Front Derailleur Cable Clamp V2 - rear quarter view
    Tour Easy Front Derailleur Cable Clamp V2 – rear quarter view

    The view from the left side shows the cable angles slightly to the rear, but the smaller angle should make it happier:

    Tour Easy Front Derailleur Cable Clamp V2 - side view
    Tour Easy Front Derailleur Cable Clamp V2 – side view

    Probably should have used black PETG. Next time, for sure!

    The OpenSCAD source code as a GitHub Gist:

    // Tour Easy Derailleur Cable Clamp
    // Ed Nisley KE4ZNU – June 2017
    /* [Build Options] */
    Layout = "Build"; // [Build, Show]
    /* [Extrusion] */
    ThreadThick = 0.25; // [0.20, 0.25]
    ThreadWidth = 0.40; // [0.40]
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    /* [Hidden] */
    Protrusion = 0.01; // [0.01, 0.1]
    HoleWindage = 0.2;
    ID = 0;
    OD = 1;
    LENGTH = 2;
    /* [Cable Clamp] */
    FrameOD = 25.7; // Tour Easy has hard inch tubing + paint
    Ferrule = [1.5,5.1,12.0]; // cable ferrule
    EntryPoint = [0,13,60]; // cable entry to derailleur, +Y to rear of bike
    CableTilt = -20; // tilt from parallel to frame tube
    CableTheta = 0; // rotation around clamp from +X axis
    /* [Screws and Inserts] */
    ClampScrew = [3.0,5.5,35.0]; // M3 button / socket head cap screw
    ClampWasher = [3.7,7.0,0.7]; // M3 washer
    ClampNut = [3.0,6.0,4.0]; // M3 nylock nut
    /*
    ClampScrew = [4.0,7.0,25.0]; // M4 button head cap screw
    ClampWasher = [4.5,9.0,0.8]; // M4 washer
    ClampNut = [4.0,8.0,5.0]; // M4 nylock nut
    */
    NutShift = -0; // slide bushing toward nut for clearance
    //- Set clamp ring dimensions
    WallThick = 10.0;
    BushingSides = 8;
    Bushing = [ClampScrew[ID],
    // ClampWasher[OD]/cos(180/8) + 4*ThreadWidth,
    Ferrule[LENGTH]/cos(180/BushingSides),
    ClampScrew[LENGTH] – 2*ClampWasher[LENGTH] – ClampNut[LENGTH]];
    Ring = [FrameOD + HoleWindage,FrameOD + 2*WallThick,Ferrule[LENGTH]];
    ClampScrewOC = IntegerMultiple(FrameOD + ClampWasher[OD],1);
    echo(str(" screw OC: ",ClampScrewOC));
    ClampKerf = 0.75; // kerf between separated halves
    NumSides = 8*4;
    //- Adjust hole diameter to make the size come out right
    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);
    }
    // Construct things
    module ClampRing() {
    difference() {
    union() {
    cylinder(d=Ring[OD],h=Ring[LENGTH],$fn=NumSides); // basic ring
    for (j=[-1,1]) // screw bushings
    translate([Bushing[LENGTH]/2 + NutShift,j*ClampScrewOC/2,Ring[LENGTH]/2])
    rotate([0,-90,0]) rotate(180/BushingSides)
    cylinder(d=Bushing[OD],h=Bushing[LENGTH],$fn=BushingSides);
    intersection() {
    rotate([CableTilt,0,CableTheta]) // reinforce cable ferrule
    translate([(Ring[ID] + Ring[OD])/4,0,Ferrule[LENGTH]/2])
    rotate(180/8)
    cylinder(d=3*Ferrule[OD] + 0*ThreadWidth,2*Ferrule[LENGTH],center=true,$fn=8);
    cylinder(d=2*Ring[OD],h=Ring[LENGTH],$fn=NumSides); // basic ring
    }
    }
    translate([0,0,-Protrusion]) // frame tube
    cylinder(d=Ring[ID],h=Ring[LENGTH] + 2*Protrusion,$fn=NumSides);
    rotate([CableTilt,0,CableTheta]) // cable ferrule
    translate([(Ring[ID] + Ring[OD])/4,0,-0.25*Ferrule[LENGTH]]) {
    rotate(180/8)
    PolyCyl(Ferrule[OD],Ferrule[LENGTH],8);
    rotate(-22.5)
    PolyCyl(Ferrule[ID],2*Ferrule[LENGTH],4);
    }
    for (j=[-1,1]) // screw holes
    translate([Ring[OD]/2,j*ClampScrewOC/2,Ring[LENGTH]/2])
    rotate([0,-90,0]) rotate(180/6)
    PolyCyl(Bushing[ID],Ring[OD],6);
    for (i=[-1,1], j=[-1,1]) // screw & nut seats
    translate([i*(Bushing[LENGTH]/2) + NutShift,j*ClampScrewOC/2,Ring[LENGTH]/2])
    rotate([0,i*90,0]) rotate(180/BushingSides)
    cylinder(d=Bushing[OD],h=Bushing[LENGTH],$fn=BushingSides);
    translate([0,0,Ring[LENGTH]/2]) // slice it apart
    cube([ClampKerf,2*Ring[OD],2*Ring[LENGTH]],center=true);
    }
    }
    //- Build things
    if (Layout == "Show") {
    translate(EntryPoint)
    cube(1,center=true);
    ClampRing();
    }
    if (Layout == "Build") {
    ClampRing();
    }
  • Siglent SDS2304X Screen Shot File

    Poking the Print button on the front of the Siglent SDS2304X scope saves the screen to a BMP file (in the /BMP directory) on a USB flash drive plugged into its front-panel port:

    Siglent SDS2304X Front Panel - Print Button - USB port
    Siglent SDS2304X Front Panel – Print Button – USB port

    Which produces files like these:

    ll --block-size=1 /path-to-USB-stick/BMP/
    total 2318336
    drwxr-xr-x 2 ed ed    4096 May 23 13:13 ./
    drwxr-xr-x 4 ed ed    4096 Dec 31  1969 ../
    -rw-r--r-- 1 ed ed 1152054 May 23 13:13 SDS00001.BMP
    -rw-r--r-- 1 ed ed 1152054 May 23 13:13 SDS00002.BMP
    

    The files are 1152054 bytes long, as specified by the BMP header inside the file:

    hexdump -C /path-to-USB-stick/BMP/SDS00001.BMP | head
    00000000  42 4d 36 94 11 00 00 00  00 00 36 00 00 00 28 00  |BM6.......6...(.|
    00000010  00 00 20 03 00 00 e0 01  00 00 01 00 18 00 00 00  |.. .............|
    00000020  00 00 00 94 11 00 00 00  00 00 00 00 00 00 00 00  |................|
    00000030  00 00 00 00 00 00 01 01  01 01 01 01 01 01 01 01  |................|
    00000040  01 01 01 01 01 01 01 01  01 01 01 01 01 01 01 01  |................|
    *
    00000880  01 01 01 01 01 01 01 01  01 01 01 01 01 01 1e 1e  |................|
    00000890  1e 1e 1e 1e 1e 1e 1e 1e  1e 1e 1e 1e 1e 1e 1e 1e  |................|
    *
    00000990  1e 1e 1e 1e 1e 1e 01 01  01 01 01 01 01 01 01 01  |................|
    

    The first 14 bytes contain the Bitmap file header, with the file size in Little-Endian order in the four bytes at offset +0x02: 0x00119436 = 1152054.

    The four bytes at offset +0x0A give the offset of the pixel data: +0x36. That’s the series of 0x01 bytes in the fourth row. Unlike most images, BMP pixel arrays start at the lower left corner of the image and proceed rightward / upward to the last pixel at the upper right corner.

    The data between the Bitmap file header and the start of the pixel data contains at least a Device Independent Bitmap header, identified by its length in the first four bytes at offset +0x0E. In this case, the length of 0x28 = 40 bytes makes it a Windows (no surprise) header.

    The two bytes at +1C give the bits-per-pixel value: 0x18 = 24 = 3 bytes/pixel, so parse the pixels in RGB order.

    The four bytes at +0x12 give the bitmap width in pixels: 0x320 = 800. Each pixel row must be a multiple of 4 bytes long, which works out fine at 2400 bytes.

    The tail end of the file shows one dark pixel at the upper right:

    hexdump -C /path-to-USB-stick/BMP/SDS00001.BMP | tail
    00118330  00 cc 00 00 cc 00 00 cc  00 00 cc 00 00 cc 00 00  |................|
    00118340  cc 00 00 cc 00 00 cc 00  00 cc 00 00 cc 00 00 cc  |................|
    00118350  00 00 cc 00 00 cc 00 00  cc 0f 0f 75 1e 1e 1e 1e  |...........u....|
    00118360  1e 1e 1e 1e 1e 1e 1e 1e  1e 1e 1e 1e 1e 1e 1e 1e  |................|
    *
    00118ad0  1e 1e 1e 01 01 01 1e 1e  1e 1e 1e 1e 1e 1e 1e 1e  |................|
    00118ae0  1e 1e 1e 1e 1e 1e 1e 1e  1e 1e 1e 1e 1e 1e 1e 1e  |................|
    *
    00119430  1e 1e 1e 01 01 01                                 |......|
    

    Which looks like this, expanded by a factor of eight (clicky for more dots to reveal the situation):

    Screenshot - upper right corner - 8x expansion
    Screenshot – upper right corner – 8x expansion

    The scope can also transfer a screenshot over the network:

    lxi screenshot -a 192.168.1.42 /tmp/lxi-shot.bmp 
    Loaded siglent-sds screenshot plugin
    Saved screenshot image to /tmp/lxi-shot.bmp
    

    Which has the same header:

    hexdump -C /tmp/lxi.bmp | head
    00000000  42 4d 36 94 11 00 00 00  00 00 36 00 00 00 28 00  |BM6.......6...(.|
    00000010  00 00 20 03 00 00 e0 01  00 00 01 00 18 00 00 00  |.. .............|
    00000020  00 00 00 94 11 00 00 00  00 00 00 00 00 00 00 00  |................|
    00000030  00 00 00 00 00 00 01 01  01 01 01 01 01 01 01 01  |................|
    00000040  01 01 01 01 01 01 01 01  01 01 01 01 01 01 01 01  |................|
    *
    00000880  01 01 01 01 01 01 01 01  01 01 01 01 01 01 1e 1e  |................|
    00000890  1e 1e 1e 1e 1e 1e 1e 1e  1e 1e 1e 1e 1e 1e 1e 1e  |................|
    *
    00000990  1e 1e 1e 1e 1e 1e 01 01  01 01 01 01 01 01 01 01  |................|
    

    But the resulting file is three bytes = one pixel (!) too large:

    ll --block-size=1 /tmp/lxi.bmp
    -rw-rw-r-- 1 ed ed 1152057 May 23 19:09 /tmp/lxi.bmp
    

    The tail end of the file:

    hexdump -C /tmp/lxi.bmp | tail
    00118330  00 cc 00 00 cc 00 00 cc  00 00 cc 00 00 cc 00 00  |................|
    00118340  cc 00 00 cc 00 00 cc 00  00 cc 00 00 cc 00 00 cc  |................|
    00118350  00 00 cc 00 00 cc 00 00  cc 0f 0f 75 1e 1e 1e 1e  |...........u....|
    00118360  1e 1e 1e 1e 1e 1e 1e 1e  1e 1e 1e 1e 1e 1e 1e 1e  |................|
    *
    00118ad0  1e 1e 1e 01 01 01 1e 1e  1e 1e 1e 1e 1e 1e 1e 1e  |................|
    00118ae0  1e 1e 1e 1e 1e 1e 1e 1e  1e 1e 1e 1e 1e 1e 1e 1e  |................|
    *
    00119430  1e 1e 1e 01 01 01 01 01  0a                       |.........|
    

    Because the file header doesn’t include those three bytes, they don’t go into the image and the resulting screenshot is visually the same.

    Which looks like a picket-fence error, doesn’t it? I’d lay long odds the erroneous loop runs from 0 to NUMPIXELS, rather than 0 to NUMPIXELS-1. Raise your hand if you’ve ever made that exact mistake.

    I have no practical way to determine whether the error is inside the scope or the LXI network code, but given Siglent’s overall attention to software fit-and-finish, I suspect the former.

    One can convert BMP files to the much more compact PNG format:

    convert /tmp/lxi.bmp /tmp/lxi.png
    convert: length and filesize do not match `/tmp/lxi.bmp' @ warning/bmp.c/ReadBMPImage/829.
    

    Yes. Yes, there is a mismatch.

    The space savings is impressive, particularly in light of PNG being a lossless format:

    ll /tmp/lxi.*
    -rw-rw-r-- 1 ed ed 1.1M May 23 19:09 /tmp/lxi.bmp
    -rw-rw-r-- 1 ed ed  14K May 23 19:17 /tmp/lxi.png
    

    You can eliminate the nag by truncating the file:

    truncate --size=1152054 /tmp/lxi.bmp
    

    One could wrap it all up in a script:

    #!/bin/bash
    lxi screenshot -a 192.168.1.42 /tmp/"$1".bmp
    truncate --size=1152054 /tmp/"$1".bmp
    convert /tmp/"$1".bmp "$1".png
    echo Screenshot: "$1".png

    And then It Just Works:

    getsds2304x.sh "Test Shot Starfish"
    Loaded siglent-sds screenshot plugin
    Saved screenshot image to /tmp/Test Shot Starfish.bmp
    Screenshot: Test Shot Starfish.png
    
    Test Shot Starfish
    Test Shot Starfish

    SpaceX uses Test Shot Starfish tracks for pre-launch background music; the actual test shot was spectacular.