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

Making the world a better place, one piece at a time

  • Keychron C3 Pro Keyboard: Taming

    Keychron C3 Pro Keyboard: Taming

    Having set the Moonlander to use Auto Shift, I’ve come to depend on it, so I got a Keychron C3 Pro keyboard for one of the Basement Shop’s PCs because it glows in the dark and can be configured with QMK:

    Keychron C3 Pro - tamed
    Keychron C3 Pro – tamed

    The default setup has rainbow hues cycling across the keyboard, which I find entirely too distracting. Although you can manually select the solid-color variant from the myriad possibilities using the keyboard, I forced a solid color with this config.h file:

    #define RGB_MATRIX_DEFAULT_ON true // Sets the default enabled state, if none has been set
    #define RGB_MATRIX_DEFAULT_MODE RGB_MATRIX_SOLID_COLOR // Sets the default mode, if none has been set
    #define RGB_MATRIX_DEFAULT_HUE 36  // Sets the default hue value, if none has been set
    #define RGB_MATRIX_DEFAULT_SAT 255 // Sets the default saturation value, if none has been set
    #define RGB_MATRIX_DEFAULT_VAL 255 // Sets the default brightness value, if none has been set
    
    

    Enabling Auto Shift requires this rules.mk file:

    AUTO_SHIFT_ENABLE = yes
    

    Both of those go in the keymap directory defining the keyboard mapping for my custom setup:

    qmk_firmware/keyboards/keychron/c3_pro/ansi/rgb/keymaps/ednisley
    

    The keymap.c file remains unchanged, although I’m mildly tempted to toss the Mac layouts overboard.

    For the record, setting QMK to compile that keyboard configuration goes like this:

    qmk setup -H /base_directory/…/qmk_firmware
    <snippage>
    qmk new-keymap -kb keychron/c3_pro/ansi/rgb -km ednisley
    qmk config user.keyboard=keychron/c3_pro/ansi/rgb user.keymap=ednisley
    qmk compile
    qmk flash
    

    Flashing the keyboard firmware goes like this:

    • Run qmk flash
    • Unplug the USB cable
    • Hold down the Esc key
    • Plug in the USB cable
    • Release the Esc key

    I should boot the Atreus configuration into the current decade, but that’s for later.

  • Earplug Case

    Earplug Case

    A no-assembly-needed earplug case from Printables will be more easily found in Mary’s purse than the previous small bag:

    Earplug case
    Earplug case

    That’s the “grippy bits” version of the model, which really is easier to open than the straight-sided version.

    I printed a few more, loaded them with earplugs, and put them where they may come in handy. In retrospect, I should have used clear PETG to show off the retina-burn plugs.

    Living in the future is great!

  • Outlet Strip Bench Mount

    Outlet Strip Bench Mount

    A spate of tidying-up led to mounting an outlet strip along the back of a bench:

    Outlet Bench Mount - installed
    Outlet Bench Mount – installed

    Rather than drill holes into the top of the bench for those screws, they fit into M4 brass inserts heat-staked into the brackets:

    Outlet Bench Mount - show view
    Outlet Bench Mount – show view

    The holes for those inserts aren’t centered side-to-side on the brackets, because the screw holes aren’t centered on the bent-steel angles forming the outlet strip endplates.

    The bottom arm on the brackets probably isn’t necessary, but they kept the outlet strip from crawling away while I match-drilled two holes for the screws into the side of the benchtop.

    For obvious reasons, the brackets print on their sides:

    Outlet Bench Mount - build view
    Outlet Bench Mount – build view

    Another outlet strip from a different manufacturer is, of course, different, but changing three parameters in the OpenSCAD program summons a different bracket from the vasty digital deep:

    Outlet Bench Mount - different brand
    Outlet Bench Mount – different brand

    Parametric modeling and a 3D printer are exactly the right hammers for the job …

    The OpenSCAD source code as a GitHub Gist:

    // Shower soap dish
    // Ed Nisley – KE4ZNU
    // 2026-06-04
    include <BOSL2/std.scad>
    Layout = "Show"; // [Show,Build]
    /* [Hidden] */
    HoleWindage = 0.2;
    Protrusion = 0.1;
    NumSides = 3*3*4;
    Gap = 10.0/2;
    $fn=NumSides;
    ID = 0;
    OD = 1;
    LENGTH = 2;
    BenchThick = 21.0; // workbench top
    ScrewOD = 4.0; // into edge of bench
    Insert = [4.0,5.5,10.0]; // robust M4 insert
    WallThick = 10.0;
    BaseThick = 10.0;
    OutletBase = [15.0,40.0];
    HoleOffset = 6.5; // from outside edge of bracket
    HoleOC = 24.0;
    MountOA = [OutletBase.x,OutletBase.y,BenchThick + Insert[LENGTH] + 1.0 + BaseThick];
    //———-
    // Build it
    module Mount() {
    difference() {
    cuboid(MountOA,rounding=1.0,anchor=BOTTOM + BACK);
    up(BaseThick)
    fwd(WallThick)
    cuboid([2*MountOA.x,MountOA.y,BenchThick],anchor=BOTTOM + BACK);
    up(BaseThick + BenchThick/2) back(Protrusion)
    ycyl(OutletBase.y,d=ScrewOD,circum=true,$fn=6,anchor=BACK);
    for (j=[-1,1])
    fwd(MountOA.y/2 + j*HoleOC/2)
    right(HoleOffset – MountOA.x/2)
    up(MountOA.z + Protrusion)
    cyl(Insert[LENGTH],d=Insert[OD],circum=true,$fn=6,anchor=TOP);
    }
    }
    //———-
    // Build it
    if (Layout == "Show") {
    left(Gap + MountOA.x/2)
    Mount();
    right(Gap + MountOA.x/2)
    xflip() Mount(); // mirror for the other end of the outlet strip
    }
    if (Layout == "Build") {
    left(MountOA.z/2)
    up(MountOA.x/2)
    yrot(90)
    Mount();
    fwd(1.5*MountOA.y)
    left(MountOA.z/2 – BenchThick/2 – Insert[LENGTH]/2)
    zrot(180)
    up(MountOA.x/2)
    yrot(-90)
    xflip() Mount(); // mirror for the other end of the outlet strip
    }
  • Prusa MK4: Mesh Bed Leveling Temperature vs. 0.8 mm Ruby Durozzle

    Prusa MK4: Mesh Bed Leveling Temperature vs. 0.8 mm Ruby Durozzle

    After going through the ritual required to install the 0.8 mm nozzle and preload the filament, the hot end looks like this before installing the silicone sock:

    Prusa MK4 hot end - 0.8 mm Durozzle ruby - front
    Prusa MK4 hot end – 0.8 mm Durozzle ruby – front

    The aluminum block doesn’t look nearly as awful as these pictures suggest; those plastic smears serve as reminders of a few previous printing mishaps.

    The nozzle is a 0.8 mm Durozzle with a ruby tip suitable for abrasive filaments like PETG-CF, although this is gooey squishy “natural” TPU:

    Prusa MK4 hot end - 0.8 mm Durozzle ruby - bottom
    Prusa MK4 hot end – 0.8 mm Durozzle ruby – bottom

    The first patio table foot test piece in TPU had terrible adhesion to the Textured Sheet, which I eventually tracked down to an excessively thick first layer. Given that the MK4 homes the axes and performs mesh bed leveling probes over the build area, this was difficult to believe, particularly because it had never been a problem with the Prusa 0.4 mm ObXidian hardened steel nozzle.

    More poking around showed some of the plastic drool left on the outside of the nozzle from the previous print session (as shown in the two pictures above) could remain hardened or at least “not squishy” despite the nozzle being heated before homing and mesh probing. Because probing depends on having the nozzle touch the platform, anything between the nozzle and the steel sheet will raise the Z=0 position and cause all the layers to be too high.

    As far as I can tell, ruby has a thermal coefficient around 40 W/m·K, roughly the same as steel. Both are considerably lower than the 200-ish W/m·K for the aluminum block surrounding the nozzle tube, suggesting most of whatever temperature gradient there may be occurs between the heater and the nozzle, not in the nozzle.

    While puzzling that out, I noticed the nozzle heated to only 160 °C prior to homing and probing, which seemed low for a filament calling for 230 to 250 °C during printing. Ordinary PETG heated to 170 °C, so something was different.

    More puzzling showed the Start G-Code section of the printer’s Custom G-Code sets the home / probe temperature, herein reformatted for readability:

    M140 S[first_layer_bed_temperature] ; set bed temp
    
    M104 T0 S{((filament_notes[0]=~/.*MBL160.*/) ? 160 : 
    (filament_notes[0]=~/.*HT_MBL10.*/) ? (first_layer_temperature[0] - 10) : 
    (filament_type[0] == "PC" or filament_type[0] == "PA") ? (first_layer_temperature[0] - 25) : 
    (filament_type[0] == "FLEX") ? 210 : 
    (filament_type[0]=~/.*PET.*/) ? 175 : 
    170)} ; set extruder temp for bed leveling
    
    M109 T0 R{((filament_notes[0]=~/.*MBL160.*/) ? 160 : 
    (filament_notes[0]=~/.*HT_MBL10.*/) ? (first_layer_temperature[0] - 10) : 
    (filament_type[0] == "PC" or filament_type[0] == "PA") ? (first_layer_temperature[0] - 25) : 
    (filament_type[0] == "FLEX") ? 210 : 
    (filament_type[0]=~/.*PET.*/) ? 175 : 
    170)} ; wait for temp
    
    

    The bursts of line noise after the M104 Set Extruder Temperature and M109 Set Extruder Temperature and Wait commands consist of nested ternary operators sifting placeholder variables defined in other parts of the slicer configuration.

    I had set up the eSun TPU 95A filament parameters based on Prusa’s TPU definition. I eventually discovered that definition includes the text MBL160 in its Notes section, which satisfies the regex in the first ternary operator:

    (filament_notes[0]=~/.*MBL160.*/) ? 160 : … snippage …
    

    Which then emitted the M104 T0 S160 and M109 R160 commands into the G-Code to set the temperature.

    After considerably more flailing around while figuring this out, I changed the filament Notes to read:

    HT_MBL10 -- force higher probe temperature for Durozzle ruby nozzle
    mbl160 -- disabled by lowercase
    

    Which then falls through to the regex in the second ternary operator:

    (filament_notes[0]=~/.*HT_MBL10.*/) ? (first_layer_temperature[0] - 10) : 
    

    Which sets the temperature to 10 °C below the first layer temperature, which I had set to 230 °C, so the probing now occurs at 220 °C.

    I am not making this up.

    Although that may be a bit too hot, the drool on the nozzle softens nicely and smashes flat during probing, thus solving the immediate problem and, without further ado, produced good round and square TPU feet.

  • Translucent vs. Transparent PETG Soap Dishes

    Translucent vs. Transparent PETG Soap Dishes

    In addition to printing bendy objects with TPU, the 0.8 mm nozzle 3D-prints PETG into thin walls with better transparency than the default 0.4 mm nozzle:

    Clear PETG - 0.4 vs 0.8 mm nozzle - side view
    Clear PETG – 0.4 vs 0.8 mm nozzle – side view

    The wall is now 1.0 mm thick, rather than 0.6 mm, and is much closer to being transparent. Those gray links from the RPi camera mount inside the dishes help show the difference.

    The 2.0 mm thick base plate is also more transparent, but mostly just reveals the 0.4 mm thick infill layers:

    Clear PETG - 0.4 vs 0.8 mm nozzle - top view
    Clear PETG – 0.4 vs 0.8 mm nozzle – top view

    More study is needed, even if we already have far more soap dishes than strictly necessary.

  • HQ Sixteen: Needle Bar Reorientation

    HQ Sixteen: Needle Bar Reorientation

    The original needle bar orientation for Mary’s Handiquilter HQ Sixteen put the needle clamp screw (a black-oxide socket head cap screw with the end flattened) about 45° from the rear of the needle bar:

    HQ Sixteen - original needle foot orientation
    HQ Sixteen – original needle foot orientation

    The hex driver passes through the sight hole letting you verify the needle is inserted all the way into the holder before tightening the screw.

    It turns out needles fitting the HQ Sixteen come in two varieties, both with nominal 2.0 mm shanks. Mary’s stock has slightly different and entirely consistent diameters around their eyeballometric typical value:

    • Round shank = 1.94 mm (-0.00 / +0.02 mm)
    • Flatted shank = 2.04 mm (-0.02 / +0.04 mm)

    The round shank needles fit easily into the needle holder, but most of the flatted needles simply would not go in. The difference felt like a burr somewhere inside the bore, rather than a uniformly too-small bore: a burr is easy to imagine around the threaded hole for the lock screw.

    Orienting a round-shank needle is exceedingly fiddly, because the groove above the thread hole must be aligned exactly to the front of the needle bar to mesh properly with the bobbin mechanism, but snugging the screw invariably rotates the shank.

    While you might think the locking screw would properly orient flatted-shank needles by tightening on the flat, you would be wrong. The flat is at the back of the machine when the groove and hole are properly oriented, which means the locking screw bears on the rounded part of the needle, right at the edge of the flat. Mary was generally unable to use even the few flatted needles that fit into the needle bar, because tightening the screw tended to grab the flat, rotate the needle, and lock it firmly in the wrong orientation.

    It is worth nothing that all of the other machines around here have locking screws arranged exactly as you’d expect: tightening the screw onto the shaft flat correctly aligns the needle with zero fiddling.

    Pictures of various HQ Sixteen machines found on the InterWebs show their needle bar and locking screw can be oriented anywhere from nearly in front to entirely in the back, suggesting:

    • Whoever aligns those machines doesn’t care about needle orientation
    • Everybody uses round-shank needles
    • Anybody using flatted-shank needles is an outlier

    I suggested rotating the needle bar to put the screw in back and, if possible, remove the burr inside the bore. After considerable discussion, my plan was approved.

    The needle bar slides vertically in a machined block, driven by a link attached to the machine’s main shaft:

    HQ Sixteen Handi-feet conversion - foot rod clamp
    HQ Sixteen Handi-feet conversion – foot rod clamp

    The surface of the needle rod has a yellow / amber color from the slick coating that must not be disturbed, to the extent the maintenance instructions require a plastic-lined clamp for adjustments.

    The vertical position of the needle rod in the clamp determines the “timing” of the needle with respect to the hook on the whirling bobbin case where the magic happens. Setting the timing requires a Special Service Tool that I do not have and likely never will, so the vertical position must not change while rotating the rod in the clamp.

    So, we begin.

    Removing the machine cover requires removing the Control Pod electronics box with all its cables to get access to the last screw, so this is a nontrivial operation.

    Position the shaft at Bottom Dead Center, then measure the distance from the ruler foot to the needle plate:

    HQ Sixteen - Ruler foot clearance
    HQ Sixteen – Ruler foot clearance

    The correct distance is 0.5 mm and the taper gauge shows it at 0.6 mm, but all I need here is putting it back at the same height after I remove the foot.

    Position the shaft exactly at Top Dead Center (as shown in the second picture), then stack gage blocks under the needle bar as shown in the top picture. For reference, the gauge block set showing which blocks went into that stack:

    HQ Sixteen - gage blocks used
    HQ Sixteen – gage blocks used

    Although I didn’t need the absolute measurement, it’s 0.551 inch = 0.300 + 0.150 + 0.101 inch = 13.995 mm. It’s less than 0.552 inch = 14.021; I decided fiddling with the fourth decimal place would be counterproductive.

    With the needle bar held at that height, stick a screwdriver through the hole intended for this purpose and loosen the clamp screw:

    HQ Sixteen - needle bar clamp
    HQ Sixteen – needle bar clamp

    Yes, the hole is slightly misaligned with the screw, presumably because aligning it properly would put the hole too close to the edge of the frame casting for comfortable drilling. You could make this adjustment without removing the cover, but I’m not that type of guy.

    Rotate the needle bar to put the locking screw exactly at the back, verify the bottom of the bar rests on the gauge blocks, tighten the clamp screw, and verify the bottom of the bar rests on the gauge blocks:

    HQ Sixteen - needle bar reoriented
    HQ Sixteen – needle bar reoriented

    Again, the hex driver shows the observation hole orientation.

    Acceptance testing requires a practice quilt, but the machine lights up properly and moves smoothly with a needle in place, so it’s pretty close to being correct.

    This was one of those jobs requiring about two hours of setup, twenty seconds of adjustment, and half an hour of put-away.

  • Bike Rack Tray Holder: Stretchy Tiedown Straps

    Bike Rack Tray Holder: Stretchy Tiedown Straps

    The tray holder on Mary’s bike worked well:

    Bike Rack Tray Holder - in use
    Bike Rack Tray Holder – in use

    Except for having the bungee cord run across the middle of the tray where it blocks access for larger trays and tends to bend the taller leaves.

    Well, I can fix that:

    Bike Rack Tray Holder - straps - rear
    Bike Rack Tray Holder – straps – rear

    The front tiedown is similar:

    Bike Rack Tray Holder - straps - front
    Bike Rack Tray Holder – straps – front

    They’re printed from TPU: rectangular blocks and chains, ending in wire hooks bashed from a coat hanger. The M4 button-head screws thread into (uncrushed) rivnuts, which seemed easier to manage than square nuts in this situation.

    The chains are just thick circles, with half of the top links sunk into the blocks:

    Stretchy Straps - build layout
    Stretchy Straps – build layout

    You’d (well, I’d) want to build them one at a time, because sometimes this happens:

    Bike Rack Tray Holder - bad platform adhesion
    Bike Rack Tray Holder – bad platform adhesion

    Based on those measurements, I raised the extruder by 0.1 mm, but apparently did a poor job of cleaning / flattening the cold TPU on the nozzle and got it wrong. As a result, the first layer didn’t get squooshed properly onto the BuildTak, came unstuck, and produced art . The track down the middle of the photo shows traces of a previous, badly over-squooshed test chain.

    The stretched TPU relaxes enough to leave very little tension after a day, as shown by the unhooked right chain:

    Bike Rack Tray Holder - straps - relaxing
    Bike Rack Tray Holder – straps – relaxing

    However, that make the chains exactly the right length, so they require even more force to get the hooks off the rack. After relaxing for another day, the stretched chains return to roughly their original lengths, so it’s all good.

    The OpenSCAD source code as a GitHub Gist:

    // TPU Tiedown Straps for bike rack tray holder
    // Ed Nisley – KE4ZNU
    // 2026-05-14
    include <BOSL2/std.scad>
    Layout = "Build"; // [Show,Build,Chain,Blocks,Front,Rear]
    /* [Hidden] */
    HoleWindage = 0.2;
    Protrusion = 0.01;
    NumSides = 4*3*2*4;
    Gap = 5.0;
    $fn=NumSides;
    LinkID = 7.0;
    LinkOD = 10.0;
    LinkOC = 14.0;
    LinkHeight = 4.0;
    JointWidth = 2.0;
    FrontChainAngle = 30; // from vertical
    FrontChainLength = 80.0; // nominal length
    RearChainAngle = 20; // from vertical
    RearChainLength = 100.0; // nominal length
    BlockOA = [80.0,12.0,15.0];
    InsertOC = 30.0;
    //—–
    // Define things
    module Chain(n=2) {
    render()
    difference() {
    union() {
    hull() {
    cyl(LinkHeight,d=JointWidth,anchor=BOTTOM,rounding=0.0);
    back((n – 1)*LinkOC)
    cyl(LinkHeight,d=JointWidth,anchor=BOTTOM,rounding=0.0);
    }
    for (i = [0:n-1])
    back(i*LinkOC)
    cyl(LinkHeight,d=LinkOD,anchor=BOTTOM,rounding=0.0);
    }
    for (i = [0:n-1])
    back(i*LinkOC)
    down(Protrusion)
    cyl(LinkHeight + 2*Protrusion,d=(LinkID + HoleWindage),anchor=BOTTOM,rounding=-1.0);
    }
    }
    module FrontBlock() {
    difference() {
    cuboid(BlockOA,anchor=BOTTOM,chamfer=1.0,except=BACK);
    for (i = [-1:1])
    right(i*InsertOC) down(Protrusion) {
    cyl(BlockOA.z + 2*Protrusion,d=4.0 + HoleWindage,anchor=BOTTOM); // screw clearance
    cyl(1.5,d=9.0,anchor=BOTTOM); // insert head
    cyl(11.0,d=6.0,anchor=BOTTOM); // insert body
    }
    }
    }
    module RearBlock() {
    up(BlockOA.z/2) fwd(BlockOA.y/2)
    difference() {
    cuboid(BlockOA,anchor=FRONT,chamfer=1.0,except=BACK);
    for (i = [-1:1])
    right(i*InsertOC) fwd(Protrusion) {
    ycyl(BlockOA.z + 2*Protrusion,d=4.0 + HoleWindage,anchor=FRONT); // screw clearance
    ycyl(1.5,d=9.0,anchor=FRONT); // insert head
    ycyl(11.0,d=6.0,anchor=FRONT); // insert body
    }
    }
    }
    module FrontAssembly(cl=FrontChainLength,ca=FrontChainAngle) {
    Links = ceil(cl / LinkOC);
    union() {
    up(cl*cos(ca)) {
    FrontBlock();
    back(BlockOA.y/2)
    xrot(90)
    for (i = [-1,1])
    left(i*InsertOC/2)
    zrot(-i*ca + 180)
    Chain(Links);
    }
    }
    }
    module RearAssembly(cl=RearChainLength,ca=RearChainAngle) {
    Links = ceil(cl / LinkOC);
    union() {
    up(cl*cos(ca)) {
    RearBlock();
    back(BlockOA.y/2)
    xrot(90)
    for (i = [-1,1])
    left(i*InsertOC/2)
    zrot(-i*ca + 180)
    Chain(Links);
    }
    }
    }
    //—–
    // Build things
    if (Layout == "Chain")
    Chain();
    if (Layout == "Blocks") {
    fwd(BlockOA.y)
    FrontBlock();
    back(BlockOA.y)
    RearBlock();
    }
    if (Layout == "Front")
    FrontAssembly();
    if (Layout == "Rear")
    RearAssembly();
    if (Layout == "Show") {
    fwd(BlockOA.y)
    FrontAssembly();
    back(BlockOA.y)
    zrot(180)
    RearAssembly();
    }
    if (Layout == "Build") {
    fwd(BlockOA.z + Gap/2)
    up(BlockOA.y/2)
    xrot(-90)
    down(FrontChainLength*cos(FrontChainAngle))
    FrontAssembly();
    back(BlockOA.z + Gap/2)
    zrot(180)
    up(BlockOA.y/2)
    xrot(-90)
    down(RearChainLength*cos(RearChainAngle))
    RearAssembly();
    }