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

  • Vacuum Tube LEDs: Hard Drive Platter Base

    Stainless steel socket head and button head screws add a certain techie charm to the hard drive platter mirroring the Noval tube:

    Noval - Black PETG base - magenta phase
    Noval – Black PETG base – magenta phase

    Black PETG, rather than cyan or natural filament, suppresses the socket’s glow and emphasizes the tube’s internal lighting:

    Noval tube on platter - button-head screws
    Noval tube on platter – button-head screws

    The base puts the USB-to-serial adapter on the floor and stands the Pro Mini against a flat on the far wall:

    Noval tube socket and base - interior layout
    Noval tube socket and base – interior layout

    A notch for the cable seems like a useful addition subtraction to the socket, because that cable tie just doesn’t look right. I used 4 mm threaded inserts, as those button head screws looked better.

    The solid model looks like you’d expect:

    Vacuum Tube Lights - hard drive platter base - solid model
    Vacuum Tube Lights – hard drive platter base – solid model

    Those are 3 mm threaded inserts, again to get the right head size screw on the platter.

    The height of the base depends on the size of the socket, with the model maintaining a bit of clearance above the USB adapter. The OD depends on the platter OD, with a fixed overhang, and the insert BCD depends on the OD / insert OD / base wall thickness.

    Although I’m using an Arduino Pro Mini and a separate USB-to-serial adapter, a (knockoff) Arduino Nano would be better and cheaper, although the SMD parts on the Nano’s bottom surface make it a bit thicker and less suitable for foam-tape mounting.

    I drilled the platter using manual CNC:

    Hard drive platter - Noval base drilling
    Hard drive platter – Noval base drilling

    After centering the origin on the platter hole, the hole positions (for a 71 mm BCD) use LinuxCNC’s polar notation:

    g0 @[71/2]^45
    g0 @[71/2]^[45+90]
    g0 @[71/2]^[45+180]
    g0 @[71/2]^-45
    

    I used the Joggy Thing for manual drilling after each move; that’s easier than figuring out the appropriate g81 feed & speed.

    The 3D printed base still looks a bit chintzy compared with the platter, but it’s coming along.

    The OpenSCAD source code as a GitHub Gist:

    // Vacuum Tube LED Lights
    // Ed Nisley KE4ZNU February … September 2016
    Layout = "PlatterBase"; // Cap LampBase USBPort Bushings
    // Socket(s) (Build)FinCap Platter[Base|Fixture]
    DefaultSocket = "Noval";
    Section = false; // cross-section the object
    Support = true;
    //- 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
    // https://en.wikipedia.org/wiki/Tube_socket#Summary_of_Base_Details
    // punch & screw OC modified for drive platter chassis plate
    // platter = 25 mm ID
    // CD = 15 mm ID with raised ring at 37 mm, needs screw head clearance
    T_NAME = 0; // common name
    T_NUMPINS = 1; // total, with no allowance for keying
    T_PINBCD = 2; // tube pin circle diameter
    T_PINOD = 3; // … diameter
    T_PINLEN = 4; // … length (must also clear evacuation tip / spigot)
    T_HOLEOD = 5; // nominal panel hole from various sources
    T_PUNCHOD = 6; // panel hole optimized for inch-size Greenlee punches
    T_TUBEOD = 7; // envelope or base diameter
    T_PIPEOD = 8; // light pipe from LED to tube base (clear evac tip / spigot)
    T_SCREWOC = 9; // mounting screw holes
    // Name pins BCD dia length hole punch tube pipe screw
    TubeData = [
    ["Mini7", 8, 9.53, 1.016, 7.0, 16.0, 25.0, 18.0, 5.0, 35.0], // punch 11/16, screw 22.5 OC
    ["Octal", 8, 17.45, 2.36, 10.0, 36.2, (8 + 1)/8 * inch, 32.0, 11.5, 47.0], // screw 39.0 OC
    ["Noval", 10, 11.89, 1.1016, 7.0, 22.0, 25.0 , 21.0, 7.5, 35.0], // punch 7/8, screw 28.0 OC
    ["Magnoval", 10, 17.45, 1.27, 9.0, 29.7, (4 + 1)/4 * inch, 46.0, 12.4, 38.2], // similar to Novar
    ["Duodecar", 13, 19.10, 1.05, 9.0, 32.0, (4 + 1)/4 * inch, 38.0, 12.5, 47.0], // screw 39.0 OC
    ];
    ID = 0;
    OD = 1;
    LENGTH = 2;
    Pixel = [7.0,10.0,3.0]; // ID = contact patch, OD = PCB dia, LENGTH = overall thickness
    SocketNut = // socket mounting: threaded insert or nut recess
    // [3.5,5.2,7.2] // 6-32 insert
    [4.0,6.0,5.9] // 4 mm short insert
    ;
    NutSides = 8;
    SocketShim = 2*ThreadThick; // between pin holes and pixel top
    SocketFlange = 1.5; // rim around socket below punchout
    PanelThick = 1.5; // socket extension through punchout
    FinCutterOD = 1/8 * inch;
    FinCapSize = [(Pixel[OD] + 2*FinCutterOD),30.0,(10.0 + 2*Pixel[LENGTH])];
    USBPCB =
    // [28,16,6.5] // small Sparkfun knockoff
    [36,18 + 1,5.8 + 0.4] // Deek-Robot fake FTDI with ISP header
    ;
    Platter = [25.0,95.0,1.26]; // hard drive platter dimensions
    //———————-
    // 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);
    }
    //———————-
    // Tube cap
    CapTube = [4.0,3/16 * inch,10.0]; // brass tube for flying lead to cap LED
    CapSize = [Pixel[ID],(Pixel[OD] + 2.0),(CapTube[OD] + 2*Pixel[LENGTH])];
    CapSides = 8*4;
    module Cap() {
    difference() {
    union() {
    cylinder(d=CapSize[OD],h=(CapSize[LENGTH]),$fn=CapSides); // main cap body
    translate([0,0,CapSize[LENGTH]]) // rounded top
    scale([1.0,1.0,0.65])
    sphere(d=CapSize[OD]/cos(180/CapSides),$fn=CapSides); // cos() fixes slight undersize vs cylinder
    cylinder(d1=(CapSize[OD] + 2*3*ThreadWidth),d2=CapSize[OD],h=1.5*Pixel[LENGTH],$fn=CapSides); // skirt
    }
    translate([0,0,-Protrusion]) // bore for wiring to LED
    PolyCyl(CapSize[ID],(CapSize[LENGTH] + 3*ThreadThick + Protrusion),CapSides);
    translate([0,0,-Protrusion]) // PCB recess with clearance for tube dome
    PolyCyl(Pixel[OD],(1.5*Pixel[LENGTH] + Protrusion),CapSides);
    translate([0,0,(1.5*Pixel[LENGTH] – Protrusion)]) // small step + cone to retain PCB
    cylinder(d1=(Pixel[OD]/cos(180/CapSides) + HoleWindage),d2=Pixel[ID],h=(Pixel[LENGTH] + Protrusion),$fn=CapSides);
    translate([0,0,(CapSize[LENGTH] – CapTube[OD]/(2*cos(180/8)))]) // hole for brass tube holding wire loom
    rotate([90,0,0]) rotate(180/8)
    PolyCyl(CapTube[OD],CapSize[OD],8);
    }
    }
    //———————-
    // Heatsink tube cap
    module FinCap() {
    CableOD = 3.5; // cable + braid diameter
    BulbOD = 3.75 * inch; // bulb OD; use 10 inches for flat
    echo(str("Fin Cutter: ",FinCutterOD));
    FinSides = 2*4;
    BulbRadius = BulbOD / 2;
    BulbDepth = BulbRadius – sqrt(pow(BulbRadius,2) – pow(FinCapSize[OD],2)/4);
    echo(str("Bulb OD: ",BulbOD," recess: ",BulbDepth));
    NumFins = floor(PI*FinCapSize[ID] / (2*FinCutterOD));
    FinAngle = 360 / NumFins;
    echo(str("NumFins: ",NumFins," angle: ",FinAngle," deg"));
    difference() {
    union() {
    cylinder(d=FinCapSize[ID],h=FinCapSize[LENGTH],$fn=2*NumFins); // main body
    for (i = [0:NumFins – 1]) // fins
    rotate(i * FinAngle)
    hull() {
    translate([FinCapSize[ID]/2,0,0])
    rotate(180/FinSides)
    cylinder(d=FinCutterOD,h=FinCapSize[LENGTH],$fn=FinSides);
    translate([(FinCapSize[OD] – FinCutterOD)/2,0,0])
    rotate(180/FinSides)
    cylinder(d=FinCutterOD,h=FinCapSize[LENGTH],$fn=FinSides);
    }
    rotate(FinAngle/2) // cable entry boss
    translate([FinCapSize[ID]/2,0,FinCapSize[LENGTH]/2])
    cube([FinCapSize[OD]/4,FinCapSize[OD]/4,FinCapSize[LENGTH]],center=true);
    }
    for (i = [1:NumFins – 1]) // fin inner gullets, omit cable entry side
    rotate(i * FinAngle + FinAngle/2) // joint isn't quite perfect, but OK
    translate([FinCapSize[ID]/2,0,-Protrusion])
    rotate(0*180/FinSides)
    cylinder(d=FinCutterOD/cos(180/FinSides),h=(FinCapSize[LENGTH] + 2*Protrusion),$fn=FinSides);
    translate([0,0,-Protrusion]) // PCB recess
    PolyCyl(Pixel[OD],(1.5*Pixel[LENGTH] + Protrusion),FinSides);
    PolyCyl(Pixel[ID],(FinCapSize[LENGTH] – 3*ThreadThick),FinSides); // bore for LED wiring
    translate([0,0,(FinCapSize[LENGTH] – 3*ThreadThick – 2*CableOD/(2*cos(180/8)))]) // cable inlet
    rotate(FinAngle/2) rotate([0,90,0]) rotate(180/8)
    PolyCyl(CableOD,FinCapSize[OD],8);
    if (BulbOD <= 10.0 * inch) // curve for top of bulb
    translate([0,0,-(BulbRadius – BulbDepth + 2*ThreadThick)]) // … slightly flatten tips
    sphere(d=BulbOD,$fn=16*FinSides);
    }
    }
    //———————-
    // Aperture for USB-to-serial adapter snout
    // These are all magic numbers, of course
    module USBPort() {
    translate([0,USBPCB[0]])
    rotate([90,0,0])
    linear_extrude(height=USBPCB[0])
    polygon(points=[
    [0,0],
    [USBPCB[1]/2,0],
    [USBPCB[1]/2,0.5*USBPCB[2]],
    [USBPCB[1]/3,USBPCB[2]],
    [-USBPCB[1]/3,USBPCB[2]],
    [-USBPCB[1]/2,0.5*USBPCB[2]],
    [-USBPCB[1]/2,0],
    ]);
    }
    //———————-
    // Box for Leviton ceramic lamp base
    module LampBase() {
    Insert = [3.5,5.2,7.2]; // 6-32 brass insert to match standard electrical screws
    Bottom = 3.0;
    Base = [4.0*inch,4.5*inch,20.0 + Bottom];
    Sides = 12*4;
    Retainer = [3.5,11.0,1.0]; // flat fiber washer holding lamp base screws in place
    StudSides = 8;
    StudOC = 3.5 * inch;
    Stud = [Insert[OD], // insert for socket screws
    min(15.0,1.5*(Base[ID] – StudOC)/cos(180/StudSides)), // OD = big enough to merge with walls
    (Base[LENGTH] – Retainer[LENGTH])]; // leave room for retainer
    union() {
    difference() {
    rotate(180/Sides)
    cylinder(d=Base[OD],h=Base[LENGTH],$fn=Sides);
    rotate(180/Sides)
    translate([0,0,Bottom])
    cylinder(d=Base[ID],h=Base[LENGTH],$fn=Sides);
    translate([0,-Base[OD]/2,Bottom + 1.2]) // mount on double-sided foam tape
    rotate(0)
    USBPort();
    }
    for (i = [-1,1])
    translate([i*StudOC/2,0,0])
    rotate(180/StudSides)
    difference() {
    cylinder(d=Stud[OD],h=Stud[LENGTH],$fn=StudSides);
    translate([0,0,Bottom])
    PolyCyl(Stud[ID],(Stud[LENGTH] – (Bottom – Protrusion)),6);
    }
    }
    }
    //———————-
    // Base for hard drive platters
    module PlatterBase(TubeName = DefaultSocket) {
    PCB =
    [36,18,3] // Arduino Pro Mini
    ;
    Tube = search([TubeName],TubeData,1,0)[0];
    SocketHeight = Pixel[LENGTH] + SocketShim + TubeData[Tube][T_PINLEN] – PanelThick;
    echo(str("Base for ",TubeData[Tube][0]," socket"));
    Overhang = 5.5; // platter overhangs base by this much
    Bottom = 4*ThreadThick;
    Base = [(Platter[OD] – 3*Overhang), // smaller than 3.5 inch Sch 40 PVC pipe…
    (Platter[OD] – 2*Overhang),
    2.0 + max(PCB[1],(2.0 + SocketHeight + USBPCB[2])) + Bottom];
    Sides = 24*4;
    echo(str(" Height: ",Base[2]," mm"));
    Insert = // platter mounting: threaded insert or nut recess
    // [3.5,5.2,7.2] // 6-32 insert
    [3.9,5.0,8.0] // 3 mm – long insert
    ;
    NumStuds = 4;
    StudSides = 8;
    Stud = [Insert[OD], // insert for socket screws
    2*Insert[OD], // OD = big enough to merge with walls
    Base[LENGTH]]; // leave room for retainer
    StudBCD = floor(Base[ID] – Stud[OD] + (Stud[OD] – Stud[ID])/2);
    echo(str("Platter screw BCD: ",StudBCD," mm"));
    PCBInset = Base[ID]/2 – sqrt(pow(Base[ID]/2,2) – pow(PCB[0],2)/4);
    union() {
    difference() {
    rotate(180/Sides)
    cylinder(d=Base[OD],h=Base[LENGTH],$fn=Sides);
    rotate(180/Sides)
    translate([0,0,Bottom])
    cylinder(d=Base[ID],h=Base[LENGTH],$fn=Sides);
    translate([0,-Base[OD]/2,Bottom + 1.2]) // mount PCB on foam tape
    rotate(0)
    USBPort();
    }
    for (a = [0:(NumStuds – 1)]) // platter mounting studs
    rotate(180/NumStuds + a*360/(NumStuds))
    translate([StudBCD/2,0,0])
    rotate(180/StudSides)
    difference() {
    cylinder(d=Stud[OD],h=Stud[LENGTH],$fn=2*StudSides);
    translate([0,0,Bottom])
    PolyCyl(Stud[ID],(Stud[LENGTH] – (Bottom – Protrusion)),StudSides);
    }
    intersection() { // microcontroller PCB mounting plate
    rotate(180/Sides)
    cylinder(d=Base[OD],h=Base[LENGTH],$fn=Sides);
    translate([-PCB[0]/2,(Base[ID]/2 – PCBInset),0])
    cube([PCB[0],Base[OD]/2,Base[LENGTH]],center=false);
    }
    difference() {
    intersection() { // totally ad-hoc bridge around USB opening
    rotate(180/Sides)
    cylinder(d=Base[OD],h=Base[LENGTH],$fn=Sides);
    translate([-1.25*USBPCB[1]/2,-(Base[ID]/2),0])
    cube([1.25*USBPCB[1],2.0,Base[LENGTH]],center=false);
    }
    translate([0,-Base[OD]/2,Bottom + 1.2]) // mount PCB on foam tape
    rotate(0)
    USBPort();
    }
    }
    }
    //———————-
    // Drilling fixture for disk platters
    module PlatterFixture() {
    StudOC = [1.16*inch,1.16*inch]; // Sherline tooling plate screw spacing
    StudClear = 5.0;
    BasePlate = [(20 + StudOC[0]*ceil(Platter[OD] / StudOC[0])),(Platter[OD] + 10),7.0];
    PlateRound = 10.0; // corner radius
    difference() {
    hull() // basic block
    for (i=[-1,1], j=[-1,1])
    translate([i*(BasePlate[0]/2 – PlateRound),j*(BasePlate[1]/2 – PlateRound),0])
    cylinder(r=PlateRound,h=BasePlate[2],$fn=4*4);
    for (i=[-1:1], j=[-1:1]) // index marks
    translate([i*100/2,j*100/2,BasePlate[2] – 2*ThreadThick])
    cylinder(d=1.5,h=1,$fn=6);
    for (i=[-1,1], j=[-1,0,1]) // holes for tooling plate studs
    translate([i*StudOC[0]*ceil(Platter[OD] / StudOC[0])/2,j*StudOC[0],-Protrusion])
    PolyCyl(StudClear,BasePlate[2] + 2*Protrusion,6);
    translate([0,0,-Protrusion]) // center clamp hole
    PolyCyl(StudClear,BasePlate[2] + 2*Protrusion,6);
    translate([0,0,BasePlate[2] – Platter[LENGTH]]) // disk locating recess
    linear_extrude(height=(Platter[LENGTH] + Protrusion),convexity=2)
    difference() {
    circle(d=(Platter[OD] + 1),$fn=8*4);
    circle(d=Platter[ID],$fn=8*4);
    }
    translate([0,0,BasePlate[2] – 4.0]) // drilling recess
    linear_extrude(height=(4.0 + Protrusion),convexity=2)
    difference() {
    circle(d=(Platter[OD] – 10),$fn=8*4);
    circle(d=(Platter[ID] + 10),$fn=8*4);
    }
    }
    }
    //———————-
    // Tube Socket
    module Socket(Name = DefaultSocket) {
    NumSides = 6*4;
    Tube = search([Name],TubeData,1,0)[0];
    echo(str("Building ",TubeData[Tube][0]," socket"));
    echo(str(" Punch: ",TubeData[Tube][T_PUNCHOD]," mm = ",TubeData[Tube][T_PUNCHOD]/inch," inch"));
    echo(str(" Screws: ",TubeData[Tube][T_SCREWOC]," mm =",TubeData[Tube][T_SCREWOC]/inch," inch OC"));
    OAH = Pixel[LENGTH] + SocketShim + TubeData[Tube][T_PINLEN];
    BaseHeight = OAH – PanelThick;
    difference() {
    union() {
    linear_extrude(height=BaseHeight) // base outline
    hull() {
    circle(d=(TubeData[Tube][T_PUNCHOD] + 2*SocketFlange),$fn=NumSides);
    for (i=[-1,1])
    translate([i*TubeData[Tube][T_SCREWOC]/2,0])
    circle(d=2.0*SocketNut[OD],$fn=NumSides);
    }
    cylinder(d=TubeData[Tube][T_PUNCHOD],h=OAH,$fn=NumSides); // boss in chassis punch hole
    }
    for (i=[0:(TubeData[Tube][T_NUMPINS] – 1)]) // tube pins
    rotate(i*360/TubeData[Tube][T_NUMPINS])
    translate([TubeData[Tube][T_PINBCD]/2,0,(OAH – TubeData[Tube][T_PINLEN])])
    rotate(180/4)
    PolyCyl(TubeData[Tube][T_PINOD],(TubeData[Tube][T_PINLEN] + Protrusion),4);
    for (i=[-1,1]) // mounting screw holes & nut traps / threaded inserts
    translate([i*TubeData[Tube][T_SCREWOC]/2,0,-Protrusion]) {
    PolyCyl(SocketNut[OD],(SocketNut[LENGTH] + Protrusion),NutSides);
    PolyCyl(SocketNut[ID],(OAH + 2*Protrusion),NutSides);
    }
    translate([0,0,-Protrusion]) { // LED recess
    PolyCyl(Pixel[OD],(Pixel[LENGTH] + Protrusion),8);
    }
    translate([0,0,(Pixel[LENGTH] – Protrusion)]) { // light pipe
    rotate(180/TubeData[Tube][T_NUMPINS])
    PolyCyl(TubeData[Tube][T_PIPEOD],(OAH + 2*Protrusion),TubeData[Tube][T_NUMPINS]);
    }
    }
    // Totally ad-hoc support structures …
    if (Support) {
    color("Yellow") {
    for (i=[-1,1]) // nut traps
    translate([i*TubeData[Tube][T_SCREWOC]/2,0,(SocketNut[LENGTH] – ThreadThick)/2])
    for (a=[0:5])
    rotate(a*30 + 15)
    cube([2*ThreadWidth,0.9*SocketNut[OD],(SocketNut[LENGTH] – ThreadThick)],center=true);
    if (Pixel[OD] > TubeData[Tube][T_PIPEOD]) // support pipe only if needed
    translate([0,0,(Pixel[LENGTH] – ThreadThick)/2])
    for (a=[0:7])
    rotate(a*22.5)
    cube([2*ThreadWidth,0.9*Pixel[OD],(Pixel[LENGTH] – ThreadThick)],center=true);
    }
    }
    }
    //———————-
    // Greenlee punch bushings
    module PunchBushing(Name = DefaultSocket) {
    PunchScrew = 9.5;
    BushingThick = 3.0;
    Tube = search([Name],TubeData,1,0)[0];
    echo(str("Building ",TubeData[Tube][0]," bushing"));
    NumSides = 6*4;
    difference() {
    union() {
    cylinder(d=Platter[ID],h=BushingThick,$fn=NumSides);
    cylinder(d=TubeData[Tube][T_PUNCHOD],h=(BushingThick – Platter[LENGTH]),$fn=NumSides);
    }
    translate([0,0,-Protrusion])
    PolyCyl(PunchScrew,5.0,8);
    }
    }
    //———————-
    // Build it
    if (Layout == "Cap") {
    if (Section)
    difference() {
    Cap();
    translate([-CapSize[OD],0,CapSize[LENGTH]])
    cube([2*CapSize[OD],2*CapSize[OD],3*CapSize[LENGTH]],center=true);
    }
    else
    Cap();
    }
    if (Layout == "FinCap") {
    if (Section) render(convexity=5)
    difference() {
    FinCap();
    // translate([0,-FinCapSize[OD],FinCapSize[LENGTH]])
    // cube([2*FinCapSize[OD],2*FinCapSize[OD],3*FinCapSize[LENGTH]],center=true);
    translate([-FinCapSize[OD],0,FinCapSize[LENGTH]])
    cube([2*FinCapSize[OD],2*FinCapSize[OD],3*FinCapSize[LENGTH]],center=true);
    }
    else
    FinCap();
    }
    if (Layout == "BuildFinCap")
    translate([0,0,FinCapSize[LENGTH]])
    rotate([180,0,0])
    FinCap();
    if (Layout == "LampBase")
    LampBase();
    if (Layout == "PlatterBase")
    PlatterBase();
    if (Layout == "PlatterFixture")
    PlatterFixture();
    if (Layout == "USBPort")
    USBPort();
    if (Layout == "Bushings")
    PunchBushing();
    if (Layout == "Socket")
    if (Section) {
    difference() {
    Socket();
    translate([-100/2,0,-Protrusion])
    cube([100,50,50],center=false);
    }
    }
    else
    Socket();
    if (Layout == "Sockets") {
    translate([0,50,0])
    Socket("Mini7");
    translate([0,20,0])
    Socket("Octal");
    translate([0,-15,0])
    Socket("Duodecar");
    translate([0,-50,0])
    Socket("Noval");
    translate([0,-85,0])
    Socket("Magnoval");}
  • Red Oaks Mill APRS iGate: KE4ZNU-10

    APRS coverage of this part of the Mighty Wappinger Creek Valley isn’t very good, particularly for our bicycle radios (low power, crappy antennas, lousy positions), so I finally got around to setting up a receive-only APRS iGate in the attic.

    The whole setup had that lashed-together look:

    KE4ZNU-10 APRS iGate - hardware
    KE4ZNU-10 APRS iGate – hardware

    It’s sitting on the bottom attic stair, at the lower end of a 10 °F/ft gradient, where the Pi 3’s onboard WiFi connects to the router in the basement without any trouble at all.

    After about a week of having it work just fine, I printed a case from Thingiverse:

    KE4ZNU-10 APRS iGate - RPi TNC-Pi case
    KE4ZNU-10 APRS iGate – RPi TNC-Pi case

    Minus the case, however, you can see a TNC-Pi2 kit atop a Raspberry Pi 3, running APRX on a full-up Raspbian Jessie installation:

    RPi TNC-Pi2 stack - heatshrink spacers
    RPi TNC-Pi2 stack – heatshrink spacers

    You must solder the TNC-Pi2 a millimeter or two above the feedthrough header to keep the component leads off the USB jacks. The kit includes a single, slightly too short, aluminum standoff that would be perfectly adequate, but I’m that guy: those are four 18 mm lengths of heatshrink tubing to stabilize the TNC, with the obligatory decorative Kapton tape.

    The only misadventure during kit assembly came from a somewhat misshapen 100 nF ceramic cap:

    Monolithic cap - 100 nF - QC failure
    Monolithic cap – 100 nF – QC failure

    Oddly, it measured pretty close to the others in the kit package. I swapped in a 100 nF ceramic cap from my heap and continued the mission.

    The threaded brass inserts stand in for tiny 4-40 nuts that I don’t have. The case has standoffs with small holes; I drilled-and-tapped 4-40 threads and it’ll be all good.

    The radio, a craptastic Baofeng UV-5R, has a SMA-RP to UHF adapter screwed to the cable from a mobile 2 meter antenna on a random slab of sheet metal on the attic floor. It has Kenwood jack spacing, but, rather than conjure a custom plug, I got a clue and bought a pair of craptastic Baofeng speaker-mics for seven bucks delivered:

    Baofeng speaker-mic wiring
    Baofeng speaker-mic wiring

    For reference, the connections:

    Baofeng speaker-mic cable - pins and colors
    Baofeng speaker-mic cable – pins and colors

    Unsoldering the speaker-mic head and replacing it with a DE-9 connector didn’t take long.

    The radio sits in the charging cradle, which probably isn’t a good idea for the long term. The available Baofeng “battery eliminators” appear to be even more dangerously craptastic than the radios and speaker-mics; I should just gut the cheapest one and use the shell with a better power supply.

    I initially installed Xastir on the Pi, but it’s really too heavyweight for a simple receive-only iGate. APRX omits the fancy map displays and runs perfectly well in a headless installation with a trivial setup configuration.

    There are many descriptions of the fiddling required to convert the Pi 3’s serial port device names back to the Pi / Pi 2 “standard”. I did some of that, but in point of fact none’s required for the TNC-Pi2; use the device name /dev/serial0 and it’s all good:

    <interface>
    serial-device /dev/serial0 19200 8n1 KISS
    callsign $mycall # callsign defaults to $mycall
    tx-ok false # transmitter enable defaults to false
    telem-to-is false # set to 'false' to disable
    </interface>
    

    Because the radio looks out over an RF desert, digipeating won’t be productive and I’ve disabled the PTT. All the received packets go to the Great APRS Database in the Cloud:

    server   noam.aprs2.net
    

    An APRS reception heat map for the last few days in August:

    KE4ZNU-10 Reception Map - 2016-08
    KE4ZNU-10 Reception Map – 2016-08

    The hot red square to the upper left reveals a peephole through the valley walls toward Mary’s Vassar Farms garden plot, where her bike spends a few hours every day. The other hotspots show where roads overlap the creek valley; the skinny purple region between the red endcaps covers the vacant land around the Dutchess County Airport. The scattered purple blocks come from those weird propagation effects that Just Happen; one of the local APRS gurus suggests reflections from airplane traffic far overhead.

    An RPi 3 is way too much computer for an iGate: all four cores run at 0.00 load all day long. On the other paw, it’s $35 and It Just Works.

  • Miniblind Cord Caps

    After smashing one of the cord pulls between the sash and the frame:

    Miniblind cord caps - installed
    Miniblind cord caps – installed

    The glittery PETG looks surprisingly good in the sunlight that will eventually change it into dullness. The black flecks come from optical effects in the plastic, not the usual burned PETG snot.

    The solid model is basically a hull around two “spheres”, truncated on top & bottom:

    Miniblind cord cap - solid model
    Miniblind cord cap – solid model

    The interior has a taper to accommodate the knot, but they’re chunky little gadgets:

    Miniblind cord cap - solid model - bottom
    Miniblind cord cap – solid model – bottom

    I thought the facets came out nicely, even if they’re mostly invisible in the picture.

    Each pull should build separately to improve the surface finish, so I arranged five copies in sequence from front to back:

    Miniblind cord cap - 5 sequential - Slic3r preview
    Miniblind cord cap – 5 sequential – Slic3r preview

    If you’re using an M2, the fans hanging off the front of the filament drive housing might come a bit too close for comfort, so rotate ’em upward and out of the way.

    If you remove the interior features and flip ’em upside down, they’d work well in Spiral Vase mode. You’d have to manually drill the top hole, though, because a hole through the model produces two shells.

    The OpenSCAD source code as a GitHub Gist:

    // Cap for miniblind cord
    // Ed Nisley KE4ZNU – August 2016
    //- Extrusion parameters – must match reality!
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    Protrusion = 0.1;
    HoleWindage = 0.2;
    //——
    // Dimensions
    OD1 = 0;
    OD2 = 1;
    LENGTH = 2;
    Cap = [9.0,16.0,25.0];
    Cord = [2.5,7.0,Cap[LENGTH] – 5];
    NumSides = 8;
    //———————-
    //- Build it
    difference() {
    hull() { // overall shape
    translate([0,0,Cap[LENGTH] – Cap[OD1]/2])
    sphere(d=Cap[OD1],$fn=NumSides);
    translate([0,0,0.5*Cap[OD2]/2])
    sphere(d=Cap[OD2],$fn=2*NumSides); // round the bottom just a bit
    }
    translate([0,0,-Cap[LENGTH]/2]) // trim bottom
    cube([2*Cap[OD2],2*Cap[OD2],Cap[LENGTH]],center=true);
    translate([0,0,Cap[LENGTH] + 0.8*Cap[OD1]]) // trim top (arbitrarily)
    cube([2*Cap[OD1],2*Cap[OD1],2*Cap[OD1]],center=true);
    translate([0,0,-Protrusion])
    cylinder(d=Cord[OD1],h=(Cap[LENGTH] + 2*Protrusion),$fn=NumSides);
    translate([0,0,-Protrusion])
    cylinder(d1=Cord[OD2],d2=Cord[OD1],h=(Cord[LENGTH] + Protrusion),$fn=NumSides);
    }
  • Counterfeit FTDI USB-Serial Adapter Roundup

    As part of the vacuum tube lighting project, I picked up a bunch of USB-Serial adapters, with the intent of simply building them into the lamp base along with a knockoff Arduino Pro Mini, then plugging in a cheap USB wall wart for power. An Arduino Nano might make more sense, but this lets me use the Pro Minis for other projects where power comes from elsewhere.

    Anyhow, I deliberately paid a few bucks extra for “genuine” FTDI chips, knowing full well what was about to happen:

    Assorted FT232 Converters
    Assorted FT232 Converters

    The two boards on the bottom have been in my collection forever and seem to be genuine FTDI; the one on the left came from Sparkfun:

    FT232RL - genuine
    FT232RL – genuine

    The top six have counterfeit chips, although you’d be hard-pressed to tell from the laser etching:

    FT232RL - fake
    FT232RL – fake

    In addition to the boards, I picked up the blue square-ish cable adapters for the HP 7475A plotter project and, again, paid extra for “genuine” FTDI chips. The other adapters, based on Prolific PL2303 chips, I’ve had basically forever:

    Assorted FT232 Converters - Cabled
    Assorted FT232 Converters – Cabled

    Those two have chips with different serial numbers: genuine FTDI chips get different serial numbers programmed during production. The counterfeits, well, they’re all pretty much the same.

    Display the serial numbers thusly:

    lsusb
    Bus 002 Device 024: ID 0403:6001 Future Technology Devices International, Ltd FT232 Serial (UART) IC
    ... snippage ...
    udevadm info --query=all --attribute-walk  --name=/dev/bus/usb/002/024 | grep ser
        ATTR{serial}=="A6005qSB"
    

    All the counterfeit FTDI chips report the same serial number: A50285BI. The PL2303 chips don’t report serial numbers.

    For my simple needs, they all work fine, but apparently fancier new microcontrollers expect more from their adapters and the counterfeits just can’t live up to their promises.

    For a while, FTDI released Windows drivers that bricked counterfeit chips; the Linux drivers were unaffected.

  • ITead Studio Quasi-Colorduino RGB LED Matrix Shield: Redesign Doodles

    Some notes on a recent acquisition that ought to allow random dots with individual brightness control (unlike my simple resistor-limited hack job):

    Color Shield - DM163 M54565 - demo
    Color Shield – DM163 M54565 – demo

    A Colorduino is a dedicated board that combines an Arduino-class microcontroller with hardware drivers for an 8×8 RGB LED matrix, with daisy-chaining I/O to build bigger displays. The Colors Shield you see above omits the Arduino circuitry and daisy-chaining hardware: it plugs atop an ordinary Arduino UNO-class board as a dedicated 8×8 tile driver.

    I do not profess to understand the ancestry & family tree of those designs and their various incarnations. This schematic doesn’t match the knockoff hardware in hand, which isn’t surprising after half a dozen years of relentless product cheapnification:

    ITeadStudio - RGB LED shield - DM163 M54564 - SPI notes
    ITeadStudio – RGB LED shield – DM163 M54564 – SPI notes

    It comes close enough for a big-picture overview…

    The DM163 has 8×3 constant current sink PWM pins that connect to the column cathodes of the RGB matrix. It provides either 8 or 6 bits of PWM control for each output, with either 6 or 8 bits of gamma correction to make the grayscale shades work out properly (those are separate shift registers and the PWM generators use both, so the chip doesn’t care how you divvy up the 14 bits).

    The three 1 kΩ resistors set the current to 60 mA per output pin. The LED matrix might support anywhere from  70 to 120 mA peak current per LED, but I doubt the supplied matrix matches any of the available datasheets. The total current depends on the number of LEDs lit on each row, so large dark areas are a Good Thing.

    The serial protocol looks enough like SPI to get by, with controls for Reset, Latch, and Bank Select.

    The board has no power supply other than the single Arduino VCC pin, so you’re looking at a peak of 24 x 60 mA = 1.44 A through that pin. The Arduino regulator must supply that load pretty much full-time, which is obviously a Bad Thing; plan on plenty of dark areas.

    The DM163 SPI connections don’t use the Arduino’s hardware SPI, so it’s full-frontal bit-banging all the way. Three DM163 control bits use a trio of analog inputs as digital outputs. No harm in that, works fine with the knockoff Neopixels.

    The M54564 is a PNP high-side driver converting logic-level inputs to the current required for the row anodes of the matrix. The eight input bits are non-contiguous across the Arduino’s digital outputs. You could turn on all the M54564 outputs at once, which would be a Bad Thing.

    You shift 24 bytes of RGB data into the DM163 and latch the data, then raise one of the M54564 inputs to enable a given row of LEDs, which light up with the corresponding colors.

    The bit-banged SPI runs at 1.9 µs/bit and sending all 24 bits to the DM163 requires 450 µs. With a 100 Hz refresh, that’s a mere 5% overhead, but the fact that the board soaks up essentially all the I/O pins means the Arduino isn’t not doing much else in the way of real-world interaction.

    The Arduino driver, of dubious provenance, sets Timer 0 for 100-ish Hz interrupts. Each interrupt shifts another batch of bytes into the DM163 and selects the appropriate row. The driver uses a double-buffered array that soaks up 2x8x8x3 = 384 bytes of precious RAM, in addition to a bunch of working storage.

    If I were (re)designing this board…

    A separate power input jack for the DM163 that might optionally feed the Arduino’s VIN raw power pin.

    Use the Arduino SPI hardware, dammit.

    Put an HC595 shift register behind the M54564, so you’d shift 24 + 8 = 32 bits into the board, then strobe the latches. That eliminates eight digital pins used as a parallel port.

    You’d surely want to disable the row driver while switching the column drivers to avoid ghosting, so figure on a separate output enable for the HC595. That tri-states the 595’s outputs; although the M54564 has internal pulldowns, it might need more.

    It’s entirely usable as-is, but sheesh it’d be so easy to do a better job. That wouldn’t be software compatible with all the Arduino Love for the existing boards out there; there’s no point.

     

  • Epson R380 Printer: Resetting the Waste Ink Counter Again

    The Epson R380 printer never gets turned off, so it rarely has a chance to complain. After a powerdown due to refreshing the UPS batteries, it lit up with the dreaded “Service required. Visit your friendly Epson repair center” message that indicates you should just throw the printer out, because replacing the internal ink absorber mats / draining the internal tank is, mmm, economically infeasible when you pay somebody else to do it.

    Having done this before, though, it’s almost easy…

    • Pop a PC with a Windows partition off the to-be-recycled stack
    • Boot System Rescue CD
    • Back up the partition to a junk hard drive, just for practice
    • Copy the subdirectory of sketchy utilities to the Windows drive
    • Boot Windows (with no network connection)
    • Run sketchy utility to reset the ink counter
    • Boot SRC, restore partition
    • Return hard drive & PC to their respective piles
    • Declare victory and move on

    This time, a sketchy utility that resembled the Official Epson Reset Program actually reset something and the printer started up normally. As before, however, the saved MBR didn’t match the on-disk MBR, suggesting that either I don’t understand how to save / restore the MBR or that something once again meddled with the MBR in between the backup and the restore.

    I’ve emptied the waste ink tank maybe three times since the last reset: plenty of ink down the drain. Fortunately, I loves me some good continuous-flow ink supply action…

    Sheesh & similar remarks.

  • Random LED Dots: Entropy Library for Moah Speed with Less Gimcrackery

    A discussion over the Squidwrench Operating Table about injecting entropy into VMs before / during their boot sequence reminded me that I wanted to try the Entropy library with my 8×8 RGB LED matrix:

    8x8 RGB LED Matrix - board overview
    8×8 RGB LED Matrix – board overview

    The original version trundled along with random numbers produced by timing Geiger counter ticks. The second version, digitizing the amplified noise from a reverse-biased PN junction, ran much faster.

    What’s new & different: the Entropy library measures the jitter between the ATmega328 watchdog timer’s RC oscillator and the ceramic resonator (on Pro Mini boards) driving the CPU. It cranks out four bytes of uncorrelated bits every half-second, which isn’t quite fast enough for a sparkly display, but re-seeding the Arduino PRNG whenever enough entropy arrives works well enough.

    One could, of course, re-seed the PRNG with Geiger bits or junction noise to the same effect. The key advantage of the Entropy library: no external hardware required. The downside: no external hardware required, so, minus those techie transistors / resistors / op amps, it will look like Just Another Arduino Project.

    Reverse-bias noise amplifier - detail
    Reverse-bias noise amplifier – detail

    Le sigh.

    In any event, the Entropy library has excellent documentation and works perfectly.

    The Arduino PRNG can produce results fast enough for wonderfully twinkly output that’s visually indistinguishable from the “true” random numbers from the Geiger counter or PN junction. I dialed it back to one update every 5 ms, because letting it free-run turned the display into an unattractive blur.

    The top trace shows the update actually happens every 6 ms:

    Entropy TRNG - LED update vs refresh
    Entropy TRNG – LED update vs refresh

    The lower trace shows that each matrix row refresh takes about a millisecond. Refreshes occur on every main loop iteration and interfere with the update, not that that makes any difference. Should it matter, subtract one from the update period and it’ll be all good.

    The Arduino source code as a GitHub Gist:

    // Random LED Dots
    // Based on Entropy library using watchdog timer jitter
    // https://sites.google.com/site/astudyofentropy/project-definition/timer-jitter-entropy-sources/entropy-library
    // Ed Nisley – KE4ANU – August 2016
    #include <Entropy.h>
    //———-
    // Pin assignments
    const byte PIN_HEARTBEAT = 8; // DO – heartbeat LED
    const byte PIN_SYNC = A3; // DO – scope sync
    const byte PIN_LATCH = 4; // DO – shift register latch clock
    const byte PIN_DIMMING = 9; // AO – LED dimming control
    // These are *hardware* SPI pins
    const byte PIN_MOSI = 11; // DO – data to shift reg
    const byte PIN_MISO = 12; // DI – data from shift reg (unused)
    const byte PIN_SCK = 13; // DO – shift clock to shift reg (also Arduino LED)
    const byte PIN_SS = 10; // DO – -slave select (must be positive for SPI output)
    //———-
    // Constants
    #define UPDATE_MS 5
    //———-
    // Globals
    // LED selects are high-active bits and low-active signals: flipped in UpdateLEDs()
    // *exactly* one row select must be active in each element
    typedef struct {
    const byte Row;
    byte ColR;
    byte ColG;
    byte ColB;
    } LED_BYTES;
    // altering the number of rows & columns will require substantial code changes…
    #define NUMROWS 8
    #define NUMCOLS 8
    LED_BYTES LEDs[NUMROWS] = {
    {0x80,0,0,0},
    {0x40,0,0,0},
    {0x20,0,0,0},
    {0x10,0,0,0},
    {0x08,0,0,0},
    {0x04,0,0,0},
    {0x02,0,0,0},
    {0x01,0,0,0},
    };
    byte RowIndex;
    #define LEDS_ON 0
    #define LEDS_OFF 255
    unsigned long MillisNow;
    unsigned long MillisThen;
    //– Helper routine for printf()
    int s_putc(char c, FILE *t) {
    Serial.write(c);
    }
    //– Useful stuff
    // Free RAM space monitor
    // From http://playground.arduino.cc/Code/AvailableMemory
    uint8_t * heapptr, * stackptr;
    void check_mem() {
    stackptr = (uint8_t *)malloc(4); // use stackptr temporarily
    heapptr = stackptr; // save value of heap pointer
    free(stackptr); // free up the memory again (sets stackptr to 0)
    stackptr = (uint8_t *)(SP); // save value of stack pointer
    }
    void TogglePin(char bitpin) {
    digitalWrite(bitpin,!digitalRead(bitpin)); // toggle the bit based on previous output
    }
    void PulsePin(char bitpin) {
    TogglePin(bitpin);
    TogglePin(bitpin);
    }
    //———
    //– SPI utilities
    void EnableSPI(void) {
    digitalWrite(PIN_SS,HIGH); // make sure this is high!
    SPCR |= 1 << SPE;
    }
    void DisableSPI(void) {
    SPCR &= ~(1 << SPE);
    }
    void WaitSPIF(void) {
    while (! (SPSR & (1 << SPIF))) {
    // TogglePin(PIN_HEARTBEAT);
    continue;
    }
    }
    byte SendRecSPI(byte Dbyte) { // send one byte, get another in exchange
    SPDR = Dbyte;
    WaitSPIF();
    return SPDR; // SPIF will be cleared
    }
    void UpdateLEDs(byte i) {
    SendRecSPI(~LEDs[i].ColB); // low-active outputs
    SendRecSPI(~LEDs[i].ColG);
    SendRecSPI(~LEDs[i].ColR);
    SendRecSPI(~LEDs[i].Row);
    analogWrite(PIN_DIMMING,LEDS_OFF); // turn off LED to quench current
    PulsePin(PIN_LATCH); // make new shift reg contents visible
    analogWrite(PIN_DIMMING,LEDS_ON);
    }
    //—————
    // Set LED from integer
    // On average, this leaves the LED unchanged for 1/8 of the calls…
    void SetLED(unsigned long Value) {
    byte Row = Value & 0x07;
    byte Col = (Value >> 3) & 0x07;
    byte Color = (Value >> 6) & 0x07;
    byte BitMask = (0x80 >> Col);
    // printf("%u %u %u %u\r\n",Row,Col,Color,BitMask);
    LEDs[Row].ColR &= ~BitMask;
    LEDs[Row].ColR |= (Color & 0x04) ? BitMask : 0;
    LEDs[Row].ColG &= ~BitMask;
    LEDs[Row].ColG |= (Color & 0x02) ? BitMask : 0;
    LEDs[Row].ColB &= ~BitMask;
    LEDs[Row].ColB |= (Color & 0x01) ? BitMask : 0;
    }
    //——————
    // Set things up
    void setup() {
    pinMode(PIN_HEARTBEAT,OUTPUT);
    digitalWrite(PIN_HEARTBEAT,HIGH); // show we arrived
    pinMode(PIN_SYNC,OUTPUT);
    digitalWrite(PIN_SYNC,LOW);
    pinMode(PIN_MOSI,OUTPUT); // SPI-as-output is not strictly necessary
    digitalWrite(PIN_MOSI,LOW);
    pinMode(PIN_SCK,OUTPUT);
    digitalWrite(PIN_SCK,LOW);
    pinMode(PIN_SS,OUTPUT);
    digitalWrite(PIN_SS,HIGH); // OUTPUT + HIGH is required to make SPI output work
    pinMode(PIN_LATCH,OUTPUT);
    digitalWrite(PIN_LATCH,LOW);
    Serial.begin(57600);
    fdevopen(&s_putc,0); // set up serial output for printf()
    printf("Random LED Dots – Watchdog Entropy\r\nEd Nisley – KE4ZNU – August 2016\r\n");
    Entropy.initialize(); // start up entropy collector
    //– Set up SPI hardware
    SPCR = B01110001; // Auto SPI: no int, enable, LSB first, master, + edge, leading, f/16
    SPSR = B00000000; // not double data rate
    EnableSPI(); // turn on the SPI hardware
    SendRecSPI(0); // set valid data in shift registers: select Row 0, all LEDs off
    //– Dimming pin must use fast PWM to avoid beat flicker with LED refresh rate
    // Timer 1: PWM 9 PWM 10
    analogWrite(PIN_DIMMING,LEDS_OFF); // disable column drive (hardware pulled it low before startup)
    TCCR1A = B10000001; // Mode 5 = fast 8-bit PWM with TOP=FF
    TCCR1B = B00001001; // … WGM, 1:1 clock scale -> 64 kHz
    //– lamp test: send a white flash through all LEDs
    printf("Lamp test begins: white flash each LED…");
    digitalWrite(PIN_HEARTBEAT,LOW); // turn off while panel blinks
    analogWrite(PIN_DIMMING,LEDS_ON); // enable column drive
    for (byte i=0; i<NUMROWS; i++) {
    for (byte j=0; j<NUMCOLS; j++) {
    LEDs[i].ColR = LEDs[i].ColG = LEDs[i].ColB = 0x80 >> j;
    for (byte k=0; k<NUMROWS; k++) {
    UpdateLEDs(k);
    delay(25);
    }
    LEDs[i].ColR = LEDs[i].ColG = LEDs[i].ColB = 0;
    }
    }
    UpdateLEDs(NUMROWS-1); // clear the last LED
    printf(" done!\r\n");
    //– Preload LEDs with random values
    digitalWrite(PIN_HEARTBEAT,LOW);
    uint32_t rn = Entropy.random();
    printf("Preloading LED array with seed: %08lx\r\n",rn);
    randomSeed(rn);
    for (byte Row=0; Row<NUMROWS; Row++) {
    for (byte Col=0; Col<NUMCOLS; Col++) { // Col runs backwards, but we don't care
    LEDs[Row].ColR |= random(2) << Col; // random(2) returns 0 or 1
    LEDs[Row].ColG |= random(2) << Col;
    LEDs[Row].ColB |= random(2) << Col;
    }
    UpdateLEDs(Row);
    }
    check_mem();
    printf("SP: %u HP: %u Free RAM: %u\r\n",stackptr,heapptr,stackptr – heapptr);
    printf("Running…\r\n");
    MillisThen = millis();
    }
    //——————
    // Run the test loop
    void loop() {
    unsigned long Hash;
    uint32_t rn;
    MillisNow = millis();
    // Re-seed the generator whenever we get enough entropy
    if (Entropy.available()) {
    digitalWrite(PIN_HEARTBEAT,HIGH);
    rn = Entropy.random();
    // printf("Random: %08lx ",rn);
    randomSeed(rn);
    digitalWrite(PIN_HEARTBEAT,LOW);
    }
    // If it's time for a change, whack a random LED
    if ((MillisNow – MillisThen) > UPDATE_MS) {
    MillisThen = MillisNow;
    SetLED(random());
    }
    // Refresh LED array to maintain the illusion of constant light
    UpdateLEDs(RowIndex++);
    if (RowIndex >= NUMROWS) {
    RowIndex = 0;
    PulsePin(PIN_SYNC);
    }
    }
    view raw TimerDots.ino hosted with ❤ by GitHub