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: Software

General-purpose computers doing something specific

  • HQ Sixteen: Handlebar Control Button Caps

    HQ Sixteen: Handlebar Control Button Caps

    Each of the HQ Sixteen’s handlebars has a cap with control buttons:

    HQ Sixteen control caps - side view
    HQ Sixteen control caps – side view

    The left cap:

    HQ Sixteen control caps - left
    HQ Sixteen control caps – left

    The right cap:

    HQ Sixteen control caps - OEM right
    HQ Sixteen control caps – OEM right

    The membrane switch overlay has textured bumps, although both of us have trouble finding them.

    The Start / Stop switch gets the most use and, as you’d expect, has become intermittent after two decades of use.

    Mary thinks a Start / Stop switch on both caps would be an improvement, letting her position quilting rulers with her right hand and run the machine with her left hand & thumb. I don’t know how the switches are wired, but the wiring suggests either simple single-bit inputs or a small matrix.

    She also finds membrane switches difficult to press, so I’m in the process of replacing the control caps with something more to her liking.

    The current concept goes a little something like this:

    HQ Sixteen control caps - new caps
    HQ Sixteen control caps – new caps

    Stipulated: my art hand is weak.

    Those are little bitty SMD switches:

    HQ Sixteen control caps - new caps overview
    HQ Sixteen control caps – new caps overview

    They’re easy to locate by touch, with a stem length chosen to “feel right” when pushed.

    They have been grievously misapplied:

    HQ Sixteen control caps - switches
    HQ Sixteen control caps – switches

    The solid model has three main pieces and a lock for the ribbon cable:

    Control Button Caps - solid model - build view
    Control Button Caps – solid model – build view

    Those pockets keep the switches oriented while the glue cures.

    Two screws through the handlebar secure each cap. Handi-Quilter drove sheet metal screws into their OEM caps, distorting them enough to jam solidly into the handlebars. I’ve been reluctant to apply enough force to loosen them, so they remain frozen in place until the current quilt is done.

    The new plugs have recesses for M3 square nuts to make them easily removable. As with the handlebar angle adapters, I’ll glue the plugs into the caps.

    A slightly exploded view shows how the pieces fit together:

    Control Button Caps - solid model - show view gapped
    Control Button Caps – solid model – show view gapped

    The switch plate sits recessed into the cap to allow room for the label (about which, more later):

    Control Button Caps - solid model - show view assembled
    Control Button Caps – solid model – show view assembled

    The OpenSCAD source code as a GitHub Gist:

    // Handiquilter HQ Sixteen handlebar control button caps
    // Ed Nisley – KE4ZNU
    // 2025-04-05
    include <BOSL2/std.scad>
    Layout = "Show"; // [Show,Build,Grip,Body,Face,FaceBack,Plug,CableLock]
    // Angle w.r.t. handlebar
    FaceAngle = 30; // [10:45]
    // Separation in Show display
    Gap = 5; // [0:20]
    /* [Hidden] */
    HoleWindage = 0.2;
    Protrusion = 0.1;
    NumSides = 2*3*4;
    WallThick = 3.0;
    ID = 0;
    OD = 1;
    LENGTH = 2;
    Grip = [19.7,22.4,15.0]; // (7/8)*INCH = 22.2 mm + roughness, LENGTH=OEM insertion depth
    GripRadius = Grip[OD]/2;
    FoamOD = 34.0; // handlebar foam
    FoamRadius = FoamOD/2;
    SwitchBody = [6.3,6.3,4.0]; // does not include SMD leads
    SwitchStemOD = 3.5 + 2*HoleWindage;
    SwitchOC = 10.0; // center-to-center switch spacing
    LabelThick = 0.5; // laminated overlay
    FaceRim = 2.0; // rim around faceplate
    FaceThick = 2.0; // … plate thickness
    FaceDepth = FaceThick + LabelThick; // inset allowing for faceplate label
    CapOD = 38.0; // overall cap diameter
    CapTrim = FoamRadius; // flat trim on front
    CapBase = 5.0; // bottom thickness
    Cap = [FoamOD – FaceRim,CapOD,CapBase + CapOD*tan(FaceAngle)];
    echo(Cap=Cap);
    TargetSize = 4.0; // laser alignment targets
    TargetsOC = [40.0,40.0];
    Cable = [10.0,2.0,WallThick]; // aperture for cable lock
    ScrewAngles = [-45,45]; // mounting screws
    Screw = [2.0,3.0,7.0]; // OEM = sheet metal screw
    ScrewOffset = 6.0; // from top of grip tube
    SquareNut = [3.0,5.5,2.3 + 0.4]; // M3 square nut OD = side, LENGTH + inset allowance
    NutInset = GripRadius – sqrt(pow(GripRadius,2) – pow(SquareNut[OD],2)/4);
    PlugOA = [(Grip[ID] – 2*WallThick),(Grip[ID] – 1.0),(CapBase + ScrewOffset + 10.0)];
    echo(PlugOA=PlugOA);
    //———-
    // Define objects
    //—–
    // Handlebar tube
    module GripTube() {
    difference() {
    tube(3*Grip[LENGTH],GripRadius,Grip[ID]/2,anchor=TOP);
    for (a = ScrewAngles) {
    down(ScrewOffset) zrot(a-90)
    right(GripRadius)
    yrot(90) cylinder(d=Screw[OD],h=Screw[LENGTH],center=true,$fn=6);
    }
    }
    }
    //—–
    // SVG outline of faceplate for laser cuttery
    module FaceShape(Holes=true,Targets=false) {
    difference() {
    scale([1,1/cos(FaceAngle)])
    difference() {
    circle(d=(Cap[OD] – 2*FaceRim),$fn=144);
    fwd(CapTrim – FaceRim)
    square(Cap[OD],anchor=BACK);
    }
    if (Holes)
    for (i=[-1:1]) // arrange switch stem holes
    right(i*SwitchOC)
    zrot(180/8) circle(d=SwitchStemOD,$fn=32);
    }
    if (Targets)
    for (i = [-1,1], j = [-1,1])
    translate([i*TargetsOC.x/2,j*TargetsOC.y/2])
    square(2.0,center=true);
    }
    //—–
    // Faceplate backing sheet
    // Switch bodies indented into bottom, so flip to build
    module FacePlate(Thick=FaceThick,Holes=true) {
    difference() {
    linear_extrude(height=Thick,convexity=5)
    FaceShape(Holes);
    up(SwitchBody.z/4)
    for (i = [-1:1])
    right(i*SwitchOC)
    cube(SwitchBody,anchor=TOP);
    }
    }
    //—–
    // Cap body
    module CapBody() {
    $fn=48;
    up(CapBase + (Cap[OD]/2)*tan(FaceAngle)) xrot(FaceAngle)
    difference() {
    xrot(-FaceAngle)
    down(CapBase + (Cap[OD]/2)*tan(FaceAngle))
    difference() {
    cylinder(d=Cap[OD],h=Cap[LENGTH]);
    fwd(CapTrim) down(Protrusion)
    cube(2*Cap[LENGTH],anchor=BACK+BOTTOM);
    up(CapBase)
    difference() {
    cylinder(d=Cap[ID],h=Cap[LENGTH]);
    fwd(CapTrim – 2*FaceRim)
    cube(2*Cap[LENGTH],anchor=BACK+BOTTOM);
    }
    down(Protrusion)
    cylinder(d=Grip[ID],h=Cap[LENGTH]);
    }
    cube(2*Cap[OD],anchor=BOTTOM);
    down(FaceDepth)
    FacePlate(FaceDepth + Protrusion,Holes=false);
    }
    }
    //—–
    // Plug going into grip handlebar
    module CapPlug() {
    $fn=48;
    difference() {
    tube(PlugOA[LENGTH],id=PlugOA[ID],od=PlugOA[OD],anchor=BOTTOM)
    position(TOP)
    tube(CapBase,id=PlugOA[ID],od=Grip[ID],anchor=TOP);
    for (a = ScrewAngles)
    up(PlugOA.z – CapBase – ScrewOffset) zrot(a-90)
    right(PlugOA[ID]/2)
    yrot(90) {
    cube([SquareNut[OD],SquareNut[OD],SquareNut[LENGTH] + NutInset],center=true);
    zrot(180/6)
    cylinder(d=(SquareNut[ID] + 2*HoleWindage),h=PlugOA[ID],center=true,$fn=6);
    }
    }
    }
    //—–
    // Lock plate for ribbon cable
    module CableLock() {
    difference() {
    cuboid([2*Cable.x,PlugOA[ID],WallThick],rounding=WallThick/2,anchor=BOTTOM);
    for (j = [-1,1])
    back(j*Cable.y) down(Protrusion)
    cube(Cable + [0,0,2*Protrusion],anchor=BOTTOM);
    }
    }
    //———-
    // Build things
    if (Layout == "Grip") {
    color("Silver",0.5)
    GripTube();
    }
    if (Layout == "Face")
    FaceShape(Targets=true);
    if (Layout == "FaceBack")
    FacePlate();
    if (Layout == "Body")
    CapBody();
    if (Layout == "Plug")
    CapPlug();
    if (Layout == "CableLock")
    CableLock();
    if (Layout == "Show") {
    color("Green")
    up(CapBase)
    CableLock();
    color("Orange")
    down(Gap)
    down(PlugOA[LENGTH] – CapBase)
    CapPlug();
    color("Cyan",(Gap > 4)? 1.0 : 0.2)
    CapBody();
    color("White",(Gap > 4)? 1.0 : 0.5)
    up(Gap*cos(FaceAngle)) fwd(Gap*sin(FaceAngle))
    up(CapBase + (Cap[OD]/2)*tan(FaceAngle) – FaceDepth)
    back(FaceDepth*sin(FaceAngle)) xrot(FaceAngle)
    FacePlate();
    down(3*Gap) {
    color("Silver",0.5)
    GripTube();
    down(Gap)
    color("Gray",0.5)
    tube(3*Grip[LENGTH],FoamRadius,Grip[OD]/2,anchor=TOP);
    }
    }
    if (Layout == "Build") {
    right((Gap + Cap[OD])/2)
    CapBody();
    left((Gap + Cap[OD])/2)
    zrot(180) up(FaceThick) xrot(180)
    FacePlate();
    fwd(Gap + Cap[OD])
    up(PlugOA[LENGTH]) xrot(180) zrot(180)
    CapPlug();
    fwd(Cap[OD]/2)
    zrot(90)
    CableLock();
    }

  • Inkscape: LightBurn Layer Color Palette

    Inkscape: LightBurn Layer Color Palette

    Inkscape is not a CAD program (neither is LightBurn), but for my simple needs it works well enough, with the compelling advantage that OpenSCAD can import named layers and extrude them into solid models.

    LightBurn can import Inkscape SVG images to define the patterns for laser cutting / engraving and will automatically put the vectors into layers corresponding to their colors if and only if the SVG image uses colors from the LightBurn palette. Regrettably, picking those colors from the default Inkscape palette is essentially impossible, but you can have Inkscape use a palette file that displays only the LightBurn colors corresponding to its layers.

    I conjured this GIMP / Inkscape palette file based on the table in a LightBurn forum post, plus tool layer colors from another post:

    GIMP / Inkscape Palette
    Name: LightBurn Layers
    #
      0   0   0 BLACK
    255 255 255 WHITE
      0   0   0 LBRN #0
      0   0 255 LBRN #1
    255   0   0 LBRN #2
      0 224   0 LBRN #3
    208 208   0 LBRN #4
    255 128   0 LBRN #5
      0 224 224 LBRN #6
    255   0 255 LBRN #7
    180 180 180 LBRN #8
      0   0 160 LBRN #9
    160   0   0 LBRN #10
      0 160   0 LBRN #11
    160 160   0 LBRN #12
    192 128   0 LBRN #13
      0 160 255 LBRN #14
    160   0 160 LBRN #15
    128 128 128 LBRN #16
    125 135 185 LBRN #17
    187 119 132 LBRN #18
     74 111 227 LBRN #19
    211  63 106 LBRN #20
    140 215 140 LBRN #21
    240 185 141 LBRN #22
    246 196 225 LBRN #23
    250 158 212 LBRN #24
     80  10 120 LBRN #25
    180  90   0 LBRN #26
      0  71  84 LBRN #27
    134 250 136 LBRN #28
    255 219 102 LBRN #29
    243 105  38 LBRN T1
     12 150 217 LBRN T2
    

    Plunk that file (which I named Lightburn.gpl) into /home/ed/.config/inkscape/palettes/, restart Inkscape, then select it (the Name line defines its mmm name):

    Inkscape - selecting LightBurn palette
    Inkscape – selecting LightBurn palette

    Which lays a row of the LightBurn layer colors along the the Inkscape window:

    Inkscape - LightBurn palette
    Inkscape – LightBurn palette

    The text after the RGB triplet in each file line appears as the tool tip for the color swatch:

    Inkscape - LightBurn palette tooltip
    Inkscape – LightBurn palette

    Because LightBurn uses only the vector Stroke and ignores its Fill, you (well, I) must become accustomed to Shift-clicking palette colors.

    You can fetch a similar palette file directly from the LightBurn doc, although minus the tool tips. GIMP and Inkscape have many palettes available, should you make artsy drawings where subtle color shading matters.

    I generally use only a few cheerful primary colors, because I have trouble distinguishing (heck, in some cases even seeing) the more subtle colors against LightBurn’s light (or dark) workspace background. I assign the layer cut settings using the Material Library: reds for cutting, blues for marking, and grays for engraving.

    When I need more than maybe half a dozen colors, I (eventually) realize I’m trying to be too clever and split the project into separate LightBurn files.

  • Piping Yubikey TOTP To xclip

    Piping Yubikey TOTP To xclip

    Rather than fiddle with the GUI program for my Yubikey, I use the ykman CLI program for TOTP authentication, because there’s always a command prompt / terminal open on the portrait monitor:

    ykman oath accounts code -s ama
    161413
    

    Double-click to select the number in the terminal, then either copy-n-paste or middle-click into the target field of whatever needs convincing I am truly me, myself, and I.

    I finally got a Round Tuit and piped the output into xclip to put the number into the clipboard:

    ykman oath accounts code -s ama | xclip
    

    Which lets me go directly to pasting or middle-clicking.

    The command history is big enough that I now type only:

    Ctrl-R ama
    

    Which brings up the most recent version of the command, whereupon I whack Enter to execute it. Similar abbreviations extract the commands for dozen-odd companies / banks / institutions / whatever I deal with.

    When I need a hint:

    ykman oath accounts list
    

    Should’a done that long ago.

    For reference, a treatise on Yubikey config and usage.

    Bonus! A cat:

    Gray Cat - 2023-05-23
    Gray Cat – 2023-05-23

    Because SEO demands a picture.

  • HQ Sixteen: Front Horizontal Spool Adapter

    HQ Sixteen: Front Horizontal Spool Adapter

    Mary wanted a horizontal spool adapter mounted closer to the front of her HQ Sixteen, in the M5 threaded hole where the Official Horizontal Adapter would go:

    HQ Sixteen - front spool adapter - installed
    HQ Sixteen – front spool adapter – installed

    Yes, the pin through the spool is fluorescent edge-lit orange acrylic that looks wonderful in sunlight and is much more amusing than the black rod in the adapter atop the power supply pod.

    The top of the machine case is not flat, level, or easy to model, so I deployed the contour gauge again, with some attention to keeping the edge pins parallel & snug along the machine sides:

    HQ Sixteen - machine profile measurement
    HQ Sixteen – machine profile measurement

    Tracing the edge of the pins onto paper, scanning, and feeding it into Inkscape let me lay a few curves:

    HQ Sixteen - top profile curve - Inkscape fitting
    HQ Sixteen – top profile curve – Inkscape fitting

    The laser-cut chipboard test pieces show the iterations producing closer and closer fits to the machine.

    Importing the final SVG image into OpenSCAD and extruding it produced a suitable solid model of the machine’s case:

    HQ Sixteen - machine solid model
    HQ Sixteen – machine solid model

    Subtract that shape from the bottom of the adapter to get a perfect fit atop the machine:

    HQ Sixteen - horizontal thread spool adapter - front pin - solid model - show
    HQ Sixteen – horizontal thread spool adapter – front pin – solid model – show

    Early results are encouraging, although the cheap polyester thread Mary got from a friend’s pile and is using for practice untwists itself after passing through the tension disks on its way to the needle. She’ll load much better thread for the real quilt.

    The OpenSCAD source code and SVG of the HQ Sixteen’s top profile as a GitHub Gist:

    // HQ Sixteen – horizontal thread spool adapter for front pin
    // Ed Nisley – KE4ZNU
    // 2025-04-07
    include <BOSL2/std.scad>
    Layout = "Show"; // [Show,Build,Base,Wall,Frame]
    /* [Hidden] */
    Protrusion = 0.1;
    HoleWindage = 0.2;
    ID = 0;
    OD = 1;
    LENGTH = 2;
    WallThick = 8.0;
    BaseThick = 12.0;
    Washer = [5.0,10.0,1.0]; // M5 washer
    Spool = [0.25*INCH,50.0,55.0]; // maximum thread spool
    SpoolClearance = [2.0,5.0,5.0]; // spool pin pointed to +X axis
    SpoolPin = [Spool[ID],Spool[ID],Spool[LENGTH] + WallThick + SpoolClearance.x];
    BasePlate = [WallThick + SpoolClearance.x + 13.0, // X flush with side of machine
    Spool[OD]/2 + 2*SpoolClearance.y,
    BaseThick];
    BaseOffset = [(BasePlate.x Washer[OD]),Washer[OD],0.0]; // left front corner w.r.t. pin
    SpoolOC = [0, // relative to left front top of Base
    BasePlate.y/2,
    SpoolClearance.z + Spool[OD]/2 + BaseThick/2];
    //———-
    // Construct the pieces
    // HQ Sixteen top frame profile
    // Aligned with hole somewhere along X=0, front edge at Y=0
    // Lengthened slightly to cut cleanly
    module MachineFrame(Length=BasePlate.y + 2*Protrusion) {
    back(BasePlate.y + Protrusion) xrot(90)
    linear_extrude(height=Length,convexity=5,center=false)
    import("HQ Sixteen – top profile curve.svg",layer="Top Profile");
    }
    // Baseplate
    // Aligned with hole one washer diameter in from corner
    module Base() {
    $fn=18;
    difference() {
    fwd(Washer[OD])
    difference() {
    right(Washer[OD])
    cuboid(BasePlate,anchor=RIGHT+FRONT+CENTER,rounding=BaseThick/2,edges=RIGHT);
    MachineFrame();
    }
    down(BasePlate.z)
    cylinder(d=SpoolPin[OD] + HoleWindage,h=2*BasePlate.z);
    up(BasePlate.z/2 Washer[LENGTH])
    cylinder(d=Washer[OD] + HoleWindage,h=2*Washer[LENGTH]);
    }
    }
    // Wall holding spool pin
    module Wall() {
    $fn=36;
    translate(BaseOffset) {
    difference() {
    union() {
    translate(SpoolOC)
    right(WallThick)
    cylinder(SpoolClearance.x,d=Spool[OD]/2,orient=RIGHT);
    hull() {
    translate(SpoolOC)
    cylinder(WallThick,d=Spool[OD]/2,orient=RIGHT);
    up(BasePlate.z/2 1)
    cube([WallThick,BasePlate.y,1],center=false);
    }
    }
    translate(SpoolOC) left(Protrusion)
    cylinder(SpoolPin[LENGTH],d=SpoolPin[OD],orient=RIGHT);
    }
    }
    }
    module Adapter() {
    Base();
    Wall();
    }
    //———-
    // Show & build the results
    if (Layout == "Base")
    Base();
    if (Layout == "Wall")
    Wall();
    if (Layout == "Frame")
    MachineFrame();
    if (Layout == "Show") {
    Adapter();
    color("Gray",0.5)
    MachineFrame(60);
    color("Green",0.75)
    translate(BaseOffset)
    translate(SpoolOC)
    cylinder(SpoolPin[LENGTH],d=SpoolPin[OD],orient=RIGHT,$fn=18);
    }
    if (Layout == "Build")
    up(BaseOffset.x)
    yrot(90)
    Adapter();
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.

  • HQ Sixteen: Padded Table Shims

    HQ Sixteen: Padded Table Shims

    The HQ Sixteen has been running at higher speeds as Mary practices using its stitch regulator and the vibrations shook several of the table shims (blocks, whatever) onto the floor. I hope a layer of EVA foam provides enough compliance to keep them in place:

    HQ Sixteen - padded table shim - installed
    HQ Sixteen – padded table shim – installed

    The foam is 2 mm thick, so subtracting that from the nominal thickness makes the new blocks come out right.

    A short module extracts the footprint for export as an SVG image to laser-cut both the foam and the adhesive sheet required to stick it in place:

    module ShimPad(Thickness = PadThick) {
    
        if (Thickness)
            linear_extrude(height=Thickness)
                projection(cut=true)
                    ShimBlock();
        else
            projection(cut=true)
                ShimBlock();
    
    }
    

    It turns out linear_extrude() chokes on a zero height.

    When handed a nonzero Thickness, the code generates a simulated foam sheet:

    HQ Sixteen - table shims - solid model - padded
    HQ Sixteen – table shims – solid model – padded

    The footprint looks about like you’d expect:

    HQ Sixteen - table shims - solid model - pad outline
    HQ Sixteen – padded table shim – installed

    Import into LightBurn, duplicate it sufficiently, set the speed & power & kerf for EVA foam, then cut ’em out:

    HQ Sixteen - table shims - padding cuts
    HQ Sixteen – table shims – padding cuts

    Ditto for the adhesive, stick together, and upgrade the fleet.

    If these shake loose, snippets of adhesive film will stick them firmly to the underside of the table panels.

    Update: Yeah, they needed sticky snippets. Whole lotta shakin’ goin’ on with that machine!

  • Floor Lamp Remote Control Holder

    Floor Lamp Remote Control Holder

    The remote control for the floor lamp across the Reading Room will never again wander away into the clutter:

    Floor lamp remote holder - in use
    Floor lamp remote holder – in use

    The magnet in its back snuggles against a steel disk embedded in the holder:

    Floor lamp remote holder - installed
    Floor lamp remote holder – installed

    A magnetic field visualization sheet revealed the magnet:

    Floor lamp remote holder - magnet field visualization
    Floor lamp remote holder – magnet field visualization

    Extract the remote’s profiles with a contour gauge:

    Floor lamp remote holder - pin contour gauge
    Floor lamp remote holder – pin contour gauge

    Trace the outlines and lay smooth curves around them with Inkscape:

    Remote profiles - Inkscape curves
    Remote profiles – Inkscape curves

    They needed a slight lengthening to account for the gauge pin diameter & deflection, but this isn’t a precision project.

    Do the same with a scan of the front face, import the curves into OpenSCAD, extrude them, create a solid model of the remote from their mutual intersection, then add a cylinder to punch the depression for the steel plate:

    Floor Lamp Remote Holder - solid model - bottom
    Floor Lamp Remote Holder – solid model – bottom

    The chonky model corners stick out too far compared to the stylin’ curves on the real remote, but I made the holder shorter than the remote specifically to avoid fussing with such details.

    Subtract the remote from a nicely rounded cuboid and knock out a cylinder for the pipe it’ll mount on to produce the holder:

    Floor Lamp Remote Holder - solid model - Show view
    Floor Lamp Remote Holder – solid model – Show view

    I briefly considered a circumferential clamp around the pipe before coming to my senses and making the pipe diameter 2 mm larger to accommodate a strip of double-sided foam tape.

    The magnet gets a ferocious grip on the plate and I defined the result to be All Good™.

    The OpenSCAD source code and SVG paths as a GitHub Gist:

    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
    // Floor Lamp Remote Holder
    // Ed Nisley – KE4ZNU
    // 2025-03-29
    include <BOSL2/std.scad>
    Layout = "Holder"; // [Show,Build,Remote,Holder]
    BaseAngle = 30; // [0:50]
    /* [Hidden] */
    RemoteOA = [92.0,40.0,14.5];
    PoleOD = 16.0; // lamp pole
    MagnetOD = 20.0; // steel plate under magnet
    MagnetOffset = [11.0,0,-2.0];
    TapeThick = 1.2;
    HolderOA = [60.0,35.0,PoleOD/3 + 4.0 + RemoteOA.z/2];
    HolderRadius = 5.0;
    Gap = 10.0;
    //———-
    // Define shapes
    module RemoteBody() {
    union() {
    intersection() {
    fwd(RemoteOA.y/2) up(RemoteOA.z/2)
    linear_extrude(h=RemoteOA.z,center=true)
    import("Floor Lamp Remote – outlines.svg",layer="Top Outline");
    zrot(90) xrot(90)
    linear_extrude(h=RemoteOA.x,center=true)
    import("Floor Lamp Remote – outlines.svg",layer="End Outline");
    xrot(90)
    linear_extrude(h=RemoteOA.y,center=true)
    import("Floor Lamp Remote – outlines.svg",layer="Side Outline");
    }
    translate(MagnetOffset)
    cylinder(d=MagnetOD,h=RemoteOA.z,$fn=4*3*4);
    }
    }
    module Holder() {
    difference() {
    cuboid(HolderOA,anchor=BOTTOM,rounding=HolderRadius,except=TOP);
    down((PoleOD + 2*TapeThick)*(1/2 – 1/3))
    yrot(90)
    cylinder(d=PoleOD + 2*TapeThick,h=2*HolderOA.x,center=true);
    up(HolderOA.z – RemoteOA.z/2)
    RemoteBody();
    }
    }
    //———-
    // Build things
    if (Layout == "Remote")
    RemoteBody();
    if (Layout == "Holder")
    Holder();
    if (Layout == "Show") {
    color("White")
    Holder();
    color("Gray",0.75)
    up(HolderOA.z – RemoteOA.z/2 + Gap)
    RemoteBody();
    color("Green",0.5)
    down((PoleOD + 2*TapeThick)*(1/2 – 1/3))
    yrot(90)
    cylinder(d=PoleOD + 2*TapeThick,h=2*HolderOA.x,center=true);
    }
    if (Layout == "Build") {
    Holder();
    }

  • LED Strip Lights: Window Moulding Mounts

    LED Strip Lights: Window Moulding Mounts

    The object of the game being to tilt the LED strip lights at (maybe) 30° to put more light higher on the wall and further out on the ceiling, with the overriding constraint of no visible holes. Given their eventual home atop the window moulding along the front wall of the Living Sewing Room, these seemed adequate:

    LED Bar Lamp Mount - solid model
    LED Bar Lamp Mount – solid model

    The hole on the angled part fits an M4 brass insert and the recessed holes capture the washer-like head of a sharp-point lath screw.

    Two pairs applied to the lights sitting atop the Fabric Cabinets served to verify the fit:

    LED strip light - moulding mount - on cabinet
    LED strip light – moulding mount – on cabinet

    They’re held firmly by the aluminum extrusion and don’t need a bigger footprint to remain stable.

    So I made another six, stuck on ⅞ inch strips of aluminized Mylar (cut from a bag in much better condition), and drilled holes where they can’t be seen:

    LED strip light - moulding mount - installed
    LED strip light – moulding mount – installed

    It’s almost too bright in there with 3 × 40 W of LED lights washing the wall and ceiling:

    LED strip light - moulding mount - lit
    LED strip light – moulding mount – lit

    I don’t like the cold 6000 K color temperature, but Mary doesn’t mind it. They fill the Sewing Table with shadowless / glareless light, although that kind of light makes the place look like a store.

    I think moving the strip lower and away from the wall could hide the entire mount from view.

    Contrary to what I expected, the Mylar reflectors must be at least an inch tall to avoid Baily’s Beads seen from across the room:

    LED strip light - short reflector
    LED strip light – short reflector

    With all that in mind, we’ll run these for a while to shake out any other improvements.

    The OpenSCAD source code as a GitHub Gist:

    // LED light bar mounts
    // Ed Nisley – KE4ZNU
    // 2025-03-16
    include <BOSL2/std.scad>
    Layout = "Show"; // [Show,Build,ScrewMount,BarMount]
    BaseAngle = 30; // [0:50]
    /* [Hidden] */
    ID = 0;
    OD = 1;
    LENGTH = 2;
    Protusion = 0.1;
    NumSides = 3*4;
    Radius = 1.5;
    $fn = NumSides;
    MouldWidth = 18.0; // nominal (3/4) * INCH, but lots of paint slop
    MouldScrew = [4.7,12.0,2.6]; // clearance, head OD, head thick
    Insert = [4.0,5.5,6.0 + 3.0]; // heat-set brass without pilot end
    BarClip = [33.0,15.0,11.0]; // snaps around led base
    ScrewBlockOA = [MouldWidth,MouldScrew[OD] + 2*Radius + 2.0,10.0];
    BarBlockOA = [BarClip.x*cos(BaseAngle),15.0,BarClip.x*sin(BaseAngle) + 2*ScrewBlockOA.z];
    Gap = 2.0 + max(ScrewBlockOA.y,BarBlockOA.y);
    //———-
    // Define shapes
    module ScrewMount() {
    difference(){
    cuboid(ScrewBlockOA,anchor=BOTTOM,rounding=Radius,except=[FRONT,BOTTOM,LEFT]);
    up(ScrewBlockOA.z – MouldScrew[LENGTH])
    zrot(180/NumSides)
    cylinder(d=MouldScrew[OD],h=MouldScrew[LENGTH] + Protusion);
    down(Protusion)
    cylinder(d=MouldScrew[ID],h=2*ScrewBlockOA.z);
    }
    }
    module BarMount() {
    difference() {
    cuboid(BarBlockOA,anchor=CENTER,rounding=Radius,edges=RIGHT);
    yrot(BaseAngle)
    cube([3*BarBlockOA.x,2*BarBlockOA.y,BarBlockOA.z],anchor=BOTTOM);
    yrot(BaseAngle)
    cylinder(d=Insert[OD],h=2*Insert[LENGTH],anchor=CENTER);
    }
    }
    module Mount() {
    union() {
    right(ScrewBlockOA.x/2) back(ScrewBlockOA.y/2)
    ScrewMount();
    right(BarBlockOA.x/2) fwd(BarBlockOA.y/2) up(BarBlockOA.z/2)
    BarMount();
    }
    }
    //———-
    // Build things
    if (Layout == "ScrewMount")
    ScrewMount();
    if (Layout == "BarMount")
    BarMount();
    if (Layout == "Show")
    Mount();
    if (Layout == "Build") {
    yflip_copy(Gap) Mount();
    }