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

  • Streamripper Setup

    The Intertubes occasionally clog up while streaming low-bit-rate audio, for no reason I can fathom, leading to discontent in the User Community when it affects quiet classical music played in the dead of night. Given that the stream from far-off Switzerland consists entirely of public-domain performances, I set up Raspbian Lite on a headless Raspberry Pi 1 Model B+ in a spot where it won’t be disturbed:

    Raspberry Pi 1 for streamripper
    Raspberry Pi 1 for streamripper

    Connecting to the Pi with a screen session, so as to allow disconnection & reconnection without anguish, I fired streamripper thusly:

    streamripper http://relay.publicdomainproject.org/classical.mp3.m3u -xs2 -o larger -u "MPlayer2" -m 30 -r -R 6 --with-id3v1
    

    The -r -R 6 options set up a relay stream on http://ripper.local:8000 for the benefit of my local streamers, so only one trickle of bits crosses the Atlantic.

    The total CPU load amounts to a percent or two, tops, so a single-core 700 MHz Pi has no trouble keeping up.

    Because streamripper slings bits in more-or-less real time, the local servers don’t get any track title data when they start up, so the OLED display doesn’t update immediately. This is less of a problem than you might think, as we’re generally not hanging on the display to find out exactly which Vivaldi bassoon concerto is playing.

    Given a suitable collection of tracks, I’ll set up an icecast server as the classical music “station” for the streamers, but that’s an adventure for another day. I also want to splice separate movements back into continuous symphonies, the way they’re suppose to be heard.

  • GIMP Menu Verbiage

    GIMP seems to set up its menu structure during installation, with sensible, if lengthy, hardcoded addresses and menu names for your remote (network) scanner based on its host and USB port location:

    (proc-def "xsane-net-3a-plex760-2e-local-3a-genesys-3a-libusb-3a-002-3a-002" 2
    … snippage …
    (menu-path "<Image>/File/Create/Acquire/XSane/net:plex760.local:genesys:libusb:002:002")
    

    Should you happen to plug the scanner into a different PC or USB port, perhaps while replacing a failed system, then you must change those hideous strings all by yourself.

    So, for example, plugging the aforementioned scanner into a randomly chosen USB port on a new-to-me Dell Optiplex 9020 showing up as plex9020.local on the network produces this identification string:

    [ed@shiitake ~]$ scanimage -L
    device `net:plex9020.local:genesys:libusb:003:003' is a Canon LiDE 120 flatbed scanner
    

    GIMP’s ~/.gimp-2.8/pluginrc file defines the device address:

    (proc-def "xscanimage-net-3a-plex9020-2e-local-3a-genesys-3a-libusb-3a-003-3a-003" 2
    

    The -3a- string seem to be an escape sequence for the colon symbol separating parts of the address. Why we need so many different escape sequence standards mmm escapes me at the moment.

    The menu-path string defines the text appearing in the GIMP UI, so you can use a somewhat more readable generic name:

    (menu-path "<Image>/File/Create/Acquire/xscanimage/Plex9020-scanner")
    

    The ~/.gimp-2.8/menurc file contains GIMP’s keyboard accelerators, which (apparently) must match the revised proc-def strings:

    ; (gtk_accel_path "<Actions>/plug-in/xscanimage-net-3a-plex9020-2e-local-3a-genesys-3a-libusb-3a-003-3a-003" "")
    

    A keyboard accelerator for the scanner wouldn’t save any appreciable amount of time or effort, so (I think) the semicolon marks it as Disabled in the UI.

    It is remarkably easy to make a one-character typo while doing this, particularly if you’re using sed to change All. The. Strings. at once.

    There is, AFAICT, no documentation, which almost certainly means I don’t know where to look.

  • LibreOffice 5.3+ vs. Adobe Type 1 Fonts

    LibreOffice from 5.3 onward (Xubuntu 18.04 uses LO 6.0) no longer supports Adobe Type 1 fonts, which comes as a surprise to those of us who actually bought fonts, back in the day, and have been using them ever since. Apparently, Windows dropped Type 1 font support some time ago.

    Based on some hints, I set up the Adobe Font Development Kit for OpenType. It’s a Python thing, preferably running in a virtual environment to avoid screwing up the rest of one’s system with bizarre dependencies. It seems one (“I”) must not update pip using pip after installing python-pip using apt-get; recovering from that mess was good for another hour of flailing.

    The default AFDKO installation spat out an error message about ufolib (I am not making this up) being at 2.1.1, instead of the required 2.3.1. In for a penny, in for a pound, I updated ADFKO with the “prerelease” option:

    pip install -U afdko --pre
    

    Which fetched ufolib 2.3.1, apparently from wherever Python keeps its prerelease stash. I have NFC what’s going on with any of this.

    An Adobe blog post on the AFDKO tx tool suggested it can convert Type 1 fonts to CFF (a.k.a. Adobe Type 2) fonts and some poking around suggested CFF also figures in OTF fonts.

    tx -cff -n -N -A awb_____.pfb
    --- Filename: awb_____.pfb
    --- FontName: ACaslon-Bold
    tx: --- awb_____.pfb
    tx: (cfw) unhinted
    tx: (cfw) unhinted
    tx: (cfw) unhinted
    tx: (cfw) unhinted
    tx: (cfw) unhinted
    tx: (cfw) There are 222 additional reports of 'unhinted'.
    

    The -A option replaces the bizarre Adobe 8.3 file names with actual font information:

    awrg____.pfb ⇒ ACaslon-Bold.cff
    awbi____.pfb ⇒ ACaslon-BoldItalic.cff
    awi_____.pfb ⇒ ACaslon-Italic.cff
    awrg____.pfb ⇒ ACaslon-Regular.cff
    awsb____.pfb ⇒ ACaslon-Semibold.cff
    awsbi___.pfb ⇒ ACaslon-SemiboldItalic.cff
    

    Regrettably, CFF files don’t actually work as fonts, at least as far as LibreOffice 6.0 (or whatever it uses as a font engine) is concerned.

    Although it’s possible to convert fonts locally with fontforge, doing it one-by-one is tedious and the learning curve for its Python scripting feature seems rather steep. I fired the most vital fonts at Convertio, an online converter running fontforge in the background, got a matching pile of OTF fonts, and installed them in /usr/share/fonts/custom/type1 to indicate their heritage.

    Whereupon LO rammed into a problem I’d had before. The solution this time required sorting the various Caslon and American Typewriter fonts into different “font families” and forcing the TTF names to match their new families. The difference between Medium and Regular seems to have Gone Away.

    I should just use Comic Sans and be done with it …

  • Fireball Island Figures

    A cousin asked if my 3D printer could replace some figures gone missing from their old Fireball Island game board, a classic apparently coming out in a new & improved version.

    Fortunately, solid models exist on Thingiverse:

    Fireball Island figure - Thingiverse 536867
    Fireball Island figure – Thingiverse 536867

    Unfortunately, the left arm requires support, which Slic3r supplies with great exuberance:

    Fireball Island figure - Slic3r support
    Fireball Island figure – Slic3r support

    The vast tower on the figure’s right side (our left) seemed completely unnecessary, not to mention I have no enthusiasm for the peril inherent in chopping away so much plastic, so I replaced it with a simple in-model pillar:

    Figure Support Mods
    Figure Support Mods

    The pillar leans from an adhesion-enhancing lily pad and ends one layer below the left hand, with all dimensions and angles chosen on the fly to make the answer come out right.

    Works like a champ:

    Fireball Island Figures - orange - on platform
    Fireball Island Figures – orange – on platform

    The dark band down the middle comes from the Pixel’s shutter.

    They emerged with some PETG hair, the removal of which I left as an end-user experience.

    I mailed a small box containing figures printed in my (limited!) palette of four colors, some spares Just In Case™, and a few QC rejects showing the necessity of lily pads.

    Game on!

    The OpenSCAD source code as a GitHub Gist:

    // Adding support under Fireball Island figure arm
    import("/mnt/bulkdata/Project Files/Thing-O-Matic/Fireball Island/Fireball Island figure – 100k.stl", convexity=5);
    translate([6.5,-4.0,0]) {
    intersection(){
    translate([-10/2,-10/2,0])
    cube([10,10,11.6],center=false);
    rotate([0,-5.0,0])
    rotate(180/6)
    cylinder(d=4.0,h=30,$fn=6,center=true);
    }
    translate([8/4,0,0])
    rotate(180/6)
    cylinder(d=8,h=0.2,$fn=6);
    }
  • Streaming Radio Player: I2C Display

    Although I2C on the Raspberry Pi fails with devices using clock stretching, cheap I2C OLED displays seem to work well enough to not generate any problems search-able with the obvious keywords:

    RPi I2C OLED
    RPi I2C OLED

    Given a picture of the header pinout, the wiring is trivially easy:

    RPi I2C OLED - RPi header detail
    RPi I2C OLED – RPi header detail

    Using yellow for the ground hurts a bit, but that’s what I get for peeling the SPI cable down to four wires. The pin directly adjacent to the green wire is also ground, should that be easier to reach.

    Tweaking the Luma driver to use I2C doesn’t require much:

    #from luma.core.interface.serial import spi
    from luma.core.interface.serial import i2c
    
    ... snippage ...
    
    # reduce SPI bus from default 8 MHz to (maybe) avoid OLED failure-to-start
    #serial = spi(device=0,port=0,bus_speed_hz=1000000)
    
    # use I2C bus to avoid SPI timing spec failure
    serial = i2c(port=1,address=(0x78 >> 1))     # PCB label = 0x78, low bit = R/W
    

    The OLED PCB lists the I2C address with the R/W bit

    And then It Just Works, with one gotcha. Although the Python program shuts itself and the system down, the wall wart continues to supply power and, because the I2C bus doesn’t include a Reset line, the OLED display doesn’t know the RPi has gone away. So you must issue a command to turn it off before shutting down:

    device.cleanup()        # ideally, switches to low-power mode
    rc = subp.call(['sudo','shutdown','-P','now'])
    

    Now, to discover what works … oddly … with these displays.

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