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.

Author: Ed

  • LTSpice Diode Models Sorted By Forward Voltage

    LTSpice includes a bunch of LEDs I’ll never own, so finding a tabulation of their forward voltages helped match them against various LEDs on hand. The table was sorted by the forward voltage at the diode’s rated average current, which wasn’t helpful for my simple needs, so I re-sorted it on the Vf @ If = 20 mA column over on the right:

    Part #       Mfg             Is         N      Iavg Vf@Iavg  Vd@If
    QTLP690C     Fairchild    1.00E-22    1.500    0.16   1.90    1.82
    PT-121-B     Luminous     4.35E-07    8.370   20.00   3.84    2.34
    LUW-W5AP     OSRAM        6.57E-08    7.267    2.00   3.26    2.39
    LXHL-BW02    Lumileds     4.50E-20    2.600    0.40   2.95    2.75
    W5AP-LZMZ-5K Lumileds     3.50E-17    3.120    2.00   3.13    2.76
    LXK2-PW14    Lumileds     3.50E-17    3.120    1.60   3.11    2.76
    AOT-2015     AOT          5.96E-10    6.222    0.18   3.16    2.80
    NSSW008CT-P  Nichia       2.30E-16    3.430    0.04   2.92    2.86
    NSSWS108T    Nichia       1.13E-18    3.020    0.04   2.99    2.94
    NSPW500BS    Nichia       2.70E-10    6.790    0.03   3.27    3.20
    NSCW100      Nichia       1.69E-08    9.626    0.03   3.60    3.50

    The currents come from plugging the various constants into the Schockley Diode Equation and turning the crank.

    One could, of course, measure the constants for the diodes on hand to generate a proper Spice model, but that seems like a lot of work for what’s basically a blinking LED.

  • Cheap LED Assortment: Forward Voltage

    Cheap LED Assortment: Forward Voltage

    Starting with a box of cheap LEDs from halfway around the planet:

    LED kit - case
    LED kit – case

    Measuring the forward voltages didn’t take much effort:

    5mm 3mm LED kit - Vf tests
    5mm 3mm LED kit – Vf tests

    The top array fed the LEDs from a bench power supply through a 470 Ω resistor, with the voltage adjusted to make the current come out right. The bottom array came from the Siglent SDM3045 multimeter’s diode test function, which goes up to 4 V while applying about 400 µA to the diode (the 20 µA header is wrong).

    These numbers come into play when blinking an LED from a battery, because a battery voltage much below the Vf value won’t produce much light. It’s a happy coincidence that a single lithium cell can light a white or blue LED …

    For comparison, the forward voltages from another batch of LEDs:

    ROYGBUIW - LED Color vs Vf
    ROYGBUIW – LED Color vs Vf

    Those all look a bit higher at 20 mA, but everything about the measurements is different, so who knows?

  • AA Alkaline Battery Holder

    AA Alkaline Battery Holder

    A battery holder for AA alkaline cells descends directly from the NP-BX1 version:

    Astable Multivibrator - Alkaline Batteries - solid model - Show layout
    Astable Multivibrator – Alkaline Batteries – solid model – Show layout

    The square recesses fit single contact pads on the left and a “positive-to-negative conversion” plate on the right, all secured with dabs of acrylic adhesive:

    Alkaline AA holder - contacts
    Alkaline AA holder – contacts

    Although the OpenSCAD code contains an array of battery dimensions, it only works for AA cells.

    The recess on the far left is where you solder the wires onto the contact tabs, with the wires leading outward through the holes in the lid. The case needs an indexing feature to hold the lid square while gluing it down.

    Alkaline cells cells do not have current-limiting circuitry, so a low-current PTC fuse seems like a Good Idea. I initially thought of hiding it in the recess, but the Brutalist nature of the astables suggests open air.

    The OpenSCAD source code as a GitHub Gist:

    // Astable Multivibrator
    // Holder for Alkaline cells
    // Ed Nisley KE4ZNU August 2020
    /* [Layout options] */
    CellName = "AA"; // [AA] — does not work with anything else
    NumCells = 2;
    Layout = "Case"; // [Build,Show,Lid]
    Struts = -1; // [0:None, -1:Dual, 1:Quad]
    // Extrusion parameters – must match reality! */
    /* [Hidden] */
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    function IntegerLessMultiple(Size,Unit) = Unit * floor(Size / Unit);
    Protrusion = 0.1; // make holes end cleanly
    inch = 25.4;
    //- Basic dimensions
    WallThick = IntegerMultiple(3.0,ThreadWidth);
    CornerRadius = WallThick/2;
    FloorThick = IntegerMultiple(3.0,ThreadThick);
    TopThick = IntegerMultiple(2.0,ThreadThick);
    WireOD = 1.7; // wiring from pins to circuitry
    Gap = 5.0;
    // Cylindrical cell sizes
    // https://en.wikipedia.org/wiki/List_of_battery_sizes#Cylindrical_batteries
    CELL_NAME = 0;
    CELL_OD = 1;
    CELL_OAL = 2;
    CellData = [
    ["AAAA",8.3,42.5],
    ["AAA",10.5,44.5],
    ["AA",14.5,50.5],
    ["C",26.2,50],
    ["D",34.2,61.5],
    ["A23",10.3,28.5],
    ["CR123A",17.0,34.5],
    ["18650",18.8,65.2], // bare 18650 with button end
    ["18650Prot",19.0,70.0], // protected 18650 = 19670 plus a bit
    ];
    CellIndex = search([CellName],CellData,1,0)[0];
    echo(str("Cell index: ",CellIndex," = ",CellData[CellIndex][CELL_NAME]));
    //- Contact dimensions
    CONTACT_NAME = 0;
    CONTACT_WIDE = 1;
    CONTACT_HIGH = 2;
    CONTACT_THICK = 3; // plate thickness
    CONTACT_TIP = 4; // tip to rear face
    CONTACT_TAB = 5; // solder tab width
    ContactData = [
    ["AA+",12.2,12.2,0.3,1.7,3.5], // pos bump
    ["AA-",12.2,12.2,0.3,5.0,3.5], // half-compressed neg spring
    ["AA+-",28.2,12.2,0.3,5.0,0], // pos-neg bridge
    ["Li+",18.5,16.0,0.3,2.8,5.5],
    ["Li-",18.5,16.0,0.3,6.0,5.5],
    ];
    function ConDat(name,dim) = ContactData[search([name],ContactData,1,0)[0]][dim];
    ContactRecess = 2*ConDat(str(CellName,"+"),CONTACT_THICK);
    ContactOC = CellData[CellIndex][CELL_OD];
    WireBay = 6.0; // room for wiring to contacts
    //- Wire struts
    StrutDia = 1.6; // AWG 14 = 1.6 mm
    StrutSides = 3*4;
    ID = 0;
    OD = 1;
    LENGTH = 2;
    StrutBase = [StrutDia,StrutDia + 2*5*ThreadWidth, // ID = wire, OD = buildable
    FloorThick + CellData[CellIndex][CELL_OD]]; // base is flush with cell top
    //- Holder dimensions
    BatterySize = [CellData[CellIndex][CELL_OAL] + // cell
    ConDat(str(CellName,"+"),CONTACT_TIP) + // pos contact
    ConDat(str(CellName,"-"),CONTACT_TIP) – // neg contact
    2*ContactRecess, // sink into wall
    NumCells*CellData[CellIndex][CELL_OD],
    CellData[CellIndex][CELL_OD]
    ];
    echo(str("Battery space: ",BatterySize));
    CaseSize = [3*WallThick + // end walls + wiring partition
    BatterySize.x + // cell
    WireBay, // wiring bay
    2*WallThick + BatterySize.y,
    FloorThick + BatterySize.z
    ];
    BatteryOffset = (CaseSize.x – (2*WallThick +
    CellData[CellIndex][CELL_OAL] +
    ConDat(str(CellName,"-"),CONTACT_TIP))
    ) /2 ;
    ThumbRadius = 0.75 * CaseSize.z;
    StrutOC = [IntegerLessMultiple(CaseSize.x – 2*CornerRadius -2*StrutBase[OD],5.0),
    IntegerMultiple(CaseSize.y + StrutBase[OD],5.0)];
    StrutAngle = atan(StrutOC.y/StrutOC.x);
    echo(str("Strut OC: ",StrutOC));
    LidSize = [2*WallThick + WireBay + ConDat(str(CellName,"+"),CONTACT_THICK), CaseSize.y, FloorThick/2];
    //———————-
    // 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);
    }
    //– Overall case with origin at battery center
    module Case() {
    difference() {
    union() {
    hull()
    for (i=[-1,1], j=[-1,1])
    translate([i*(CaseSize.x/2 – CornerRadius),
    j*(CaseSize.y/2 – CornerRadius),
    0])
    cylinder(r=CornerRadius/cos(180/8),h=CaseSize.z,$fn=8); // cos() fixes undersize spheres!
    if (Struts)
    for (i = (Struts == 1) ? [-1,1] : -1) { // strut bases
    hull()
    for (j=[-1,1])
    translate([i*StrutOC.x/2,j*StrutOC.y/2,0])
    rotate(180/StrutSides)
    cylinder(d=StrutBase[OD],h=StrutBase[LENGTH],$fn=StrutSides);
    translate([i*StrutOC.x/2,0,StrutBase[LENGTH]/2])
    cube([2*StrutBase[OD],StrutOC.y,StrutBase[LENGTH]],center=true); // blocks for fairing
    for (j=[-1,1]) // hemisphere caps
    translate([i*StrutOC.x/2,
    j*StrutOC.y/2,
    StrutBase[LENGTH]])
    rotate(180/StrutSides)
    sphere(d=StrutBase[OD]/cos(180/StrutSides),$fn=StrutSides);
    }
    }
    translate([BatteryOffset,0,BatterySize.z/2 + FloorThick]) // cells
    cube(BatterySize + [0,0,Protrusion],center=true);
    translate([BatterySize.x/2 + BatteryOffset + ContactRecess/2 – Protrusion/2, // contacts
    0,
    BatterySize.z/2 + FloorThick])
    cube([ContactRecess + Protrusion,
    ConDat(str(CellName,"+-"),CONTACT_WIDE),
    ConDat(str(CellName,"+-"),CONTACT_HIGH)
    ],center=true);
    translate([-(BatterySize.x/2 – BatteryOffset + ContactRecess/2 – Protrusion/2),
    ContactOC/2,
    BatterySize.z/2 + FloorThick])
    cube([ContactRecess + Protrusion,
    ConDat(str(CellName,"+"),CONTACT_WIDE),
    ConDat(str(CellName,"+"),CONTACT_HIGH)
    ],center=true);
    translate([-(BatterySize.x/2 – BatteryOffset + ContactRecess/2 – Protrusion/2),
    -ContactOC/2,
    BatterySize.z/2 + FloorThick])
    cube([ContactRecess + Protrusion,
    ConDat(str(CellName,"-"),CONTACT_WIDE),
    ConDat(str(CellName,"-"),CONTACT_HIGH)
    ],center=true);
    translate([-CaseSize.x/2 + WireBay/2 + WallThick, // wire bay
    0,
    BatterySize.z/2 + FloorThick + Protrusion/2])
    cube([WireBay,
    BatterySize.y,
    BatterySize.z + Protrusion
    ],center=true);
    for (j=[-1,1])
    translate([-(BatterySize.x/2 – BatteryOffset + WallThick/2), // contact tabs
    j*ContactOC/2,
    BatterySize.z + FloorThick – Protrusion])
    cube([2*WallThick,
    ConDat(str(CellName,"+"),CONTACT_TAB),
    (BatterySize.z – ConDat(str(CellName,"+"),CONTACT_HIGH))
    ],center=true);
    if (false)
    translate([0,0,CaseSize.z]) // finger cutout
    rotate([90,00,0])
    cylinder(r=ThumbRadius,h=2*CaseSize.y,center=true,$fn=22);
    if (Struts)
    for (i2 = (Struts == 1) ? [-1,1] : -1) { // strut wire holes and fairing
    for (j=[-1,1])
    translate([i2*StrutOC.x/2,j*StrutOC.y/2,FloorThick])
    rotate(180/StrutSides)
    PolyCyl(StrutBase[ID],2*StrutBase[LENGTH],StrutSides);
    for (i=[-1,1], j=[-1,1]) // fairing cutaways
    translate([i*StrutBase[OD] + (i2*StrutOC.x/2),
    j*StrutOC.y/2,
    -Protrusion])
    rotate(180/StrutSides)
    PolyCyl(StrutBase[OD],StrutBase[LENGTH] + 2*Protrusion,StrutSides);
    }
    }
    }
    module Lid() {
    difference() {
    hull()
    for (i=[-1,1], j=[-1,1], k=[-1,1])
    translate([i*(LidSize.x/2 – CornerRadius),
    j*(LidSize.y/2 – CornerRadius),
    k*(LidSize.z – CornerRadius)]) // double thickness for flat bottom
    sphere(r=CornerRadius/cos(180/8),$fn=8);
    translate([0,0,-LidSize.z]) // remove bottom
    cube([(LidSize.x + 2*Protrusion),(LidSize.y + 2*Protrusion),2*LidSize.z],center=true);
    for (j=[-1,1]) // wire holes
    translate([0,j*LidSize.y/4,-Protrusion])
    PolyCyl(WireOD,2*LidSize.z,6);
    }
    }
    //——————-
    // Build it!
    if (Layout == "Case")
    Case();
    if (Layout == "Lid")
    Lid();
    if (Layout == "Build") {
    rotate(-90)
    translate([CaseSize.x/2 + Gap,0,0])
    Case();
    rotate(-90)
    translate([-LidSize.x/2 – Gap,0,0])
    Lid();
    }
    if (Layout == "Show") {
    Case();
    translate([-CaseSize.x/2 + LidSize.x/2,0,(CaseSize.z + Gap)])
    Lid();
    }

  • Round Soaker Hose Clamp

    Round Soaker Hose Clamp

    An aging round soaker hose sprang a leak large enough to gouge a crater under a tomato plant, so I conjured a short clamp from the longer round hose splints:

    Soaker Hose Clamp - round - installed
    Soaker Hose Clamp – round – installed

    The shiny stuff is the plastic backing on strips of silicone tape intended to prevent the high-pressure water from squirting through the porous 3D printed plastic. The fat drop hanging from the hose shows some leakage around the tape; an occasional drop is perfectly OK.

    The leak faces the round side of the bottom half of the clamp, which probably doesn’t make any difference.

    I hope the washers occupy enough of the minimal surface to render aluminum backing plates superfluous:

    Soaker Hose Clamp - round - kitted
    Soaker Hose Clamp – round – kitted

    Creating the 3D model required nothing more than shortening the original splint to 30 mm with two screws along each side. While I was at it, I had Slic3r make three clamps to put two in the Garden Dedicated Hydraulic Repair Kit for later use:

    Round Soaker Hose Splice - 30mm - Slic3r
    Round Soaker Hose Splice – 30mm – Slic3r

    Change two lines in the OpenSCAD code and it’s done.

    Also: clamps for flat soaker hoses.

  • Tour Easy Daytime Running Light: Second Fracture

    Tour Easy Daytime Running Light: Second Fracture

    While clearing some overhanging brush along the rail trail, I probably wedged a branch between the LC40 flashlight and the fairing:

    Fairing Flashlight Mount - brush clearing
    Fairing Flashlight Mount – brush clearing

    Aaaand twisted it enough to fracture the mount:

    Fairing Flashlight Mount - another fracture
    Fairing Flashlight Mount – another fracture

    A closer look shows the infill just ripped apart:

    Fairing Flashlight Mount - another failure - detail
    Fairing Flashlight Mount – another failure – detail

    I can’t be sure that’s what happened, because the mount actually failed several miles later, after I hit one of the potholes along Raymond Avenue. Fortunately, I saw it swinging away from the fairing, hanging by its last few threads, and managed to grab it before it vanished.

    Fairing Flashlight Mount - Catch a Falling Mount
    Fairing Flashlight Mount – Catch a Falling Mount

    I set Slic3r for 30% infill on the replacement, but the running light been riding my fairing for three years and seems strong enough under normal use.

  • Quilting Hexagon Template Generator: Knobless Half-Triangle

    Quilting Hexagon Template Generator: Knobless Half-Triangle

    Although I’d put the same knob on the half-triangle end piece template as on the equilateral triangle template for piecing hexagons into strips, Mary decided a flat chip would be easier to use:

    Quilting Hex Template - family - knobless half-triangle
    Quilting Hex Template – family – knobless half-triangle

    Bonus: you can now flip it over to cut the other half-triangles, if you haven’t already figured out how to cut two layers of fabric folded wrong sides together.

    While I was at it, the knob on the triangle became optional, too. Flipping that one doesn’t buy you much, though.

    The OpenSCAD source as a GitHub Gist has been ever so slightly tweaked:

    // Quilting – Hexagon Templates
    // Ed Nisley KE4ZNU – July 2020
    // Reverse-engineered to repair a not-quite-standard hexagon quilt
    // Useful geometry:
    // https://en.wikipedia.org/wiki/Hexagon
    /* [Layout Options] */
    Layout = "Build"; // [Build, HexBuild, HexPlate, TriBuild, TriPlate, EndBuild, EndPlate]
    //——-
    //- Extrusion parameters must match reality!
    // Print with 2 shells
    /* [Hidden] */
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    HoleFinagle = 0.2;
    HoleFudge = 1.00;
    function HoleAdjust(Diameter) = HoleFudge*Diameter + HoleFinagle;
    Protrusion = 0.1; // make holes end cleanly
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    inch = 25.4;
    //——-
    // Dimensions
    /* [Layout Options] */
    FinishedWidthInch = 2.75;
    FinishedWidth = FinishedWidthInch * inch;
    SeamAllowanceInch = 0.25;
    SeamAllowance = SeamAllowanceInch * inch;
    TemplateThick = 3.0;
    TriKnob = true;
    EndKnob = false;
    /* [Hidden] */
    FinishedSideInch = FinishedWidthInch/sqrt(3);
    FinishedSide = FinishedSideInch * inch;
    echo(str("Finished side: ",FinishedSideInch," inch"));
    CutWidth = FinishedWidth + 2*SeamAllowance;
    CutSide = CutWidth/sqrt(3);
    echo(str("Cut side: ",CutSide / inch," inch"));
    // Make polygon-circles circumscribe the target widths
    TemplateID = FinishedWidth / cos(180/6);
    TemplateOD = CutWidth / cos(180/6);
    /* [Hidden] */
    TriRadius = FinishedSide/sqrt(3);
    TriPoints = [[TriRadius,0],
    [TriRadius*cos(120),TriRadius*sin(120)],
    [TriRadius*cos(240),TriRadius*sin(240)]
    ];
    echo(str("TriPoints: ",TriPoints));
    EndPoints = [[TriRadius,0],
    [TriRadius*cos(120),TriRadius*sin(120)],
    [TriRadius*cos(120),0]
    ];
    echo(str("EndPoints: ",EndPoints));
    TipCutRadius = 2*(TriRadius + SeamAllowance); // circumscribing radius of tip cutter
    TipPoints = [[TipCutRadius,0],
    [TipCutRadius*cos(120),TipCutRadius*sin(120)],
    [TipCutRadius*cos(240),TipCutRadius*sin(240)]
    ];
    HandleHeight = 1 * inch;
    HandleLength = (TemplateID + TemplateOD)/2;
    HandleThick = IntegerMultiple(3.0,ThreadWidth);
    HandleSides = 12*4;
    StringDia = 4.0;
    StringHeight = 0.6*HandleHeight;
    DentDepth = HandleThick/4;
    DentDia = 15 * DentDepth;
    DentSphereRadius = (pow(DentDepth,2) + pow(DentDia,2)/4)/(2*DentDepth);
    KnobOD = 15.0; // Triangle handle
    KnobHeight = 20.0;
    //——-
    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=HoleAdjust(FixDia)/2,h=Height,$fn=Sides);
    }
    //——-
    // Hex template
    module HexPlate() {
    difference() {
    cylinder(r=TemplateOD/2,h=TemplateThick,$fn=6);
    translate([0,0,-Protrusion])
    cylinder(r=TemplateID/2,h=(TemplateThick + 2*Protrusion),$fn=6);
    }
    for (i=[1:6/2])
    rotate(i*60)
    translate([0,0,TemplateThick/2])
    cube([HandleLength,HandleThick,TemplateThick],center=true);
    }
    module HexHandle() {
    difference() {
    rotate([90,0,0])
    scale([1,HandleHeight/(TemplateOD/2),1])
    rotate(180/HandleSides)
    cylinder(d=HandleLength,h=HandleThick,center=true,$fn=HandleSides);
    translate([0,0,-HandleHeight])
    cube([2*TemplateOD,2*TemplateOD,2*HandleHeight],center=true);
    translate([0,HandleThick,StringHeight])
    rotate([90,090,0])
    rotate(180/8)
    PolyCyl(StringDia,2*HandleThick,8);
    for (j=[-1,1]) {
    translate([0,j*(DentSphereRadius + HandleThick/2 – DentDepth),StringHeight])
    rotate(180/48)
    sphere(r=DentSphereRadius,$fn=48);
    }
    }
    }
    module HexTemplate() {
    HexPlate();
    HexHandle();
    }
    //——-
    // Triangle template
    module TriPlate() {
    linear_extrude(height=TemplateThick)
    intersection() {
    offset(delta=SeamAllowance) // basic cutting outline
    polygon(points=TriPoints);
    rotate(180)
    polygon(points=TipPoints);
    }
    }
    module TriTemplate() {
    union() {
    if (TriKnob)
    cylinder(d=KnobOD,h=KnobHeight,$fn=HandleSides);
    TriPlate();
    }
    }
    //——-
    // End piece template
    module EndPlate() {
    linear_extrude(height=TemplateThick)
    intersection() {
    offset(delta=SeamAllowance) // basic cutting outline
    polygon(points=EndPoints);
    rotate(180)
    polygon(points=TipPoints);
    }
    }
    module EndTemplate() {
    union() {
    if (EndKnob)
    translate([0,(TriRadius/2)*sin(30),0])
    cylinder(d=KnobOD,h=KnobHeight,$fn=HandleSides);
    EndPlate();
    }
    }
    //——-
    // Build it!
    if (Layout == "HexPlate")
    HexPlate();
    if (Layout == "HexBuild")
    HexTemplate();
    if (Layout == "TriPlate")
    TriPlate();
    if (Layout == "TriBuild")
    TriTemplate();
    if (Layout == "EndPlate")
    EndPlate();
    if (Layout == "EndBuild")
    EndTemplate();
    if (Layout == "Build") {
    translate([1.5*TriRadius,-TriRadius,0])
    rotate(180/6)
    TriTemplate();
    translate([-1.5*TriRadius,-TriRadius,0])
    rotate(180/6)
    EndTemplate();
    translate([0,TemplateOD/2,0])
    HexTemplate();
    }

  • Power Outage

    Power Outage

    Just before Tropical Storm Isaias rolled through, my hygrometer reached a new high:

    Pre-Isaias humidity
    Pre-Isaias humidity

    The National Weather Service reported 99% at the airport a few miles away, so the meter’s calibration seems about right.

    Shortly thereafter, the humidity dropped to the mid-70s as the wind picked up and, over the next few hours, falling branches took out vast swaths of Central Hudson’s electrical infrastructure. My little generator saved our refrigerator & freezer during 15 hours of outage; three days later, thousands of folks around us still have no power.

    A confluence of other events, none nearly so dramatic, will throttle my posting over the next two weeks.

    We’re OK and hope you’re OK, too …