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: Electronics Workbench

Electrical & Electronic gadgets

  • Raspberry Pi HQ Camera Mount

    Raspberry Pi HQ Camera Mount

    As far as I can tell, Raspberry Pi cases are a solved problem, so 3D printing an intricate widget to stick a Pi on the back of an HQ camera seems unnecessary unless you really, really like solid modeling, which, admittedly, can be a thing. All you really need is a simple adapter between the camera PCB and the case of your choice:

    HQ Camera Backplate - OpenSCAD model
    HQ Camera Backplate – OpenSCAD model

    A quartet of 6 mm M2.5 nylon spacers mount the adapter to the camera PCB:

    RPi HQ Camera - nylon standoffs
    RPi HQ Camera – nylon standoffs

    The plate has recesses to put the screw heads below the surface. I used nylon screws, but it doesn’t really matter.

    The case has all the right openings, slots in the bottom for a pair of screws, and costs six bucks. A pair of M3 brass inserts epoxied into the plate capture the screws:

    RPi HQ Camera - case adapter plate - screws
    RPi HQ Camera – case adapter plate – screws

    Thick washers punched from an old credit card go under the screws to compensate for the case’s silicone bump feet. I suppose Doing the Right Thing would involve 3D printed spacers matching the cross-shaped case cutouts.

    Not everyone agrees with my choice of retina-burn orange PETG:

    RPi HQ Camera - 16 mm lens - case adapter plate
    RPi HQ Camera – 16 mm lens – case adapter plate

    Yes, that’s a C-mount TV lens lurking in the background, about which more later.

    The OpenSCAD source code as a GitHub Gist:

    // Raspberry Pi HQ Camera Backplate
    // Ed Nisley KE4ZNU 2020-09
    //– 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
    CamPCB = [39.0,39.0,1.5]; // Overall PCB size, plus a bit
    CornerRound = 3.0; // … has rounded corners
    CamScrewOC = [30.0,30.0,0]; // … mounting screw layout
    CamScrew = [2.5,5.0,2.2]; // … LENGTH = head thickness
    Standoff = [2.5,5.5,6.0]; // nylon standoffs
    Insert = [3.0,4.0,4.0];
    WallThick = IntegerMultiple(2.0,ThreadWidth);
    PlateThick = Insert[LENGTH];
    CamBox = [CamPCB.x + 2*WallThick,
    CamPCB.y + 2*WallThick,
    Standoff.z + PlateThick + CamPCB.z + 1.0];
    PiPlate = [90.0,60.0,PlateThick];
    PiPlateOffset = [0.0,(PiPlate.y – CamBox.y)/2,0];
    PiSlotOC = [0.0,40.0];
    PiSlotOffset = [3.5,3.5];
    NumSides = 2*3*4;
    TextDepth = 2*ThreadThick;
    //———————-
    // 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);
    }
    //———————-
    // Build it
    difference() {
    union() {
    hull() // camera enclosure
    for (i=[-1,1], j=[-1,1])
    translate([i*(CamBox.x/2 – CornerRound),j*(CamBox.y/2 – CornerRound),0])
    cylinder(r=CornerRound,h=CamBox.z,$fn=NumSides);
    translate(PiPlateOffset)
    hull()
    for (i=[-1,1], j=[-1,1]) // Pi case plate
    translate([i*(PiPlate.x/2 – CornerRound),j*(PiPlate.y/2 – CornerRound),0])
    cylinder(r=CornerRound,h=PiPlate.z,$fn=NumSides);
    }
    hull() // camera PCB space
    for (i=[-1,1], j=[-1,1])
    translate([i*(CamPCB.x/2 – CornerRound),j*(CamPCB.y/2 – CornerRound),PlateThick])
    cylinder(r=CornerRound,h=CamBox.z,$fn=NumSides);
    translate([0,-CamBox.y/2,PlateThick + CamBox.z/2])
    cube([CamScrewOC.x – Standoff[OD],CamBox.y,CamBox.z],center=true);
    for (i=[-1,1], j=[-1,1]) // camera screws with head recesses
    translate([i*CamScrewOC.x/2,j*CamScrewOC.y/2,-Protrusion]) {
    PolyCyl(CamScrew[ID],2*CamBox.z,6);
    PolyCyl(CamScrew[OD],CamScrew[LENGTH] + Protrusion,6);
    }
    for (j=[-1,1]) // Pi case screw inserts
    translate([0,j*PiSlotOC.y/2 + PiSlotOffset.y,-Protrusion] + PiPlateOffset)
    PolyCyl(Insert[OD],2*PiPlate.z,6);
    translate([-PiPlate.x/2 + (PiPlate.x – CamBox.x)/4,0,PlateThick – TextDepth/2] + PiPlateOffset)
    cube([15.0,30.0,TextDepth + Protrusion],center=true);
    }
    translate([-PiPlate.x/2 + (PiPlate.x – CamBox.x)/4 + 3,0,PlateThick – TextDepth – Protrusion] + PiPlateOffset)
    linear_extrude(height=TextDepth + Protrusion,convexity=2)
    rotate(-90)
    text("Ed Nisley",font="Arial:style=Bold",halign="center",valign="center",size=4,spacing=1.05);
    translate([-PiPlate.x/2 + (PiPlate.x – CamBox.x)/4 – 3,0,PlateThick – TextDepth – Protrusion] + PiPlateOffset)
    linear_extrude(height=TextDepth + Protrusion,convexity=2)
    rotate(-90)
    text("KE4ZNU",font="Arial:style=Bold",halign="center",valign="center",size=4,spacing=1.05);

  • Raspberry Pi Streaming Video Loopback

    Raspberry Pi Streaming Video Loopback

    As part of spiffing my video presence for SquidWrench Zoom meetings, I put a knockoff RPi V1 camera into an Az-El mount, stuck it to a Raspberry Pi, installed the latest OS Formerly Known as Raspbian, did a little setup, and perched it on the I-beam over the workbench:

    Raspberry Pi - workbench camera setup
    Raspberry Pi – workbench camera setup

    The toothbrush head has a convenient pair of neodymium magnets affixing the RPi’s power cable to the beam, thereby preventing the whole lashup from falling off. The Pi, being an old Model B V 1.1, lacks onboard WiFi and requires a USB WiFi dongle. The white button at the lower right of the heatsink properly shuts the OS down and starts it up again.

    Zoom can show video only from video devices / cameras attached to the laptop, so the trick is to make video from the RPi look like it’s coming from a local laptop device.

    Start by exporting video from the Raspberry Pi:

    raspivid --nopreview -t 0 -rot 180 -awb sun --sharpness -50 --flicker 60hz -w 1920 -h 1080 -ae 48 -a 1032 -a 'RPi Cam1 %Y-%m-%d %X'  -b 1000000 -l -o tcp://0.0.0.0:5000

    The -rot 180 -awb sun --sharpness -50 --flicker 60hz parameters make the picture look better. The bottom of the video image There is no way to predict which side of the video will be on the same side as the cable, if that’s any help figuring out which end is up, and the 6500 K LED tubes apparently fill the shop with “sun”.

    The -l parameter causes raspivid to wait until it gets an incoming tcp connection on port 5000 from any other IP address, whereupon it begins capturing video and sending it out.

    Then, on the laptop, create a V4L loopback device:

    sudo modprobe v4l2loopback devices=1 video_nr=10 exclusive_caps=1 card_label="Workbench"

    Zoom will then include a video source identified as “Workbench” in its list of cameras.

    Now fetch video from the RPi and ram it into the loopback device:

    ffmpeg -f h264 -i tcp://192.168.1.50:5000 -f v4l2 -pix_fmt yuv420p /dev/video10

    VLC knows it as /dev/video10:

    RPi - V4L loopback - screen grab
    RPi – V4L loopback – screen grab

    That’s the edge of the workbench over there on the left, looking distinctly like a cliff.

    The RPi will happily stream video all day long to ffmpeg while you start / stop the display program pulling the bits from the video device. However, killing ffmpeg also kills raspivid, requiring a manual restart of both programs. This isn’t a dealbreaker for my simple needs, but it makes unattended streaming from, say, a yard camera somewhat tricky.

    There appear to be an infinite number of variations on this theme, not all of which work, and some of which rest upon an unsteady ziggurat of sketchy / unmaintained software.

    Addendum: If you have a couple of RPi cameras, it’s handy to run the matching ssh and ffmpeg sessions in screen / tmux / whatever terminal multiplexer you prefer. I find it easier to flip through those sessions with Ctrl-A N, rather than manage half a dozen tabs in a single terminal window. Your mileage may differ.

  • Multimeter Current-sense Resistor

    Multimeter Current-sense Resistor

    Replacing the battery in an old Craftsman (!) multimeter brought its 10 A current-sense resistor into the light:

    Multimeter current resistor - nipped copper wire
    Multimeter current resistor – nipped copper wire

    Unlike the contemporary AN8008/9 meters, it looks like an ordinary copper wire trimmed to the proper resistance by nipping it with a cutter.

    It measures something under 10 mΩ, so I’m sure they adjusted the resistance by applying a known current and watching the meter reading while crunching the wire until the proper value appears.

    I may have actually used the 10 A range, but I’d be hard pressed to say when or why, so the resistor is at least as good as it needs to be!

  • More AAA-to-AA Alkaline Adapters

    More AAA-to-AA Alkaline Adapters

    Having a handful of not-dead-yet AAA alkalines and a bunch of LED blinkies built for AA alkalines, a pair of adapters seemed in order:

    AAA-to-AA Alkaline Adapters - installed
    AAA-to-AA Alkaline Adapters – installed

    The blinkies need a somewhat wider base than they’d get from a pair of AAA alkalines, so it’s not quite as dumb as it may seem.

    In any event, the positive terminal comes from a brass rod:

    AAA-to-AA Alkaline Adapters - brass terminal
    AAA-to-AA Alkaline Adapters – brass terminal

    Nobody will ever see the fancy Hilbert Curve infill around the brass:

    AAA-to-AA Alkaline Adapters - end view
    AAA-to-AA Alkaline Adapters – end view

    In this application, they’ll go from not-dead-yet to oh-it’s-dead faster than AA cells, so I can watch how the blinkies work with lower voltages.

  • 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 …

  • 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.