The Smell of Molten Projects in the Morning

Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.

Category: Software

General-purpose computers doing something specific

  • Software Testing, Lack Thereof

    Software Testing, Lack Thereof

    Not what you want to see when trying to extract a month’s worth of data from your blood pressure meter:

    Omron - Bluetooth sync error message
    Omron – Bluetooth sync error message

    Perhaps the format string should be $1$s or %1%s, not a mixture?

    It’s typical for Omron to issue two updates in quick succession after months of no activity, so programming by successive approximation may be a thing.

  • Sherline vs. LinuxCNC 2.9.8

    Sherline vs. LinuxCNC 2.9.8

    Notes on finally getting the Sherline CNC mill operating in its new home, with a suitable Axis startup image:

    Sherline setup 2026-06
    Sherline setup 2026-06

    The gray countertop from its former home sits on foam strips soaking up a slight warp with enough isolation to keep things quiet.

    The gantry required the usual fiddling to make the cable hoist the Z axis directly upward, with the orange flag on the counterweight barely visible below the monitor.

    I recently touched the box of precision XYZ positioners and there might be something useful, albeit grossly overqualified, in there to simplify dropping the laser pointer beam directly through the spindle bore.

    A clean installation of LinuxCNC 2.9.8 on an ancient Dell Optiplex 9020 proceeded smoothly. Installing x11vnc let the rest of the proceedings happen from upstairs in the Comfy Chair. For unknown reasons, vinagre works better than Reminna as a VNC client, after recalling F11 enters / exits fullscreen mode.

    The mesaflash utility accompanying LinuxCNC 2.9.8 did not recognize the Mesa 6i25 card. Fetching & compiling the most recent version (3.5.17) cleared that hump and flashed the bitmap:

    sudo mesaflash --verbose --device 5i25 --write 5i25/configs/hostmot2/5i25_prob_rfx2.bit
    

    The 6i25 wants to be known as a 5i25, with its jumpers in their default positions:

    Mesa 6i25 - jumper locations
    Mesa 6i25 – jumper locations

    For unknown reasons, the button originally known as btn-trigger became btn-joystick for a while and has now reverted to btn-trigger. It’s labeled 1 in the four-button cluster:

    Logitech Dual-Action Gamepad - relabeled
    Logitech Dual-Action Gamepad – relabeled

    Which required changing the pin name in the Kicad library component:

    Sherline HAL schematic - Logitech button 1 name
    Sherline HAL schematic – Logitech button 1 name

    Which required converting the old Kicad library format into the new Kicad library format, a completely automatic process without, AFAICT, any unpleasant side effects.

    The new name fed into the schematic as expected, after minor fumbling while re-adding the modified component and setting its annotation number:

    Sherline HAL schematic - Logitech AZ button logic
    Sherline HAL schematic – Logitech AZ button logic

    The X axis home microswitch has (apparently) become more bouncy while it was idle, so I increased the number of samples before HAL sees a change in that GPIO input:

    Sherline HAL schematic - home switch debounce
    Sherline HAL schematic – home switch debounce

    The dbounce block runs in the servo thread at 1 m per tick, so those 20 samples take all of 20 ms while the X axis moves 0.15 mm. At some point I should apply a scope to that switch, but for now It Just Works™.

    Considerably to my surprise, compiling the modified Kicad schematic into a HAL file proceeded without incident, despite various Python updates in the last five years.

    Kicad produces an “intermediate XML file” containing the netlist data intended for a conversion / export program, which is basically what my Kicad-to-HAL lashup does. I told Kicad to use Bash’s true command as a converter:

    Sherline HAL schematic - netlist export
    Sherline HAL schematic – netlist export

    So I can run the Kicad-to-HAL converter manually:

    python ../Kicad\ Conversion/Kicad-to-HAL.py Sherline\ HAL\ -\ Logitech\ Gamepad\ jogging.xml Sherline.hal
    

    You could tell Kicad to run it and it would probably Just Work™, but I’m used to peering at the results and dinking with my program.

    Which produced a new Sherline.hal file that Just Worked™ with the existing Sherline.ini file containing the configuration constants.

    The Sherline has cut only air so far, but, as the man said, “E pur si muove.”

  • Photo Backdrop: Crossbar Improvement

    Photo Backdrop: Crossbar Improvement

    The instructions for the Photo Backdrop frame (upon which we hang Mary’s quilts for photos) suggest the crossbar fits on like this:

    Photo Backdrop - OEM crossbar installation
    Photo Backdrop – OEM crossbar installation

    The slot in the bottom is wider than the M10 stud, so the crossbar tends to flop around while assembling it overhead. I immediately replaced the wingnut with a chunky knob for better griptitude, but was never happy with how poorly the whole thing fit together.

    This is dramatically better:

    Photo Backdrop - fitting installed
    Photo Backdrop – fitting installed

    The crossbar is upside-down from the OEM instructions, but the bottom of the gray plug holds the tube firmly to the tripod while the nut seats firmly on the plug’s flat top:

    Photo Backdrop - fitting top view
    Photo Backdrop – fitting top view

    A snippet of 3M 300LSE adhesive sheet holds the plug in place, so that’s one less thing to fiddle with on each end.

    The solid model holds no surprises:

    Photo Backdrop Fittings - solid model
    Photo Backdrop Fittings – solid model

    Of course, it builds with the flat end downward.

    The OpenSCAD source code as a GitHub Gist:

    // Photo backdrop fitting
    // Ed Nisley – KE4ZNU
    // 2026-07-01
    include <BOSL2/std.scad>
    Layout = "Show"; // [Build,Show]
    /* [Hidden] */
    ID = 0;
    OD = 1;
    LENGTH = 2;
    HoleWindage = 0.2;
    Protrusion = 0.01;
    NumSides = 3*2*4;
    Clearance = 0.3;
    $fn=NumSides;
    Tube = [23.0 – HoleWindage,1*INCH,100.0]; // arbitrary length
    Aperture = [24.0,15.0,100.0]; // oblong nut hole, arbitrary Z
    Washer = [10.5,20.0,1.5]; // M10
    //—–
    // Define things
    module Fitting() {
    difference() {
    union() {
    intersection() {
    xcyl(Tube[LENGTH],d=Tube[ID]);
    cuboid(Aperture,rounding=Aperture.y/2,edges="Z");
    }
    cuboid([Aperture.x,Aperture.y,Tube[OD]/2],rounding=Aperture.y/2,edges="Z",anchor=BOTTOM);
    }
    cyl(Aperture.z,d=Washer[ID]);
    }
    }
    //—–
    // Build things
    if (Layout == "Show") {
    Fitting();
    }
    if (Layout == "Build") {
    up(Tube[OD]/2)
    xrot(180)
    Fitting();
    }

  • 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.

  • 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.

  • Prusa MK4: Nozzle Change Checklist

    Prusa MK4: Nozzle Change Checklist

    Both the round and square TPU patio table feet came from a 0.8 mm nozzle on the Prusa MK4, which produces results much faster than the venerable Makergear M2’s 0.35 mm nozzle. However, for unknown reasons a 0.8 mm nozzle is not compatible with the MMU3, so changing from and to the default 0.4 mm nozzle requires a somewhat complex ritual.

    For context, the MK4 extruder and hot end:

    Prusa MK4 - extruder overview
    Prusa MK4 – extruder overview

    Because the MK4 automatically unloads the filament from the extruder (with help from auto-retracting filament spools) when using the MMU, the hot end doesn’t have any filament in it. Disconnect the PTFE tube from the fitting atop the extruder, insert the end of the TPU filament from its Polydryer box, and …

    Change to 0.8 mm nozzle:

    • Remove silicone sock from hot end
    • Install fixture to hold the hot end in place
    • Loosen the two knobs clamping the nozzle
    • Loosen nozzle with 7 mm socket wrench
    • Unscrew & remove nozzle by hand
    • Install new nozzle by hand
    • Tighten nozzle with wrench
    • Tighten those two knobs
    • Remove fixture
    • Install silicone sock

    Change MK4 settings using the LCD panel:

    • SettingsMMU = Off
    • SettingsHardwarePrinthead = 0.8 mm

    Then, with the TPU filament poked into the top of the extruder:

    • FilamentLoad Filament =FLEX

    You’ll want to extrude a few lengths just to settle everything in place.

    Switching back to the 0.4 mm nozzle proceeds in the opposite direction, starting with:

    • FilamentUnload Filament

    Something of a nuisance, but not unbearable.