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.

Month: January 2016

  • Great Blue Heron at Red Oaks Mill Dam

    A Great Blue Heron kept watch over us from the decaying spillway as we walked along on Old Mill Road:

    Heron at Red Oaks Mill Dam - spillway
    Heron at Red Oaks Mill Dam – spillway

    A mid-stream perch provided a better vantage point:

    Heron at Red Oaks Mill Dam - midstream
    Heron at Red Oaks Mill Dam – midstream

    The concrete slab in the lower right corner came from the dam breast.

    The camera never lies, but if you could look upward just a bit, you’d see the unending stream of cars passing by on Red Oaks Mill Road at the Rt 376 intersection …

  • Bathroom Light Switch: Contact Autopsy

    The dual switch controlling the bathroom lights began requiring some fiddling, which was not to be tolerated. After replacing the switch, I cracked the old one open to see what’s inside…

    The failed side of the switch controlled the lights over the sink:

    Light switch contacts - lights
    Light switch contacts – lights

    The side for the ceiling vent fan + light got much less use, still worked, and look a bit less blasted.

    Light switch contacts - ceiling fan
    Light switch contacts – ceiling fan

    Not much to choose between the two. It’s been running for nigh onto two decades, so …

  • Monthly Image: Hawk Overhead

    We often see a hawk perched atop a street lamp along Hooker Avenue, but this is the closest we’ve come:

    This slideshow requires JavaScript.

    That first wingbeat must be exhilarating:

    Hawk on Hooker 2015-12-26 - detail - 0236
    Hawk on Hooker 2015-12-26 – detail – 0236
    Hawk on Hooker 2015-12-26 - detail - 0248
    Hawk on Hooker 2015-12-26 – detail – 0248

    There doesn’t seem to be much behind the notion of reincarnation, but one interation as a bird would be edifying…

  • Ham It Up Noise Source

    An RTL-SDR receiver & Ham It Up RF upconverter arrived, with the intent of poking at LF signals. The upconverter circuit board also contains a mostly populated RF noise source:

    Ham-It-Up v1.3 noise source - schematic
    Ham-It-Up v1.3 noise source – schematic

    Being a sucker for noise sources, I spent some time pondering the circuitry.

    The as-built board has a 0 Ω jumper instead of the 6 dB pad along the upper right edge:

    Ham-It-Up v1.3 - noise components
    Ham-It-Up v1.3 – noise components

    The previous version had a pi bandpass filter in place of the pad and you could certainly repopulate it with two caps and a teeny inductor if you so desired.

    I added the SMA connector, which isn’t quite identical to the IF output connector above it:

    Ham-It-Up v1.3 - noise SMA
    Ham-It-Up v1.3 – noise SMA

    That will require a new hole in the end plate that I’ll get around to shortly. It also needs an external switch connected to the Enable jumper, but that’s in the nature of fine tuning.

    I’m awaiting a handful of adapters & cables from halfway around the planet…

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