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

  • Keyboard Tray Raising

    Keyboard Tray Raising

    Long ago and far away, I moved the keyboards off our desk surfaces to a more convenient location on a tray under the middle drawer. Mary’s desk recently gained a somewhat thinner keyboard with a thumbwheel volume control, so she wanted the tray moved up:

    Keyboard Tray Relocation - in place
    Keyboard Tray Relocation – in place

    The supports on either side started out as 2×4 lumber with a slot cut (using the radial arm saw I no longer have) for the aluminum sheet:

    Keyboard Tray Relocation - support view
    Keyboard Tray Relocation – support view

    For the record, a pair of screws hold each support to the drawer:

    Keyboard Tray Relocation - support screw
    Keyboard Tray Relocation – support screw

    Not elegant. Works fine. Good enough!

    Tiny Bandsaw™ wasn’t designed for ripsawing lumber, but the same Proxxon 10/14 TPI blade I use for aluminum worked better than I expected to lop a 1-¼ inch strip from the wood slats:

    Keyboard Tray Relocation - bandsaw fixture
    Keyboard Tray Relocation – bandsaw fixture

    That’s a reenactment based on a true story. The wood scraps clamped on the bandsaw table leave enough clearance for the 2×4 slide to freely, yet not enough for the blade to wander off track.

    You can tell how long ago I built the original trays: nary a trace of 3D printing!

  • Nutmeg Season

    Nutmeg Season

    Well, it’s really zucchini bread season, with grated nutmeg among the spices (*):

    Zucchini bread - minus QC sample
    Zucchini bread – minus QC sample

    Having recently bought a very sharp grater, I hauled out a small vise to save my fingertips:

    Nutmeg grating - mini-vise
    Nutmeg grating – mini-vise

    The dark lunette comes from a previous clamping attempt; it takes a while to find the most secure pin arrangement.

    Grate a flat:

    Nutmeg grating - first flat
    Nutmeg grating – first flat

    I’ve always enjoyed the surprisingly intricate patterns inside what looks like a bland nut.

    Flip it over, flatten the other side, and grab it in an even smaller vise:

    Nutmeg grating - flat clamping
    Nutmeg grating – flat clamping

    In truth, that vise is intended for small cylinders, not flattened nuts, but I figured it’d suffice for light-duty use. Grate parallel to the vise screw, reclamp as needed, and it worked out reasonably well.

    Eventually, you have a pile of powder and one cubic nutmeg:

    Nutmeg grating - results
    Nutmeg grating – results

    I’m sure there’s a way to grate the remaining cube, but I’m unwilling to shred my fingertips.

    Tip the powder into a small jar and repeat as needed. Each nutmeg produces about 5 grams and I did three of the things this time.

    Yummy!

    (*) We omit the cloves and knock the sugar down by half. Your tastes will surely differ.

    Update: Mary’s recipe!

  • Long-gone Labeling

    Long-gone Labeling

    These appeared while I was extricating the 3-axis positioner from an old project:

    Migrated felt-tip pen labels
    Migrated felt-tip pen labels

    I’m reasonably sure those labels started with blue ink from my all-time favorite Ultra-Fine-Point Sharpie markers on address labels covered with ordinary matte tape. Fourteen years on, the X, Y, and Drive legends are pretty much indistinguishable.

    Nothing lasts …

  • Bike Helmet Mirror: Ball Mount

    Bike Helmet Mirror: Ball Mount

    Nine years ago, I didn’t know how enough to design a bike helmet mirror with a ball mount, but even an old dog can learn a new trick:

    Helmet Mirror Ball Mount - on helmet
    Helmet Mirror Ball Mount – on helmet

    However, it’s worth noting my original, butt-ugly Az-El mounts lasted for all of those nine years, admittedly with adjustments along the way, which is far more than the commercial mounts making me unhappy enough to scratch my itch.

    The mount adapts the split spherical clamp from the daytime running light:

    Helmet Mirror Mount - Ball
    Helmet Mirror Mount – Ball

    Scaling it down for a 10 mm polypropylene ball around the base of the 30 mm inspection mirror’s shaft simplified everything:

    Helmet Mirror Ball Mount - drilled ball test
    Helmet Mirror Ball Mount – drilled ball test

    I’m reasonably sure I couldn’t have bought 100 polypro balls for eight bucks a decade ago, but we’ll never know. Drilling the hole was a complete botch job, about which more later. The shaft came from a spare mirror mount I made up a while ago; a new shaft appears below.

    The solid model, like Gaul, is in three parts divided:

    Helmet Mirror Ball Mount - Slic3r
    Helmet Mirror Ball Mount – Slic3r

    The helmet plate (on the right) has a slight indent more-or-less matching the helmet curvature and gets a layer of good double-stick foam tape. The clamp base (on the left) has a pair of brass inserts epoxied into matching recesses below the M3 clearance holes:

    Helmet Mirror Ball Mount - inserts
    Helmet Mirror Ball Mount – inserts

    A layer of epoxy then sticks the helmet plate in place, with the inserts providing positive alignment:

    Helmet Mirror Ball Mount - plates
    Helmet Mirror Ball Mount – plates

    The clamp screws pull the inserts against the plastic in the clamp base, so they can’t pull out or through, and the plates give the epoxy enough bonding surface that (I’m pretty sure) they won’t ever come apart.

    I turned down a 2 mm brass insert to fit inside the butt end of the mirror shaft and topped it off with a random screw harvested from a dead hard drive:

    Helmet Mirror Ball Mount - assembled - rear view
    Helmet Mirror Ball Mount – assembled – rear view

    At the start, it wasn’t obvious the shaft would stay stuck in the ball, so I figured making it impossible to pull out would eliminate the need to find it by the side of the road. As things turned out, the clamp exerts enough force to ensure the shaft ain’t goin’ nowhere, so I’ll plug future shafts with epoxy.

    The front side of the clamp looks downright sleek:

    Helmet Mirror Ball Mount - assembled - front view
    Helmet Mirror Ball Mount – assembled – front view

    Well, how about “chunky”?

    The weird gray-black highlights are optical effects from clear / natural PETG, rather than embedded grunge; it looks better in person. I should have used retina-burn orange or stylin’ black.

    This mount is much smaller than the old one and should, in the event of a crash, not cause much injury. Based on how the running light clamp fractures, I expect the clamp will simply tear out of the base on impact. In the last decade, neither of us has crashed, so I don’t know what the old mount would do.

    The clamp is 7 mm thick (front-to-back), set by the M3 washer diameter, with 1.5 mm of ball sticking out on each side. The model has a kerf one thread high (0.25 mm) between the pieces to add clamping force and, with the screws tightened down, moving the ball requires a disturbingly large effort. I added a touch of rosin and that ball straight-up won’t move, which probably means the shaft will bend upon droppage; I have several spare mirrors in stock.

    On the other paw, the ball turns smoothly in the clamp and it’s easy to position the shaft as needed: much better than the old Az-El mount!

    The inspection mirror hangs from a double ball joint which arrives with a crappy screw + nut. I epoxied the old mirror mount nut in place, but this time around I drilled the plates for a 3 mm stainless SHCS, used a wave washer for a bit of flexible force, and topped it off with a nyloc nut:

    Helmet Mirror Ball Mount - mirror joint
    Helmet Mirror Ball Mount – mirror joint

    I’m unhappy with how it looks and don’t like how the washer hangs in free space between those bumps, so I may eventually turn little brass fittings to even things out. It’s either that or more epoxy.

    So far, though, it’s working pretty well and both units meet customer requirements.

    The OpenSCAD source code as a GitHub Gist:

    // Bike helmet mirror mount – ball joint
    // Ed Nisley KE4ZNU 2020-09
    /* [Layout options] */
    Layout = "Build"; // [Build, Show, Plate, Base, Clamp]
    //– Extrusion parameters
    // Extrusion parameters
    /* [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;
    ID = 0;
    OD = 1;
    LENGTH = 2;
    //- Basic dimensions
    MountDia = 30.0; // footprint on helmet
    BallDia = 10.0;
    BallRad = BallDia / 2;
    WallThick = IntegerMultiple(2.0,ThreadWidth);
    FloorThick = IntegerMultiple(2.0,ThreadThick);
    CornerRound = 2.0;
    Insert = [3.0,4.0,4.0]; // threaded brass insert
    Screw = [3.0,5.5,25.0]; // clamp screw
    Washer = [3.7,7.0,0.7]; // washer
    ShowGap = 2.0;
    BuildGap = 5.0;
    //– Helmet Interface Plate
    ScrewOC = BallDia + 2*WallThick + Screw[ID];
    echo(str("Screw OC: ",ScrewOC));
    Clamp = [ceil(Washer[OD]), // barely holds washer under screw
    ScrewOC + Washer[OD], // minimal clearance for washer
    BallDia +2*FloorThick // screw fits through insert
    ];
    Kerf = ThreadThick;
    echo(str("Clamp: ",Clamp));
    HelmetCX = 60.0; // empirical helmet side curve
    HelmetMX = 3.0;
    HelmetRX = (pow(HelmetMX,2) + pow(HelmetCX,2)/4)/(2*HelmetMX);
    HelmetPlateC = MountDia;
    HelmetPlateTheta = atan(HelmetPlateC/HelmetRX);
    HelmetPlateM = 2*HelmetRX*pow(sin(HelmetPlateTheta/4),2);
    echo(str("Plate indent: ",HelmetPlateM));
    HelmetPlateThick = max(FloorThick,0.6*Insert[LENGTH]) + HelmetPlateM;
    echo(str("Screw length: ",Clamp.z + Insert[LENGTH]));
    MountSides = 2*3*4;
    //———————-
    // Useful routines
    module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
    Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    FixDia = Dia / cos(180/Sides);
    cylinder(r=(FixDia + HoleWindage)/2,h=Height,$fn=Sides);
    }
    //———————-
    // Clamp frame around ball
    module ClampFrame() {
    difference() {
    union() {
    hull()
    for (i=[-1,1], j=[-1,1]) {
    translate([i*(Clamp.x/2 – CornerRound),j*(Clamp.y/2 – CornerRound),Clamp.z/2 – CornerRound])
    sphere(r=CornerRound,$fn=24);
    translate([i*(Clamp.x/2 – CornerRound),j*(Clamp.y/2 – CornerRound),-Clamp.z/2])
    cylinder(r=CornerRound,$fn=24);
    }
    for (j=[-1,1])
    translate([0,j*ScrewOC/2,0])
    rotate(180/12)
    cylinder(d=Washer[OD],h=Clamp.z/2,$fn=12);
    }
    sphere(d=BallDia + HoleWindage,$fn=48);
    cube([2*MountDia,2*MountDia,Kerf],center=true);
    for (j=[-1,1])
    translate([0,j*ScrewOC/2,-Screw[LENGTH]])
    rotate(180/6)
    PolyCyl(Screw[ID],2*Screw[LENGTH],6);
    }
    }
    module ClampSelect(Section) {
    XlateZ = (Section == "Top") ? Clamp.z/2 :
    (Section == "Bottom") ? -Clamp.z/2 :
    0;
    intersection(convexity=5) {
    ClampFrame();
    translate([0,0,XlateZ])
    cube([2*Clamp.x,2*Clamp.y,Clamp.z + 2*Protrusion],center=true);
    }
    }
    //———————-
    // Concave plate fitting helmet shell
    module HelmetPlate() {
    render()
    difference() {
    cylinder(d=MountDia,h=HelmetPlateThick,$fn=MountSides);
    translate([0,0,HelmetPlateThick – HelmetPlateM + HelmetRX])
    sphere(r=HelmetRX,$fn=128);
    for (j=[-1,1])
    translate([0,j*ScrewOC/2,-Protrusion]) {
    PolyCyl(Insert[OD],0.6*Insert[LENGTH] + Protrusion,6);
    PolyCyl(Screw[ID],2*HelmetPlateThick,6);
    }
    }
    }
    //———————-
    // Base of clamp ring
    module MountBase() {
    difference() {
    union() {
    cylinder(d=MountDia,h=FloorThick,$fn=MountSides);
    translate([0,0,FloorThick + Clamp.z/2])
    ClampSelect("Bottom");
    }
    for (j=[-1,1])
    translate([0,j*ScrewOC/2,-Protrusion])
    rotate(180/6)
    PolyCyl(Insert[OD],0.6*Insert[LENGTH] + Protrusion,6);
    }
    }
    //———————-
    // Lash it together
    if (Layout == "Plate") {
    HelmetPlate();
    }
    if (Layout == "Base") {
    MountBase();
    }
    if (Layout == "Clamp") {
    ClampFrame();
    }
    if (Layout == "Show") {
    rotate([180,0,0])
    HelmetPlate();
    translate([0,0,ShowGap]) {
    MountBase();
    color("Ivory",0.3)
    translate([0,0,Clamp.z/2 + FloorThick + ShowGap/2])
    sphere(d=BallDia);
    translate([0,0,Clamp.z/2 + FloorThick + ShowGap])
    ClampSelect("Top");
    }
    }
    if (Layout == "Build") {
    translate([MountDia/2 + BuildGap,0,0])
    HelmetPlate();
    translate([-(MountDia/2 + BuildGap),0,0])
    MountBase();
    translate([0,MountDia/2 + BuildGap,Clamp.z/2])
    rotate([0,180,0])
    rotate(90)
    ClampSelect("Top");
    }

    The original doodles include a bit of dress-up fairing that didn’t make the cut:

    Helmet Mirror Ball Mount - doodles
    Helmet Mirror Ball Mount – doodles
  • Discrete LM3909: Blue LED Radome

    Discrete LM3909: Blue LED Radome

    Dropping a simplified ping-pong ball radome for a Piranha RGB LED atop a discrete LM3909 on the AA alkaline cell holder:

    Discrete LM3909 Radome - AA alkaline
    Discrete LM3909 Radome – AA alkaline

    The solid model has screw holes for the lid and the revised LED spider:

    Astable Multivibrator - Alkaline AA Base - radome - solid model
    Astable Multivibrator – Alkaline AA Base – radome – solid model

    The RGB LED needs only two wires, as the LM3909 circuit can blink only one LED. I tried all three colors, but only blue and green justify the LM3909 hairball; red can get along with the astable circuit.

    The LED wires connect across a 1 MΩ resistor serving as a mechanical strut between the 9.1 kΩ resistor on the left and the 10 Ω ballast resistor on the right.

    Fresh alkaline cells at 3.0 V put 3.3 V across the blue LED with a 37 mA peak current. Older cells at 2.3 V produce 2.9 V at 15 mA. Dead cells at 1.9 V still fire the LED with 2.7 V at 4.2 mA, although the flash is barely visible in ordinary room light.

    The lovely blue ball looks better in person!

    The OpenSCAD source code as a GitHub Gist:

    // Astable Multivibrator
    // Holder for Alkaline cells
    // Ed Nisley KE4ZNU August 2020
    // 2020-09 add LED radome
    /* [Layout options] */
    Layout = "Build"; // [Build,Show,Lid,Spider]
    /* [Hidden] */
    CellName = "AA"; // [AA] — does not work with anything else
    NumCells = 2; // [2] — likewise
    Struts = -1; // [0:None, -1:Dual, 1:Quad] — Quad is dead
    // Extrusion parameters
    /* [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.5; // battery & LED wiring
    WireOC = 4;
    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;
    // FIXME search() needs special-casing to properly find AAA and AAAA
    // Which is why CellName is limited to AA
    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]]; // LENGTH = 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];
    LidScrew = [2.0,3.8,7.0]; // M2 pan head screw (LENGTH = threaded)
    LidScrewOC = CaseSize.y/2 – CornerRadius – LidScrew[OD]; // allow space around screw head
    //- Piranha LEDs
    PiranhaBody = [8.0,8.0,8.0]; // Z = heatsink fins + body + lens height
    PiranhaPin = 0.0; // trimmed pin length beyond heatsink
    PiranhaPinsOC = [5.0,5.0]; // pin XY distance
    PiranhaRecess = PiranhaBody.z + PiranhaPin/2; // minimum LED recess depth
    BallOD = 40.0; // radome sphere
    BallSides = 4*StrutSides; // nice smoothness
    BallPillar = [norm([PiranhaBody.x,PiranhaBody.y]), // ID
    norm([PiranhaBody.x,PiranhaBody.y]) + 3*WallThick, // OD
    StrutBase[OD] + PiranhaBody.z]; // height to base of chord
    echo(str("Pillar OD: ",BallPillar[OD]));
    BallChordM = BallOD/2 – sqrt(pow(BallOD/2,2) – (pow(BallPillar[OD],2))/4);
    echo(str("Ball chord depth: ",BallChordM));
    //———————-
    // 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);
    }
    // Spider for single LED atop struts, with the ball
    module DualSpider() {
    difference() {
    union() {
    for (j=[-1,1]) {
    translate([0,j*StrutOC.y/2,StrutBase[OD]/2])
    rotate(180/StrutSides)
    sphere(d=StrutBase[OD]/cos(180/StrutSides),$fn=StrutSides);
    translate([0,j*StrutOC.y/2,0])
    rotate(180/StrutSides)
    cylinder(d=StrutBase[OD],h=StrutBase[OD]/2,$fn=StrutSides);
    }
    translate([0,0,StrutBase[OD]/4]) // connecting bars
    cube([StrutBase[OD]*cos(180/StrutSides),StrutOC.y,StrutBase[OD]/2],center=true);
    cylinder(d=BallPillar[OD],h=BallPillar[LENGTH],$fn=BallSides);
    }
    for (j=[-1,1]) // strut wires
    translate([0,j*StrutOC.y/2,-Protrusion])
    PolyCyl(StrutBase[ID],StrutBase[OD]/2,6);
    for (n=[-1,1]) // LED wiring
    rotate(n*90)
    translate([StrutOC.x/3,0,-Protrusion])
    PolyCyl(StrutBase[ID],StrutBase[OD],6);
    translate([0,0,BallOD/2 + BallPillar[LENGTH] – BallChordM]) // ball inset
    sphere(d=BallOD);
    translate([0,0,BallPillar.z – PiranhaRecess + BallPillar.z/2]) // LED inset
    cube(PiranhaBody + [HoleWindage,HoleWindage,BallPillar.z],center=true); // XY clearance
    translate([0,0,StrutBase[OD]/2 + WireOD/2 + 0*Protrusion]) // wire channels
    cube([WireOD,BallPillar[OD] + 2*WallThick,WireOD],center=true);
    }
    }
    //– Overall case with origin at battery center
    module Case() {
    union() {
    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 with screw bosses
    0,
    BatterySize.z/2 + FloorThick + Protrusion/2])
    cube([WireBay,
    2*LidScrewOC – LidScrew[ID] – 2*4*ThreadWidth,
    BatterySize.z + Protrusion
    ],center=true);
    for (j=[-1,1]) // screw holes
    translate([-CaseSize.x/2 + WireBay/2 + WallThick,
    j*LidScrewOC,
    CaseSize.z – LidScrew[LENGTH] + Protrusion])
    PolyCyl(LidScrew[ID],LidScrew[LENGTH],6);
    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);
    translate([0,0,ThreadThick – Protrusion]) // recess around name
    cube([0.6*CaseSize.x,8,2*ThreadThick],center=true);
    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);
    }
    }
    translate([0,0,0])
    linear_extrude(height=2*ThreadThick + Protrusion,convexity=10)
    mirror([0,1,0])
    text(text="KE4ZNU",size=6,spacing=1.20,font="Arial:style:Bold",halign="center",valign="center");
    }
    }
    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*WireOC,-Protrusion])
    PolyCyl(WireOD,2*LidSize.z,6);
    for (j=[-1,1])
    translate([0,j*LidScrewOC,-Protrusion])
    PolyCyl(LidScrew[ID],2*LidSize.z,6);
    }
    }
    //——————-
    // Build it!
    if (Layout == "Case")
    Case();
    if (Layout == "Lid")
    Lid();
    if (Layout == "Spider")
    if (Struts == -1)
    DualSpider();
    else
    cube(10,center=true);
    if (Layout == "Build") {
    rotate(90)
    Case();
    translate([0,-(CaseSize.x/2 + LidSize.x/2 + Gap),0])
    rotate(90)
    Lid();
    if (Struts == -1)
    translate([CaseSize.x/2,0,0])
    DualSpider();
    }
    if (Layout == "Show") {
    Case();
    translate([-CaseSize.x/2 + LidSize.x/2,0,(CaseSize.z + Gap)])
    Lid();
    }

  • Discrete LM3909: Blue LED Waveforms

    Discrete LM3909: Blue LED Waveforms

    The circuitry and instrumentation is essentially the same discrete LM3909 as before:

    LM3909 - blue - test setup
    LM3909 – blue – test setup

    With a few minor tweaks:

    • Blue LED, forward voltage 2.56 to 2.97 V
    • 24 Ω R1
    • One Q2 current mirror transistor driving Q3

    With a pair of fresh AA alkaline cells producing 3.1 V (not the NiMH Duracells you see in the picture), the blue LED blinks brightly.

    The 610 mV peak voltage across R1 shows the LED starts at 25.4 mA:

    LM3909 blue - 3.1 V - R1 24 ohm
    LM3909 blue – 3.1 V – R1 24 ohm

    The capacitor reaches 1 V, then goes about 150 mV into reverse charge during the flash (note the different horizontal scales):

    LM3909 blue - 3.1 V - C1 V
    LM3909 blue – 3.1 V – C1 V

    The Darlington version of Q1 seems to do a decent job of keeping the cap out of reverse charge. A Shottky diode would add a few hundred mV, but I doubt there’s anything nasty going on inside the cap as it stands.

    The blue LED has a forward drop of 2.97 V at 20 mA, so I’m surprised the voltage across it hits 3.1 V at 25 mA:

    LM3909 blue - 3.1 V - LED V
    LM3909 blue – 3.1 V – LED V

    Very little of the voltage appears across Q3, the driver transistor:

    LM3909 blue - 3.1 V - Q3 coll
    LM3909 blue – 3.1 V – Q3 coll

    With a pair of nearly dead alkaline cells for a 2.0 V supply, the LED current peak drops to 4.6 mA:

    LM3909 blue - 2.0 V - R1 24 ohm
    LM3909 blue – 2.0 V – R1 24 ohm

    The LED lights brightly, then fades away exactly like you’d expect from that waveform.

    The cap still charges to about 1 V and stays well above 0 V during the (much longer) flash:

    LM3909 blue - 2.0 V - C1 voltage
    LM3909 blue – 2.0 V – C1 voltage

    The voltage across the LED now reaches only 2.7 V, which is substantially higher than the 2.0 V battery supply and exactly why the LM3909 existed:

    LM3909 blue - 2.0 V - LED voltage
    LM3909 blue – 2.0 V – LED voltage

    Q3 continues to saturate, although you can see the effect of the decreased base drive during the flash:

    LM3909 blue - 2.0 V - Q3 coll
    LM3909 blue – 2.0 V – Q3 coll

    The blue LED won’t light at 1.3 V, but still gives out a weak flash at 1.7 V, so I’d say the tweaked LM3909 circuitry works reasonably well.

  • Monthly Image: Mantis Mating

    Monthly Image: Mantis Mating

    The Praying Mantis in the Butterfly Bush is definitely female:

    Praying Mantis Mating - front
    Praying Mantis Mating – front

    I’d noticed her distended abdomen a day or two earlier, when it was highlighted in the sun and pulsing slowly. The indentations under the male’s legs shows the surface is definitely softer than the hard chitin of most insect armor:

    Praying Mantis Mating - rear
    Praying Mantis Mating – rear

    The tip of the male’s abdomen twisted around to make contact, but I have no idea what all the little doodads common to both of them back there were doing.

    The whole process started in mid-afternoon, they were still locked together six hours later, and the male was gone in the morning. The stories about female mantises eating the males seem greatly exaggerated, but she did manage to catch and eat a moth while otherwise engaged.

    We’ll keep watch for ootheca on the tall grasses again, although we’ll never know the rest of their story.