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: Home Ec

Things around the home & hearth

  • Delta Shower Head Holder: Now With Rotation

    Delta Shower Head Holder: Now With Rotation

    We decided a few more inches of hose would be useful, so the shower head holder now has a 90° rotation in the middle to straighten the hose from plumbing to the side clip holding it out of the way:

    Delta show head holder - installed rotated
    Delta show head holder – installed rotated

    The original rectangular connection becomes a simple square:

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

    Unlike the original version, however, building this one requires a support structure:

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

    Which turned out to be a Good Thing™, because a new cheap textured PEI steel printing plate seems to have poor adhesion despite being scrupulously clean:

    Delta shower head - poor print adhesion
    Delta shower head – poor print adhesion

    Apart from obvious defects due to the shifts while printing, the holder sufficed for acceptance testing.

    I’m not sure about that steel plate, though.

    The OpenSCAD source code as a GitHub Gist, which also contains the non-rotated version:

    // Delta shower head holder extension
    // Ed Nisley – KE4ZNU
    // 2025-05-02
    // 2026-11-09 add rotation for hose repositioning
    include <BOSL2/std.scad>
    Layout = "Show"; // [Show,Build,Plug,Socket,Connector,Retainer,Latch,Test]
    MountAngle = 30; // between OEM and new holder
    MountOffset = 40.0;
    BaseAngle = 90; // [90,0,-90]
    /* [Hidden] */
    HoleWindage = 0.2;
    Protrusion = 0.1;
    NumSides = 3*3*4;
    Gap = 5.0;
    ID = 0;
    OD = 1;
    LENGTH = 2;
    WallThick = 8.0; // holder wall thickness
    Radius = 3.0; // shapely rounding
    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
    Latch = [Insert[OD] – 0.25,Insert[OD] + 2*WallThick,1.5]; // ID = latch engagement
    RetainerSize = [14.5,25.0,6.0];
    RetainerPegThick = 3.0;
    $fn=NumSides;
    //———-
    // Define Shapes
    module Plug() {
    up(Slot.y/2)
    xrot(BaseAngle)
    down(Slot.y/2)
    difference() {
    cyl(l=Insert[LENGTH],d1=PlugSize[ID],d2=PlugSize[OD],anchor=BOTTOM);
    down(Protrusion)
    cyl(10.0 + 5,d=5.5,anchor=BOTTOM); // M4 burly insert
    }
    }
    module LatchRing() {
    difference() {
    union() {
    tube(Latch[LENGTH],id=Insert[OD],od=Latch[OD],anchor=BOTTOM);
    difference() {
    tube(Latch[LENGTH]/2,id1=Insert[OD],id2=Latch[ID],od=Insert[OD],anchor=BOTTOM)
    position(TOP)
    tube(Latch[LENGTH]/2,id1=Latch[ID],id2=Insert[OD],od=Insert[OD],anchor=BOTTOM);
    cuboid(Slot + [0,1.0,0],anchor=RIGHT+CENTER);
    }
    }
    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 Socket() {
    difference() {
    tube(l=Insert[LENGTH],id1=Insert[ID],id2=Insert[OD],wall=WallThick,anchor=BOTTOM)
    position(TOP) LatchRing();
    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 – Insert[ID]/4)
    cuboid([MountOffset + Insert[LENGTH]*sin(MountAngle),Slot.y,Slot.y + 0*Insert[LENGTH]*cos(MountAngle)],
    anchor=LEFT+BOTTOM);
    left(MountOffset)
    down(Protrusion)
    cyl(10.0 + 5,d=5.5,anchor=BOTTOM); // M4 burly insert
    yrot(MountAngle) right(Insert[ID]/2 + WallThick)
    cyl(l=Insert[LENGTH],d1=Insert[ID] + 2*WallThick,d2=Insert[OD] + 2*WallThick,anchor=BOTTOM);
    }
    }
    module Retainer() {
    difference() {
    cyl(RetainerSize[LENGTH] – RetainerPegThick,d=RetainerSize[OD],anchor=BOTTOM) position(TOP)
    cyl(RetainerPegThick,d=RetainerSize[ID],anchor=BOTTOM);
    down(Protrusion)
    cyl(2*RetainerSize[LENGTH],d=4.0,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 == "Retainer")
    Retainer();
    if (Layout == "Connector")
    Connector();
    if (Layout == "Latch")
    LatchRing();
    if (Layout == "Test")
    render()
    difference() {
    left(0.75*Insert[LENGTH])
    up(Insert[OD]/2)
    yrot(90)
    Socket();
    cuboid(2*(Insert[OD] + 2*WallThick),anchor=RIGHT);
    }
    if (Layout == "Show") {
    Adapter();
    if (BaseAngle)
    back(sign(BaseAngle)*(RetainerSize[LENGTH] + Slot.y/2 + Gap))
    up(Slot.y/2)
    left(MountOffset)
    xrot(BaseAngle)
    Retainer();
    else
    left(MountOffset) down(RetainerSize[LENGTH] + Gap)
    Retainer();
    }
    if (Layout == "Build") {
    up(Insert[ID]/2 + WallThick + Insert[OD]/2)
    yrot(90-MountAngle)
    Adapter();
    right(Insert[LENGTH] + Insert[OD])
    Retainer();
    if (false)
    right(Insert[LENGTH] + 5.0) // fit test piece
    difference() {
    left(0.75*Insert[LENGTH])
    up(Insert[OD]/2)
    yrot(90)
    Socket();
    cuboid(2*(Insert[OD] + 2*WallThick),anchor=RIGHT);
    }
    }
    // 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();
  • Furnace Pipe Temperature Sensor Mount: Diluting the Phlogiston

    Furnace Pipe Temperature Sensor Mount: Diluting the Phlogiston

    Being that type of guy, I enjoy keeping track of the gas furnace’s behavior and the least invasive method is monitoring the inlet and outlet water temperatures. I already have an Ambient Weather station for other things, including a pair of WH31P temperature sensor probes for the heat pump’s outdoor exhaust air and indoor duct temperature. Unfortunately, those sensors top out at (an undocumented in the description) 140 °F and the furnace water runs around 180 °F.

    Because I’m more interested in furnace run time than exact water temperature, putting the sensor somewhere between the 180 °F pipe and the 60 °F basement air seemed reasonable.

    A quick test showed it might work:

    Furnace pipe sensor - proof of concept
    Furnace pipe sensor – proof of concept

    That’s a random cart coin for insulation under a random heatsink for cooling, with the WH31P probe clamped under a snippet of closed-cell packing foam.

    Emboldened by 180 °F water in the pipe producing 80 °F at the probe, I conjured a more formal mount from the vasty digital deep:

    Furnace Pipe Sensor Mount - solid model - show view
    Furnace Pipe Sensor Mount – solid model – show view

    Which looks pretty much like that in real life:

    Furnace pipe sensor - installed
    Furnace pipe sensor – installed

    A layer of heatsink tape holds it to the plastic block, which in turn just sits on the rather crusty pipe surface. The blue wire is a thermocouple reporting the actual pipe temperature and, if the sensor isn’t getting enough heat, a blob of heatsink grease on the pipe will surely help.

    The spring arms originally held the heatsink to a PCB atop a Northbridge chip (or some such), so those solderless connectors crimped on shock cord should make it feel at home.

    Installing the sensors immediately raised the outdoor temperature by 20 °F at the beginning of an atypically warm November week, so it’ll be a while before I have any results.

  • Bee Tunnel Nests: End of Season

    Bee Tunnel Nests: End of Season

    Most of the tenants in the bee tunnel nests have departed:

    Bee Tunnel Nests - Nov 2026 - small A
    Bee Tunnel Nests – Nov 2026 – small A

    I assume the end caps with holes mark successful nests:

    Bee Tunnel Nests - Nov 2026 - small B
    Bee Tunnel Nests – Nov 2026 – small B

    Some bees overwinter in the nests, so they should emerge from the tunnels with intact end caps:

    Bee Tunnel Nests - Nov 2026 - small C
    Bee Tunnel Nests – Nov 2026 – small C

    Very few of the larger bamboo tubes met the acceptance criteria:

    Bee Tunnel Nests - Nov 2026 - large
    Bee Tunnel Nests – Nov 2026 – large

    Other critters seem to have used the tubes for various purposes, though, so they didn’t go to waste.

    I put these up in late April, with the June and July results showing much of the nesting happens shortly thereafter. Next year I should install the tubes in March; there’s no shortage of bamboo up the hill!

  • Electrolux Dust Brush: Replacement Rubber

    Electrolux Dust Brush: Replacement Rubber

    The Electrolux Model G vacuum cleaner that Came With The Previous House™ has a dust brush attachment resembling a creature from a deep sea trench:

    Electroxlux Brush Ball - OEM assembled
    Electroxlux Brush Ball – OEM assembled

    Those bristles recently fell out during a cleaning session:

    Electroxlux Brush Ball - OEM with brush
    Electroxlux Brush Ball – OEM with brush

    I will never know their backstory, but the rough-cut metal ring strongly suggests they’re a replacement for the original Electrolux bristles. Given that the Model G dates back to the early sixties, that puppy has been around for six decades and counting.

    The rubber insert in the aluminum ball had long since fossilized:

    Electroxlux Brush Ball - OEM rubber
    Electroxlux Brush Ball – OEM rubber

    Not knowing if it would come out in approximately one piece, a doodle records whatever dimensions I could measure:

    Electroxlux Brush Ball - exterior measurements
    Electroxlux Brush Ball – exterior measurements

    Surprisingly, it remained just flexible enough:

    Electroxlux Brush Ball - OEM rubber removed
    Electroxlux Brush Ball – OEM rubber removed

    So I could measure a few more rubber dimensions:

    Electroxlux Brush Ball - rubber measurements
    Electroxlux Brush Ball – rubber measurements

    And ball dimensions:

    Electroxlux Brush Ball - metal measurements
    Electroxlux Brush Ball – metal measurements

    After screwing around drawing Beziér curves for maybe half an hour, I realized a crude polygonal approximation would work perfectly well when 3D printed in TPU with a 0.8 mm nozzle:

    Electroxlux Brush Ball - show 5 view
    Electroxlux Brush Ball – show 5 view

    Which had the compelling advantage I could exactly specify the points and tweak them for an exact fit:

    SplitHeight = 15;
    SplitCube = [100,100,SplitHeight - Protrusion];
    
    NozzleHeight = 15 + SplitHeight;          // above top of ball nozzle
    
    Pts = [
      [61,11],[62,6],[61,2],[55,0],[48,0],    // outside bottom
      [38,5],[33,11],
      [31,SplitHeight],[26,SplitHeight],      // atop ball nozzle rim
      [26,18],[27,19],[31,NozzleHeight],[33,NozzleHeight],    // bristle reinforcement
      [39,19],[40,18],[40,16],
      [35,SplitHeight],
      [36,11],[40,6],                         // inside bottom
      [45,3],[48,2],[52,1],[55,1],
      [59,3],[60,6],[58,9],[59,11],                  // brush metal rim seat
      [61,11],
    ];
    
    

    Turn those points into a polygon, spin it into a ring, and add a ghostly plane to show where the model separates into printable halves:

    if (Layout == "Show") {
        scale([0.5,0.5,1.0])
          rotate_extrude(angle=Slice,start=0,convexity=5) polygon(Pts);
        up(SplitHeight)
          color("Orange",0.2)
            cuboid([100,100,Protrusion],anchor=BOTTOM);
    }
    

    The scale() compensates for the list of points having easily measured real-life diameters, but the polygon() must be at the radius away from the origin; you could divide all the X axis coordinates in the list by two. Dimensions along the Z axis remain unchanged.

    Like this:

    Electroxlux Brush Ball - show 360 view
    Electroxlux Brush Ball – show 360 view

    Split it along the ghostly plane into printable parts:

    if (Layout == "Build") {
      fwd(50)
        intersection() {
          scale([0.5,0.5,1.0])
            rotate_extrude(convexity=5) polygon(Pts);
          cuboid(SplitCube,anchor=BOTTOM);
        }
      back(30)
        intersection() {
          down(SplitHeight)
            scale([0.5,0.5,1.0])
              rotate_extrude(convexity=5) polygon(Pts);
          cuboid(SplitCube,anchor=BOTTOM);
        }
    
    }
    

    Like this:

    Electroxlux Brush Ball - build view
    Electroxlux Brush Ball – build view

    Run the model through PrusaSlicer, install the 0.8 mm nozzle in the MK4, load TPU filament, let the magic happens, then dab JB PlasticBonder urethane adhesive around the mating rim of the bottom part:

    Electroxlux Brush Ball - gluing part
    Electroxlux Brush Ball – gluing part

    Yeah, black would be more better, but tan is fine.

    Flip it over, eyeballometrically center the gooey ring in the wide rim of the nozzle, and let it cure.

    Which eventually looks like it grew that way:

    Electroxlux Brush Ball - replacement TPU
    Electroxlux Brush Ball – replacement TPU

    See? Crude low-poly models FTW!

    As usual, however, it takes two to make a thing go right:

    Electroxlux Brush Ball - TPU versions
    Electroxlux Brush Ball – TPU versions

    The first pass in the center front showed the outer rim needed a deeper recess to grip the metal ring around the bristles; I’m sure I could pry the bristles out of the second version if lives depended on it.

    I added the elongated nozzle to stand halfway up inside the bristles to keep them from collapsing inward under suction:

    Electroxlux Brush Ball - nozzle in brush
    Electroxlux Brush Ball – nozzle in brush

    It’s definitely better than it was!

    Might last another half-century; the original Electrolux vacuums are survivors.

  • Halloween Candy: Now With Bones

    Halloween Candy: Now With Bones

    A skeletal hand and foot seem appropriate:

    Halloween Candy - skeleton parts
    Halloween Candy – skeleton parts

    Making them less obvious may be more effective:

    Halloween Candy - hidden skeleton parts
    Halloween Candy – hidden skeleton parts

    However, leftover junk food is known to be a Bad Idea™ …

  • Standard Cardboard Box Layouts

    Standard Cardboard Box Layouts

    Being in the midst of conjuring cardboard into seasonal driveway decorations:

    Halloween beetles and Tiny-Rexes
    Halloween beetles and Tiny-Rexes

    I collected all the cardboard box layouts into a LightBurn Art Library:

    Cardboard Box Layouts
    Cardboard Box Layouts

    The tiny gray grid over on the left represents the 700×500 mm laser platform: chop the boxes apart and burn the flaps separately.

    The Amazon box codes represent what’s in the Great Heap o’ Boxes at the moment and I’m whittling away at the Home Depot boxes that moved us here. Amazon has approximately a zillion different sizes, few of which I’ve ever seen.

    Now that LightBurn has a true nesting function, you can select the shapes-to-cut + a cardboard sheet and cram everything into as many of those sheets as needed:

    Sheep - nested in HD box flaps
    Sheep – nested in HD box flaps

    The Art Library file format isn’t documented, so I exported all the layouts into one gargantuan SVG image 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.

    The SVG image has a 5 mm stroke width to make the vectors visible at that scale. LightBurn ignores the stroke width, but a real SVG handler will make them look like crayon on a paper towel.

    They’re all colored blue, corresponding to LightBurn layer C01, for better visibility, but put them (back) on tool layer T2 so they don’t get cut.

  • Outdoor Lamp Glass Retainers

    Outdoor Lamp Glass Retainers

    Tucking seasonal decorations into the lamps bracketing the garage door would be much easier if the rubber buttons retaining the glass panes were squishy cylinders, rather than fossilized pebbles:

    Outdoor Lamp Glass Retainers - old vs new
    Outdoor Lamp Glass Retainers – old vs new

    The five tidy buttons on the right are black TPU, hot off the MK4’s printbed:

    Outdoor Lamp Glass Retainers - PrusaSlicer preview
    Outdoor Lamp Glass Retainers – PrusaSlicer preview

    The flange allows for some, shall we say, dimensional variation in the installation:

    Outdoor Lamp Glass Retainers - old vs new - installed
    Outdoor Lamp Glass Retainers – old vs new – installed

    They’re just a tube atop a cylinder:

    nx = ceil(sqrt(NumButtons));
    ny = nx;
    
    union()
    for (nx = [0:nx-1], ny = [0:ny-1])
      right(nx*(FlangeOD + Gap))
        back(ny*(FlangeOD + Gap))
          render()
          union() {
            cyl(FlangeHeight,d=FlangeOD,circum=true,anchor=BOTTOM);
            tube(ButtonOA.z,od=ButtonOA[OD],id=ScrewOD,circum=true,anchor=BOTTOM);
          }
    
    

    Feel free to set up your own dimensions, in the unlikely event you have lamps in need of similar buttons.

    Each of the three lamps has sixteen buttons and, although PrusaSlicer allows you to set the number of instances, its Arrange tool doesn’t make a tidy / compact layout. Doing the layout in OpenSCAD saves a bunch of hassle, with the overall union() turning the array into a single object.

    It might not be the best decoration, but it should suffice:

    Outdoor Lamp Glass Retainers - with decoration
    Outdoor Lamp Glass Retainers – with decoration

    Now, to make critters for the driveway …