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

Pimping and using a Mostly Printed CNC Machine

  • GCMC Guilloche Plot Generator

    It turns out the Spirograph patterns I’d been using to wring out the MPCNC are also known as Guilloché, perhaps after the guy who invented a lathe-turning machine to engrave them. Sounds pretentious, but they still look nice:

    Guilloche 591991062 - scanned
    Guilloche 591991062 – scanned

    With the ballpoint pen / knife collet holder in mind, I stripped the tool changes out of the Spirograph generator GCMC source code, set the “paper size” to a convenient 100 mm square, and tidied up the code a bit.

    As with Spirograph patterns, changing the random number seed produces entirely different results. A collection differing in the last digit, previewed online:

    Seed = 213478836:

    Guilloche 213478836
    Guilloche 213478836

    Seed = 213478837:

    Guilloche 213478837
    Guilloche 213478837

    Seed = 213478838:

    Guilloche 213478838
    Guilloche 213478838

    Seed = 213478839:

    Guilloche 213478839
    Guilloche 213478839

    They’re such unique snowflakes …

    The Bash script now accepts a single parameter to force the PRNG seed to a value you presumably want to plot again, rather than just accept whatever the Gods of Cosmic Jest will pick for you.

    The GCMC source code and Bash (or whatever) script feeding it as a GitHub Gist:

    // Spirograph simulator for MPCNC used as plotter
    // Ed Nisley KE4ZNU – 2017-12-23
    // Adapted for Guillioche plots with ball point pens – 2018-09-25
    // Spirograph equations:
    // https://en.wikipedia.org/wiki/Spirograph
    // Loosely based on GCMC cycloids.gcmc demo:
    // https://gitlab.com/gcmc/gcmc/tree/master/example/cycloids.gcmc
    // Required command line parameters:
    // -D Pen=n pen selection, 0 = no legend, 1 = current pen
    // -D PaperSize=[x,y] overall sheet size: [17in,11in]
    // -D PRNG_Seed=i non-zero random number seed
    include("tracepath.inc.gcmc");
    include("engrave.inc.gcmc");
    //—–
    // Greatest Common Divisor
    // https://en.wikipedia.org/wiki/Greatest_common_divisor#Using_Euclid's_algorithm
    // Inputs = integers without units
    function gcd(a,b) {
    if (!isnone(a) || isfloat(a) || !isnone(b) || isfloat(b)) {
    warning("GCD params must be dimensionless integers:",a,b);
    }
    local d = 0; // power-of-two counter
    while (!((a | b) & 1)) { // remove and tally common factors of two
    a >>= 1;
    b >>= 1;
    d++;
    }
    while (a != b) {
    if (!(a & 1)) {a >>= 1;} // discard non-common factor of 2
    elif (!(b & 1)) {b >>= 1;} // … likewise
    elif (a > b) {a = (a – b) >> 1;} // gcd(a,b) also divides a-b
    else {b = (b – a) >> 1;} // … likewise
    }
    local GCD = a*(1 << d); // form gcd
    // message("GCD: ",GCD);
    return GCD;
    }
    //—–
    // Max and min functions
    function max(x,y) {
    return (x > y) ? x : y;
    }
    function min(x,y) {
    return (x < y) ? x : y;
    }
    //—–
    // Pseudo-random number generator
    // Based on xorshift:
    // https://en.wikipedia.org/wiki/Xorshift
    // http://www.jstatsoft.org/v08/i14/paper
    // Requires initial state from calling script
    // -D "PRNG_Seed=whatever"
    // You can get nine reasonably random digits from $(date +%N)
    function XORshift() {
    local x = PRNG_State;
    x ^= x << 13;
    x ^= x >> 17;
    x ^= x << 5;
    PRNG_State = x;
    return x;
    }
    //—–
    // Spirograph tooth counts mooched from:
    // http://nathanfriend.io/inspirograph/
    // Stators includes both inside and outside counts, because we're not fussy
    Stators = [96, 105, 144, 150];
    Rotors = [24, 30, 32, 36, 40, 45, 48, 50, 52, 56, 60, 63, 64, 72, 75, 80, 84];
    //—–
    // Start drawing things
    // Set initial parameters from command line!
    comment("PRNG seed: ",PRNG_Seed);
    PRNG_State = PRNG_Seed;
    // Define some useful constants
    AngleStep = 2.0deg;
    Margins = [12mm,12mm] * 2;
    comment("Paper size: ",PaperSize);
    PlotSize = PaperSize – Margins;
    comment("PlotSize: ",PlotSize);
    //—–
    // Set up gearing
    s = (XORshift() & 0xffff) % count(Stators);
    StatorTeeth = Stators[s];
    comment("Stator ",s,": ",StatorTeeth);
    r = (XORshift() & 0xffff) % count(Rotors);
    RotorTeeth = Rotors[r];
    comment("Rotor ",r,": ",RotorTeeth);
    GCD = gcd(StatorTeeth,RotorTeeth); // reduce teeth to ratio of least integers
    comment("GCD: ",GCD);
    StatorN = StatorTeeth / GCD;
    RotorM = RotorTeeth / GCD;
    L = to_float((XORshift() & 0x3ff) – 512) / 100.0; // normalized pen offset in rotor
    comment("Offset: ", L);
    sgn = sign((XORshift() & 0xff) – 128);
    K = sgn*to_float(RotorM) / to_float(StatorN); // normalized rotor dia
    comment("Dia ratio: ",K);
    Lobes = StatorN; // having removed all common factors
    Turns = RotorM;
    comment("Lobes: ", Lobes);
    comment("Turns: ", Turns);
    //—–
    // Crank out a list of points in normalized coordinates
    Path = {};
    Xmax = 0.0;
    Xmin = 0.0;
    Ymax = 0.0;
    Ymin = 0.0;
    for (a = 0.0deg ; a <= Turns*360deg ; a += AngleStep) {
    x = (1 – K)*cos(a) + L*K*cos(a*(1 – K)/K);
    if (x > Xmax) {Xmax = x;}
    elif (x < Xmin) {Xmin = x;}
    y = (1 – K)*sin(a) – L*K*sin(a*(1 – K)/K);
    if (y > Ymax) {Ymax = y;}
    elif (y < Ymin) {Ymin = y;}
    Path += {[x,y]};
    }
    //message("Max X: ", Xmax, " Y: ", Ymax);
    //message("Min X: ", Xmin, " Y: ", Ymin); // min will always be negative
    Xmax = max(Xmax,-Xmin); // odd lobes can cause min != max
    Ymax = max(Ymax,-Ymin); // … need really truly absolute maximum
    //—–
    // Scale points to actual plot size
    PlotScale = [PlotSize.x / (2*Xmax), PlotSize.y / (2*Ymax)];
    comment("Plot scale: ", PlotScale);
    Points = scale(Path,PlotScale); // fill page, origin at center
    //—–
    // Start drawing lines
    feedrate(1500.0mm);
    TravelZ = 1.5mm;
    PlotZ = -1.0mm;
    //—–
    // Box the outline for camera alignment
    goto([-,-,TravelZ]);
    goto([-PaperSize.x/2,-PaperSize.y/2,-]);
    goto([-,-,PlotZ]);
    foreach ( {[-1,1], [1,1], [1,-1], [-1,-1]} ; pt) {
    move([pt.x*PaperSize.x/2,pt.y*PaperSize.y/2,-]);
    }
    goto([-,-,TravelZ]);
    //—–
    // Draw the plot
    tracepath(Points, PlotZ);
    //—–
    // Add legends
    // … only for nonzero Pen
    if (Pen) {
    feedrate(250mm);
    TextFont = FONT_HSANS_1_RS;
    TextSize = [2.5mm,2.5mm];
    TextLeading = 1.5; // line spacing as multiple of nominal text height
    line1 = typeset("Seed: " + PRNG_Seed + " Stator: " + StatorTeeth + " Rotor: " + RotorTeeth,TextFont);
    line2 = typeset("Offset: " + L + " GCD: " + GCD + " Lobes: " + Lobes + " Turns: " + Turns,TextFont);
    maxlength = TextSize.x * max(line1[-1].x,line2[-1].x);
    textpath = line1 + (line2 – [-, TextLeading, -]); // undef – n -> undef to preserve coordinates
    textorg = [-maxlength/2,PaperSize.y/2 – 0*Margins.y/2 – 2*TextLeading*TextSize.y/2];
    placepath = scale(textpath,TextSize) + textorg;
    comment("Legend begins");
    engrave(placepath,TravelZ,PlotZ);
    attrpath = typeset("Ed Nisley – KE4ZNU – softsolder.com",TextFont);
    attrorg = [-(TextSize.x * attrpath[-1].x)/2,-(PaperSize.y/2 – Margins.y/2 + 2*TextLeading*TextSize.y)];
    placepath = scale(attrpath,TextSize) + attrorg;
    comment("Attribution begins");
    engrave(placepath,TravelZ,PlotZ);
    }
    goto([PaperSize.x/2,PaperSize.y/2,25.0mm]); // done, so get out of the way
    view raw Guilloche.gcmc hosted with ❤ by GitHub
    # Guilloche G-Code Generator
    # Ed Nisley KE4ZNU – September 2018
    Paper='PaperSize=[100mm,100mm]'
    Flags="-P 2"
    LibPath="-I /opt/gcmc/library"
    Spirograph='/mnt/bulkdata/Project Files/Mostly Printed CNC/Patterns/Guilloche.gcmc'
    Prolog="/home/ed/.config/gcmc/prolog.gcmc"
    Epilog="/home/ed/.config/gcmc/epilog.gcmc"
    ts=$(date +%Y%m%d-%H%M%S)
    if [ -n "$1" ] # if parameter
    then
    rnd=$1 # .. use it
    else
    rnd=$(date +%N) # .. otherwise use nanoseconds
    fi
    fn='Guilloche_'${ts}_$rnd'.ngc'
    echo Output: $fn
    p=1
    rm -f $fn
    echo "(File: "$fn")" > $fn
    #cat $Prolog >> $fn
    gcmc -D Pen=$p -D $Paper -D PRNG_Seed=$rnd $Flags $LibPath -G $Prolog -g $Epilog "$Spirograph" >> $fn
    #cat $Epilog >> $fn
    view raw guilloche.sh hosted with ❤ by GitHub
  • MPCNC: Modified Drag Knife Adapter Spring Constant

    The bars on the original MPCNC drag knife / plotter pen adapter had a 100 g/mm spring constant:

    MPCNC - Plotter pen force test
    MPCNC – Plotter pen force test

    Making the bars slightly thicker improved their print-ability:

    MPCNC knife adapter mods - OpenSCAD model
    MPCNC knife adapter mods – OpenSCAD model

    The reddish tint marks the new bars, with their location carefully tweaked to be coincident with the stock STL.

    Shoving the pen into the scale with 0.1 mm steps produces another unnervingly linear plot:

    Modified MPCNC pen adapter - Spring Constant data
    Modified MPCNC pen adapter – Spring Constant data

    Real plotter pens want about 20 g of force, so this isn’t the holder you’re looking for.

    A bunch of plots at Z=-1.0 mm turned out well with the ballpoint pen insert, though:

    MPCNC Modifed pen adapter - first plots
    MPCNC Modifed pen adapter – first plots

    The globs apparently come from plotting too fast for conditions; reducing the speed to 1500 mm/min works better.

  • MPCNC: Modified Drag Knife Adapter

    A trio of Cutter Cutting Plotter Blade Holders arrived:

    Collet pen holder
    Collet pen holder

    Despite the name, they’re not well-suited for drag knife blades, because they’re collets gripping a 2 mm shaft. The blade doesn’t rotate unless the plotter / cutter rotates the entire holder, which is actually a thing.

    I got ’em because the snout of a common ball-point pen refill measures about 2 mm:

    Collet pen holder - detail
    Collet pen holder – detail

    The glob around the tip comes from plotting too fast for conditions; about 1500 mm/min works better for continuous lines and 250 mm/min improves text.

    The stock MPCNC adapter has a single recess suited for Genuine Plotter Pens, but the knurled lock ring on these cheapies sticks out far enough to make them wobbly. This being an inconvenience up with which I need not put, a few lines of OpenSCAD tweak the stock STL:

    MPCNC knife adapter mods - OpenSCAD model
    MPCNC knife adapter mods – OpenSCAD model

    The original STL is ivory, new cuts are cyan, and additions are reddish.

    The two support beams are now 1.6 mm = four thread widths, for improved slicing with a 0.35 mm nozzle and a higher spring constant.

    It’s by-and-large indistinguishable from the old adapter:

    MPCNC - Pen Holder Detail
    MPCNC – Pen Holder Detail

    Which I was using upside-down, because the flange fit better.

    The MPCNC works reasonably well as a pen plotter with a genuine ballpoint pen:

    MPCNC Ballpoint pen plots
    MPCNC Ballpoint pen plots

    The OpenSCAD source code as a GitHub Gist:

    // Adding clearance for eBay collet pen holder
    MPCNC_OD = 12.0; // pen holder OD (matches STL curvature)
    MPCNC_Z = 8.9; // Z offset of pen axis
    Pen_OD = 11.5; // actual pen body OD
    ID = 0;
    OD = 1;
    LENGTH = 2;
    Flange = [11.5,15.7,2.2]; // actual pen body flange
    Locknut = [16.0,16.0,2.8]; // knurled locknut
    Locknut_Offset = 4.5; // flange center to locknut
    Wall = [41.0,4 * 0.4,9.0]; // thicker walls for more spring and better fill
    $fn = 32; // default cylinder sides
    difference() {
    translate([-(101.3 + MPCNC_OD/2),-111.9,0]) // put pen axis above Y axis, flange centered on X axis
    import("/mnt/bulkdata/Project Files/Mostly Printed CNC/Accessories/Tool Holders/MPCNC_525_Drag_Knife-1860310.STL",
    convexity=5);
    if (true) // improve holder-to-mount fit if needed
    translate([0,60/2,8.90])
    rotate([90,0,0])
    cylinder(d=MPCNC_OD,h=60);
    translate([0,0,MPCNC_Z]) // improve flange slot clearance
    rotate([90,0,0])
    cylinder(d=Flange[OD] + 1.0,h=Flange[LENGTH] + 0.5,center=true);
    translate([0,Locknut_Offset – 0.5,MPCNC_Z]) // add locknut clearance
    rotate([-90,0,0])
    cylinder(d=Locknut[OD] + 1.5,h=Locknut[LENGTH] + 1.5,center=false);
    }
    # translate([-(27.0 + Wall.x/2),0,0]) { // embiggen walls for higher spring constant
    translate([0,-24.4,0])
    cube(Wall);
    translate([0,20.6 – Wall.y,0])
    cube(Wall);
    }
  • MPCNC: Re-Relocated Probe Camera

    Although the camera doesn’t hit anything, it seemed entirely too exposed out in front:

    MPCNC - relocated camera - front view
    MPCNC – relocated camera – front view

    So I moved it to the back, where I can’t see it and maybe won’t clobber it:

    MPCNC Re-Relocated USB Camera
    MPCNC Re-Relocated USB Camera

    The camera sensor is now almost exactly aligned with the XY axes, so the goofy rotation is gone and the offsets look better:

    bCNC - Rear-mount Camera Probe Config
    bCNC – Rear-mount Camera Probe Config

    The size of the “10 mm” inner circle at the crosshair depends on the target distance, so it’ll be smaller for surfaces clamped onto and thus rising above the table. Depending on how much that matters, I can tweak the camera focus and scale factor to make the answer come out right.

    The setup at the home position looked like this from a different perspective:

    MPCNC - Rear-mounted USB Camera
    MPCNC – Rear-mounted USB Camera

    No operational change, just a cleanup.

  • MPCNC: Tweaked GRBL Config

    These GRBL configuration constants seem to work well with the DW660 router in the MPCNC gantry:

    $$
    $0=10
    $1=255
    $2=0
    $3=2
    $4=0
    $5=0
    $6=0
    $10=1
    $11=0.010
    $12=0.002
    $13=0
    $20=1
    $21=1
    $22=1
    $23=0
    $24=500.000
    $25=2500.000
    $26=250
    $27=3.000
    $30=30000
    $31=0
    $32=0
    $100=100.000
    $101=100.000
    $102=400.000
    $110=8000.000
    $111=8000.000
    $112=3000.000
    $120=2000.000
    $121=2000.000
    $122=2000.000
    $130=635.000
    $131=465.000
    $132=103.000
    —–
    $n
    $N0=F150
    $N1=G10L2P1X-633Y-463Z-3
    —–
    $#
    [G54:-633.000,-463.000,-3.000]
    [G55:0.000,0.000,0.000]
    [G56:0.000,0.000,0.000]
    [G57:0.000,0.000,0.000]
    [G58:0.000,0.000,0.000]
    [G59:0.000,0.000,0.000]
    [G28:-418.670,-282.016,-3.000]
    [G30:-628.000,-3.000,-3.000]
    [G92:0.000,0.000,0.000]
    [TLO:0.000]
    [PRB:0.000,0.000,0.000:0]
    view raw MPCNC-GRBL.cfg hosted with ❤ by GitHub

    The overall XY travel is slightly smaller than the initial configuration, because the router sticks out further than the penholder I’d been using. Increasing the $27 Homing Pulloff distance to 3 mm leaves a comfortable space beyond the limit switches after homing to the positive end:

    MPCNC - X-axis endstop - home
    MPCNC – X-axis endstop – home

    Adjusting the $13[01] XY travel distances and switch positions on the other end of the rail leaves a similar comfort zone at the negative end:

    MPCNC - X-axis endstop - X min
    MPCNC – X-axis endstop – X min

    Both switches now live on the rear X-axis rail and appear as seen from behind the bench; they just look backwards. The Y-axis switches are on the left rail and look exactly the same.

    The XY travel works out to 630 × 460 mm = 24.8 × 18.1 inch, which is Good Enough.

    Some fiddling with the Z axis limit switch tape mask produces a nice round 100 mm = 3.9 inch vertical travel. The Z-axis rails just barely clear the table at the lower limit and just barely stay in the bottom bearings at the upper limit, so it’s a near thing. In practical terms, the rails or the tool will smash into the workpiece sitting atop the table before the limit switch trips.

    Setting both $20=1 Soft Limits and $21=1 Hard Limits may be excessive, but I vastly prefer having the firmware detect out-of-range moves and the hardware forcibly shut down if the firmware loses track of its position, rather than letting it grind away until I can slap the BRS. The steppers aren’t powerful enough to damage anything, of course, so it’s a matter of principle.

    The $N0=F150 sets the initial speed, as the default F0 seems to (sometimes) confuse bCNC’s auto-level grid probing.

    The $N1=G10L2P1X-633Y-463Z-3 sets the default G54 coordinate origin to the front-left corner, with Z=0 at the home position up top, so as to prevent surprises. I expect to use G55 for most work holder touchoffs, although we’ll see how that plays out.

    The G28 and G30 settings depend on the tool change location and the Z-axis probe location, so they’re still not cast in concrete.

  • MPCNC: Relocated Camera

    The original camera position put it close to the MPCNC’s DW660 spindle:

    MPCNC - original camera location
    MPCNC – original camera location

    Unfortunately, it sat slightly too close to the gantry roller along the X-axis for comfort.

    The effort required to pry the mount off its hot-melt glue bed showed it wasn’t ever going to shake loose, so I fired up the glue gun and stuck it to a better spot on the XY assembly:

    MPCNC - relocated camera - front view
    MPCNC – relocated camera – front view

    Seen from the side:

    MPCNC - relocated camera - side view
    MPCNC – relocated camera – side view

    Bonus: it’s now trivially easy to tweak the locking screw!

    Realigning the camera and recalibrating its offset proceeded as before.

  • Magnetic Field Visualization

    Thinking about springs to apply downforce on plotting pen holders suggested magnets, so I extricated some neodymium bars from my collection of power toothbrush heads:

    Magnets - single
    Magnets – single

    A snippet of magnetic field visualization film shows a dipole pattern:

    Magnets - single - field visualization
    Magnets – single – field visualization

    Snapping two of them together in line:

    Magnets - in line
    Magnets – in line

    … produces a quadrupole:

    Magnets - in line - field visualization
    Magnets – in line – field visualization

    Now, if only I had some magnetic monopoles, this whole thing would be easier!