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.

Tag: MK4

Prusa Mk 4 3D printer with MMU3 feeder

  • Delta Shower Head Holder Extension

    Delta Shower Head Holder Extension

    The original shower head being too far overhead for Mary’s reach, I installed a Delta ProClean Shower Head which would also be too high. It has a hose, which means I can adjust the height:

    Delta shower head holder extension - installed
    Delta shower head holder extension – installed

    The InterWebs offer several 3D-printable versions of such a thing, but Delta offers many different shower heads, some of which are visually (to my eyes, anyway) indistinguishable from the 75740SN you see here. The model I tried did not fit the holder I have, so I conjured one from the vasty digital deep:

    Delta shower head holder extension - solid model
    Delta shower head holder extension – solid model

    It builds standing on that tidy cutoff:

    Delta shower head holder extension - PrusaSlicer warning
    Delta shower head holder extension – PrusaSlicer warning

    Despite PrusaSlicer’s kvetching about the “collapsing overhang” inside the socket, it came out fine.

    The shower head is still slightly too high for her, but now I can print another one with a longer offset and a slightly smaller plug to fit deeper in the OEM socket.

    Worst case, there’s a wall-mounted holder to put the shower head at shoulder height.

    The OpenSCAD source code as a GitHub Gist:

    // Delta shower head holder extension
    // Ed Nisley – KE4ZNU
    // 2025-05-02
    include <BOSL2/std.scad>
    Layout = "Show"; // [Show,Build,Plug,Socket,Connector]
    MountAngle = 30; // between OEM and new holder
    MountOffset = 20.0;
    /* [Hidden] */
    HoleWindage = 0.2;
    Protrusion = 0.1;
    NumSides = 3*3*4;
    ID = 0;
    OD = 1;
    LENGTH = 2;
    Insert = [21.0,24.5,35.0]; // hose connector taper
    PlugSize = Insert + [-0.3,-0.3,0]; // … for better fit in OEM holder
    Slot = [Insert[OD],14.6,3*Insert[LENGTH]]; // slot on OEM holder, arbitrary length
    WallThick = 8.0; // holder wall thickness
    Radius = 3.0; // shapely rounding
    $fn=NumSides;
    //———-
    // Define Shapes
    module Plug() {
    cyl(l=Insert[LENGTH],d1=PlugSize[ID],d2=PlugSize[OD],anchor=BOTTOM);
    }
    module Socket() {
    difference() {
    tube(l=Insert[LENGTH],id1=Insert[ID],id2=Insert[OD],wall=WallThick,anchor=BOTTOM);
    cuboid(Slot + [0,1.0,0],anchor=LEFT+CENTER);
    right(Insert[OD]/2)
    cube([Insert[OD],Insert[OD] + 2*WallThick,3*Insert[LENGTH]],anchor=LEFT+CENTER);
    }
    }
    module Connector() {
    difference() {
    left(MountOffset)
    cuboid([MountOffset + Insert[LENGTH]*sin(MountAngle),Slot.y,Insert[LENGTH]*cos(MountAngle)],
    anchor=LEFT+BOTTOM);
    yrot(MountAngle) right(Insert[ID]/2 + WallThick)
    cyl(l=Insert[LENGTH],d1=Insert[ID] + 2*WallThick,d2=Insert[OD] + 2*WallThick,anchor=BOTTOM);
    }
    }
    module Adapter() {
    union() {
    left(MountOffset)
    Plug();
    yrot(MountAngle) right((Insert[ID] + 2*WallThick)/2)
    Socket();
    Connector();
    }
    }
    //———-
    // Build things
    if (Layout == "Plug")
    Plug();
    if (Layout == "Socket")
    Socket();
    if (Layout == "Connector")
    Connector();
    if (Layout == "Show")
    Adapter();
    if (Layout == "Build")
    up(Insert[ID]/2 + 1*WallThick + Insert[OD]/2)
    yrot(90-MountAngle)
    Adapter();
  • Improved Sony AS30V Helmet Mount Adapter Plate

    Improved Sony AS30V Helmet Mount Adapter Plate

    Last week a wind gust blew my Tour Easy over while resting on its kickstand at Mary’s garden; I rarely depend on the kickstand for that very reason, but some days are like that. Anyhow, the mount for the Sony AS30V helmet camera did exactly what it should by releasing the camera, rather than grinding it into the ground.

    Calling it a “mount” may be overstating the case:

    Sony HDR-AS30V camera on bike helmet - inverted
    Sony HDR-AS30V camera on bike helmet – inverted

    I was still using that helmet, albeit with a better mirror mount, but it was getting rather crusty and the hook-n-loop straps were definitely sun-faded, so I built a better mount with an adapter plate matching a new-old-stock helmet from the stash:

    Sony AS30V Helmet mount - side view
    Sony AS30V Helmet mount – side view

    The white slab atop the helmet curves to match the helmet contour, with the ridge fitting into the vent slot:

    AS30 helmet mount - solid model - show view
    AS30 helmet mount – solid model – show view

    OK, the helmet isn’t orange, but you get the idea. The sphere has a 153 mm radius, calculated from the Official Sony helmet mount’s bottom curve, minus a ring shaping the central groove:

    AS30 helmet mount - solid model - tab ring
    AS30 helmet mount – solid model – tab ring

    This upside-down view shows the interesting parts:

    AS30 helmet mount - solid model
    AS30 helmet mount – solid model

    The flat side sticks to the camera’s holder with a custom-cut sheet of craft adhesive shaped like this:

    AS30 helmet mount - glue
    AS30 helmet mount – glue

    The overall outline of those things comes from a scan of the bottom of the Sony camera holder, passed through Inkscape and LightBurn to generate the curves:

    AS30 Baseplate scan
    AS30 Baseplate scan

    The large notches in the sides pass hook-n-loop straps intended to break away when the helmet hits the ground again. The front tunnel (of two, because symmetry) passes a cable tie preventing the camera from parting company with the mount during normal riding and holding the yellow latch in the Locked position:

    Sony AS30V Helmet mount - rear view
    Sony AS30V Helmet mount – rear view

    It is just barely possible to slide the cable tie over the front of the camera to release the latch.

    The camera rides upside-down to protect the lens from scuffs and scrapes. Fortunately, there’s a setting to invert the picture.

    For completeness, the front view:

    Sony AS30V Helmet mount - front view
    Sony AS30V Helmet mount – front view

    The furry patch covers the microphone pores to kill (most of) the wind noise.

    The sharp ventral angle matches the helmet’s midline ridge in the back, but obviously isn’t needed over the vent hole in the front. I decided to not bother making a comprehensive model of the hole, not least because I didn’t really know the camera’s exact front-to-back location.

    Works fine where it sits, though:

    Burnett Signal Timing - 2025-04-23
    Burnett Signal Timing – 2025-04-23

    NYSDOT’s signal timing at Burnett Blvd and Rt 55 remains bicycle-hostile, same as it ever was.

    The OpenSCAD source code and baseplate shape as a GitHub Gist:

    // Sony AS30 helmet mount
    // Ed Nisley – KE4ZNU
    // 2025-04-20
    include <BOSL2/std.scad>
    Layout = "Show"; // [Show,Build,Ball,Tab,Glue]
    Gap = 5; // [0:5:20]
    /* [Hidden] */
    HoleWindage = 0.2;
    Protrusion = 0.1;
    WallThick = 1.0; // enough stiffness against flat pad
    HelmetRadius = 153.0; // from chord equation on curved pad = magic number
    Groove = [30.0,100,3.0,]; // roughly the groove along helmet midline
    Pad = [38,53,10]; // baseplate size, thick enough without fancy trig
    Strap = [3.0,15.0,10*Pad.z]; // hook-n-loop strap holes, double-thick
    Tie = [100,6.0,2.0 + Protrusion]; // cable tie around camera
    TieOffset = 14.0; // … from end of pad
    $fn=96;
    //———-
    // Define shapes
    module Ball() {
    difference() {
    sphere(r=HelmetRadius);
    Tab();
    }
    }
    // Rough approximation of the helmet groove
    module Tab() {
    m = 2.0; // roughly the chord height beyond the tab
    rotate_extrude(convexity=10) {
    right(HelmetRadius)
    zrot(180)
    polygon([
    [0,0],
    [0,Groove.x/2],[Groove.z + m,Groove.x/2],[m,0],
    [Groove.z + m,-Groove.x/2],[0,-Groove.x/2],
    [0,0]
    ],convexity=10);
    }
    }
    // Baseplate with all the cutouts
    module BasePlate() {
    difference() {
    linear_extrude(height=Pad.z,convexity=10)
    import("AS30 Baseplate layout.svg",layer="Baseplate");
    up(WallThick + HelmetRadius)
    yrot(90)
    Ball();
    for (i = [-1,1]) // strap clearance at edge of helmet hole
    right(i*Groove.x/2)
    cube([(Pad.x – Groove.x)/2,Strap.y,Strap.z],center=true);
    for (i = [-1,1]) // cut through edge of pad
    right(i*Pad.x/2)
    cube([(Pad.x – Groove.x),Strap.y,Strap.z],center=true);
    for (j = [-1,1])
    fwd(j*(Pad.y/2 – TieOffset)) up(WallThick)
    cuboid(Tie,anchor=BOTTOM);
    }
    }
    //———-
    // Build things
    if (Layout == "Glue")
    projection(cut=true)
    BasePlate();
    if (Layout == "Tab")
    Tab();
    if (Layout == "Show") {
    xrot(180)
    BasePlate();
    down(WallThick + HelmetRadius + Gap)
    yrot(90)
    color("Orange",0.75) Ball();
    }
    if (Layout == "Build")
    BasePlate();
    if (Layout == "Ball")
    Ball();
    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <!– Created with Inkscape (http://www.inkscape.org/) –>
    <svg
    width="11in"
    height="8.5in"
    viewBox="0 0 279.40056 215.90043"
    version="1.1"
    id="SVGRoot"
    inkscape:version="1.4.1 (93de688d07, 2025-03-30)"
    sodipodi:docname="AS30 Baseplate layout.svg"
    xml:space="preserve"
    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape&quot;
    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd&quot;
    xmlns:xlink="http://www.w3.org/1999/xlink&quot;
    xmlns="http://www.w3.org/2000/svg&quot;
    xmlns:svg="http://www.w3.org/2000/svg&quot;
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#&quot;
    xmlns:cc="http://creativecommons.org/ns#"><sodipodi:namedview
    id="namedview7"
    pagecolor="#ffffff"
    bordercolor="#0000ff"
    borderopacity="1"
    inkscape:pageshadow="0"
    inkscape:pageopacity="0"
    inkscape:pagecheckerboard="1"
    inkscape:document-units="mm"
    showgrid="true"
    units="mm"
    gridtolerance="9.9"
    guidetolerance="10.4"
    inkscape:snap-perpendicular="true"
    inkscape:snap-tangential="true"
    width="700mm"
    borderlayer="false"
    inkscape:showpageshadow="true"
    viewbox-width="700"
    guidecolor="#ff00e3"
    guideopacity="0.49803922"
    inkscape:zoom="1.6945884"
    inkscape:cx="86.451671"
    inkscape:cy="111.82656"
    inkscape:window-width="1780"
    inkscape:window-height="1091"
    inkscape:window-x="0"
    inkscape:window-y="0"
    inkscape:window-maximized="0"
    inkscape:current-layer="layer1"
    objecttolerance="31"
    inkscape:deskcolor="#d1d1d1"
    showguides="true"><inkscape:grid
    type="xygrid"
    id="grid9"
    units="mm"
    spacingx="5"
    spacingy="5"
    dotted="false"
    empspacing="2"
    originx="148.5"
    originy="127.29919"
    color="#ff0000"
    opacity="0.18431373"
    empcolor="#4040ff"
    empopacity="0.49411765"
    visible="true" /><sodipodi:guide
    position="157.7549,120.64599"
    orientation="1,0"
    id="guide1"
    inkscape:locked="false" /></sodipodi:namedview><defs
    id="defs2" /><g
    inkscape:label="Baseplate"
    inkscape:groupmode="layer"
    id="layer1"
    transform="translate(0,5.4354331)"><path
    id="path1"
    style="fill:none;fill-rule:evenodd;stroke:#0c96d9;stroke-width:0.0998686;stroke-linejoin:round"
    d="m -18.99969,190.42075 3.09576,-6.45581 h 32.08112 l 2.82285,6.45581 -10e-6,40.00473 -3.02778,6.53957 -31.33905,-0.14658 -3.63324,-6.39299 z"
    sodipodi:nodetypes="ccccccccc"
    inkscape:label="Aligned path" /></g><g
    inkscape:groupmode="layer"
    id="layer2"
    inkscape:label="Original"><image
    width="57.658115"
    height="65.193459"
    preserveAspectRatio="none"
    xlink:href="AS30%20Baseplate%20scan.jpg"
    id="image1"
    x="112.42073"
    y="67.772316"
    transform="rotate(0.87516737,-355.84202,2.7177945)"
    style="display:inline" /><path
    id="rect1"
    style="fill:none;fill-rule:evenodd;stroke:#0c96d9;stroke-width:0.0998686;stroke-linejoin:round"
    d="m 120.39572,83.160307 3.09576,-6.45581 h 32.08112 l 2.82285,6.45581 -1e-5,40.004733 -3.02778,6.53957 -31.33905,-0.14658 -3.63324,-6.39299 z"
    sodipodi:nodetypes="ccccccccc"
    transform="translate(0,5.4354331)" /></g><metadata
    id="metadata11"><rdf:RDF><cc:Work
    rdf:about=""><cc:license
    rdf:resource="http://creativecommons.org/licenses/by-nc-sa/4.0/&quot; /></cc:Work><cc:License
    rdf:about="http://creativecommons.org/licenses/by-nc-sa/4.0/"><cc:permits
    rdf:resource="http://creativecommons.org/ns#Reproduction&quot; /><cc:permits
    rdf:resource="http://creativecommons.org/ns#Distribution&quot; /><cc:requires
    rdf:resource="http://creativecommons.org/ns#Notice&quot; /><cc:requires
    rdf:resource="http://creativecommons.org/ns#Attribution&quot; /><cc:prohibits
    rdf:resource="http://creativecommons.org/ns#CommercialUse&quot; /><cc:permits
    rdf:resource="http://creativecommons.org/ns#DerivativeWorks&quot; /><cc:requires
    rdf:resource="http://creativecommons.org/ns#ShareAlike&quot; /></cc:License></rdf:RDF></metadata></svg>
    view raw gistfile1.txt hosted with ❤ by GitHub
  • 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();
    }

  • 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();
    }