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

  • Makergear M2: Slic3r Configuration

    What you see here represents a stake in the ground, rather than the be-all and end-all configuration. Remember that my intent is to get the M2 working with its more-or-less stock hardware and firmware, make some straightforward improvements, then transition to LinuxCNC for better control and measurement.

    While printing a variety of test objects (which I’ll describe shortly), I casually permuted the temperatures, speeds, and timings to gradually improve the results. In nearly all cases, the M2 performs much better than my old and highly modified Thing-O-Matic ever did, so the machinery lives up to its reputation.

    Slic3r seems to be nearly as good at slicing as Skeinforge 50 and much faster, although it doesn’t handle very thin walls quite right and produces bizarre speed glitches on (or near?) layers with bridges. I wasn’t taking notes during any of this, which means you should regard it as hearsay evidence at best, but, on the whole, slic3r seems to work just fine for the non-pathological objects I generally build.

    This is the Slic3r config.ini file, which (I think) contains all of the configuration information now distributed throughout the smaller files controlled by slic3r.ini:

    # generated by Slic3r 0.9.8 on Tue Apr  9 08:43:22 2013
    bed_size = 190,250
    bed_temperature = 70
    bottom_solid_layers = 3
    bridge_fan_speed = 100
    bridge_flow_ratio = 1
    bridge_speed = 100
    brim_width = 0
    complete_objects = 0
    cooling = 1
    default_acceleration = 0
    disable_fan_first_layers = 3
    duplicate = 1
    duplicate_distance = 6
    duplicate_grid = 1,1
    end_gcode = ;-- Slic3r End G-Code for M2 starts --\n;  Ed Nisley KE4NZU - March 2013\nM104 S0        ; drop extruder tempeature\nM140 S0        ; drop bed temperature\nM106 S0        ; bed fan off\nG1 Z195    F2500    ; lower bed\nG1 X0 Y0 F30000    ; center nozzle\nM84         ; disable motors\n;-- Slic3r End G-Code ends --
    external_perimeter_speed = 75
    extra_perimeters = 1
    extruder_clearance_height = 20
    extruder_clearance_radius = 20
    extruder_offset = 0x0
    extrusion_axis = E
    extrusion_multiplier = 0.9
    extrusion_width = 0.40
    fan_always_on = 0
    fan_below_layer_time = 30
    filament_diameter = 1.70
    fill_angle = 45
    fill_density = 0.10
    fill_pattern = honeycomb
    first_layer_bed_temperature = 70
    first_layer_extrusion_width = 0
    first_layer_height = 100%
    first_layer_speed = 30
    first_layer_temperature = 165
    g0 = 0
    gap_fill_speed = 100
    gcode_arcs = 0
    gcode_comments = 0
    gcode_flavor = reprap
    infill_acceleration = 0
    infill_every_layers = 1
    infill_extruder = 1
    infill_extrusion_width = 0
    infill_speed = 200
    layer_gcode =
    layer_height = 0.25
    max_fan_speed = 100
    min_fan_speed = 50
    min_print_speed = 20
    min_skirt_length = 15
    notes =
    nozzle_diameter = 0.35
    only_retract_when_crossing_perimeters =
    output_filename_format = [input_filename_base].gcode
    perimeter_acceleration = 0
    perimeter_extruder = 1
    perimeter_extrusion_width = 0
    perimeter_speed = 100
    perimeters = 1
    post_process =
    print_center = 0,0
    randomize_start = 1
    retract_before_travel = 1
    retract_length = 1.0
    retract_length_toolchange = 5
    retract_lift = 0
    retract_restart_extra = 0
    retract_restart_extra_toolchange = 0
    retract_speed = 300
    rotate = 0
    scale = 1
    skirt_distance = 5
    skirt_height = 1
    skirts = 1
    slowdown_below_layer_time = 10
    small_perimeter_speed = 25
    solid_fill_pattern = concentric
    solid_infill_below_area = 70
    solid_infill_every_layers = 0
    solid_infill_speed = 100
    start_gcode = ;-- Slic3r Start G-Code for M2 starts --\n;  Ed Nisley KE4NZU - March 2013\nM140 S[first_layer_bed_temperature]    ; start bed heating\nG90                ; absolute coordinates\nG21                ; millimeters\nM83                ; relative extrusion distance\nM84                ; disable stepper current\nG4 S3            ; allow Z stage to freefall to the floor\nG28 X0            ; home X\nG92 X-95        ; set origin to 0 = center of plate\nG1 X0 F30000    ; origin = clear clamps on Y\nG28 Y0            ; home Y\nG92 Y-125         ; set origin to 0 = center of plate\nG1 Y-122 F30000    ; set up for prime near front edge\nG28 Z0            ; home Z\nG92 Z1.0            ; set origin to measured z offset\nM190 S[first_layer_bed_temperature]    ; wait for bed to finish heating\nM109 S[first_layer_temperature]    ; set extruder temperature and wait\nG1 Z0.0 F2500    ; plug extruder on plate\nG1 E10 F300        ; prime to get pressure\nG1 Z5 F2500        ; rise above blob\nG1 Y-115 F30000    ; move away\nG1 Z0.0 F2500    ; dab nozzle to remove outer snot\nG4 P1            ; pause to clear\nG1 Z0.1            ; clear bed for travel\n;-- Slic3r Start G-Code ends --
    support_material = 0
    support_material_angle = 0
    support_material_extruder = 1
    support_material_extrusion_width = 0
    support_material_pattern = rectilinear
    support_material_spacing = 2.5
    support_material_speed = 100
    support_material_threshold = 0
    temperature = 165
    threads = 2
    toolchange_gcode =
    top_solid_infill_speed = 50
    top_solid_layers = 3
    travel_speed = 500
    use_relative_e_distances = 0
    vibration_limit = 0
    z_offset = 0
    
  • Makergear M2: Marlin Configuration Tweaks

    The slightly customized version of Marlin shipped with the M2 works well enough, but some of the constants required adjustment.

    Most of the changes appear in the Configuration.h file…

    Start by tweaking the version info so you know what’s in the Flash ROM every time it starts up:

    #define STRING_VERSION_CONFIG_H "2013-03-27" //Personal revision number for changes to THIS file.
    #define STRING_CONFIG_H_AUTHOR "Ed Nisley - KE4ZNU" //Who made the changes.
    

    I reduced the maximum temperatures to match the Makergear-defined limits, even though those are far beyond what I expect to use. I’ll probably cut them back even further, but they’ll do for now:

    // When temperature exceeds max temp, your heater will be switched off.
    // This feature exists to protect your hotend from overheating accidentally, but *NOT* from thermistor short/failure!
    // You should use MINTEMP for thermistor short/failure protection.
    // Ed Nisley KE4ZNU - 26 March 2013 - reduce to M2 limit
    #define HEATER_0_MAXTEMP 230
    #define HEATER_1_MAXTEMP 230
    #define HEATER_2_MAXTEMP 230
    #define BED_MAXTEMP 125
    

    Long ago, I settled on a much lower extrusion temperature for ABS in the Thing-O-Matic than nearly everyone else and that’s also holding true for PLA in the M2, so I reduced the minimum allowable temperature limit from 170 °C to 120 °C. The firmware seems to use a “less-than-or-equal” test, so it prevents extrusion at exactly 120 °C. Close enough:

    // M2 - reduce to allow much cooler PLA extrusion
    // KE4ZNU - 24 March 2013
    //#define EXTRUDE_MINTEMP 170
    #define EXTRUDE_MINTEMP 120
    

    Before I added the shim around the Z axis leadscrew bearing, the default homing speed excited a howling mechanical resonance. Increasing the homing speed moved the vibration away from the resonance, but the real cure was to reduce the motor current, which eliminated the four dead spots per full step:

    // KE4ZNU - 24 March 2013 - M2 - Goose Z feedrate to avoid resonance
    //#define HOMING_FEEDRATE {50*60, 50*60, 4*60, 0}  // set the homing speeds (mm/min)
    #define HOMING_FEEDRATE {50*60, 50*60, 6*60, 0}  // set the homing speeds (mm/min)
    

    Dan Newman’s comments to the post about the Z axis performance calculations suggest some rationalization should happen among all the maximum speed and acceleration settings; they appear to be quite inconsistent right now. This will take a bit of measurement, but I think substantive measurements & changes must wait until I get the LinuxCNC controller running.

    The block of constants for the motor currents includes some misleading comments. I added the RAMBo design equations, which will become invalid when a board iteration changes either the supply voltage or the sense resistor value, and a bit of explanatory text.

    The extruder current seemed slightly low, as it would skip steps while infilling large bottom layers. This happened before I lowered the extruder temperature, so the nominal value may be right on the edge of goodness. The new value of 1.5 A heats the motor to about 120 °C, which is higher than I’d like for something attached to a plastic mount, but I anticipate some changes there in the near future.

    I increased the XY motor currents to 1.5 A in anticipation of using higher speeds, although that wasn’t based on any evidence. The motors now run at about 120 °C, which is OK because they’re attached to solid metal parts (albeit without heatsink compound).

    As described earlier, reducing the Z motor current to 0.6 A from 1.1 A didn’t materially affect the maximum torque, dramatically smoothed the motion, and slightly reduced the temperature. It still runs at nearly 130 °C, despite heatsinking to the chassis, and is in line for replacement.

    // Motor Current setting (Only functional when motor current pins are connected to digipot)
    // Values 0-255
    // RAMBO 135 = ~0.75A, 185 = ~1A
    // Ed Nisley KE4ZNU - 25 March 2013 - increase XY current to 1.5 A (185 was 135)
    //		decrease Z current to nominal 600 mA (75 was 135) based on 19 V / 28 ohm winding
    //		value = 255 * (0.8 * Imax) / 1.66 V
    // Ed Nisley KE4ZNU - 27 March 2013 - increase E current to 1.5 A (185 was 165)
    //		to support 300 mm/s XY extrusion speed
    #define DIGIPOT_MOTOR_CURRENT
    #define X_CURRENT 185
    #define Y_CURRENT 185
    #define Z_CURRENT 75
    #define E0_CURRENT 185 //For MakerGear M2, 165 is a good starting point
    #define E1_CURRENT 125
    

    The only change to Configuration_adv.h increases the stepper timeout to allow the build platform to reach operating temperature; the default value shut off the motors just before printing started. The value obviously depends on the start and end temperatures, so more testing is in order:

    //default stepper release if idle
    // Ed Nisley KE4ZNU - 25 March 2013 - make deactivate timeout exceed plate heating time
    #define DEFAULT_STEPPER_DEACTIVE_TIME 400
    
  • Makergear M2 vs. LinuxCNC: Project Overview

    M2 - cushwa Owl - half scale
    M2 – cushwa Owl – half scale

    During the course of my Makerbot Thing-O-Matic experience, I concluded:

    • Enthusiasm may get a product out, but engineering makes it work
    • Plywood and plastic do not produce a stable 3D printer
    • Measurements matter
    • 8-bit microcontrollers belong in the dustbin of history

    With that in mind, I’ve long thought that LinuxCNC (formerly EMC2) would provide a much better basis for the control software required for a 3D printer than the current crop of Arduino-based microcontrollers. LinuxCNC provides:

    • Hard real time motion control with proven performance
    • A robust, well-defined hardware interface layer
    • Ladder-logic machine control
    • Isolated userspace programming
    • Access to a complete Linux distro’s wealth of programs / utilities
    • Access to an x86 PC’s wealth of hardware gadgetry

    Rather than (try to) force-fit new functions in an Arduino microcontroller, I decided it would be interesting to retrofit a DIY 3D printer with a LinuxCNC controller, improve the basic hardware control and sensing, instrument the extruder, then take measurements that might shed some light on DIY 3D printing’s current shortcomings.

    The overall plan looks like this:

    • Start with a Makergear M2
    • See what the stock hardware can do
    • Replace the RAMBo controller with LinuxCNC
    • See what the hardware can do with better drivers
    • Adapt the G-Code / M-Code processing to use more-or-less stock Marlin G-Code
    • Add useful controllers along the lines of the Joggy Thing
    • Improve the platform height / level sensing
    • Rebuild the extruder with temperature and force sensors
    • Start taking measurements!

    My reasons for choosing the Makergear M2 as the basis for this project should be obvious:

    • All metal: no plywood, no acrylic (albeit a plastic filament drive)
    • Decent stepper motors (with one notable exception)
    • Reasonable hot end design
    • Good reputation

    The first step of the overall plan included a meticulously documented M2 build that I figured would take a month or two, what with the usual snafus and gotchas that accompany building any complex mechanism. Quite by coincidence, a huge box arrived on my birthday (the Thing-O-Matic arrived on Christmas Eve, so perhaps this is a tradition), the day when I learned that Mad Phil had entered his final weeks of life.

    As the Yiddish proverb puts it: If you wish to hear G*d laugh, tell him of your plans.

    So I converted a box of parts into a functional M2 3D printer over the course of four intense days, alternating between our living room floor and a card table in Phil’s home office, showing him how things worked, getting his advice & suggestions, and swapping “Do you remember when?” stories. Another few days sufficed for software installation, configuration, and basic tuneup; I managed to show him some shiny plastic doodads just before he departed consensus reality; as nearly as I can tell, we both benefited from the distractions.

    Which means I don’t have many pictures or much documentation of the in-process tweakage that produced a functional printer. The next week or so of posts should cover the key points in enough detail to be useful.

    Not to spoil the plot or anything: a stock M2 works wonderfully well.

    Owl - half size - left
    Owl – half size – left

    For example, a half-scale cushwa owl printed in PLA at 165 °C with no bed cooling and these Slic3r parameters:

    • 500 mm/s move
    • 300 mm/s infill
    • 200 mm/s solid infill
    • 100 mm/s internal perimeter
    • 50 mm/s bottom layer
    • 30 mm/s external perimeter
    • 1 mm retract @ 300 mm/s

    The beak came out slightly droopy and each downward-pointing feather dangles a glittery drop. There’s room for improvement, but that’s pretty good a week after opening a box o’ parts…

  • LibreOffice RegEx Backreferences

    I needed to replace all the ordinary spaces between numeric values and their units (as in 3.5 V) with non-breaking spaces. LibreOffice Writer implements regular expression searches, but their notion of marking and replacing references trips me up every time. This part of the Fine Manual describing how parenthesized targets work will come in handy again:

    In the Search for box:

    Defines the characters inside the parentheses as a reference. You can then refer to the first reference in the current expression with “\1”, to the second reference with “\2”, and so on.

    For example, if your text contains the number 13487889 and you search using the regular expression (8)7\1\1, “8788” is found.

    You can also use () to group terms, for example, “a(bc)?d” finds “ad” or “abcd”.

    In the Replace with box:

    Use $ (dollar) instead of \ (backslash) to replace references. Use $0 to replace the whole found string.

    As nearly as I can tell, there is no escape sequence that denotes a non-breaking space, so I had to manually enter one using Shift+Ctrl+Spacebar, copy it, and paste it into the replacement text string.

    The search-and-replace dialog looked like this:

    LibreOffice RegEx Backreferences
    LibreOffice RegEx Backreferences

    Yes, you can search for strings inside parentheses, use parentheses to mark references, and then jam references in the replacement. This makes my head hurt every time: programming as an experimental science…

  • Broom Handle Screw Thread: Replacement Plug

    Somehow, we wound up with a broom handle and a broom head, the former missing a threaded stub that was firmly lodged in the latter. A few minutes of Quality Shop Time sawed off the end of the handle and unscrewed the stub to produce this array of fragments:

    Broken broom handle thread
    Broken broom handle thread

    It’s a cylindrical Thing tailor-made for (or, back in the day, by!) a lathe. My lathe has quick-change gears that can actually cut a 5 TPI thread, but that seems like a lot of work for such a crude fitting. Instead, an hour or so of desk work produced this:

    Broom Handle Screw - solid model - overview
    Broom Handle Screw – solid model – overview

    Some after-the-fact search-fu revealed that the thread found on brooms and paint rollers is a 3/4-5 Acme. Machinery’s Handbook has 13 pages of data for various Acme screw threads, making a distinction between General Purpose Acme threads and Stub Acme Threads: GP thread depth = 0.5 × pitch, Stub = 0.3 × pitch. For a 5 TPI thread = 0.2 inch pitch, that’s GP = 0.1 inch vs. Stub = 0.06 inch.

    I measured a 5.0 mm pitch (which should be 5.08 mm = 0.2 inch exactly) and a crest-to-root depth of 1.4 mm = 0.055 inch, which makes them look like 3/4-5 Stub Acme threads. But, I didn’t know that at the time; a simple half-cylinder 2.5 mm wide and 1.25 mm tall was a pretty close match to what I saw on the broken plastic part.

    Although OpenSCAD’s MCAD library has some screw forms, they’re either machine screws with V threads or ball screws with spheres. The former obviously weren’t appropriate and the latter produced far too many facets, so I conjured up a simpler shape: 32 slightly overlapping cylinders per turn, sunk halfway in the shaft at their midpoint, and tilted at the thread’s helix angle.

    Broom Handle Screw - thread model closeup
    Broom Handle Screw – thread model closeup

    The OpenSCAD source code has a commented-out section that removes a similar shape from the shaft between the raised thread, but that brought the rendering to its knees. Fortunately, it turned out to be unnecessary, but it’s there if you want it.

    With the shaft diameter set to the “root diameter” of the thread and the other dimensions roughly matching the broken plastic bits, this emerged an hour later:

    Broom handle screw plug - as built
    Broom handle screw plug – as built

    The skirt thread was 0.25 to 0.30 mm thick, so the first-layer height tweak and packing density adjustments worked fine and all the dimensions came out perfectly. The cylindrical thread form doesn’t have much overhang and the threads came out fine; I think the correct straight-sided form would have more problems.

    The hole down the middle accommodates a 1/4-20 bolt that applies enough clamping force to keep the shaft in compression, which ought to prevent it from breaking in normal use. I intended to use a hex bolt, but found a carriage bolt that was exactly the right length and had a head exactly the same diameter as the shaft, so I heated it with a propane torch and mushed its square shank into the top of the hexagonal bolt hole (the source code now includes a square recess):

    Broom handle screw plug - in handle
    Broom handle screw plug – in handle

    The dimples on the side duplicate the method that secured the original plastic piece: four dents punched into the metal handle lock the plastic in place. It seems to work reasonably well, though, and is certainly less conspicuous than the screws I’d use.

    Screwing it in place shows that it’s slightly too long (I trimmed the length in the source code):

    Broom handle installed
    Broom handle installed

    It’s back in service, ready for use…

    The OpenSCAD source code:

    // Broom Handle Screw End Plug
    // Ed Nisley KE4ZNU March 2013
    
    // Extrusion parameters must match reality!
    //  Print with +1 shells and 3 solid layers
    
    ThreadThick = 0.25;
    ThreadWidth = 2.0 * ThreadThick;
    
    HoleWindage = 0.2;
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    Protrusion = 0.1;			// make holes end cleanly
    
    //----------------------
    // Dimensions
    
    PI = 3.14159265358979;
    
    PostOD = 22.3;              // post inside metal handle
    PostLength = 25.0;
    
    FlangeOD = 24.0;            // stop flange
    FlangeLength = 3.0;
    
    PitchDia = 15.5;            // thread center diameter
    ScrewLength = 20.0;
    
    ThreadFormOD = 2.5;         // diameter of thread form
    ThreadPitch = 5.0;
    
    BoltOD = 7.0;               // clears 1/4-20 bolt
    BoltSquare = 6.5;          	// across flats
    BoltHeadThick = 3.0;
    
    RecessDia = 6.0;			// recesss to secure post in handle
    
    OALength = PostLength + FlangeLength + ScrewLength; // excludes bolt head extension
    
    $fn=8*4;
    
    echo("Pitch dia: ",PitchDia);
    echo("Root dia: ",PitchDia - ThreadFormOD);
    echo("Crest dia: ",PitchDia + ThreadFormOD);
    
    //----------------------
    // Useful routines
    
    module Cyl_Thread(pitch,length,pitchdia,cyl_radius,resolution=32) {
    
    Cyl_Adjust = 1.25;                      // force overlap
    
        Turns = length/pitch;
        Slices = Turns*resolution;
        RotIncr = 1/resolution;
        PitchRad = pitchdia/2;
        ZIncr = length/Slices;
        helixangle = atan(pitch/(PI*pitchdia));
        cyl_len = Cyl_Adjust*(PI*pitchdia)/resolution;
    
        union() {
            for (i = [0:Slices-1]) {
                translate([PitchRad*cos(360*i/resolution),PitchRad*sin(360*i/resolution),i*ZIncr])
                    rotate([90+helixangle,0,360*i/resolution])
                        cylinder(r=cyl_radius,h=cyl_len,center=true,$fn=12);
            }
        }
    }
    
    module PolyCyl(Dia,Height,ForceSides=0) {			// based on nophead's polyholes
    
      Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    
      FixDia = Dia / cos(180/Sides);
    
      cylinder(r=(FixDia + HoleWindage)/2,
               h=Height,
    	   $fn=Sides);
    }
    
    module ShowPegGrid(Space = 10.0,Size = 1.0) {
    
      Range = floor(50 / Space);
    
    	for (x=[-Range:Range])
    	  for (y=[-Range:Range])
    		translate([x*Space,y*Space,Size/2])
    		  %cube(Size,center=true);
    
    }
    
    //-------------------
    // Build it...
    
    ShowPegGrid();
    
    difference() {
        union() {
            cylinder(r=PostOD/2,h=PostLength);
            cylinder(r=PitchDia/2,h=OALength);
            translate([0,0,PostLength])
                cylinder(r=FlangeOD/2,h=FlangeLength);
            translate([0,0,(PostLength + FlangeLength)])
                Cyl_Thread(ThreadPitch,(ScrewLength - ThreadFormOD/2),PitchDia,ThreadFormOD/2);
        }
    
        translate([0,0,-Protrusion])
            PolyCyl(BoltOD,(OALength + 2*Protrusion),6);
        translate([0,0,(OALength - BoltHeadThick)])
            PolyCyl(BoltSquare,(BoltHeadThick + Protrusion),4);
    
    //    translate([0,0,(PostLength + FlangeLength + ThreadFormOD)])
    //        Cyl_Thread(ThreadPitch,(ScrewLength - ThreadFormOD/2),PitchDia,ThreadFormOD/2);
    
    	for (i = [0:90:270]) {
    		rotate(i)
    			translate([PostOD/2,0,PostLength/2])
    				sphere(r=RecessDia/2,$fn=8);
    	}
    }
    
  • Windows Update: Updating the Updater

    This sort of thing happens with Free Software, too, but I prefer Microsoft’s phrasing…

    Windows 7 Update Update
    Windows 7 Update Update

    Let’s see… that’s various forms of “update” used as nouns, proper nouns, adjectives, and a gerund…

  • Upstart vs. NFS Mounts vs. LightDM: Success!

    That comment suggested a different solution to the problem of having the display manager start before the NFS mounts complete. When that happens, you can sign in and start programs that won’t have access to their data, producing all manner of heartache and confusion.

    One complication: it seems /etc/rc.local starts and runs before the network (among other tidbits) gets connected and becomes ready, which means you can’t just plunk your code in that file like you used to, at least not in Ubuntu. Fixing that requires an upstart script triggered when the network interface finally hauls itself to its feet.

    There’s no actual link between the NFS mount commands and the display manager startup, but it seems that if you don’t attempt to mount the NFS shares before the network becomes active (which is what happen with shares automounted through /etc/fstab), but wait for the network to come up and then issue the mounts, the shares mount almost instantly and become ready by the time the display manager presents the login screen. That’s better than the kludge I had figured out and works fine, so I’ll run with it until something else breaks.

    The not-quite-deterministic fix has three parts:

    • Use noauto in the fstab entries for the NFS shares
    • Create an upstart script to mount those shares after eth0 lights up
    • Allow lightdm to start up normally (i.e., remove my hackish attempts)

    A sample line from fstab, with the vital noauto option:

    oyster:/mnt/bulkdata	/mnt/bulkdata	nfs	noauto,noatime	0 0
    

    The /etc/init/local.conf script assumes the network interface will be eth0, which does not generalize to wireless networks on laptops and suchlike. You could add some Boolean logic to wait for the first of several interfaces, I suppose:

    description "Stuff that should be in /etc/rc.local"
    author "Ed Nisley - KE4ZNU"
    
    start on (local-filesystems and net-device-up IFACE=eth0)
    stop on shutdown
    script
    
    logger Starting local init...
    
    logger Mounting NFS filesystems
    mount /mnt/bulkdata
    mount /mnt/userfiles
    mount /mnt/diskimages
    mount /mnt/music
    
    logger Ending local init
    
    end script
    

    The lightdm.conf file reverts to the distribution version, with this starting trigger:

    start on ((filesystem
               and runlevel [!06]
               and started dbus
               and (drm-device-added card0 PRIMARY_DEVICE_FOR_DISPLAY=1
                    or stopped udev-fallback-graphics))
              or runlevel PREVLEVEL=S)
    

    It’s worth noting that the upstart interpreter hates comment lines embedded within statements: it does not regard them as whitespace and does not ignore them. Just don’t do it. That explains some of the problems I encountered before, but fixing those problems did not eliminate the overall issue.

    The end result of all that hocus-pocus makes the box boot the way it used to: the display manager comes up promptly, presents the GUI login screen, and the NFS mounts are ready when you are.