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

  • Smashed Glass Coaster: Rivers of Crack

    Smashed Glass Coaster: Rivers of Crack

    Looking at that big smashed-glass coaster from a different angle showed interesting patterns:

    Printed Fragment Coaster - 165mm - long cracks
    Printed Fragment Coaster – 165mm – long cracks

    Although the larger fragments were still holding together when I laid them in their recesses, they apparently consist of several sub-fragments with larger continuous cracks letting the epoxy flow / ooze inside.

    Now that I know what to look for, the original picture also shows them, albeit less distinctly:

    Printed Fragment Coaster 165mm - overview
    Printed Fragment Coaster 165mm – overview

    They’re not obvious in the scanned image of the fragments, although I could convince myself I see some:

    Fragments 165mm square - scan sample
    Fragments 165mm square – scan sample

    The many smaller fragments I’ve been turning into coasters probably separated from similar large chunks along such cracks, which is why I’ve never seen rivers of crack before.

    Apologies if you arrived here expecting a tirade concerning the drug trade … :grin:

  • Mini-lathe Change Gear Generator: Redux

    Mini-lathe Change Gear Generator: Redux

    Because the BOSL2 library includes a gear generator, I can now avoid creating a gear outline in Inkscape and importing it into my stacked change gear generator.

    The labels now snuggle closer to the shaft and (barely) fit on smaller gears:

    Mini-lathe stacked change gears - 28T - solid model
    Mini-lathe stacked change gears – 28T – solid model

    The stacked B-C gears for the jack shaft work as before, with both labels on the top gear:

    Mini-lathe stacked change gears - 28-50T - solid model
    Mini-lathe stacked change gears – 28-50T – solid model

    The admittedly flimsy motivation for all this was to make a 28 tooth gear to cut a 0.9 mm pitch, thus filling an obvious hole in the gear table.

    My collection of gears could do 21-60-81-50, but the 81 T gear collides with the screw holding the 21 T gear. Rearranging it to 21-50-81-60 showed the B-C gears exceeded the space available.

    Because it’s all ratios and a 28 T gear is 4/3 bigger than 21 T, reducing the rest of the train by 3/4 should work. In fact, it produced a reasonable 28-80-81-50 chain:

    Mini-lathe change gears - 28T installed
    Mini-lathe change gears – 28T installed

    The fact that I do not anticipate ever needing to cut a 0.9 mm pitch has nothing whatsoever to do with it; that gear will surely come in handy for something.

    While I was at it, I made a 27 T gear, because 27 = 21 × 9/7:

    Mini-lathe stacked change gears - 27T - PrusaSlicer preview
    Mini-lathe stacked change gears – 27T – PrusaSlicer preview

    You can never have enough change gears. Right?

    The OpenSCAD source code as a GitHub Gist:

    // LMS Mini-Lathe
    // Change gears with stacking
    // Ed Nisley – KE4ZNU
    // 2020-05 use Inkscape SVG gears
    // 2025-12 use BOSL2 gear generator
    include <BOSL2/std.scad>
    include <BOSL2/gears.scad>
    /* [Gears] */
    TopGear = 0; // zero for single gear
    BottomGear = 28;
    /* [Hidden] */
    ThreadThick = 0.20;
    HoleWindage = 0.2;
    Protrusion = 0.1; // make holes end cleanly
    /* [Dimensions] */
    ShaftOD = 12.0;
    GearThick = 7.75;
    Keyway = [3.5,3.0,3*GearThick]; // x on radius, y on perim
    LegendEnable = (TopGear == 0 && BottomGear > 27) || (TopGear > 27);
    LegendThick = 2*ThreadThick;
    LegendZ = (TopGear ? 2*GearThick : GearThick) – LegendThick;
    LegendSize = 5;
    LegendRecess = [8,6,LegendThick];
    LegendOffset = [0,LegendRecess.y/2 + ShaftOD/2 + HoleWindage,LegendZ + LegendRecess.z/2];
    //———————–
    // Build it!
    union() {
    difference() {
    union() {
    spur_gear(mod=1,teeth=BottomGear,thickness=GearThick,shaft_diam=ShaftOD + HoleWindage,anchor=BOTTOM);
    if (TopGear)
    spur_gear(mod=1,teeth=TopGear,thickness=2*GearThick,shaft_diam=ShaftOD + HoleWindage,anchor=BOTTOM);
    }
    right(ShaftOD/2)
    down(Protrusion)
    cube(Keyway,anchor=CENTER+BOTTOM);
    if (LegendEnable) {
    translate(LegendOffset)
    cube(LegendRecess + [0,0,Protrusion],anchor=CENTER);
    if (TopGear)
    zrot(180)
    translate(LegendOffset)
    cube(LegendRecess + [0,0,Protrusion],anchor=CENTER);
    }
    }
    if (LegendEnable)
    translate([0,0,LegendZ – Protrusion])
    linear_extrude(height=LegendThick + Protrusion,convexity=10) {
    translate([LegendOffset.x,LegendOffset.y])
    text(text=str(BottomGear),size=LegendSize,font="Arial:style:Bold",halign="center",valign="center");
    if (TopGear)
    zrot(180)
    translate([LegendOffset.x,LegendOffset.y])
    text(text=str(TopGear),size=LegendSize,font="Arial:style:Bold",halign="center",valign="center");
    }
    }
  • Sears Humidifier Bottle Cap Reinforcement

    Sears Humidifier Bottle Cap Reinforcement

    In the midst of the humidification season, I spotted this while refilling one of the ancient Sears Humidifier bottles:

    Humidifier bottle cap reinforcement - crack
    Humidifier bottle cap reinforcement – crack

    While it’s possible to buy replacement caps, this seemed more appropriate:

    Humidifier bottle cap reinforcement - installed
    Humidifier bottle cap reinforcement – installed

    It’s PETG-CF, of course:

    Bottle cap reinforcement - solid model
    Bottle cap reinforcement – solid model

    The shape is a ring with a simplified model of the cap removed from the middle:

    Bottle cap reinforcement - lid solid model
    Bottle cap reinforcement – lid solid model

    It fits snugly over the cap atop a thin layer of JB PlasticBonder that should hold it in place forevermore:

    Humidifier bottle cap reinforcement - bottom view
    Humidifier bottle cap reinforcement – bottom view

    The other side shows the crack over on the right:

    Humidifier bottle cap reinforcement - top view
    Humidifier bottle cap reinforcement – top view

    Close inspection showed a few smaller cracks, so that cap was likely an original.

    I made another ring for the other cap, only to find it was slightly larger with a black washer inside: apparently a previous owner had replaced one of the caps. The OpenSCAD program has measurements for both, not that you have either.

    The OpenSCAD source code as a GitHub Gist:

    // Humidifier bottle cap reinforcement
    // Ed Nisley – KE4ZNU
    // 2025-11-29
    include <BOSL2/std.scad>
    Layout = "Show"; // [Show,Build,Cap]
    /* [Hidden] */
    Protrusion = 0.1;
    //—–
    // Bottle cap/valve
    // Collects all the magic numbers in one place
    Left = false; // the caps are different, of course
    CapODs = Left ? [43.0,42.1] : [43.1,42.9]; // [0] = base of cap
    CapHeight = 10.0;
    Notch = [0.6,2.0,8.5 + Protrusion]; // Z + hack for slight angle
    NumRibs = 24;
    RibAngle = 90 – atan(CapHeight/((CapODs[0]-CapODs[1])/2));
    echo(RibAngle=RibAngle);
    $fn=2*NumRibs;
    module Cap() {
    difference() {
    cyl(CapHeight,d1=CapODs[1],d2=CapODs[0],anchor=BOTTOM);
    for (a=[0:NumRibs-1])
    zrot(a*360/NumRibs)
    right(CapODs[1]/2) down(Protrusion)
    yrot(RibAngle)
    cuboid(Notch,anchor=RIGHT+BOTTOM);
    }
    }
    //—–
    // Reinforcing ring
    RingThick = 3.0;
    module Ring() {
    render()
    difference() {
    tube(CapHeight,od=CapODs[0] + 2*RingThick,id=CapODs[1] – 2*Notch.x,anchor=BOTTOM);
    Cap();
    }
    }
    // Build things
    if (Layout == "Cap")
    Cap();
    if (Layout == "Build" || Layout == "Show")
    Ring();
  • Large Smashed Glass Coaster

    Large Smashed Glass Coaster

    Those of long memory will recall our vermiculture setup in the basement that turns kitchen scraps into plant food. We accumulate scraps in plastic milk jugs, which jugs get recycled after they become grody.

    I finally made a decorative coaster to keep the sometimes-wet jug off the counter:

    Printed Fragment Coaster 165mm - in use
    Printed Fragment Coaster 165mm – in use

    This used several of the larger smashed glass fragments from the collection:

    Fragments 165mm square - scan sample
    Fragments 165mm square – scan sample

    They all fit inside a 165 mm square, with the conformal perimeter disguising the outline:

    Printed Fragment Coaster 165mm - overview
    Printed Fragment Coaster 165mm – overview

    I printed the frame with the same blue PETG-CF that leaked epoxy the last time around. Using the correct filament setting (Extrusion Multiplier = 1.0) produced an epoxy-tight frame:

    Printed Fragment Coaster 165mm - epoxy filling
    Printed Fragment Coaster 165mm – epoxy filling

    The overall process:

    • Run a bead of epoxy around the edge of each recess
    • Fill in the center with a thin layer
    • Squish the metallized paper reflector in place starting from one end to ease the bubbles out
    • Cover the reflector with another layer of epoxy
    • Lay the glass fragment down starting at one end
    • Press gently down to get all the bubbles out
    • Cover the glass with more epoxy

    I dripped enough epoxy on each fragment to form a meniscus without having it go over the rim:

    Printed Fragment Coaster 165mm - epoxy meniscus
    Printed Fragment Coaster 165mm – epoxy meniscus

    The Basement Shop temperature is just over 60 °F, so I put a heating pad in a huge ziplock bag, laid an aluminum sheet atop it as a heat spreader, put some waxed paper on the aluminum just in case, then did the filling described above:

    Printed Fragment Coaster 165mm - warming setup
    Printed Fragment Coaster 165mm – warming setup

    A cardboard box on top helped the heating pad keep the coaster at a uniform 85 °F, slightly warmer than the epoxy instructions recommend, but it cured overnight with a wonderfully shiny surface.

    Now that I have the process down, making glittery coasters is surprisingly easy.

  • PolyDryer Humidity: White PETG

    PolyDryer Humidity: White PETG

    The white PETG filament started out at 39 %RH and 50 g of silica gel dragged it down to 23 %RH after a three days: still unusually high.

    The beads weighed 54.6 g, a weight gain of 9 %, which is about as much as they’ll take. I replaced them with 50 g of new-from-the-bottle beads and the meter dropped to 14 %RH overnight.

    Running the tiny fan for another day made no difference:

    Polydryer Box desiccant tray - fan
    Polydryer Box desiccant tray – fan

    Thereby confirming my suspicion that air circulation inside the box isn’t nearly as much of a problem as I expected.

    So filament need not arrive bone-dry and, with enough surface area exposed to the air, silica gel beads can adsorb their limit of water vapor in a day or two.

  • PolyDryer Humidity: November

    PolyDryer Humidity: November

    The measurements:

    2025-11-042025-11-112025-11-19
    Filament%RH%RHWeight – gWt gain – gGain %%RH
    PETG White393927.42.49.6%23
    PETG Black252626.71.76.8%14
    PETG Orange302326.51.56.0%23
    PETG Blue182327.02.08.0%10
    PETG-CF Blue252526.51.56.0%18
    PETG-CF Black222226.31.35.2%14
    PETG-CF Gray282826.51.56.0%18
    Empty → PETG Clear31n/a26.81.87.2%18
    TPU – Clear282926.31.35.2%14
    W empty → TPU – K232726.81.87.2%18

    All the boxes now have filament spools and 50 g of silica gel divided equally between the humidity meter and the tray in the bottom of the box:

    Polydryer Box desiccant tray - installed
    Polydryer Box desiccant tray – installed

    The PETG White in the first row is the new spool loaded last month. I think the 39 %RH indicates the spools do not necessarily arrive bone-dry in their vacuum-sealed bags with a tiny desiccant packet.

    Conversely, both the PETG Clear and TPU K filaments are new spools that seem reasonably dry out of their bags.

    The auto-rewind spindle in the PETG Orange filament hasn’t been working quite right, so I opened the box a few times. It now has a new PETG-CF spindle.

  • Bird Feeder Tray Mount

    Bird Feeder Tray Mount

    The mixed flock attending the bird feeder in the back yard scatters enough seeds to attract the deer, so I added a tray underneath to catch the overspray:

    Bird Feeder Tray Mount - installed
    Bird Feeder Tray Mount – installed

    Well, two trays, because it took a couple of iterations to make the solid model match reality:

    Bird Feeder Tray Mount - show layout
    Bird Feeder Tray Mount – show layout

    The n-1 iteration was Close Enough™ and two trays are obviously better than one.

    The “trays” are stray lids from the six gallon buckets we use for many purposes, including root-cellaring the vegetable garden harvest. The lid’s solid model was straightforward:

    Bird Feeder Tray Mount - lid model
    Bird Feeder Tray Mount – lid model

    Removing the lid from a solid block produces the most complex part of the mount:

    Bird Feeder Tray Mount - mount layout
    Bird Feeder Tray Mount – mount layout

    An aluminum plate on the outside (the gray slab in the overall view above) distributes the strain from the two M6 screws across the block.

    A smaller block on the inside of the lid has a pair of square nuts:

    Bird Feeder Tray Mount - segment layout
    Bird Feeder Tray Mount – segment layout

    All three parts build from their flattest side:

    Bird Feeder Tray Mount - build layout
    Bird Feeder Tray Mount – build layout

    The downward facing clamp arch in the main block didn’t need support, but the square nut sockets in the segment definitely came out better with little support blocks inside; PrusaSlicer does a good job with most support structures.

    The n-1 iteration used M6 rivnuts that were slightly too long after making the lid model match reality, so I switched to square nuts. The OpenSCAD code calculates the segment block length to match the actual screws, but 75 mm M6 screws and square nuts are barely long enough.

    I clamped the outer block to the lid as a drill guide for the first hole, then pinned the block with a screw to ensure it didn’t slip while drilling the second hole:

    Bird Feeder Tray Mount - drilling setup
    Bird Feeder Tray Mount – drilling setup

    Those were freehanded in the drill press at low speed with serious concentration; some things you just gotta do that way.

    The mixed flock overwhelmingly approves the trays, to the extent a dozen birds clamor to use them: definitely a crowd-pleaser!

    I’m certain you can buy pole-mounted trays, but what’s the fun in that?

    The OpenSCAD source code as a GitHub Gist:

    // Bird feeder tray mount
    // Ed Nisley – KE4ZNU
    // 2025-11-06
    include <BOSL2/std.scad>
    Layout = "Show"; // [Build,Show,Lid,Mount,Segment,Nut]
    /* [Hidden] */
    ID = 0;
    OD = 1;
    LENGTH = 2;
    HoleWindage = [0.2,0.2,0.2];
    Protrusion = 0.1;
    NumRibs = 12; // stiffening ribs
    NumSides = 8*NumRibs;
    $fn=NumSides;
    Gap = 5.0;
    WallThick = 9.0; // robust walls
    Kerf = 1.0; // clamp cut
    TapeThick = 0.5; // wrap around pole
    LidOD = 12; // main diameter in inches
    PoleOD = 1*INCH;
    PlateThick = 2.0; // backing plate for clamp
    Screw = [6.0,12.0,6.0]; // thread OD, washerOD, head
    ScrewLength = 75.0;
    ScrewOC = 60.0; // chosen to clear stiffening ribs in lid
    LidLayers = [ // bottom to top, ID = 0 means solid disk, LENGTH = exterior measurement
    [0,(LidOD-2*(3/8))*INCH,Protrusion], // 0 – below zero to prevent Z fighting
    [0,(LidOD-2*(3/8))*INCH,(3/8)*INCH], // 1 – base inside bucket
    [0,(LidOD+2*(1/8))*INCH,(1/8)*INCH], // 2 – flange
    [(LidOD-2*(1/2))*INCH,LidOD*INCH,(7/8)*INCH], // 3 – sealing ring
    ];
    LidOAH = LidLayers[1][LENGTH] + LidLayers[2][LENGTH] + LidLayers[3][LENGTH];
    LidTopDepth = (3/4)*INCH; // from highest part of interior
    MountBlockWidth = ScrewOC + 2*WallThick;
    BaseSagitta = LidLayers[1][OD]/2 – sqrt((LidLayers[1][OD]/2)^2 – (MountBlockWidth^2)/4);
    echo(BaseSagitta=BaseSagitta);
    PoleOffset = BaseSagitta + ((LidLayers[2][OD] – LidLayers[1][OD])/2) + WallThick + PoleOD/2;
    MountBlock = [PoleOffset + PoleOD/2 + WallThick – PlateThick,MountBlockWidth,LidOAH];
    echo(MountBlock=MountBlock);
    SegBlockOffset = ScrewLength – MountBlock.x – PlateThick; // assumes recessed
    SegmentBlock = [2*SegBlockOffset,MountBlock.y,LidTopDepth];
    Rib = [2*6.0,5.0,LidTopDepth]; // lid stiffening ribs
    RibAlign = 0 * 180/NumRibs; // position ribs wrt mount
    EdgeRadius = 3.0;
    //—–
    // Rivnut
    // The model collects all the magic numbers right here
    /*
    RivnutOAL = 15.0;
    module Rivnut() {
    union() {
    cyl(1.6,d=13.0,circum=true,anchor=BOTTOM);
    cyl(RivnutOAL,d=9.0,circum=true,anchor=BOTTOM);
    }
    }
    */
    //—–
    // Square nut
    // The model collects all the magic numbers right here
    NutOAL = 5.0;
    module SquareNut() {
    cuboid([10.0,10.0,5.0],anchor=BOTTOM);
    }
    //—–
    // Bucket lid
    // Centered at XY=0, Z=0 at top of exterior flange
    module BucketLid(Interior=true,Expand=false) {
    render()
    union() {
    down(LidLayers[2][LENGTH])
    cyl(LidLayers[1][LENGTH],d=LidLayers[1][OD],anchor=TOP);
    cyl(LidLayers[2][LENGTH],d=LidLayers[2][OD],anchor=TOP);
    if (Interior) {
    if (false)
    down(Expand ? Protrusion : 0)
    tube(LidLayers[3][LENGTH] + (Expand ? 2*Protrusion : 0),
    id=LidLayers[3][ID],od=(Expand ? 2 : 1)*LidLayers[3][OD],anchor=BOTTOM);
    else
    difference() {
    cyl(LidLayers[3][LENGTH] + (Expand ? 2*Protrusion : 0),
    d=(Expand ? 2 : 1)*LidLayers[3][OD],anchor=BOTTOM);
    up(LidLayers[3][LENGTH] – LidTopDepth)
    cyl(LidTopDepth + (Expand ? 2*Protrusion : 0),
    d=LidLayers[3][ID],anchor=BOTTOM);
    }
    up(LidLayers[3][LENGTH] – LidTopDepth)
    for (i=[0:(NumRibs – 1)])
    zrot(i*360/NumRibs + RibAlign)
    right(LidLayers[3][ID]/2)
    cuboid(Rib,anchor=BOTTOM,rounding=1,edges="Z");
    }
    else
    down(Expand ? Protrusion : 0)
    cyl(LidLayers[3][LENGTH] + (Expand ? 2*Protrusion : 0),
    d=(Expand ? 2 : 1)*LidLayers[3][OD],anchor=BOTTOM);
    }
    }
    // Mount clamp
    module Mount() {
    render()
    difference() {
    cuboid(MountBlock,anchor=BOTTOM+LEFT,rounding=EdgeRadius,edges="X");
    left(LidLayers[1][OD]/2 – BaseSagitta)
    up(LidLayers[1][LENGTH] + LidLayers[2][LENGTH])
    BucketLid(Interior=false);
    right(PoleOffset) {
    cyl(3*MountBlock.z,d=(PoleOD + HoleWindage.x + 2*TapeThick),circum=true,anchor=CENTER);
    cuboid([Kerf,2*MountBlock.y,3*MountBlock.z]);
    }
    if (false)
    right(MountBlock.x – PlateThick)
    cuboid(3*[PlateThick,MountBlock.y,MountBlock.z],anchor=LEFT);
    up(LidOAH – LidLayers[3][LENGTH]/2)
    for (j=[-1,1])
    fwd(j*ScrewOC/2) {
    cyl(ScrewLength,d=Screw[ID] + HoleWindage.x,circum=true,orient=RIGHT,anchor=BOTTOM,$fn=6,spin=180/6);
    if (false)
    right(MountBlock.x + Protrusion)
    cyl(Screw[LENGTH] + Protrusion,d=Screw[OD] + HoleWindage.x,circum=true,
    orient=LEFT,anchor=BOTTOM,$fn=12,spin=180/12);
    }
    }
    }
    // Nut block segment inside lid
    module NutSegment() {
    render()
    difference() {
    cuboid(SegmentBlock,anchor=BOTTOM,rounding=EdgeRadius,edges="X");
    down(LidLayers[3][LENGTH] – LidTopDepth)
    left(LidLayers[1][OD]/2 – BaseSagitta)
    BucketLid(Interior=true,Expand=true);
    up(LidTopDepth – LidLayers[3][LENGTH]/2)
    for (j=[-1,1])
    fwd(j*ScrewOC/2) {
    left(SegmentBlock.x/2)
    cyl(ScrewLength,d=Screw[ID],circum=true,anchor=BOTTOM,$fn=6,spin=180/6,orient=RIGHT);
    left(SegmentBlock.x/2)
    yrot(90)
    SquareNut();
    }
    }
    }
    //—–
    // Build things
    if (Layout == "Lid")
    BucketLid();
    if (Layout == "Mount")
    Mount();
    if (Layout == "Segment")
    NutSegment();
    if (Layout == "Nut")
    Rivnut();
    if (Layout == "Show") {
    down(LidLayers[1][LENGTH] + LidLayers[2][LENGTH]) {
    Mount();
    color("Orange",0.5)
    up(LidOAH – LidLayers[3][LENGTH]/2)
    right(MountBlock.x + PlateThick)
    for (j=[-1,1])
    fwd(j*ScrewOC/2)
    cyl(ScrewLength,d=Screw[ID],circum=true,orient=LEFT,anchor=BOTTOM);
    }
    up(LidLayers[3][LENGTH] – LidTopDepth)
    NutSegment();
    color("Gray",0.4)
    right(PoleOffset)
    cylinder(3*MountBlock.z,d=(PoleOD),anchor=CENTER);
    color("Gray",0.4)
    left(LidLayers[1][OD]/2 – BaseSagitta)
    BucketLid();
    color("White",0.7)
    down(LidLayers[1][LENGTH] + LidLayers[2][LENGTH])
    right(MountBlock.x + 2*PlateThick)
    difference() {
    cuboid([PlateThick,MountBlock.y,MountBlock.z],anchor=BOTTOM+LEFT,rounding=EdgeRadius,edges="X");
    up(LidOAH – LidLayers[3][LENGTH]/2)
    for (j=[-1,1])
    fwd(j*ScrewOC/2)
    cyl(ScrewLength,d=Screw[ID],circum=true,orient=RIGHT,anchor=CENTER);
    }
    }
    if (Layout == "Build") {
    render()
    union() {
    difference() {
    left(MountBlock.z + Gap/2)
    up(PoleOffset – Kerf/2)
    yrot(90)
    Mount();
    cuboid([3*MountBlock.z,2*MountBlock.y,3*MountBlock.x],anchor=TOP);
    }
    render()
    right(Gap/2)
    intersection() {
    up(MountBlock.x)
    yrot(90)
    Mount();
    up(MountBlock.x – PoleOffset)
    right(MountBlock.z/2)
    cuboid([2*MountBlock.z,2*MountBlock.y,MountBlock.x],anchor=TOP);
    }
    right(2*MountBlock.z – BaseSagitta)
    up(SegmentBlock.x/2)
    yrot(-90)
    NutSegment();
    }
    }