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: Ersatz Plate Cap

    Lighting up that old voltage regulator tube conclusively demonstrated there’s no point in conjuring high voltages in this day & age. Nay, verily, merely lighting the filament of some tubes would require more power than seems reasonable.

    1B3GT high-voltage regulator tube in the Box o’ Hollow State Electronics suggested a different approach:

    1B3GT HV tube regulator
    1B3GT HV tube regulator

    With only a slight loss of historical accuracy, one could light the tube from the top with a Neopixel LED tucked into a similar cap, with power-and-data arriving through a suitably antiqued flying lead. That won’t work on tubes like that 1B3GT with an actual plate terminal  at the top, nor with small Noval / miniature 7-pin tubes topped with an evacuation tip, but it’s fine for tubes like this 6SN7GTB:

    6SN7GTB Vacuum Tube
    6SN7GTB Vacuum Tube

    Obviously, you want a relatively small cap atop the tube, lest the LED visually overwhelm the tube. Some preliminary tests (a.k.a. screwing around) showed that the mica spacer holding the dual triode elements together lights up wonderfully well and diffuses the glow throughout the tube.

    Adafruit has relatively large round (and smaller roundish) Neopixel breakout boards, but I bought a bunch of knockoff Neopixels mounted on a 10 mm circular PCB from the usual eBay supplier:

    Vacuum Tube LEDs - plate lead - connections
    Vacuum Tube LEDs – plate lead – connections

    Some PET braid tucked into a snippet of brass tubing dresses up a length of what might once have been audio cable. The braid wants to fray on the ends; confining it with heatstink or brass tubing is mandatory.

    That’s a 1 µF ceramic SMD cap soldered between the +5 V and Gnd traces, atop a snippet of Kapton tape, in the hopes that it will help the 100 nF cap (on the other side of the board) tamp down the voltage dunks from PWM current pulses through that long thin wire. The leads come off toward the center to bend neatly upward into the cap.

    Duplicating that old plate cap on the 1B3GT would be a fool’s errand, so I went full frontal Vader:

    Vacuum Tube Lights - cap solid model - Overview
    Vacuum Tube Lights – cap solid model – Overview

    The interior recesses the LED far enough to allow for the tube’s top curvature, with a conical adapter to the smaller wiring channel that allows for more plastic supporting the brass tube:

    Vacuum Tube Lights - cap solid model - section
    Vacuum Tube Lights – cap solid model – section

    A glob of epoxy inside the cap anchors the PCB and fuses all the loose ends / floppy wires / braid strands into a solid block that will never come apart again.

    It should be printed (or primered and painted) with opaque black or maybe Bakelite Brown, but right now I have cyan PETG and want to see how it plays, soooo:

    Vacuum Tube LEDs - plate lead - overview
    Vacuum Tube LEDs – plate lead – overview

    The cap floats in mid-air over a defunct Philips 60 W halogen bulb that I’ve been saving for just such an occasion. Obviously, you must epoxy / glue the cap in place for a permanent display.

    The OpenSCAD source code as a Github gist:

    // Vacuum Tube LED Lights
    // Ed Nisley KE4ZNU January 2016
    Layout = "Cap"; // Show Build Cap Box Octal Noval Mini7
    Section = true; // cross-section the object
    //- 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
    T_NAME = 0;
    T_NUMPINS = 1; // Socket specifications
    T_PINCIRC = 2;
    T_PINDIA = 3;
    T_SOCKDIA = 4;
    TubeBase = [
    ["Mini7", 8, 9.53, 1.016, 19.0],
    ["Octal", 8, 17.45, 2.36, 33.0],
    ["Noval",10, 11.89, 1.1016,20.5],
    ];
    ID = 0;
    OD = 1;
    LENGTH = 2;
    Pixel = [7.0,10.0,3.0]; // ID = contact patch, OD = PCB dia, LENGTH = overall thickness
    //———————-
    // 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);
    }
    //———————-
    // Tube cap
    CapTube = [4.0,3/16 * inch,10.0]; // brass tube for flying lead to cap LED
    CapSize = [Pixel[ID],(Pixel[OD] + 3.0),(CapTube[OD] + 2*Pixel[LENGTH])];
    CapSides = 6*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)]) // step + cone to retain PCB
    cylinder(d1=(Pixel[OD]/cos(180/CapSides)),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);
    }
    }
    //———————-
    // 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 == "Build") {
    Cap();
    Spigot();
    }
  • Poughkeepsie ACM Chapter Presentation: Plotting Like It’s 1989!

    I’ll be giving an in-depth talk about my adventures restoring that old HP 7475A plotter for the Poughkeepsie ACM Chapter at Marist College this evening:

    Superformula Plot - Composite D
    Superformula Plot – Composite D

    This being the Association for Computing Machinery, I will talk a bit about the Superformula that makes it all possible:

    Gielis Superformula - parameters
    Gielis Superformula – parameters

    The presentation will look a lot like this: ACM – Plotting Like Its 1989. The PDF doesn’t include my patter, but perhaps the linky love on each screen can fill in the details.

    If you’re following along, the Python source code running on the plotter as a GitHub Gist:

    from chiplotle import *
    from math import *
    from datetime import *
    from time import *
    from types import *
    import random
    def superformula_polar(a, b, m, n1, n2, n3, phi):
    ''' Computes the position of the point on a
    superformula curve.
    Superformula has first been proposed by Johan Gielis
    and is a generalization of superellipse.
    see: http://en.wikipedia.org/wiki/Superformula
    Tweaked to return polar coordinates
    '''
    t1 = cos(m * phi / 4.0) / a
    t1 = abs(t1)
    t1 = pow(t1, n2)
    t2 = sin(m * phi / 4.0) / b
    t2 = abs(t2)
    t2 = pow(t2, n3)
    t3 = -1 / float(n1)
    r = pow(t1 + t2, t3)
    if abs(r) == 0:
    return (0, 0)
    else:
    # return (r * cos(phi), r * sin(phi))
    return (r, phi)
    def supershape(width, height, m, n1, n2, n3,
    point_count=10 * 1000, percentage=1.0, a=1.0, b=1.0, travel=None):
    '''Supershape, generated using the superformula first proposed
    by Johan Gielis.
    – `points_count` is the total number of points to compute.
    – `travel` is the length of the outline drawn in radians.
    3.1416 * 2 is a complete cycle.
    '''
    travel = travel or (10 * 2 * pi)
    # compute points…
    phis = [i * travel / point_count
    for i in range(1 + int(point_count * percentage))]
    points = [superformula_polar(a, b, m, n1, n2, n3, x) for x in phis]
    # scale and transpose…
    path = []
    for r, a in points:
    x = width * r * cos(a)
    y = height * r * sin(a)
    path.append(Coordinate(x, y))
    return Path(path)
    # RUN DEMO CODE
    if __name__ == '__main__':
    override = False
    plt = instantiate_plotters()[0]
    # plt.write('IN;')
    if plt.margins.soft.width < 11000: # A=10365 B=16640
    maxplotx = (plt.margins.soft.width / 2) – 100
    maxploty = (plt.margins.soft.height / 2) – 150
    legendx = maxplotx – 2900
    legendy = -(maxploty – 750)
    tscale = 0.45
    numpens = 4
    # prime/10 = number of spikes
    m_values = [n / 10.0 for n in [11, 13, 17, 19, 23]]
    # ring-ness 0.1 to 2.0, higher is larger
    n1_values = [
    n / 100.0 for n in range(55, 75, 2) + range(80, 120, 5) + range(120, 200, 10)]
    else:
    maxplotx = plt.margins.soft.width / 2
    maxploty = plt.margins.soft.height / 2
    legendx = maxplotx – 3000
    legendy = -(maxploty – 900)
    tscale = 0.45
    numpens = 6
    m_values = [n / 10.0 for n in [11, 13, 17, 19, 23, 29, 31,
    37, 41, 43, 47, 53, 59]] # prime/10 = number of spikes
    # ring-ness 0.1 to 2.0, higher is larger
    n1_values = [
    n / 100.0 for n in range(15, 75, 2) + range(80, 120, 5) + range(120, 200, 10)]
    print " Max: ({},{})".format(maxplotx, maxploty)
    # spiky-ness 0.1 to 2.0, higher is spiky-er (mostly)
    n2_values = [
    n / 100.0 for n in range(10, 60, 2) + range(65, 100, 5) + range(110, 200, 10)]
    plt.write(chr(27) + '.H200:') # set hardware handshake block size
    plt.set_origin_center()
    # scale based on B size characters
    plt.write(hpgl.SI(tscale * 0.285, tscale * 0.375))
    # slow speed for those abrupt spikes
    plt.write(hpgl.VS(10))
    while True:
    # standard loadout has pen 1 = fine black
    plt.write(hpgl.PA([(legendx, legendy)]))
    pen = 1
    plt.select_pen(pen)
    plt.write(hpgl.PA([(legendx, legendy)]))
    plt.write(hpgl.LB("Started " + str(datetime.today())))
    if override:
    m = 4.1
    n1_list = [1.15, 0.90, 0.25, 0.59, 0.51, 0.23]
    n2_list = [0.70, 0.58, 0.32, 0.28, 0.56, 0.26]
    else:
    m = random.choice(m_values)
    n1_list = random.sample(n1_values, numpens)
    n2_list = random.sample(n2_values, numpens)
    pen = 1
    for n1, n2 in zip(n1_list, n2_list):
    n3 = n2
    print "{0} – m: {1:.1f}, n1: {2:.2f}, n2=n3: {3:.2f}".format(pen, m, n1, n2)
    plt.select_pen(pen)
    plt.write(hpgl.PA([(legendx, legendy – 100 * pen)]))
    plt.write(
    hpgl.LB("Pen {0}: m={1:.1f} n1={2:.2f} n2=n3={3:.2f}".format(pen, m, n1, n2)))
    e = supershape(maxplotx, maxploty, m, n1, n2, n3)
    plt.write(e)
    pen = pen + 1 if (pen % numpens) else 1
    pen = 1
    plt.select_pen(pen)
    plt.write(hpgl.PA([(legendx, legendy – 100 * (numpens + 1))]))
    plt.write(hpgl.LB("Ended " + str(datetime.today())))
    plt.write(hpgl.PA([(legendx, legendy – 100 * (numpens + 2))]))
    plt.write(hpgl.LB("More at https://softsolder.com/?s=7475a&quot;))
    plt.select_pen(0)
    plt.write(hpgl.PA([(-maxplotx,maxploty)]))
    print "Waiting for plotter… ignore timeout errors!"
    sleep(40)
    while NoneType is type(plt.status):
    sleep(5)
    print "Load more paper, then …"
    print " … Press ENTER on the plotter to continue"
    plt.clear_digitizer()
    plt.digitize_point()
    plotstatus = plt.status
    while (NoneType is type(plotstatus)) or (0 == int(plotstatus) & 0x04):
    plotstatus = plt.status
    print "Digitized: " + str(plt.digitized_point)

     

  • Microscope Stage Positioner

    Given the vanishingly small depth of field provided by a cheap USB camera peering through the stereo zoom microscope, I’ve always wanted a better way of moving objects by small increments. The rehabilitated micropositioner didn’t have the right orientation or end effector:

    Micropositioner
    Micropositioner

    So I rearranged the axis slides and added a small table:

    Microscope Stage Positioner
    Microscope Stage Positioner

    That frees up the magnetic base and husky angle bracket, plus a few odds & ends, for future adventures.

    The clear base is a random chunk of acrylic, bandsawed to the proper length, then tediously squared and drilled on the Sherline:

    Microscope Stage Positioner - base squaring
    Microscope Stage Positioner – base squaring

    I briefly thought of printing the base, but came to my senses: there are better ways to make big flat surfaces.

    The little aluminum table has a nubbly spray coating that came straight from the heap and looks surprisingly good after squaring & drilling. The X axis block puts it below the platform and one screw head above the desk when the Y axis arm sits flat on the acrylic base.

    One solid model view arranges things in more-or-less the proper layout to check the alignment:

    Microscope Stage Positioner - solid model - Show layout
    Microscope Stage Positioner – solid model – Show layout

    The build layout reduces the platform space:

    Microscope Stage Positioner - Slic3r preview
    Microscope Stage Positioner – Slic3r preview

    You’re looking at four hours of PETG print time at 0.2 mm layer thickness with 15% infill and Hilbert Curve surfaces.

    All of the screws have UNF fine-pitch threads (4-48, 6-40, 8-36, stuff like that), so the solid model includes the spacing required to reuse the original screws: those big holes in the Y axis arm end in little clearance holes for the tiny screws. Some of the screws bottom out with barely two millimeters of thread engagement in the slides, while others could jam against the racks. I didn’t want to cut that many screws from my Brownell’s gun screw assortment unless I absolutely had to. So far, so good.

    I spent quite a while doodling the layout to convince myself that it would actually work:

    Microscope Stage Positioner - layout doodle
    Microscope Stage Positioner – layout doodle

    Memo to self: Next time, use a larger scale!

    Although the whole lashup works as intended, those metal hunks are way too heavy for the plastic block that fits between the Z axis drive pillar and the X axis slide: that long Y axis arm drooped toward the front by about 5 mm. A small shim raised the front of the Z axis footprint enough to level the arm, but I think the right answer is a metal upright with a bigger footprint that spreads the load.

    All that mass hanging out in mid-air turns the plastic pieces into springs: you can’t keep your fingers on the knobs. Fortunately, everything returns to the same position after you release the knob, so it’s easy to move in precise increments if you close your eyes until the view settles down.

    There’s a reason optical equipment uses cast iron, steel, and brass… but I’ll settle for plastic.

    The OpenSCAD source code as a GitHub gist:

    // Microscope Stage Positioner
    // Ed Nisley KE4ZNU January 2016
    Layout = "Build"; // Show Build
    // Base ZStand YMount XMount
    Gap = 0.0;
    //- 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
    SlipFit = 0.1;
    ZDrive = [26.0,19.6,75.0]; // stationary part of Z drive
    ZDriveOffset =[0,0,22.0]; // left front corner of stationary Z base
    ZWall = 4.0; // thickness of edge wrapped around Z columns
    YStageBlock = [25.0,61.0,17.0]; // Y stage mount + slide
    YStageOffset = [-6.0,4.0,0.0]; // offset to inner corner of Y stage holder
    YArm = [10.0,93.0,17.0]; // mount to stationary part of Y stage
    ZStage = [24.0,9.7,85.0]; // moving part of Z drive
    ZYArm = [(2*ZWall + ZStage[0]),10.0,YArm[2]]; // attaches to ZStage, same thickness as YArm
    XStageBlock = [25.0,20.0,12.0]; // X stage mount + slide
    XStageOffset = [-95.0,-15.0,-26]; // offset to rear left bottom corner of X stage slide
    XTray = [25,25,5]; // X tray attached to bottom of X 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);
    }
    //– Z Stand
    module ZStand() {
    Holes = [12.0,41.5,68.0];
    HoleOD = 3.5;
    HolesOC = 15.0;
    echo(str("Z Stand holes OC: ",HolesOC));
    ZPlate = 6.0; // thickness of Z plate = max screw grab distance
    ZStandWrap = 2.0; // length of edge wrapped around Z column
    MaxY = 9.0;
    MinY = -14.0;
    difference() {
    union() {
    linear_extrude(height=ZDriveOffset[2])
    polygon(points=[
    [-ZWall,MaxY], // limited by Z slide rack
    [ZDrive[0] + ZWall,MaxY],
    [ZDrive[0] + ZWall,MinY], // limited by X slide rack
    [-ZWall,MinY]
    ]);
    linear_extrude(height=(ZDrive[2] + ZDriveOffset[2]),convexity=4)
    polygon(points=[
    [-SlipFit,0],
    [ZDrive[0] + SlipFit,0.0],
    [ZDrive[0] + SlipFit,ZStandWrap],
    [ZDrive[0] + ZWall,ZStandWrap],
    [ZDrive[0] + ZWall,-ZPlate],
    [-ZWall,-ZPlate],
    [-ZWall,ZStandWrap],
    [-SlipFit,ZStandWrap]
    ]);
    }
    for (i = [0:len(Holes) – 1]) // holes along Z stand
    translate([ZDrive[0]/2,ZDrive[1]/2,(Holes[i] + ZDriveOffset[2])])
    rotate([90,0,0])
    PolyCyl(HoleOD,ZDrive[1]);
    for (i = [-1,1]) // mounting screw holes
    translate([i*HolesOC/2 + ZDrive[0]/2, // center the holes from side to side
    (MaxY + MinY)/2, // moby hack to put holes on midline
    -Protrusion])
    PolyCyl(3.5,0.75*ZDriveOffset[2],6);
    }
    }
    //– Y Mounting arm
    // Polygon origin at inner corner nearest the Z stand column
    module YMount() {
    YHoles = [12.0,48.0,84.0]; // mounting holes along Y stage arm, from outside in
    YScrewLength = 4.0; // screw head to Y stage mount
    ZStageBase = [(ZDrive[0] – ZStage[0])/2,(ZDrive[1] + ZStage[1]),0.0] – YStageOffset; // local coordinates of Z slide left rear corner
    ZHoles = [26.5,55.0,71.0];
    ZStageWrap = 8.0; // length of edge wrapped around Z stage
    Trim = ZStageBase[1] – ZStageWrap;
    union() {
    difference() {
    linear_extrude(height=YArm[2],convexity=5)
    polygon(points=[
    [-Trim,0.0],
    [-YStageBlock[0],0.0],
    [-YStageBlock[0],-(YArm[1] + SlipFit)],
    [-(YStageBlock[0] + YArm[0]),-(YArm[1] + SlipFit)],
    [-(YStageBlock[0] + YArm[0]),Trim],
    [-Trim,(ZStageBase[1] + ZYArm[1])],
    [(ZStageBase[0] + ZStage[0]/2),(ZStageBase[1] + ZYArm[1])],
    [(ZStageBase[0] + ZStage[0] + ZWall),(ZStageBase[1] + 0*ZYArm[1])],
    [(ZStageBase[0] + ZStage[0] + ZWall),(ZStageBase[1] – ZStageWrap)],
    [(ZStageBase[0] + ZStage[0] + SlipFit),(ZStageBase[1] – ZStageWrap)],
    [(ZStageBase[0] + ZStage[0] + SlipFit),ZStageBase[1]],
    [(ZStageBase[0] – SlipFit),ZStageBase[1]],
    [(ZStageBase[0] – SlipFit),(ZStageBase[1] – ZStageWrap)],
    [0.0,(ZStageBase[1] – ZStageWrap)],
    [0.0,Trim]
    ]);
    for (j=[0:len(YHoles) – 1]) { // Y stage mounting screws
    translate([-(YStageBlock[0] + YScrewLength),
    (-YArm[1] + YHoles[j] – 2*SlipFit),
    YArm[2]/2])
    rotate([0,-90,0]) rotate(180/6)
    PolyCyl(5.5,YArm[0],6);
    translate([-(YStageBlock[0] – Protrusion),
    (-YArm[1] + YHoles[j] – 2*SlipFit),
    YArm[2]/2])
    rotate([0,-90,0]) rotate(180/6)
    PolyCyl(2.5,2*YArm[0],6);
    }
    }
    if (true)
    difference() {
    linear_extrude(height=ZStage[2],convexity=5)
    polygon(points=[
    [(ZStageBase[0] – ZWall),(ZStageBase[1] + 5.0)],
    [(ZStageBase[0] + ZStage[0] + ZWall),(ZStageBase[1] + 5.0)],
    [(ZStageBase[0] + ZStage[0] + ZWall),(ZStageBase[1] – ZStageWrap)],
    [(ZStageBase[0] + ZStage[0] + SlipFit),(ZStageBase[1] – ZStageWrap)],
    [(ZStageBase[0] + ZStage[0] + SlipFit),ZStageBase[1]],
    [(ZStageBase[0] – SlipFit),ZStageBase[1]],
    [(ZStageBase[0] – SlipFit),(ZStageBase[1] – ZStageWrap)],
    [(ZStageBase[0] – ZWall),(ZStageBase[1] – ZStageWrap)],
    ]);
    for (k=[0:len(ZHoles) – 1])
    translate([(ZStageBase[0] + ZStage[0]/2),0.0,ZHoles[k]])
    rotate([-90,0,0])
    PolyCyl(3.5,2*ZStageBase[1],6);
    }
    }
    }
    //– X Slide attachment
    // Origin at left rear bottom of mount
    module XMount() {
    XHoles = [6.0,18.0]; // from end of X slide
    XHolesOffset = 7.0; // from bottom of X slide
    TrayHolesOC = 10.0;
    echo(str("Tray holes OC: ",TrayHolesOC));
    BlockOAH = XStageBlock[2] – XStageOffset[2] – XTray[2]; // overall height of mount
    difference() {
    translate([XStageBlock[0],0,BlockOAH])
    rotate([0,90,180])
    linear_extrude(height=XStageBlock[0],convexity=2)
    polygon(points=[
    [0,0],
    [0.0,7.0],
    [(XStageBlock[2] + SlipFit),7.0],
    [(XStageBlock[2] + SlipFit),XStageBlock[1]],
    [BlockOAH,XStageBlock[1]],
    [BlockOAH,0.0],
    ]);
    for (i=[0:len(XHoles) – 1]) // holes for X stage screws
    translate([XHoles[i],Protrusion,BlockOAH – XStageBlock[2] + XHolesOffset])
    rotate([90,0,0])
    PolyCyl(3.5,2*7.0,6);
    for (i=[-1,1]) // holes for tray mount
    translate([i*TrayHolesOC/2 + XStageBlock[0]/2,-XStageBlock[1]/2,-Protrusion])
    PolyCyl(2.5,0.75*(BlockOAH – XStageBlock[2]),6);
    }
    }
    //———————-
    // Build it
    if (Layout == "ZStand")
    ZStand();
    if (Layout == "YMount")
    YMount();
    if (Layout == "XMount")
    XMount();
    if (Layout == "Show") {
    color("lightgreen")
    ZStand();
    color("orange")
    translate(YStageOffset)
    YMount();
    color("lightblue")
    translate(XStageOffset + [0,0,-XStageOffset[2]])
    XMount();
    }
    if (Layout == "Build") {
    translate([20,0,0])
    ZStand();
    translate([YStageBlock[0]/2,0,0])
    YMount();
    translate([20,-30,0])
    XMount();
    }
  • Olfa Rotary Cutter Spacer

    At some point along the way, the bright yellow washer (they call it a “spacer”) on Mary’s 60 mm Olfa rotary cutter went missing. A casual search suggests that replacement washers come directly from Olfa after navigating their phone tree, but …

    Judging from scuffs on the rear surface, the washer serves two purposes:

    • Hold the blade close to the handle against slightly misaligned cutting forces
    • Add more compression to the wave washer under the nut

    This model is much more intricate than the stock washer:

    Olfa Rotary Cutter - backing washer
    Olfa Rotary Cutter – backing washer

    The trench across the middle of the thicker part allows a wider compression adjustment range for the wave washer and provides more thread engagement at the lightest setting for my liking. The shape comes from the chord equation based on measurements of the wave washer:

    Olfa Rotary Cutter - washer doodles
    Olfa Rotary Cutter – washer doodles

    The wave washer keys on the bolt flats: the whole affair rotates with the blade and gives the nut no inclination to unscrew. If you remove the trench, the remaining hole has the proper shape to key on the bolt and rotate with it; with the trench in place, the wave washer’s sides haul the plastic washer along with it.

    The plain ring, just two threads thick, glues bottom-to-bottom on the thicker part to soak up the air gap and provide more blade stability. It’s not entirely clear that’s a win; it’s easy to omit.

    It looks about like you’d expect:

    Olfa Rotary Cutter - washer in place
    Olfa Rotary Cutter – washer in place

    The wave washer must go on the bolt with the smooth curve downward into the trench. That orientation that wasn’t enforced by the Official Olfa spacer washer’s smooth sides.

    The nut sits upside-down to show the face that normally sits against the wave washer. I’d lay long odds that the recess around the threads originally held a conical compression spring with a penchant for joining the dust bunnies under the sewing table. You can insert the wave washer the wrong way, but it doesn’t store enough energy to go airborne unless you drop it, which did happen once with the expected result.

    The OpenSCAD source code as a GitHub gist:

    // Olfa rotary cutter backing washer
    // Ed Nisley KE4ZNU January 2016
    Layout = "Build";
    //- 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
    WasherOD = 35.0;
    WasherThick = 1.5;
    WaveOD = 14.0; // wave washer flat dia
    WaveM = 1.8; // height of wave washer bend
    BendRad = (pow(WaveM,2) + pow(WaveOD,2)/4) / (2*WaveM); // radius of wave washer bend
    echo(str("Wave washer bend radius: ",BendRad));
    SpacerID = WaveOD + 2.0;
    SpacerThick = 2*ThreadThick;
    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
    module Upper() {
    difference() {
    cylinder(d1=WasherOD,d2=(WasherOD – 2.0),h=WasherThick);
    translate([0,0,-Protrusion])
    intersection() {
    PolyCyl(8.2,2.0,8);
    cube([(6.0 + HoleWindage),10,2*WasherThick],center=true);
    }
    translate([-(WaveOD + 1.0)/2,0,BendRad])
    rotate([0,90,0]) rotate(0*180/16)
    PolyCyl(BendRad*2,(WaveOD + 1),16);
    }
    }
    module Spacer() {
    difference() {
    cylinder(d=WasherOD,h=SpacerThick);
    translate([0,0,-Protrusion])
    cylinder(d=SpacerID,h=2*SpacerThick);
    }
    }
    //———————-
    // Build it!
    if (Layout == "Show") {
    translate([0,0,SpacerThick])
    color("Cyan")
    Upper();
    color("LightCyan")
    Spacer();
    }
    if (Layout == "Build") {
    translate([-0.6*WasherOD,0,0])
    Upper();
    translate([0.6*WasherOD,0,0])
    Spacer();
    }
  • HP 7475A: Superformula Successes

    In the course of running off some Superformula plots, I found what must be my original stash of B-size plotter paper. Although it wasn’t archival paper and has yellowed a bit with age, it’s the smoothest and creamiest paper I’ve touched in quite some time: far nicer than the cheap stuff I picked up while reconditioning the HP 7475A plotter & its assorted pens.

    Once in a while, all my errors and omissions cancel out enough to produce interesting results on that historic paper, hereby documented for future reference…

    A triangle starburst:

    Superformula - triangle burst
    Superformula – triangle burst
    Superformula - triangle burst - detail
    Superformula – triangle burst – detail

    A symmetric starburst:

    Superformula - starburst
    Superformula – starburst
    Superformula - starburst - detail
    Superformula – starburst – detail

    Complex meshed ovals:

    Superformula - meshed ovals
    Superformula – meshed ovals
    Superformula - meshed ovals - details
    Superformula – meshed ovals – details

    They look better in person, of course. Although inkjet printers produce more accurate results in less time, those old pen plots definitely look better in some sense.

    The demo program lets you jam a fixed set of parameters into the plot, so (at least in principle) one could reproduce a plot from the parameters in the lower right corner. Here you go:

    The triangle starburst:

    Superformula - triangle burst - parameters
    Superformula – triangle burst – parameters

    The symmetric starburst:

    Superformula - starburst - parameters
    Superformula – starburst – parameters

    The meshed ovals:

    Superformula - meshed ovals - parameters
    Superformula – meshed ovals – parameters

    The current Python / Chiplotle source code as a GitHub gist:

    from chiplotle import *
    from math import *
    from datetime import *
    from time import *
    from types import *
    import random
    def superformula_polar(a, b, m, n1, n2, n3, phi):
    ''' Computes the position of the point on a
    superformula curve.
    Superformula has first been proposed by Johan Gielis
    and is a generalization of superellipse.
    see: http://en.wikipedia.org/wiki/Superformula
    Tweaked to return polar coordinates
    '''
    t1 = cos(m * phi / 4.0) / a
    t1 = abs(t1)
    t1 = pow(t1, n2)
    t2 = sin(m * phi / 4.0) / b
    t2 = abs(t2)
    t2 = pow(t2, n3)
    t3 = -1 / float(n1)
    r = pow(t1 + t2, t3)
    if abs(r) == 0:
    return (0, 0)
    else:
    # return (r * cos(phi), r * sin(phi))
    return (r, phi)
    def supershape(width, height, m, n1, n2, n3,
    point_count=10 * 1000, percentage=1.0, a=1.0, b=1.0, travel=None):
    '''Supershape, generated using the superformula first proposed
    by Johan Gielis.
    – `points_count` is the total number of points to compute.
    – `travel` is the length of the outline drawn in radians.
    3.1416 * 2 is a complete cycle.
    '''
    travel = travel or (10 * 2 * pi)
    # compute points…
    phis = [i * travel / point_count
    for i in range(1 + int(point_count * percentage))]
    points = [superformula_polar(a, b, m, n1, n2, n3, x) for x in phis]
    # scale and transpose…
    path = []
    for r, a in points:
    x = width * r * cos(a)
    y = height * r * sin(a)
    path.append(Coordinate(x, y))
    return Path(path)
    # RUN DEMO CODE
    if __name__ == '__main__':
    override = False
    plt = instantiate_plotters()[0]
    # plt.write('IN;')
    if plt.margins.soft.width < 11000: # A=10365 B=16640
    maxplotx = (plt.margins.soft.width / 2) – 100
    maxploty = (plt.margins.soft.height / 2) – 150
    legendx = maxplotx – 2900
    legendy = -(maxploty – 750)
    tscale = 0.45
    numpens = 4
    # prime/10 = number of spikes
    m_values = [n / 10.0 for n in [11, 13, 17, 19, 23]]
    # ring-ness 0.1 to 2.0, higher is larger
    n1_values = [
    n / 100.0 for n in range(55, 75, 2) + range(80, 120, 5) + range(120, 200, 10)]
    else:
    maxplotx = plt.margins.soft.width / 2
    maxploty = plt.margins.soft.height / 2
    legendx = maxplotx – 3000
    legendy = -(maxploty – 900)
    tscale = 0.45
    numpens = 6
    m_values = [n / 10.0 for n in [11, 13, 17, 19, 23, 29, 31,
    37, 41, 43, 47, 53, 59]] # prime/10 = number of spikes
    # ring-ness 0.1 to 2.0, higher is larger
    n1_values = [
    n / 100.0 for n in range(15, 75, 2) + range(80, 120, 5) + range(120, 200, 10)]
    print " Max: ({},{})".format(maxplotx, maxploty)
    # spiky-ness 0.1 to 2.0, higher is spiky-er (mostly)
    n2_values = [
    n / 100.0 for n in range(10, 60, 2) + range(65, 100, 5) + range(110, 200, 10)]
    plt.write(chr(27) + '.H200:') # set hardware handshake block size
    plt.set_origin_center()
    # scale based on B size characters
    plt.write(hpgl.SI(tscale * 0.285, tscale * 0.375))
    # slow speed for those abrupt spikes
    plt.write(hpgl.VS(10))
    while True:
    # standard loadout has pen 1 = fine black
    plt.write(hpgl.PA([(legendx, legendy)]))
    pen = 1
    plt.select_pen(pen)
    plt.write(hpgl.PA([(legendx, legendy)]))
    plt.write(hpgl.LB("Started " + str(datetime.today())))
    if override:
    m = 4.1
    n1_list = [1.15, 0.90, 0.25, 0.59, 0.51, 0.23]
    n2_list = [0.70, 0.58, 0.32, 0.28, 0.56, 0.26]
    else:
    m = random.choice(m_values)
    n1_list = random.sample(n1_values, numpens)
    n2_list = random.sample(n2_values, numpens)
    pen = 1
    for n1, n2 in zip(n1_list, n2_list):
    n3 = n2
    print "{0} – m: {1:.1f}, n1: {2:.2f}, n2=n3: {3:.2f}".format(pen, m, n1, n2)
    plt.select_pen(pen)
    plt.write(hpgl.PA([(legendx, legendy – 100 * pen)]))
    plt.write(
    hpgl.LB("Pen {0}: m={1:.1f} n1={2:.2f} n2=n3={3:.2f}".format(pen, m, n1, n2)))
    e = supershape(maxplotx, maxploty, m, n1, n2, n3)
    plt.write(e)
    pen = pen + 1 if (pen % numpens) else 1
    pen = 1
    plt.select_pen(pen)
    plt.write(hpgl.PA([(legendx, legendy – 100 * (numpens + 1))]))
    plt.write(hpgl.LB("Ended " + str(datetime.today())))
    plt.write(hpgl.PA([(legendx, legendy – 100 * (numpens + 2))]))
    plt.write(hpgl.LB("More at https://softsolder.com/?s=7475a&quot;))
    plt.select_pen(0)
    plt.write(hpgl.PA([(-maxplotx,maxploty)]))
    print "Waiting for plotter… ignore timeout errors!"
    sleep(40)
    while NoneType is type(plt.status):
    sleep(5)
    print "Load more paper, then …"
    print " … Press ENTER on the plotter to continue"
    plt.clear_digitizer()
    plt.digitize_point()
    plotstatus = plt.status
    while (NoneType is type(plotstatus)) or (0 == int(plotstatus) & 0x04):
    plotstatus = plt.status
    print "Digitized: " + str(plt.digitized_point)
  • Miniature Chain Mail: Handouts

    I ran off a few patches of miniature chain mail for holiday handouts to a few folks who’d appreciate them:

    Chain Mail Armor - 6x6 9.6 mm - top view
    Chain Mail Armor – 6×6 9.6 mm – top view

    A little patch like that makes a fondletoy that’s easier to pocket than, say, a planetary gear bearing and should be robust enough to withstand quite a bit of abuse.

    Alas, it turned out that recent Slic3r development versions suffered a bridging regression. The stable 1.2.9 version does the right thing:

    Slic3r 1.2.9 - good bridging
    Slic3r 1.2.9 – good bridging

    The hot-from-Github version goes diagonally, producing a pattern like an internal layer that normally sits atop the (omitted) bridge layer:

    Slic3r 7c8b710 - diagonal bridging
    Slic3r 7c8b710 – diagonal bridging

    While that might barely work, the little bitty link bars will certainly fall into the abyss:

    Slic3r 7c8b710 - diagonal bridging on links
    Slic3r 7c8b710 – diagonal bridging on links

    Given the complexity of slicing algorithms, I definitely can’t track down the problem; using the stable version for a while should suffice.

    The OpenSCAD source code as a GitHub gist:

    // Chain Mail Armor Buttons
    // Ed Nisley KE4ZNU – December 2014
    Layout = "Build"; // Link Button LB Joiner Joiners Build PillarMod
    //——-
    //- Extrusion parameters must match reality!
    // Print with 1 shell and 2+2 solid layers
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    Protrusion = 0.1; // make holes end cleanly
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    //——-
    // Dimensions
    //- Set maximum sheet size
    SheetSizeX = 125; // 170 for full sheet on M2
    SheetSizeY = 125; // 230 …
    //- Diamond or rectangular sheet?
    Diamond = false; // true = rotate 45 degrees, false = 0 degrees for square
    BendAround = "X"; // X or Y = maximum flexibility *around* designated axis
    Cap = true; // true = build bridge layers over links
    CapThick = 4 * ThreadThick; // flat cap on link: >= 3 layers for solid bridging
    Armor = true && Cap; // true = build armor button atop (required) cap
    ArmorThick = IntegerMultiple(2.0,ThreadThick); // height above cap surface
    ArmorSides = 4;
    ArmorAngle = true ? 180/ArmorSides : 0; // true -> rotate half a side for best alignment
    //- Link bar sizes
    BarThick = 3 * ThreadThick;
    BarWidth = 3.3 * ThreadWidth;
    BarClearance = 3 * ThreadThick; // vertical clearance above & below bars
    VertexHack = false; // true to slightly reduce openings to avoid coincident vertices
    //- Compute link sizes from those values
    //- Absolute minimum base link: bar width + corner angle + build clearance around bars
    // rounded up to multiple of thread width to ensure clean filling
    BaseSide = IntegerMultiple((4*BarWidth + 2*BarWidth/sqrt(2) + 3*(2*ThreadWidth)),ThreadWidth);
    BaseHeight = 2*BarThick + BarClearance; // both bars + clearance
    echo(str("BaseSide: ",BaseSide," BaseHeight: ",BaseHeight));
    //echo(str(" Base elements: ",4*BarWidth,", ",2*BarWidth/sqrt(2),", ",3*(2*ThreadWidth)));
    //echo(str(" total: ",(4*BarWidth + 2*BarWidth/sqrt(2) + 3*(2*ThreadWidth))));
    BaseOutDiagonal = BaseSide*sqrt(2) – BarWidth;
    BaseInDiagonal = BaseSide*sqrt(2) – 2*(BarWidth/2 + BarWidth*sqrt(2));
    echo(str("Outside diagonal: ",BaseOutDiagonal));
    //- On-center distance measured along coordinate axis
    // the links are interlaced, so this is half of what you think it should be…
    LinkOC = BaseSide/2 + ThreadWidth;
    LinkSpacing = Diamond ? (sqrt(2)*LinkOC) : LinkOC;
    echo(str("Base spacing: ",LinkSpacing));
    //- Compute how many links fit in sheet
    MinLinksX = ceil((SheetSizeX – (Diamond ? BaseOutDiagonal : BaseSide)) / LinkSpacing);
    MinLinksY = ceil((SheetSizeY – (Diamond ? BaseOutDiagonal : BaseSide)) / LinkSpacing);
    echo(str("MinLinks X: ",MinLinksX," Y: ",MinLinksY));
    NumLinksX = ((0 == (MinLinksX % 2)) && !Diamond) ? MinLinksX + 1 : MinLinksX;
    NumLinksY = ((0 == (MinLinksY % 2) && !Diamond)) ? MinLinksY + 1 : MinLinksY;
    echo(str("Links X: ",NumLinksX," Y: ",NumLinksY));
    //- Armor button base
    ButtonHeight = BaseHeight + BarClearance + CapThick;
    echo(str("ButtonHeight: ",ButtonHeight));
    //- Armor ornament size & shape
    // Fine-tune OD & ID to suit the number of sides…
    TotalHeight = ButtonHeight + ArmorThick;
    echo(str("Overall Armor Height: ",TotalHeight));
    ArmorOD = 1.0 * BaseSide; // tune for best base fit
    ArmorID = 10 * ThreadWidth; // make the tip blunt & strong
    //——-
    module ShowPegGrid(Space = 10.0,Size = 1.0) {
    RangeX = floor(95 / Space);
    RangeY = floor(125 / Space);
    for (x=[-RangeX:RangeX])
    for (y=[-RangeY:RangeY])
    translate([x*Space,y*Space,Size/2])
    %cube(Size,center=true);
    }
    //——-
    // Create link with armor button as needed
    module Link(Topping = false) {
    LinkHeight = (Topping && Cap) ? ButtonHeight : BaseHeight;
    render(convexity=3)
    rotate((BendAround == "X") ? 90 : 0)
    rotate(Diamond ? 45 : 0)
    union() {
    difference() {
    translate([0,0,LinkHeight/2]) // outside shape
    intersection() {
    cube([BaseSide,BaseSide,LinkHeight],center=true);
    rotate(45)
    cube([BaseOutDiagonal,BaseOutDiagonal,(LinkHeight + 2*Protrusion)],center=true);
    }
    translate([0,0,(BaseHeight + BarClearance + 0*ThreadThick – Protrusion)/2])
    intersection() { // inside shape
    cube([(BaseSide – 2*BarWidth),
    (BaseSide – 2*BarWidth),
    (BaseHeight + BarClearance + 0*ThreadThick + (VertexHack ? Protrusion/2 : 0))],
    center=true);
    rotate(45)
    cube([BaseInDiagonal,
    BaseInDiagonal,
    (BaseHeight + BarClearance + 0*ThreadThick + (VertexHack ? Protrusion/2 : 0))],
    center=true);
    }
    translate([0,0,((BarThick + 2*BarClearance)/2 + BarThick)]) // openings for bars
    cube([(BaseSide – 2*BarWidth – 2*BarWidth/sqrt(2) – (VertexHack ? Protrusion/2 : 0)),
    (2*BaseSide),
    BarThick + 2*BarClearance – Protrusion],
    center=true);
    translate([0,0,(BaseHeight/2 – BarThick)])
    cube([(2*BaseSide),
    (BaseSide – 2*BarWidth – 2*BarWidth/sqrt(2) – (VertexHack ? Protrusion/2 : 0)),
    BaseHeight],
    center=true);
    }
    if (Topping && Armor)
    translate([0,0,(ButtonHeight – Protrusion)]) // sink slightly into the cap
    rotate(ArmorAngle)
    cylinder(d1=ArmorOD,d2=ArmorID,h=(ArmorThick + Protrusion), $fn=ArmorSides);
    }
    }
    //——-
    // Create split buttons to join sheets
    module Joiner() {
    translate([-LinkSpacing,0,0])
    difference() {
    Link(false);
    translate([0,0,BarThick + BarClearance + TotalHeight/2 – Protrusion])
    cube([2*LinkSpacing,2*LinkSpacing,TotalHeight],center=true);
    }
    translate([LinkSpacing,0,0])
    intersection() {
    translate([0,0,-(BarThick + BarClearance)])
    Link(true);
    translate([0,0,TotalHeight/2])
    cube([2*LinkSpacing,2*LinkSpacing,TotalHeight],center=true);
    }
    }
    //——-
    // Build it!
    //ShowPegGrid();
    if (Layout == "Link") {
    Link(false);
    }
    if (Layout == "Button") {
    Link(true);
    }
    if (Layout == "LB") {
    color("Brown") Link(true);
    translate([LinkSpacing,LinkSpacing,0])
    color("Orange") Link(false);
    }
    if (Layout == "Build")
    for (ix = [0:(NumLinksX – 1)],
    iy = [0:(NumLinksY – 1)]) {
    x = (ix – (NumLinksX – 1)/2)*LinkSpacing;
    y = (iy – (NumLinksY – 1)/2)*LinkSpacing;
    translate([x,y,0])
    color([(ix/(NumLinksX – 1)),(iy/(NumLinksY – 1)),1.0])
    if (Diamond)
    Link((ix + iy) % 2); // armor at odd,odd & even,even points
    else
    if ((iy % 2) && (ix % 2)) // armor at odd,odd points
    Link(true);
    else if (!(iy % 2) && !(ix % 2)) // connectors at even,even points
    Link(false);
    }
    if (Layout == "Joiner")
    Joiner();
    if (Layout == "Joiners") {
    NumJoiners = max(MinLinksX,MinLinksY)/2;
    for (iy = [0:(NumJoiners – 1)]) {
    y = (iy – (NumJoiners – 1)/2)*2*LinkSpacing + LinkSpacing/2;
    translate([0,y,0])
    color([0.5,(iy/(NumJoiners – 1)),1.0])
    Joiner();
    }
    }
    if (Layout == "PillarMod") // Slic3r modification volume to eliminate pillar infill
    translate([0,0,(BaseHeight + BarClearance)/2])
    cube([1.5*SheetSizeX,1.5*SheetSizeY,BaseHeight + BarClearance],center=true);
  • Lenovo Q150: Setup for HP 7475A Plotter Demo

    Starting with a blank 120 GB SSD, I had to disable the “Plug-n-Play OS” BIOS option to get the Lenovo Q150 to boot from a System Rescue CD USB stick. While the hood was up, I told the BIOS to ignore keyboard errors so it can boot headless.

    Partitioning:

    • 50 GB ext4 partition for Mint
    • 8 GB swap
    • The remainder unallocated

    Booting & installing Mint Linux 17.2 XFCE from another USB stick went smoothly, after which it inhaled the usual gazillion updates. Rather than wait for the auto-updater to wake up and smell the repositories, I generally get it done thusly:

    sudo apt-get update
    sudo apt-get upgrade
    apt-get dist-upgrade
    apt-get autoremove
    

    Add my user to the dialout group, so I have access to the USB serial converter on /dev/ttyUSB0 that will drive the plotter.

    Configure a static IP address that appears in the appropriate /etc/hosts files.

    Install some useful packages:

    • nfs-common
    • openssh-server
    • htop and iotop

    Set up ssh for public key authentication, rather than passwords, on an unusual port, so everything else can happen from the Comfy Chair upstairs.

    Install packages that Chiplotle will need:

    • build-essential
    • python-setuptools
    • python-dev
    • python-numpy
    • python-serial
    • hp2xx

    I think some of those would be auto-installed as dependencies by the next step, but now I can remember what they are for the next time around this action loop:

    sudo easy_install -U chiplotle
    ... blank line to show underscore above ...
    

    Plug the old hard drive into a USB-SATA adapter to copy:

    Then chuck up some paper and pens to let it grind out art:

    HP 7475A - demo plot
    HP 7475A – demo plot

    It’s good clean fun…