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

  • Badge Lanyard Reel Mount

    A certain young engineer of my acquaintance now carries an ID badge and, so I hear, works in a PCB design & test venue. Seeing as how her favorite color is purple, this seemed appropriate:

    Badge Lanyard Reel - front - overall
    Badge Lanyard Reel – front – overall

    The guts came from Circuit Breaker Labs in the form of a recycled PCB trapped in acrylic resin atop a plastic housing with a spring-loaded reel inside.

    It arrived with a plastic bullet at the end of the lanyard:

    Badge Lanyard Reel - plastic bullet link
    Badge Lanyard Reel – plastic bullet link

    Which I immediately replaced with brass, because Steampunk:

    Badge Lanyard Reel - bullet cross-drill
    Badge Lanyard Reel – bullet cross-drill

    That made the plastic housing look weak, so, in a series of stepwise refinements, I conjured a much better case from the vasty digital deep:

    Badge Lanyard Reel - iterations
    Badge Lanyard Reel – iterations

    All of the many, many critical dimensions lie inside the case, where they can’t be measured accurately, so each of those iterations could improve only one or two features. The absolutely wonderful thing about OpenSCAD is having it regenerate the whole model after loosening, say, the carabiner slot by two thread thicknesses; you can do that with a full-on relational CAD drawing, but CAD drawings always seems like a lot of unnecessary work if I must figure out the equations anyway.

    The back sports my favorite Hilbert Curve infill with a nicely textured finish:

    Badge Lanyard Reel - rear - oblique
    Badge Lanyard Reel – rear – oblique

    It’d surely look better in solid brass with Hilbert curve etching.

    Black PETG doesn’t photograph well, but at least you can see the M2 brass inserts:

    Badge Lanyard Reel - lower interior
    Badge Lanyard Reel – lower interior

    The first prototype showed the inserts needed far more traction than the usual reamed holes could provide, so I added internal epoxy grooves in each hole:

    Badge Lanyard Reel Mount - show
    Badge Lanyard Reel Mount – show

    Recessing the screw heads into the top plate made them more decorative and smoother to the touch. Button-head screws would be even smoother, but IMO didn’t look quite as bold.

    After seeing how well the grooves worked, I must conjure a module tabulating all the inserts on hand and automagically generating the grooves.

    The yellow star holds up the roof of the reel recess in the build layout:

    Badge Lanyard Reel Mount - build layout - bottom
    Badge Lanyard Reel Mount – build layout – bottom

    Slic3r produced the rest of the support material for the carabiner exit slot:

    Badge Lanyard Reel Mount - bottom - Slic3r support
    Badge Lanyard Reel Mount – bottom – Slic3r support

    Those two support lumps on the right don’t actually support anything, but tweaking the support settings to disable them also killed the useful support on the left; come to find out Slic3r’s modifier meshes don’t let you disable support generation.

    The top plate required support all the way around the inside of the bezel:

    Badge Lanyard Reel Mount - top - Slic3r support
    Badge Lanyard Reel Mount – top – Slic3r support

    I carved the original plastic housing in half, roughly along its midline, and discarded the bottom section with the belt clip (it’s on the far left of the scrap pile). The top section, with PCB firmly affixed, holds the lanyard reel and anchors the retracting spring in a central slotted peg. No pictures of that, as it’s either a loose assembly of parts or a spring-loaded bomb and I am not taking it apart again.

    The lanyard passes through an eyelet that pays it out to the rotating reel. I’d definitely do that differently, were I building it from scratch, because mounting the eyelet in exactly the proper position to prevent the lanyard from stacking up on the reel and jamming against the inside of the housing turned out to be absolutely critical and nearly impossible.

    The top plate presses the original housing against the carabiner, with the cut-off section inside the carabiner’s circular embrace, which just barely worked: the PCB bezel is a millimeter smaller than the shoulder of the housing.

    All in all, I think it came out really well for a 3D printed object made by a guy who usually builds brackets:

    Badge Lanyard Reel - front - oblique
    Badge Lanyard Reel – front – oblique

    I hope she likes it …

    The OpenSCAD source code as a GitHub Gist:

    // Badge Lanyard Reel Mount
    // Ed Nisley KE4ZNU April 2017
    // Reel center at origin, lanyard exit toward +X
    Layout = "Show";
    Support = true;
    //- Extrusion parameters must match reality!
    ThreadThick = 0.20;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    Protrusion = 0.05; // make holes end cleanly
    inch = 25.4;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    //———————-
    // Dimensions
    ID = 0; // for round things
    OD = 1;
    LENGTH = 2;
    Carabiner = [30.7,35.3,3.5]; // metal carabiner around original reel
    Latch = [6.0,-15,8.0]; // wire spring latch: offset from OD + thickness
    LatchAngle = 60; // max deflection angle to center from -X direction
    LatchPoints = [[0,0],
    [Latch[1]/tan(LatchAngle),0],
    [Latch[1]/tan(LatchAngle),-Latch[1]]]; // polygon in as-cut orientation
    echo(str("Latch polygon: ",LatchPoints));
    Screw = [2.0,3.8 + 0*ThreadWidth,10.0]; // M2 screw: ID = clear, OD = head
    ScrewHeadLength = 2.0;
    ScrewSides = 8;
    ScrewRecess = 5*ThreadThick;
    MountSides = ScrewSides; // suitably gritty corners
    MountThick = Screw[LENGTH] / cos(180/MountSides) + ScrewRecess + 2.0;
    Insert = [Screw[ID],3.4,4.0]; // brass insert for screws
    BCD = Carabiner[OD] + 2.5*Insert[OD];
    BoltAngles = [20,110]; // ± angles to bolt holes
    Reel = [5.3,25.5 + 2*ThreadWidth,6.0 + 2*ThreadThick]; // lanyard cord reel
    ShimThick = 2*ThreadThick; // covers open side of reel for better sliding
    Bezel = [31.0,32.0,7.5]; // PCB holder + shell, LENGTH = post + shell
    BezelSides = 6*4;
    BezelBlock = [5.5,7.5,3.6] + [ThreadWidth,ThreadWidth,ThreadThick]; // block around lanyard eyelet
    Eyelet = [3.5,4.5,3.0];
    Bullet = [2.0,6.5,2.0]; // brass badge holder, LENGTH = recess into mount
    //———————-
    // 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);
    }
    //– Lanyard reel mockup
    module Reel() {
    cylinder(d=Reel[OD],h=Reel[LENGTH],center=true,$fn=6*4);
    }
    // Carabiner metal mockup
    // Some magic numbers lie in wait
    module Beener() {
    difference() {
    hull() {
    cylinder(d=Carabiner[OD],
    h=Carabiner[LENGTH] + 2*ThreadThick,
    center=true,$fn=BezelSides);
    translate([-Carabiner[OD]/2,0,0])
    cylinder(d=Carabiner[OD] – 2.0,
    h=Carabiner[LENGTH] + 2*ThreadThick,
    center=true,$fn=6*4);
    }
    cylinder(d=Carabiner[ID],
    h=2*Carabiner[LENGTH],
    center=true,$fn=BezelSides);
    translate([Carabiner[ID]/4,0,0])
    cube([Carabiner[ID],7.0,2*Carabiner[LENGTH]],center=true);
    }
    }
    // mockup of PCB holder atop remains of old mount with reel post
    // Z = 0 at midline of case
    module BezelMount() {
    rotate(180/BezelSides) {
    PolyCyl(Bezel[ID] + HoleWindage,MountThick,BezelSides); // PCB punches through mount
    PolyCyl(Bezel[OD] + HoleWindage,Bezel[LENGTH] – Reel[LENGTH]/2,BezelSides);
    }
    translate([Reel[OD]/2,0,BezelBlock[2]/2])
    scale([2,1,1])
    cube(BezelBlock,center=true);
    }
    // Main mount around holder & carabiner
    module Mount(Section="All") {
    render()
    difference() {
    hull() {
    for (a = BoltAngles) // spheres defining corners
    for (i=[-1,1])
    rotate(i*a)
    translate([BCD/2,0,0])
    sphere(d=MountThick,$fn=MountSides);
    cylinder(d=Carabiner[OD] + 4*ThreadWidth,
    h=MountThick,center=true); // capture carabiner ring
    }
    for (a = BoltAngles) // screw & insert holes, head recess
    for (i=[-1,1])
    rotate(i*a)
    translate([BCD/2,0,0])
    rotate(0*i*180/ScrewSides) {
    translate([0,0,-(Insert[LENGTH] + 2*ThreadThick)])
    PolyCyl(Insert[OD],
    Insert[LENGTH] + 2*ThreadThick + Protrusion,ScrewSides);
    for (k = [-2:2]) // epoxy retaining grooves
    translate([0,0,-(k*3*ThreadThick + Insert[LENGTH]/2)])
    PolyCyl(Insert[OD] + 1*ThreadWidth,
    2*ThreadThick,ScrewSides);
    PolyCyl(Screw[ID],Screw[LENGTH],ScrewSides);
    translate([0,0,MountThick/2 – ScrewRecess]) // recess screw heads
    PolyCyl(Screw[OD],Screw[LENGTH],ScrewSides);
    }
    translate([0,0,-1*ThreadThick]) // Minkowski Z extends only top surface!
    minkowski() { // space for metal carabiner
    Beener();
    // cube([ThreadWidth,ThreadWidth,2*ThreadThick]);
    cylinder(d=ThreadWidth,h=2*ThreadThick,$fn=6);
    }
    rotate([0,90,0]) rotate(180/6) // cord channel = brass tube clearance
    PolyCyl(Bullet[ID],Carabiner[ID],6);
    translate([Eyelet[LENGTH] + 2.0,0,0]) // eyelet, large end inward
    rotate([0,90,0]) rotate(180/6)
    PolyCyl(Eyelet[OD] + HoleWindage, Reel[OD]/2,6);
    if (false)
    translate([Reel[OD]/2 + Eyelet[LENGTH]/2,0,0]) // eyelet, small end outward
    rotate([0,90,0]) rotate(180/6)
    PolyCyl(Eyelet[ID],Eyelet[LENGTH],6);
    translate([(BCD/2 + MountThick/2)*cos(BoltAngles[0]) – Bullet[LENGTH],0,0]) // bullet recess
    rotate([0,90,0]) rotate(180/6)
    PolyCyl(Bullet[OD],Carabiner[ID],6);
    BezelMount(); // PCB holder clearance
    Reel(); // reel clearance
    translate([0,0,-(Reel[LENGTH] + ShimThick)/2]) // sliding plate on open side of reel
    cylinder(d=Reel[OD],h=ShimThick,center=true,$fn=6*4);
    translate([-Carabiner[OD]/2 + Latch[0],Latch[1],0])
    linear_extrude(height=Latch[2],center=true)
    polygon(LatchPoints);
    if (Section == "Upper") // display & build section cutting
    translate([0,0,-2*Carabiner[LENGTH]])
    cube(4*Carabiner,center=true);
    else if (Section == "Lower")
    translate([0,0,2*Carabiner[LENGTH]])
    cube(4*Carabiner,center=true);
    }
    if (Support) { // Completely ad-hoc support structures
    color("Yellow", Layout == "Show" ? 0.3 : 1.0) {
    if (false && Section == "Upper") {
    Spokes = BezelSides;
    Offset = 6*ThreadWidth;
    for (i = [2:Spokes – 2])
    rotate(i * 360/Spokes)
    translate([Offset,-ThreadWidth,0*(Carabiner[LENGTH]/2)/2])
    cube([Carabiner[OD]/2 – Offset – 0*ThreadWidth,
    2*ThreadWidth,
    Carabiner[LENGTH]/2],center=false);
    for (i = [0:Spokes – 1])
    rotate(i * 360/Spokes)
    translate([Offset,-ThreadWidth,0])
    cube([Bezel[OD]/2 – Offset,
    2*ThreadWidth,
    Bezel[LENGTH] – Reel[LENGTH]/2 – 2*ThreadThick],center=false);
    Bars = 7;
    render()
    difference() {
    union() {
    for (i = [-floor(Bars/2) : floor(Bars/2)])
    translate([-Carabiner[ID]/2,i*Carabiner[OD]/Bars,Carabiner[LENGTH]/4])
    cube([Carabiner[ID]/3,2*ThreadWidth,Carabiner[LENGTH]/2],center=true);
    translate([-Carabiner[ID]/2,0,ThreadThick/2])
    cube([Carabiner[ID]/3,Carabiner[ID],ThreadThick],center=true);
    }
    cylinder(d=Carabiner[ID] + 2*ThreadWidth,h=Carabiner[LENGTH]);
    }
    }
    if (Section == "Lower") {
    translate([0,0,-(Reel[LENGTH]/4 + ShimThick/2 – ThreadThick/2)])
    for (i = [0:8])
    rotate(i * 360/8)
    cube([Reel[OD] – 2*ThreadWidth,
    2*ThreadWidth,
    Reel[LENGTH]/2 + ShimThick – ThreadThick],center=true);
    if (false) {
    Bars = 7;
    render()
    difference() {
    union() {
    for (i = [-floor(Bars/2) : floor(Bars/2)])
    translate([-Carabiner[ID]/2,i*Carabiner[OD]/Bars,-Carabiner[LENGTH]/4])
    cube([Carabiner[ID]/3,2*ThreadWidth,Carabiner[LENGTH]/2],center=true);
    translate([-Carabiner[ID]/2,0,-ThreadThick/2])
    cube([Carabiner[ID]/3,Carabiner[ID],ThreadThick],center=true);
    }
    translate([0,0,-Carabiner[LENGTH]])
    cylinder(d=Carabiner[ID] + 0*ThreadWidth,h=Carabiner[LENGTH]);
    }
    }
    }
    }
    }
    }
    //———————-
    // Build it
    if (Layout == "Beener")
    Beener();
    if (Layout == "Mount")
    Mount();
    if (Layout == "Reel")
    Reel();
    if (Layout == "BezelMount")
    BezelMount();
    Gap = 25;
    if (Layout == "Show") {
    translate([0,0,Gap/2])
    Mount("Upper");
    translate([0,0,-Gap/2])
    Mount("Lower");
    color("Green",0.3)
    Beener();
    color("Brown",0.3)
    Reel();
    color("Red",0.3)
    translate([0,0,-(Reel[LENGTH] + ShimThick)/2])
    cylinder(d=Reel[OD],h=ShimThick,center=true,$fn=6*4);
    }
    if (Layout == "Build") {
    translate([(BCD + MountThick)/2,0,0])
    rotate(180)
    Mount("Upper");
    rotate([180,0,0])
    translate([-(BCD + MountThick)/2,0,0])
    Mount("Lower");
    }
    if (Layout == "BuildUpper")
    Mount("Upper");
    if (Layout == "BuildLower")
    rotate([180,0,0])
    Mount("Lower");

     

  • Microscope 60 LED Ring Light Adapter

    The Barbie-themed microscope light I built from an angel eye LED ring worked fine for the last six years (!), but a much brighter ring with 60 aimed 5 mm LEDs for $17 delivered from a US seller caught my eye:

    Microscope 60 LED ring light - in use
    Microscope 60 LED ring light – in use

    Although this ring looks much more professional, it didn’t quite fit the microscope, being designed for a round snout rather than a squarish one. This snout has a 47-ish mm threaded ring intended for filters & suchlike, so I built an adapter between that and the 60 mm ID of the LED ring:

    Microscope 60 LED Ring Light Adapter - top - Slic3r
    Microscope 60 LED Ring Light Adapter – top – Slic3r

    The ring came with three long knurled screws which I replaced with much tidier M3 socket-head screws going into those holes:

    Microscope 60 LED ring light - assembled - top
    Microscope 60 LED ring light – assembled – top

    The part going into the snout threads is deliberately (honest!) a bit small, so I could wrap it with soft tape for a good friction fit. The Barbie Ring didn’t weigh anything and I wound up using squares of double-sticky foam tape; it could come to that for this ring, too.

    The adapter features a taper on the bottom for no particularly good reason, as the field-of-view tapers inward, not outward:

    Microscope 60 LED Ring Light Adapter - bottom - Slicer
    Microscope 60 LED Ring Light Adapter – bottom – Slicer

    Seen from the bug’s POV, it’s a rather impressive spectacle:

    Microscope 60 LED ring light - assembled - bottom
    Microscope 60 LED ring light – assembled – bottom

    The control box sports a power switch and a brightness knob. Come to find out the ring is actually too bright at full throttle; a nice problem to have.

    That was easy!

    The OpenSCAD source code as a GitHub Gist:

    // LED Ring Light Mount – 60 mm ID ring
    // Ed Nisley KE4ZNU April 2017
    //- 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
    ID = 0;
    OD = 1;
    LENGTH = 2;
    ScopeThread = [43.0,46.5,4.0]; // scope snout thread, ID = minimum invisible
    LEDRing = [ScopeThread[ID],60.0,8.0];
    LEDScrewOffset = 4.0;
    LEDScrewOD = 3.0;
    LEDScrews = 3;
    OAH = ScopeThread[LENGTH] + LEDRing[LENGTH];
    NumSides = 3*4*LEDScrews; // get symmetry for screws
    //———————-
    // 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);
    }
    //———————-
    // Build it
    difference() {
    rotate(180/NumSides)
    union() {
    cylinder(d=ScopeThread[OD],h=OAH,$fn=NumSides);
    cylinder(d=LEDRing[OD],h=LEDRing[LENGTH],$fn=NumSides);
    }
    translate([0,0,-Protrusion])
    rotate(180/NumSides)
    cylinder(d=ScopeThread[ID],h=OAH + 2*Protrusion,$fn=NumSides);
    translate([0,0,-Protrusion])
    rotate(180/NumSides)
    cylinder(d1=LEDRing[OD] – 2*6*ThreadWidth,
    d2=ScopeThread[ID],
    h=LEDRing[LENGTH] + Protrusion,$fn=NumSides);
    for (i=[0:LEDScrews-1])
    rotate(i*360/LEDScrews)
    translate([LEDRing[OD]/2 – LEDScrewOD,0,LEDRing[LENGTH] – LEDScrewOffset])
    rotate([0,90,0]) rotate(180/6)
    cylinder(d=LEDScrewOD,h=LEDScrewOD + Protrusion,$fn=6);
    }

     

  • Cylindrical Cell Adapters

    An octet of Eneloop AAA cells arrived, I wanted to measure their as-delivered charge (the package says “Factory Charged With SOLAR ENERGY”, so you know it’s good), and discovered I’d given away my AAA cell holders. You can actually get inter-series adapters on eBay, but what’s the fun in that? Plus, I didn’t want to delay gratification for a month; you know how it is.

    Soooo:

    AAA to AA Adapter - top - Slic3r
    AAA to AA Adapter – top – Slic3r

    It’s basically an AA-size sleeve that fits over the AAA cell, with a lathe-turned brass post conducting juice from the + terminal of the inner cell outward:

    AAA to AA Adapter - parts
    AAA to AA Adapter – parts

    Not much to look at when it’s assembled:

    AAA to AA Adapter - assembled
    AAA to AA Adapter – assembled

    The AAA cell fits deliberately loose, because this goes into a metal clip holding everything firmly in place for the battery tester:

    AAA to AA Adapter - in use
    AAA to AA Adapter – in use

    The source code tabulates the sizes of several cylindrical cells, exactly zero other pairs of which have been tested; I expect most won’t work correctly. In particular, the table entries should include the contact button OD and thickness for each cell, so that I can turn out the proper terminal for each pair of cells. If I ever need a different adapter, I’ll beat some cooperation out of that, too.

    Discovered I needed an adapter after breakfast, started testing cells after lunch. Life is good!

    The OpenSCAD source code as a GitHub Gist:

    // Cylindrical cell adapters
    // Ed Nisley KE4ZNU April 2017
    //- 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
    OutCell = "AA"; // cell sizes
    InCell = "AAA";
    BottomClear = 3*ThreadThick; // shorten outer shell to allow base protrusion
    Terminal = [3.0,4.0,2.0]; // terminal: OD = nub dia, length = nub thickness
    NAME = 0;
    ID = 0; // for non-cell cylinders
    OD = 1;
    LENGTH = 2;
    Cells = [
    ["AAAA",8.3,42.5],
    ["AAA",10.5,44.5],
    ["AA",14.5,50.5],
    ["C",26.2,50],
    ["D",34.2,61.5],
    ["A23",10.3,28.5],
    ["CR123",17.0,34.5],
    ["18650",18.6,65.2]
    ];
    Outer = search([OutCell],Cells,1,0)[0];
    Inner = search([InCell],Cells,1,0)[0];
    echo(str("Outer cell: ",Cells[Outer][NAME]));
    echo(str("Inner cell: ",Cells[Inner][NAME]));
    echo(str("Wall: ",Cells[Outer][OD] – (Cells[Inner][OD]/cos(180/NumSides) + 2*ThreadWidth)));
    Delta = Cells[Outer][LENGTH] – Cells[Inner][LENGTH];
    echo(str("Terminal OAL: ",Delta));
    echo(str(" … head: ",Terminal[LENGTH]));
    echo(str(" … shaft: ",Delta – Terminal[LENGTH]));
    NumSides = 3*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);
    }
    //———————-
    // Construct adapter
    module Adapter() {
    difference() {
    cylinder(d=Cells[Outer][OD],
    h=Cells[Outer][LENGTH] – BottomClear – Terminal[LENGTH],
    $fn=NumSides);
    translate([0,0,Delta – Terminal[LENGTH]])
    PolyCyl(Cells[Inner][OD] + 2*ThreadWidth,
    Cells[Inner][LENGTH] + Protrusion,
    NumSides);
    translate([0,0,-Protrusion])
    PolyCyl(Terminal[ID],
    2*Cells[Outer][LENGTH],
    6);
    }
    }
    //———————-
    // Build it
    Adapter();

    The original doodle:

    AAA to AA Adapter - sketch
    AAA to AA Adapter – sketch
  • Patient Sign-In FAIL

    We must announce our arrival at the dentist by signing in through a web-based iPad app:

    Dentist iPad sign-in - network fail
    Dentist iPad sign-in – network fail

    You’ll note the signal strength indicator in the upper left shows as much RF as one might reasonably expect from a router within line-of-sight across the room.

    FWIW, I’m getting really tired of the hipster dark-gray on light-gray design ethos.

  • Raspberry Pi Slowdown

    At first, the yard camera worked fine, but a few days later the stream of JPEG images would unpredictably stall. I connect to it through a public-key SSH session and, sometimes, the login would stall for tens of seconds and, with a session set up, various exciting operation like, say, htop would unpredictably stall; if I waited long enough, they’d complete normally.

    This seemed familiar:

    Samsung 16 GB Evo MicroSD card
    Samsung 16 GB Evo MicroSD card

    It’s a known-good card from a reputable supplier, not that that means much these days. The camera flash highlights the gritty silkscreen (?) texture of the orange overlay, but the production value seems high enough to pass muster.

    Popping the card in my desktop PC showed:

    • It remains functional, at least to the extent of being mount-able and write-able
    • 3probe --time-ops /dev/sdb showed it still held 16 GB
    • fsck -fv /dev/sdb[12] shows no problems
    • Both partitions looked good

    So I shrank the main partition to 7.5 GB, copied the image to the desktop PC’s SSD, fired up the Token Windows Laptop, ran the Official SD Card Formatter, and discovered that it thought the card had only 63 MB (yes, MB) available. That’s the size of the FAT boot partition, so I returned the card to the desktop PC, unleashed gparted on it, blew away the partitions, reformatted the whole thing to one 16 GB FAT32 partition, and stuck it back in the laptop, whereupon the Official Formatter agreed it had every byte it should.

    A format-with-overwrite then proceeded apace; the card doesn’t support format-with-erase.

    Back in the desktop, I copied the saved image back onto the card which, en passant, blew away the just-created FAT format and restored the Raspbian partition structure. The 8 GB of that copy proceeded at an average 12.1 MB/s. I did not watch the transfer closely enough to notice any protracted delays.

    Back in the Pi, the card booted and ran perfectly, sending an image every second to the laptop (now running its usual Mint Linux) on the guest network:

    Turkey flock in driveway - 2017-03-21
    Turkey flock in driveway – 2017-03-21

    SSH sessions now work perfectly, too, and commands no longer jam.

    So it seems a good-quality MicroSD card can experience protracted delays while writing data, to the extent of tens of seconds, stalling the Pi in mid-operation without producing data errors or any other symptoms.

    It’s not clear the Official Formatter does anything that simply copying the image back to the card wouldn’t also accomplish, although overwriting the entire 16 GB extent of the card exercises all the cells and forces the card controller to re/de/un/allocate bad blocks. If, indeed, the blocks are bad, rather than just achingly slow.

    Moral of the story: Don’t use MicroSD cards as mass storage devices, at least not for industrial applications that require consistent performance.

  • Vacuum Tube Lights: Duodecar Rebuild

    You’ll recall the LED atop the 21HB5A tube failed, shortly after replacing the bottom LED and rewiring the ersatz plate lead, which led me to rebuild the whole thing with SK6812 RGBW LEDs. So I printed all the plastic parts again, because the duodecar tube socket’s pin circle can fit into a hard drive platter’s unmodified 25 mm hole, then drilled another platter to suit:

    Duodecar disk drilling
    Duodecar disk drilling

    The hole under the drill fits the 3.5 mm stereo socket for the ersatz plate lead, so it’s bigger than before.

    I’ve switched from Arduino Pro Minis with a separate USB converter to Arduino Nanos with an on-board CH340 USB chip, because the fake FTDI chips on the converters are a continuing aggravation:

    21HB5A base - interior
    21HB5A base – interior

    Adding those wire slots to the sockets definitely helps tidy things up; the wires no longer need a crude cable tie anchoring them to the socket mounting screws.

    I wanted to drive the LEDs from the A7 pin, rather than the A3 pin I’d been using on the Pro Minis, to keep the wires closer together, but it turns out that A6 and A7 can’t become digital output pins. So I used A5, although I may come to regret the backward incompatibility.

    In any event, the 21HB5A tube looks spiffy with its new LEDs in full effect:

    21HB5A with RBGBW LEDs - cyan violet phase
    21HB5A with RBGBW LEDs – cyan violet phase

    I dialed the white LED PWM down to 32, making the colors somewhat pastel, rather than washed-out.

    The Arduino source code as a GitHub Gist:

    // Neopixel mood lighting for vacuum tubes
    // Ed Nisley – KE4ANU – June 2016
    // September 2016 – Add Morse library and blinkiness
    // October 2016 – Set random colors at cycle end
    // March 2017 – RGBW SK6812 LEDs
    #include <Adafruit_NeoPixel.h>
    #include <morse.h>
    #include <Entropy.h>
    //———-
    // Pin assignments
    const byte PIN_NEO = A5; // DO – data out to first Neopixel
    const byte PIN_HEARTBEAT = 13; // DO – Arduino LED
    #define PIN_MORSE 12
    //———-
    // Constants
    // number of pixels
    #define PIXELS 2
    // index of the Morse output pixel and how fast it sends
    boolean Send_Morse = false;
    #define PIXEL_MORSE (PIXELS – 1)
    #define MORSE_WPM 10
    // lag between adjacent pixel, degrees of slowest period
    #define PIXELPHASE 45
    // update LEDs only this many ms apart (minus loop() overhead)
    #define UPDATEINTERVAL 50ul
    #define UPDATEMS (UPDATEINTERVAL – 1ul)
    // number of steps per cycle, before applying prime factors
    #define RESOLUTION 500
    //———-
    // Globals
    // instantiate the Neopixel buffer array
    Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIXELS, PIN_NEO, NEO_GRBW + NEO_KHZ800);
    uint32_t FullWhite = strip.Color(255,255,255,255);
    uint32_t FullOff = strip.Color(0,0,0,0);
    uint32_t MorseColor;
    struct pixcolor_t {
    unsigned int Prime;
    unsigned int NumSteps;
    unsigned int Step;
    float StepSize;
    float Phase;
    byte MaxPWM;
    };
    unsigned int PlatterSteps;
    byte PrimeList[] = {3,5,7,13,19,29};
    // colors in each LED
    enum pixcolors {RED, GREEN, BLUE, WHITE, PIXELSIZE};
    struct pixcolor_t Pixels[PIXELSIZE]; // all the data for each pixel color intensity
    uint32_t UniColor;
    unsigned long MillisNow;
    unsigned long MillisThen;
    // Morse code
    char * MorseText = " cq cq cq de ke4znu";
    LEDMorseSender Morse(PIN_MORSE, (float)MORSE_WPM);
    uint8_t PrevMorse, ThisMorse;
    //– Figure PWM based on current state
    byte StepColor(byte Color, float Phi) {
    byte Value;
    Value = (Pixels[Color].MaxPWM / 2.0) * (1.0 + sin(Pixels[Color].Step * Pixels[Color].StepSize + Phi));
    // Value = (Value) ? Value : Pixels[Color].MaxPWM; // flash at dimmest points for debug
    return Value;
    }
    //– Select three unique primes for the color generator function
    // Then compute all the step parameters based on those values
    void SetColorGenerators(void) {
    Pixels[RED].Prime = PrimeList[random(sizeof(PrimeList))];
    do {
    Pixels[GREEN].Prime = PrimeList[random(sizeof(PrimeList))];
    } while (Pixels[RED].Prime == Pixels[GREEN].Prime);
    do {
    Pixels[BLUE].Prime = PrimeList[random(sizeof(PrimeList))];
    } while (Pixels[BLUE].Prime == Pixels[RED].Prime ||
    Pixels[BLUE].Prime == Pixels[GREEN].Prime);
    do {
    Pixels[WHITE].Prime = PrimeList[random(sizeof(PrimeList))];
    } while (Pixels[WHITE].Prime == Pixels[RED].Prime ||
    Pixels[WHITE].Prime == Pixels[GREEN].Prime ||
    Pixels[WHITE].Prime == Pixels[BLUE].Prime);
    printf("Primes: %d %d %d %d\r\n",Pixels[RED].Prime,Pixels[GREEN].Prime,Pixels[BLUE].Prime,Pixels[WHITE].Prime);
    Pixels[RED].MaxPWM = 255;
    Pixels[GREEN].MaxPWM = 255;
    Pixels[BLUE].MaxPWM = 255;
    Pixels[WHITE].MaxPWM = 32;
    unsigned int PhaseSteps = (unsigned int) ((PIXELPHASE / 360.0) *
    RESOLUTION * (unsigned int) max(max(max(Pixels[RED].Prime,Pixels[GREEN].Prime),Pixels[BLUE].Prime),Pixels[WHITE].Prime));
    printf("Pixel phase offset: %d deg = %d steps\r\n",(int)PIXELPHASE,PhaseSteps);
    for (byte c=0; c < PIXELSIZE; c++) {
    Pixels[c].NumSteps = RESOLUTION * Pixels[c].Prime; // steps per cycle
    Pixels[c].StepSize = TWO_PI / Pixels[c].NumSteps; // radians per step
    Pixels[c].Step = random(Pixels[c].NumSteps); // current step
    Pixels[c].Phase = PhaseSteps * Pixels[c].StepSize;; // phase in radians for this color
    printf(" c: %d Steps: %d Init: %d Phase: %d deg",c,Pixels[c].NumSteps,Pixels[c].Step,(int)(Pixels[c].Phase * 360.0 / TWO_PI));
    printf(" PWM: %d\r\n",Pixels[c].MaxPWM);
    }
    }
    //– Helper routine for printf()
    int s_putc(char c, FILE *t) {
    Serial.write(c);
    }
    //——————
    // Set the mood
    void setup() {
    pinMode(PIN_HEARTBEAT,OUTPUT);
    digitalWrite(PIN_HEARTBEAT,LOW); // show we arrived
    Serial.begin(57600);
    fdevopen(&s_putc,0); // set up serial output for printf()
    printf("Vacuum Tube Mood Light – RGBW\r\nEd Nisley – KE4ZNU – March 2017\r\n");
    Entropy.initialize(); // start up entropy collector
    // set up pixels
    strip.begin();
    strip.show();
    // lamp test: a brilliant white flash
    printf("Lamp test: flash white\r\n");
    for (byte i=0; i<5 ; i++) {
    for (int j=0; j < strip.numPixels(); j++) { // fill LEDs with white
    strip.setPixelColor(j,FullWhite);
    }
    strip.show();
    delay(500);
    for (int j=0; j < strip.numPixels(); j++) { // fill LEDs with black
    strip.setPixelColor(j,FullOff);
    }
    strip.show();
    delay(500);
    }
    // get an actual random number
    uint32_t rn = Entropy.random();
    printf("Random seed: %08lx\r\n",rn);
    randomSeed(rn);
    // set up the color generators
    SetColorGenerators();
    // set up Morse generator
    Morse.setup();
    Morse.setMessage(String(MorseText));
    MorseColor = strip.Color(255,random(32,64),random(16),0);
    PrevMorse = ThisMorse = digitalRead(PIN_MORSE);
    printf("Morse enabled: %d at %d wpm color: %08lx\n [%s]\r\n",Send_Morse,MORSE_WPM,MorseColor,MorseText);
    MillisNow = MillisThen = millis();
    }
    //——————
    // Run the mood
    void loop() {
    if (!Morse.continueSending()) {
    printf("Restarting Morse message\r\n");
    Morse.startSending();
    }
    ThisMorse = digitalRead(PIN_MORSE);
    MillisNow = millis();
    if (((MillisNow – MillisThen) >= UPDATEMS) || // time for color change?
    (PrevMorse != ThisMorse)) { // Morse output bit changed?
    digitalWrite(PIN_HEARTBEAT,HIGH);
    if (Send_Morse && ThisMorse) { // if Morse output high, overlay flash
    strip.setPixelColor(PIXEL_MORSE,MorseColor);
    }
    PrevMorse = ThisMorse;
    strip.show(); // send out precomputed colors
    boolean CycleRun = false; // check to see if all cycles have ended
    for (byte c=0; c < PIXELSIZE; c++) { // compute next increment for 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));
    }
    else {
    CycleRun = true; // this color is still cycling
    }
    }
    // If all cycles have completed, reset the color generators
    if (!CycleRun) {
    printf("All cycles ended: setting new color generator values\r\n");
    SetColorGenerators();
    }
    for (int i=0; i < strip.numPixels(); i++) { // for each pixel
    byte Value[PIXELSIZE];
    for (byte c=0; c < PIXELSIZE; c++) { // … for each color
    Value[c] = (Pixels[c].MaxPWM / 2.0) * (1.0 + sin(Pixels[c].Step * Pixels[c].StepSize – i*Pixels[c].Phase));
    }
    UniColor = strip.Color(Value[RED],Value[GREEN],Value[BLUE],Value[WHITE]);
    strip.setPixelColor(i,UniColor);
    }
    MillisThen = MillisNow;
    digitalWrite(PIN_HEARTBEAT,LOW);
    }
    }
    view raw TubeMorse.ino hosted with ❤ by GitHub
  • Raspberry Pi Yard Camera

    The yard camera I mentioned a few days ago consists of a Raspberry Pi 3 with an Official V2 Pi Camera peering through two layers of 1955-era window glass into our back yard:

    Back Yard Camera setup - 2017-03-13
    Back Yard Camera setup – 2017-03-13

    Yes, that’s black duct tape holding it to the window pane. The extension cord draped across the floor gotta go, too.

    This being a made-in-haste lashup, I used the streamEye MJPEG HTTP streamer, started from /etc/rc.local in the usual way:

    logger -s Starting camera streamer
    sudo -u pi sh -c '/home/pi/yardcam.sh' &
    logger -s Camera running
    

    The yardcam.sh script feeds one moderate-quality frame to the streamer every second:

    /home/pi/streameye/extras/raspimjpeg.py -w 1280 -h 720 -r 1 -q 80 | streameye
    

    MJPEG has a lot to dislike as a streaming video format. In particular, without any hint of inter-frame compression, the network usage gets way too high for any reasonable frame rate.

    But it got the camera up & running in time for the March snowfall:

    Fun in Snow - 2017-03-15
    Fun in Snow – 2017-03-15

    In a nod to IoT security, the Raspberry Pi’s wireless interface sits behind the router’s firewall on our guest network, with no access to the devices on our main network. The router passes a one-port peephole from the Internet to the Pi, which protects all the other services from unwarranted attention.

    The router maintains a dynamic DNS record with a (not particularly) mnemonic URL, which seems better than an ever-changing dotted-quad IP address.

    Because the router doesn’t support hairpin connections from the main network to the guest network, I can’t monitor the video from my desktop through the outwardly visible URL. Instead, I must fire up a laptop, connect to the guest network, then connect directly to the camera at camera.local.

    You do not have a Need To Know for the URL; I’m sure it’ll appear on Shodan. I plan to take it down when the snow melts.