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.

The New Hotness

  • LED Garage Light: Desk Lamp Upcycling

    One of the heatsink panels from the defunct LED garage light now casts a uniform warm-white glow on my desk:

    LED Garage Light - desk light
    LED Garage Light – desk light

    A PCB intended as a lithium battery charger serves as a constant-current supply:

    LED Garage Light - constant current driver
    LED Garage Light – constant current driver

    The three trimpots, from left to right:

    • Constant-voltage limit adjustment
    • Full-charge current setpoint (irrelevant here)
    • Constant-current limit adjustment

    The as-received trimpot settings will be wildly inappropriate for a nominal 10 W COB LED array, so:

    • Connect the output to about 10 Ω of power resistors
    • … with an ammeter in series
    • Connect the input to a 12 VDC / 1-ish A wall wart
    • Adjust the output voltage to 10 V
    • Adjust the output current to 900 mA

    As long as the voltage limit is over about 10 V, it will (likely) never matter, as the LED forward drop doesn’t vary much with temperature. Setting it to something sensible keeps it out of the way.

    The middle trimpot apparently sets a voltage for a comparator to light an LED when the battery current drops below that level as it reaches full charge.

    Although the regulator touts its high efficiency, it does run hot and a heatsink seemed in order:

    LED Garage Light - heatsink
    LED Garage Light – heatsink

    Stipulated: the fins run the wrong way and it’s sitting in the updraft from the main heatsink. It’s Good Enough™.

    The switch on the top comes from the collection of flashlight tailcap switches and controls the 12 V input power. It’s buried up to its button in a generous dollop of JB Kwik epoxy, which seemed the least awful way to get that done.

    The solid model looks about like you’d expect:

    LED Lamp Driver case - switch housing - show solid model
    LED Lamp Driver case – switch housing – show solid model

    The OpenSCAD code exports the (transparent) lid as an SVG so I can import it into LightBurn and laser-cut some thin acrylic. Two tape snippets hold the lid in place pending more power-on hours, after which I’ll apply a few dots of cyanoacrylate adhesive and call it done.

    The case builds in two pieces that glue together to avoid absurd support structures:

    LED Lamp Driver case - switch housing - build solid model
    LED Lamp Driver case – switch housing – build solid model

    A 3D printed adapter goes between the desk lamp arm and the lamp heatsink bolt:

    LED Lamp Driver case - arm adapter - solid model
    LED Lamp Driver case – arm adapter – solid model

    The OpenSCAD source code files for the case and adapter arm as a GitHub Gist:

    // LED Lamp arm adapter
    // Ed Nisley – KE4ZNU
    // 2026-03-18
    include <BOSL2/std.scad>
    Layout = "Adapter"; // [Show,Build,ArmClamp,SinkClamp,Adapter]
    /* [Hidden] */
    HoleWindage = 0.2;
    Protrusion = 0.01;
    Gap = 5.0;
    $fn=5*3*4;
    HoleOC = 45.0;
    ArmRad = 7.5;
    ArmWidth = 11.3;
    SinkOD = 11.5;
    SinkThick = 3.2;
    SinkOC = 20.0;
    ClampThick = 5.0; // outside sink, watch thinning due to hull()
    // Define things
    // Screw & bushings in lamp arm bracket
    // … over-long bushings to prevent coincident surfaces
    module ArmClamp() {
    BushingThick = 1.5;
    BushingOD = 9.0;
    union() {
    ycyl(ArmWidth,d=4.0 + HoleWindage); // central M4 screw
    for (j=[-1,1]) {
    back(j*(ArmWidth – BushingThick + Protrusion)/2)
    ycyl(BushingThick + Protrusion,d=BushingOD);
    back(j*(ArmWidth + 10)/2)
    cuboid([2*ArmRad,10,2*ArmRad]);
    }
    }
    }
    module SinkClamp() {
    union() {
    ycyl(2*SinkOC,d=6.0 + HoleWindage); // central M6 screw
    for (j=[-1,1])
    back(j*SinkOC/2) {
    ycyl(SinkThick + Protrusion,d=SinkOD);
    cuboid([SinkOD,SinkThick + Protrusion,2*SinkOD]);
    }
    }
    }
    module Adapter() {
    difference() {
    hull() {
    right(HoleOC)
    ycyl(ArmWidth,r=ArmRad);
    ycyl(SinkOC + SinkThick + 2*ClampThick,d=SinkOD);
    }
    right(HoleOC)
    ArmClamp();
    SinkClamp();
    }
    }
    // Build it
    if (Layout == "ArmClamp")
    ArmClamp();
    if (Layout == "SinkClamp")
    SinkClamp();
    if (Layout == "Adapter")
    Adapter();
    if (Layout == "Build")
    up(SinkOD/2)
    yrot(-atan((ArmRad – SinkOD/2)/HoleOC))
    Adapter();
    // LED Constant-current driver case
    // Ed Nisley – KE4ZNU
    // 2026-03-15
    include <BOSL2/std.scad>
    Layout = "Show"; // [Show,Build,Case,Lid,LidSVG,Switch]
    /* [Hidden] */
    ThreadThick = 0.2;
    HoleWindage = 0.2;
    Protrusion = 0.01;
    Gap = 5.0;
    WallThick = 1.8;
    TapeThick = 1.5;
    DriverOA = [48.5,13.5 + TapeThick,23.5]; // PCB forward Y, pots along top to rear
    SinkOA = [31.5,12.0,15.5]; // fins forward
    SinkOffset = [(DriverOA.x – SinkOA.x)/2,0,2.0]; // from lower left front corner of PCB
    AdjPots = [14,24,34]; // screwdriver adjust offsets
    AdjOD = 3.0; // … access hole dia
    CaseOA = DriverOA + [2*WallThick,2*WallThick,2*WallThick];
    echo(CaseOA=CaseOA);
    LidOA = [CaseOA.x – WallThick,CaseOA.z – WallThick,1.0];
    Cables = [8.0,3.0 + WallThick/2,LidOA.z];
    SwitchWireOC = DriverOA.x – 6.0;
    SwitchCapBase = [DriverOA.x + WallThick,DriverOA.y + WallThick];
    SwitchCapTop = [DriverOA.x,12.0];
    SwitchCavity = [25.0,10.5,5.5];
    // Define things
    module Lid() {
    difference() {
    cuboid(LidOA,anchor=BOTTOM+FWD+LEFT);
    for (i = AdjPots)
    translate([i,LidOA.y – AdjOD/2 – WallThick/2,-Protrusion])
    cyl(LidOA.z + 2*Protrusion,d=AdjOD,anchor=BOTTOM,$fn=8,spin=180/8);
    translate([LidOA.x/2,-Protrusion,-Protrusion])
    cuboid(Cables + [0,Protrusion,2*Protrusion],rounding=1.0,edges=[BACK+LEFT,BACK+RIGHT],anchor=BOTTOM+FWD);
    }
    }
    module SwitchBox() {
    difference() {
    prismoid(SwitchCapBase,SwitchCapTop,SwitchCavity.z,anchor=BOTTOM);
    down(Protrusion)
    cuboid(SwitchCavity + [0,0,2*Protrusion],anchor=BOTTOM);
    hull()
    for (i=[-1,1])
    right(i*SwitchWireOC/2)
    zcyl(CaseOA.z,d=3.0,$fn=8,spin=180/8);
    }
    }
    module Case() {
    difference() {
    cuboid(CaseOA,chamfer=WallThick/2,anchor=BOTTOM+FWD+LEFT);
    translate([WallThick,WallThick + Protrusion,WallThick])
    cuboid(DriverOA + [0,WallThick + Protrusion,0],anchor=BOTTOM+FWD+LEFT);
    translate(SinkOffset + [WallThick,WallThick + 2*Protrusion,WallThick])
    cuboid(SinkOA,anchor=BOTTOM+BACK+LEFT);
    for (i=[-1,1])
    translate([i*SwitchWireOC/2 + CaseOA.x/2,CaseOA.y/2,CaseOA.z/2])
    zcyl(CaseOA.z,d=2.0,anchor=BOTTOM,$fn=8,spin=180/8);
    translate([WallThick/2,(CaseOA.y + LidOA.z),WallThick/2])
    xrot(90)
    scale([1,1,2])
    Lid();
    }
    }
    // Build it
    if (Layout == "Switch")
    SwitchBox();
    if (Layout == "Case")
    Case();
    if (Layout == "Lid")
    Lid();
    if (Layout == "LidSVG")
    projection(cut=true)
    Lid();
    if (Layout == "Show") {
    Case();
    translate(SinkOffset + [WallThick,WallThick + 2*Protrusion,WallThick])
    color("Gray",0.7)
    cuboid(SinkOA,anchor=BOTTOM+BACK+LEFT);
    translate([CaseOA.x/2,CaseOA.y/2,CaseOA.z])
    SwitchBox();
    translate([WallThick/2,CaseOA.y,WallThick/2])
    xrot(90)
    color("Gray",0.7)
    Lid();
    }
    if (Layout == "Build") {
    fwd(Gap)
    xrot(90)
    Case();
    translate([CaseOA.x/2,(Gap + CaseOA.y/2),0])
    SwitchBox();
    }