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

  • Web Security Warning: Say What?

    Having turned on my old Kindle Fire’s “security warnings” just to see what happens, I’m confronted by pop-ups like this on a regular basis:

    Web Security Warning
    Web Security Warning

    People who know what they’re talking about tell me there’s no way for ordinary civilians, such as I, to evaluate the validity of the “credentials” described by that pop-up. In this case, the credential apparently comes from DigiCert, which ought to be trust-able, and was issued to cmcore.com, an actual IBM subsidiary that apparently does Web analytics.

    It works fine through my desktop browsers. The Kindle, however, can’t even find digicert.com, so the problem must be an Amazon thing.

    The only response that makes sense is to continue loading: gizmodo.com might have cat pictures!

    I should just turn off the warnings and be done with it…

  • Command-line CD Ripping & Encoding

    A recent and rather battered book-on-CD posed more than the usual problems for Asunder, so I finally broke down and fiddled around with cdparanoia and lame. This has obviously been done many times before, but breaking it into two simple steps per CD makes the inevitable errors easier to find and work around.

    Invoke cdparanoia thusly to rip an entire CD into separate tracks:

    cdparanoia -B -v
    

    The files pop out sporting names like track01.cdda.wav, but they won’t be around long enough for you to develop a deep emotional attachment.

    Throw a handful of parameters at lame to convert the WAV files into tagged MP3 files:

    d=7
    for t in {01..18} ; do lame --preset tape --tt "D${d}:T${t}" --ta "Author Name" --tl "Book title" --tn "${t}/18" --tg "Audio Book" --add-id3v2 track${t}.cdda.wav D${d}-${t}.mp3 ; done
    rm track*
    

    There’s surely a way to make a double substitution work in the track sequence, but the syntax, ah, escapes me at the moment.

    You might want to not delete the WAV files until you’re happy with the MP3 results.

    In any event, that produces a sequence of MP3 files imaginatively named along the lines of D1-01.mp3, which fits neatly into the cramped LCD space available on an MP3 player.

    Your quality preferences may differ…

  • Blue Gauntlet Fencing Helmet Ear Grommet

    Our Larval Engineer practiced fencing for several years, learning the fundamental truth that you should always bring a gun to a knife fight:

    Fencing - taking a hit
    Fencing – taking a hit

    It’s time to pass the gear along to someone who can use it, but we discovered one of the ear grommets inside the helmet had broken:

    Blue Gauntlet M003-BG Helmet - broken ear grommet
    Blue Gauntlet M003-BG Helmet – broken ear grommet

    The cylinder in the middle should be attached to the washer on the left, which goes inside the helmet padding. It’s a tight push fit inside the washer on the right, which goes on the outside of the padding. Ridges along the cylinder hold it in place.

    Being an injection-molded polyethylene part, no earthly adhesive or solvent will bother it, soooo… the solid model pretty much reproduces the original design:

    Fencing Helmet Ear Grommet - show
    Fencing Helmet Ear Grommet – show

    The top washer goes inside the padding against your (well, her) ear, so I chamfered the edges sorta-kinda like the original.

    There are no deliberate ridges on the central cylinder, but printing the parts in the obvious orientation with no additional clearance makes them a very snug push fit and the usual 3D printing ridges work perfectly; you could apply adhesive if you like. The outside washer has a slight chamfer to orient the post and get it moving along.

    The posts keep the whole affair from rotating, but I’m not sure they’re really necessary.

    Printing a pair doesn’t take much longer than just one:

    Fencing Helmet Ear Grommet - build
    Fencing Helmet Ear Grommet – build

    It doesn’t look like much inside the helmet:

    Blue Gauntlet M003-BG - replacement ear grommet - installed
    Blue Gauntlet M003-BG – replacement ear grommet – installed

    The OpenSCAD source code as a gist from Github:

    // Fencing Helmet Ear Grommet
    // Ed Nisley KE4ZNU December 2015
    // Layout options
    Layout = "Show"; // Base Cap Build Show
    //- Extrusion parameters must match reality!
    // Print with +1 shells and 3 solid layers
    ThreadThick = 0.20;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    Protrusion = 0.1; // make holes end cleanly
    //———————-
    // Dimensions
    NumSides = 12*4;
    $fn = NumSides;
    //———————-
    // 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);
    }
    //——————-
    // Parts
    // Base on outside of liner
    PostOD = 15.5;
    PostLength = 8.0;
    BaseOD = 26.0;
    BaseLength = 3.4;
    module Base() {
    difference() {
    union() {
    cylinder(d=BaseOD,h=2.0);
    cylinder(d=20.0,h=BaseLength);
    for (i=[0:5])
    rotate(i*360/6)
    translate([11.5,0,0])
    rotate(180/6)
    cylinder(d1=2.5,d2=3*ThreadWidth,h=4.0,$fn=6);
    }
    translate([0,0,-Protrusion])
    // PolyCyl(PostOD,4.0,NumSides/4);
    cylinder(d=PostOD,h=PostLength,$fn=NumSides/4);
    translate([0,0,(BaseLength – 4*ThreadThick)])
    cylinder(d1=PostOD,d2=(PostOD + 2*ThreadWidth),h=(4*ThreadThick + Protrusion),$fn=NumSides/4);
    }
    }
    // Cap inside liner
    CapID = 12.0;
    CapOD = 28.0;
    CapThick = 3.0;
    module Cap() {
    difference() {
    union() {
    rotate_extrude(convexity=2)
    polygon(points=[
    [CapID/2 + CapThick/3,0.0],
    [CapOD/2 – CapThick/3,0.0],
    [CapOD/2,CapThick/2],
    [CapOD/2,CapThick],
    [CapID/2,CapThick],
    [CapID/2,CapThick – CapThick/3]
    ]);
    translate([0,0,CapThick – Protrusion])
    cylinder(d=PostOD,h=(PostLength – (CapThick – Protrusion)),$fn=NumSides/4);
    }
    translate([0,0,-Protrusion])
    PolyCyl(CapID,10.0,$fn);
    }
    }
    //———————-
    // Build it!
    if (Layout == "Base")
    Base();
    if (Layout == "Cap")
    Cap();
    BuildSpace = 30/2;
    if (Layout == "Build") {
    for (j=[-1,1])
    translate([j*BuildSpace,0,0]) {
    translate([0,-BuildSpace,0])
    Base();
    translate([0,BuildSpace,0])
    Cap();
    }
    }
    if (Layout == "Show") {
    color("LightGreen") Base();
    translate([0,0,12])
    rotate([180,0,0])
    color("LightBlue") Cap();
    }

  • Kenmore Progressive Vacuum Cleaner vs. Classic Electrolux Dust Brush

    Vacuum cleaner dust brushes, separated by millimeters and decades:

    Kenmore vs adapted Electrolux dust brushes
    Kenmore vs adapted Electrolux dust brushes

    The bulky one on the left came with our new Kenmore Progressive vacuum cleaner. It’s fine for dust on a flat horizontal or vertical surface and totally useless for dust on actual objects. It’s supposed to snap around the handle at the end of the cleaner’s flexy hose, where it helps make the entire assembly too large and too clumsy, or on the end of the “wand”, where it’s at the wrong angle. The bonus outer shell slides around the stubby bristles in the unlikely event they’re too long for the flat surface at hand.

    The brush on the right emerged from the Box o’ Electrolux Parts that Came With The House™, must be half a century old, and consists of a cast aluminum lump with various holes milled into it, adorned with luxuriously long and flexible horsehair. Suffice it to say they don’t make ’em like that any more. Heck, they probably don’t make horses with hair like that any more, either.

    The blue plastic adapter atop the aluminum ball looks like you’d expect by now:

    Electrolux Brush Adapter
    Electrolux Brush Adapter

    The short snout fits neatly into the space available inside the ball. The abrupt ledge at the top of the snout, of course, didn’t work well; I rushed the design for a show-n-tell.

    The OpenSCAD source code (as a Github gist) bevels that ledge and tweaks the interior air channel a bit:

    // Kenmore vacuum cleaner nozzle adapters
    // Ed Nisley KE4ZNU December 2015
    // Layout options
    Layout = "LuxBrush"; // MaleFitting CoilWand FloorBrush CreviceTool ScrubbyTool LuxBrush
    //- Extrusion parameters must match reality!
    // Print with +1 shells and 3 solid layers
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    Protrusion = 0.1; // make holes end cleanly
    //———————-
    // Dimensions
    ID1 = 0; // for tapered tubes
    ID2 = 1;
    OD1 = 2;
    OD2 = 3;
    LENGTH = 4;
    OEMTube = [35.0,35.0,41.7,40.5,30.0]; // main fitting tube
    EndStop = [OEMTube[ID1],OEMTube[ID2],47.5,47.5,6.5]; // flange at end of main tube
    FittingOAL = OEMTube[LENGTH] + EndStop[LENGTH];
    $fn = 12*4;
    //———————-
    // 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);
    }
    //——————-
    // Male fitting on end of Kenmore tools
    // This slides into the end of the handle or wand and latches firmly in place
    module MaleFitting() {
    Latch = [40,11.5,5.0]; // rectangle latch opening
    EntryAngle = 45; // latch entry ramp
    EntrySides = 16;
    EntryHeight = 15.0; // lower edge on *inside* of fitting
    KeyRadius = 1.0;
    translate([0,0,6.5])
    difference() {
    union() {
    cylinder(d1=OEMTube[OD1],d2=OEMTube[OD2],h=OEMTube[LENGTH]); // main tube
    hull() // insertion guide
    for (i=[-(6.0/2 – KeyRadius),(6.0/2 – KeyRadius)],
    j=[-(28.0/2 – KeyRadius),(28.0/2 – KeyRadius)],
    k=[-(26.0/2 – KeyRadius),(26.0/2 – KeyRadius)])
    translate([(i – (OEMTube[ID1]/2 + OEMTube[OD1]/2)/2 + 6.0/2),j,(k + 26.0/2 – 1.0)])
    sphere(r=KeyRadius,$fn=8);
    translate([0,0,-EndStop[LENGTH]]) // wand tube butts against this
    cylinder(d=EndStop[OD1],h=EndStop[LENGTH] + Protrusion);
    }
    translate([0,0,-OEMTube[LENGTH]]) // main bore
    cylinder(d=OEMTube[ID1],h=2*OEMTube[LENGTH] + 2*Protrusion);
    translate([0,-11.5/2,23.0 – 5.0]) // latch opening
    cube(Latch);
    translate([OEMTube[ID1]/2 + EntryHeight/tan(90-EntryAngle),0,0]) // latch ramp
    translate([(Latch[1]/cos(180/EntrySides))*cos(EntryAngle)/2,0,(Latch[1]/cos(180/EntrySides))*sin(EntryAngle)/2])
    rotate([0,-EntryAngle,0])
    intersection() {
    rotate(180/EntrySides)
    PolyCyl(Latch[1],Latch[0],EntrySides);
    translate([-(2*Latch[0])/2,0,-Protrusion])
    cube(2*Latch[0],center=true);
    }
    }
    }
    //——————-
    // Refrigerator evaporator coil wand
    module CoilWand() {
    union() {
    translate([0,0,50.0])
    rotate([180,0,0])
    difference() {
    cylinder(d1=EndStop[OD1],d2=42.0,h=50.0);
    translate([0,0,-Protrusion])
    cylinder(d1=35.0,d2=35.8,h=100);
    }
    translate([0,0,50.0 – Protrusion])
    MaleFitting();
    }
    }
    //——————-
    // Refrigerator evaporator coil wand
    module FloorBrush() {
    union() {
    translate([0,0,60.0])
    rotate([180,0,0])
    difference() {
    union() {
    cylinder(d1=EndStop[OD1],d2=32.4,h=10.0);
    translate([0,0,10.0 – Protrusion])
    cylinder(d1=32.4,d2=30.7,h=50.0 + Protrusion);
    }
    translate([0,0,-Protrusion])
    cylinder(d1=28.0,d2=24.0,h=100);
    }
    translate([0,0,60.0 – Protrusion])
    MaleFitting();
    }
    }
    //——————-
    // Crevice tool
    module CreviceTool() {
    union() {
    translate([0,0,60.0])
    rotate([180,0,0])
    difference() {
    union() {
    cylinder(d1=EndStop[OD1],d2=32.0,h=10.0);
    translate([0,0,10.0 – Protrusion])
    cylinder(d1=32.0,d2=30.4,h=50.0 + Protrusion);
    }
    translate([0,0,-Protrusion])
    cylinder(d1=28.0,d2=24.0,h=100);
    }
    translate([0,0,60.0 – Protrusion])
    MaleFitting();
    }
    }
    //——————-
    // Mystery brush
    module ScrubbyTool() {
    union() {
    translate([0,0,60.0])
    rotate([180,0,0])
    difference() {
    union() {
    cylinder(d1=EndStop[OD1],d2=31.8,h=10.0);
    translate([0,0,10.0 – Protrusion])
    cylinder(d1=31.8,d2=31.0,h=50.0 + Protrusion);
    }
    translate([0,0,-Protrusion])
    cylinder(d1=26.0,d2=24.0,h=100);
    }
    translate([0,0,60.0 – Protrusion])
    MaleFitting();
    }
    }
    //——————-
    // Electrolux brush ball
    module LuxBrush() {
    union() {
    translate([0,0,30.0])
    rotate([180,0,0])
    difference() {
    union() {
    cylinder(d1=EndStop[OD1],d2=30.8,h=10.0);
    translate([0,0,10.0 – Protrusion])
    cylinder(d1=30.8,d2=30.0,h=20.0 + Protrusion);
    }
    translate([0,0,-Protrusion])
    cylinder(d1=25.0,d2=23.0,h=30 + 2*Protrusion);
    }
    translate([0,0,30.0 – Protrusion])
    MaleFitting();
    }
    }
    //———————-
    // Build it!
    if (Layout == "MaleFitting")
    MaleFitting();
    if (Layout == "CoilWand")
    CoilWand();
    if (Layout == "FloorBrush")
    FloorBrush();
    if (Layout == "CreviceTool")
    CreviceTool();
    if (Layout == "ScrubbyTool")
    ScrubbyTool();
    if (Layout == "LuxBrush")
    LuxBrush();

    That’s  supposed to prevent the WordPress post editors from destroying the formatting…

  • Chiplotle: Better RTS-CTS Handshake Hackage

    With hardware handshaking in full effect, the Chiplotle routine that sends data to the HP 7475A plotter doesn’t need to sleep, because the Linux serial handlers take care of that under the hood. Rather than simply comment that statement out, as I did before, it’s better to test the configuration and only sleep when needed:

    The routine that extracts values from ~/.chiplotle/config.py is already included (well, imported) in the distribution’s baseplotter.py file, so all we need is a test for (the lack of) hardware handshaking:

       def _write_string_to_port(self, data):
          ''' Write data to serial port. data is expected to be a string.'''
          #assert type(data) is str
          if not isinstance(data, basestring):
             raise TypeError('string expected.')
          data = self._filter_unrecognized_commands(data)
          data = self._slice_string_to_buffer_size(data)
          for chunk in data:
             if not get_config_value('rtscts'):
                 self._sleep_while_buffer_full( )
             self._serial_port.write(chunk)
    

    The wisdom of reading a file inside the innermost loop of the serial data output routine may be debatable, but:

    • The output is 9600 b/s serial data
    • The expected result is that we’re about to wait
    • Plenty of smart folks have improved file I/O, so the read is probably a cache hit

    For all I know, it doesn’t actually read a file, but consults an in-memory data structure. Works well enough for me, anyhow.

    The configuration file I’ve been using all along looks like this (minus most of the comments):

    # -*- coding: utf-8 -*-
    serial_port_to_plotter_map = {'/dev/ttyUSB0' : 'HP7475A'}
    
    ## Serial connection parameters.
    ## Set your plotter to match these values, or vice versa..
    baudrate = 9600
    bytesize = 8
    parity = 'N'
    stopbits = 1
    timeout = 1
    xonxoff = 0
    rtscts = 1
    
    
    ## Maximum wait time for response from plotter.
    ## Every time the plotter is queried, Chiplotle will wait for
    ## a maximum of `maximum_response_wait_time` seconds.
    maximum_response_wait_time = 4
    
    
    ## Set to True if you want information (such as warnings)
    ## displayed on the console. Set to False if you don't.
    verbose = True
    

    That’s much prettier…

  • Hard Drive Platter Mood Light: Correct Phase Timing

    As noted earlier, the timing for a π/16 phase delay works out to

    218 steps = (π/16) * (1 cycle/2π) * (7 * 1000 step/cycle)

    which amounts to a delay of 5.45 s = 218 step * 25 ms/step. That means a color should appear on the top platter 11 s after it appears on the bottom platter:

    Mood Light - pi over 16 phase - composite
    Mood Light – pi over 16 phase – composite

    But when I actually got out a stopwatch and timed the colors, the bottom-to-top delay worked out to a mere 3.5 s…

    After establishing that the steps ticked along at the expected 25 ms pace, the phase-to-step calculation produced the right answer, the increments were working as expected, I finally slept on the problem (a few times, alas) and realized that the increment happened in the wrong place:

    for (int i=0; i < LEDSTRINGCOUNT; i++) { // for each layer byte Value[PIXELSIZE]; for (byte c=0; c > PIXELSIZE; c++) { // figure the new PWM values if (++Pixels[c].Step >= Pixels[c].NumSteps) {   //  ... from incremented step
                Pixels[c].Step = 0;
            }
            Value[c] = StepColor(c,-i*Pixels[c].PlatterPhase);
        }
        uint32_t UniColor = strip.Color(Value[RED],Value[GREEN],Value[BLUE]);
     
        for (int j=0; j < LEDSTRIPCOUNT; j++) {              // fill layer with color
            strip.setPixelColor(Map[i][j],UniColor);
        }
    }
    

    The outer loop runs “for each layer”, so the increment happens three times on each step, making the colors shift three times faster than they should.

    Promoting the increments to their own loop solved the problem:

    	MillisNow = millis();
    	if ((MillisNow - MillisThen) > UpdateMS) {
    		digitalWrite(PIN_HEARTBEAT,HIGH);
    		
    		for (byte c=0; c < PIXELSIZE; c++) { // step to next increment in each color if (++Pixels[c].Step >= Pixels[c].NumSteps) {
    				Pixels[c].Step = 0;
    				printf("Cycle %d steps %d at %8ld delta %ld ms\r\n",c,Pixels[c].NumSteps,MillisNow,(MillisNow - MillisThen));
    			}
    		}
    
    		for (int i=0; i < LEDSTRINGCOUNT; i++) {				// for each layer
    			byte Value[PIXELSIZE];
    			for (byte c=0; c < PIXELSIZE; c++) {				//  ... for each color
    				Value[c] = StepColor(c,-i*Pixels[c].PlatterPhase);		// figure new PWM value
    //				Value[c] = (c == RED && Value[c] == 0) ? Pixels[c].MaxPWM : Value[c];	// flash highlight for tracking
    			}
    			uint32_t UniColor = strip.Color(Value[RED],Value[GREEN],Value[BLUE]);
    			if (false && (i == 0))
    				printf("L: %d C: %08lx\r\n",i,UniColor);
    			for (int j=0; j < LEDSTRIPCOUNT; j++) {				// fill layer with color
    				strip.setPixelColor(Map[i][j],UniColor);
    			}
    		}
    		strip.show();
    
    		MillisThen = MillisNow;
    		digitalWrite(PIN_HEARTBEAT,LOW);
    	}
    

    And then It Just Worked.

    Verily, it is written: One careful measurement trumps a thousand expert opinions.

    Sheesh

    (The WordPress editor wrecked these code snippets. I’m leaving them broken so WP can maybe fix the problem.) The problem isn’t fixed, but these are OK now… as long as I don’t unleash the “improved” editor on the post, anyway.

  • Fixing the Sudo Timeout

    So I can find it again, the way to change the sudo timeout for a particular user (that would be me) involves adding a line to the /etc/sudoers file using sudo visudo, thusly:

    Defaults:       ed timestamp_timeout=90
    # blank line to make the underscore visible

    Note the colon! Should you add the timeout to the global Defaults env_reset line, then everybody gets a monster timeout, which may not be what you want.

    You can change the default editor (nano in Ubuntu) thusly:

    sudo update-alternatives --config editor

    Or, in Arch / Manjaro, add a stanza:

    Defaults	editor=/usr/bin/nano

    Because my vi hand is weak:

    • :wq to save & exit
    • :q! to bail out
    • i and a at cursor, I and A in line
    • o and O at line
    • back to command mode

    That’s all I need to insert the proper stanza & move on.