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

  • MPCNC: Spirograph Generator with Tool Changes

    An improved version of my GCMC Spirograph pattern generator, now with better annotation and tool changes:

    Spirograph pattern - overview
    Spirograph pattern – overview

    The GCMC code sets the stator and rotor gear tooth counts, the rotor diameter, and the pen offset using a pseudo-random number generator. This requires randomizing the PRNG seed, which I do in the calling script with the nanosecond of the current second: rnd=$(date +%N).

    The G-Code file name also comes from the timestamp:

    ts=$(date +%Y%m%d-%H%M%S)
    fn='Spiro_'${ts}'.ngc'
    # blank line to make the underscore visible
    

    Which means you must call the Bash script slowly to generate a pile o’ plots:

    for i in {1..60} ; do sh /mnt/bulkdata/Project\ Files/Mostly\ Printed\ CNC/Patterns/spiro.sh ; sleep 1 ; done
    

    Sift through the heap with drag-n-drop action using an online G-Code previewer. There seems no clean way to convert G-Code to a bitmap on the command line, although you can do it manually, of course.

    The GCMC program spits out the G-code for one plot at a time, so the Bash script calls it four times to fill a sheet of paper with random patterns:

    for p in $(seq 4)
    do
      rnd=$(date +%N)
      gcmc -D Pen=$p -D $Paper -D PRNG_Seed=$rnd $Flags $LibPath -q "$Spirograph" >> $fn
    done
    

    The -q parameter tells GCMC to not include the prolog and epilog files, because the calling script glues those onto the lump of G-Code for all four plots.

    The -D Pen=$p parameter tells the GCMC program which “tool” to select with a Tn M6 tool change command before starting the plot. Although plotter pens have a well-defined position in the holder and a pretty nearly constant length, you must have a tool length probe installed and configured:

    MPCNC Tool Length Probe - Plotter Pen
    MPCNC Tool Length Probe – Plotter Pen

    Set the overall sheet size in inches or millimeters to get a plot centered in the middle of the page with half-inch margins all around:

    Paper='PaperSize=[16.5in,14in]
    

    With all that in hand, those good old black ceramic-tip pens give impeccable results:

    Spirograph pattern - black ceramic pen - detail
    Spirograph pattern – black ceramic pen – detail

    The surviving ones, anyhow. I must apply my collection of Sakura Micron pens to this task.

    The other three colors come from fiber pens with reasonably good tips:

    Spirograph pattern - central details
    Spirograph pattern – central details

    They’re a lot like diatoms: all different and all alike.

    The GCMC and Bash source code as a GitHub Gist:

    # Spirograph G-Code Generator
    # Ed Nisley KE4ZNU – December 2017
    Paper='PaperSize=[16.5in,14in]'
    Flags="-P 2"
    LibPath="-I /opt/gcmc/library"
    Spirograph='/mnt/bulkdata/Project Files/Mostly Printed CNC/Patterns/Spirograph.gcmc'
    Prolog="/home/ed/.config/gcmc/prolog.gcmc"
    Epilog="/home/ed/.config/gcmc/epilog.gcmc"
    ts=$(date +%Y%m%d-%H%M%S)
    fn='Spiro_'${ts}'.ngc'
    echo Output: $fn
    rm -f $fn
    echo "(File: "$fn")" > $fn
    cat $Prolog >> $fn
    for p in $(seq 4)
    do
    rnd=$(date +%N)
    gcmc -D Pen=$p -D $Paper -D PRNG_Seed=$rnd $Flags $LibPath -q "$Spirograph" >> $fn
    done
    cat $Epilog >> $fn
    view raw spiro.sh hosted with ❤ by GitHub
    // Spirograph simulator for MPCNC used as plotter
    // Ed Nisley KE4ZNU – 2017-12-23
    // 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 for tool change and legend position
    // -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) {
    local d=0;
    if (!isnone(a) || isfloat(a) || !isnone(b) || isfloat(b)) {
    warning("Values must be dimensionless integers");
    }
    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=$(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 has 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 these variables from command line
    comment("PRNG seed: ",PRNG_Seed);
    PRNG_State = PRNG_Seed;
    // Define some useful constants
    AngleStep = 2.0deg;
    Margins = [0.5in,0.5in] * 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
    //—–
    // Set up pen
    if (Pen > 0) {
    comment("Tool change: ",Pen);
    toolchange(Pen);
    }
    //—–
    // Plot the curve
    feedrate(3000.0mm);
    safe_z = 1.0mm;
    plot_z = -1.0mm;
    tracepath(Points, plot_z);
    //—–
    // Put legend in proper location
    feedrate(500mm);
    TextSize = [3.0mm,3.0mm];
    TextLeading = 1.5; // line spacing as multiple of nominal text height
    MaxPen = 4;
    line1 = typeset("Seed: " + PRNG_Seed + " Stator: " + StatorTeeth + " Rotor: " + RotorTeeth,FONT_HSANS_1);
    line2 = typeset("Offset: " + L + " GCD: " + GCD + " Lobes: " + Lobes + " Turns: " + Turns,FONT_HSANS_1);
    maxlength = TextSize.x * max(line1[-1].x,line2[-1].x);
    textpath = line1 + (line2 – [-, TextLeading, -]); // undef – n -> undef to preserve coordinates
    if (Pen == 1 || Pen > MaxPen ) { // catch and fix obviously bogus pen selections
    textorg = [PlotSize.x/2 – maxlength,-(PlotSize.y/2 – TextLeading*TextSize.y)];
    }
    elif (Pen == 2) {
    textorg = [-PlotSize.x/2,-(PlotSize.y/2 – TextLeading*TextSize.y)];
    }
    elif (Pen == 3) {
    textorg = [PlotSize.x/2 – maxlength, PlotSize.y/2 – TextSize.y];
    }
    elif (Pen == 4) {
    textorg = [-PlotSize.x/2, PlotSize.y/2 – TextSize.y];
    }
    else {
    Pen = 0; // squelch truly bogus pens
    textorg = [0mm,0mm]; // just to define it
    }
    if (Pen) { // Pen = 0 suppresses legend
    placepath = scale(textpath,TextSize) + textorg;
    comment("Legend begins");
    engrave(placepath,safe_z,plot_z);
    }
    if (Pen == 1) { // add attribution along right margin
    attrpath = typeset("Ed Nisley – KE4ZNU – softsolder.com",FONT_HSANS_1);
    attrpath = rotate_xy(attrpath,90deg);
    attrorg = [PlotSize.x/2,5*TextLeading*TextSize.y – PlotSize.y/2];
    placepath = scale(attrpath,TextSize) + attrorg;
    comment("Attribution begins");
    engrave(placepath,safe_z,plot_z);
    }
    goto([-,-,25.0mm]);
    view raw Spirograph.gcmc hosted with ❤ by GitHub
  • MPCNC: GCMC Text vs. Speed

    GCMC includes single-stroke fonts derived from Hershey fonts, so I added a legend to the Spirograph Shakedown generator:

    MPCNC - GCMC Text - 3000 mm-min
    MPCNC – GCMC Text – 3000 mm-min

    Obviously, plotting 2.5 mm tall characters at 3000 mm/min = 50 mm/s isn’t a Good Idea on a less-than-absolutely-rigid CNC machine.

    Slowing down to 250 mm/min = 4.2 mm/s produces much better results:

    MPCNC - GCMC Text - 250 mm-min
    MPCNC – GCMC Text – 250 mm-min

    A closer look, albeit with less-than-crisp focus:

    MPCNC - GCMC Text - 250 mm-min - detail
    MPCNC – GCMC Text – 250 mm-min – detail

    This isn’t a conclusive test, but it reminds me that Speed Kills.

    The green plotter pen started life with a standard 0.3 mm felt nib, but it’s worn somewhat wider over the intervening years decades. Those 2.5 mm characters would look better coming from a narrow ceramic pen, which would require a pen change before doing the legend; using 4 mm characters would produce better results.

    The line spacing is 110% of the font X height, which obviously isn’t quite enough. Something on the order of 150% should look better.

    This GCMC code (including those mods) produces the legend:

    feedrate(250mm);
    
    textsize = [4.0mm,4.0mm];
    textat = [0.5*PlotSize.x/2,-PlotSize.y/2 + 2*textsize.y];
    
    textpath = typeset("Seed: " + PRNG_Seed + "  Stator: " + StatorTeeth + "  Rotor: " + RotorTeeth,FONT_HSANS_1);
    scalepath = scale(textpath,textsize);
    placepath = scalepath + textat;
    engrave(placepath,1.0mm,-1.0mm);
    
    textpath = typeset("Offset: " + L + "  Lobes: " + Lobes + "  Turns: " + Turns,FONT_HSANS_1);
    scalepath = scale(textpath,textsize);
    placepath = scalepath + textat + [-,-1.5*textsize.y];
    engrave(placepath,1.0mm,-1.0mm);
    
  • MPCNC: Spirograph Exerciser

    Both bCNC and GCMC include Spirograph generators with more-or-less fixed patterns and sizes, because the code serves to illustrate the software’s capabilities:

    MPCNC - bCNC Spirograph patterns
    MPCNC – bCNC Spirograph patterns
    GGMC Cycloids test patterns
    GGMC Cycloids test patterns

    I wanted to exercise my MPCNC’s entire range of travel, familiarize myself with some new GCMC features, and, en passant, mimic the actual gears in a classic Spirograph, so, of course, I had to write a Spirograph emulator from scratch:

    MPCNC - Full-platform Spirograph - multicolor
    MPCNC – Full-platform Spirograph – multicolor

    The perspective makes a 29×19 inch sheet of paper (made from three B sheets and one A sheet) look not much larger than the 17×11 inch B size sheets in the first two pictures. IRL, it’s a billboard!

    My GCMC code uses notation and formulas from a paper (tidy PDF) on a Gnuplot spirograph generator, with a dash of error checking from the GCMC source.

    The code enumerates the possible gear tooth counts in a pair of vectors from which you select the desired stator and rotor gears using integer subscripts. Because I eventually scale the results to fit the plot area, there’s no need to keep track of actual gear pitch diameters.

    Similarly, the pen offset from the center of the rotor gear is a pure number, which you can think of as the ratio of the offset to the rotor diameter. It can have either sign and may exceed unity, as needed, either of which would be difficult with a physical gear.

    Figuring the number of rotor turns required to complete the pattern requires reducing the gear ratio to a fraction with no common factors, so I wrote a Greatest Common Divisor function using Euclid’s algorithm adapted for GCMC’s bitwise tests and shifts.

    With those values in hand, a loop iterates around the entire pattern to produce a list of XY coordinates in normalized space. Because the formula doesn’t have the weird properties of the Superformula I used with the HP 7475 plotter, I think there’s no need to prune the list to eliminate tiny moves.

    Scaling the entire plot requires keeping track of the actual extents along both axes, which happens in the loop generating the normalized coordinates. A pair of gears producing an odd number of lobes can have different extents in the positive and negative directions, particularly with only a few lobes (3, 5, 7 …):

    Spirograph - 3 lobes - QnD Simulator
    Spirograph – 3 lobes – QnD Simulator

    So I accumulate all four, then scale based on the absolute maximum; I added scalar min() and max() functions.

    Converting the list of scaled points into G-Code turns out to be a one-liner using GCMC’s tracepath() library function. Previewing the results in a Web-based simulator helps weed out the duds.

    The code needs cleanup, in particular to let a Bash script set various parameters, but it’s a good start.

    The GCMC source code as a GitHub Gist:

    // Spirograph simulator for MPCNC plotter
    // Ed Nisley KE4ZNU – 2017-12
    // 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
    include("tracepath.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) {
    local d=0;
    // message("gcd(" + a + "," + b + ") = ");
    if (!isnone(a) || isfloat(a) || !isnone(b) || isfloat(b)) {
    warning("Values must be dimensionless integers");
    }
    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
    }
    g = a*(1 << d); // form gcd
    // message(" " + g);
    return g;
    }
    //—–
    // Max and min functions
    function max(x,y) {
    return (x > y) ? x : y;
    }
    function min(x,y) {
    return (x < y) ? x : y;
    }
    //—–
    // Spirograph tooth counts mooched from:
    // http://nathanfriend.io/inspirograph/
    Stators = [96, 105, 144, 150];
    Rotors = [24, 30, 32, 36, 40, 45, 48, 50, 52, 56, 60, 63, 64, 72, 75, 80, 84];
    //—–
    // Set up gearing
    s = 1; // index values should be randomized
    r = 6;
    StatorTeeth = Stators[s]; // from the universe of possible teeth
    RotorTeeth = Rotors[r];
    message("Stator: ", StatorTeeth);
    message("Rotor: ", RotorTeeth);
    L = 0.90; // normalized pen offset in rotor
    message("Pen offset: ", L);
    g = gcd(StatorTeeth,RotorTeeth); // reduce teeth to ratio of least integers
    StatorN = StatorTeeth / g;
    RotorM = RotorTeeth / g;
    K = to_float(RotorM) / to_float(StatorN); // normalized rotor dia
    Lobes = StatorN; // having removed all common factors
    Turns = RotorM;
    message("Lobes: ", Lobes);
    message("Turns: ", Turns);
    AngleStep = 2.0deg;
    //—–
    // 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
    TableSize = [25in,18in]; // largest possible plot area
    PaperSize = 0 ? [17.0in,11.0in] : TableSize;
    Margins = [0.5in,0.5in] * 2;
    Boundary = PaperSize – Margins;
    message("Boundary: ",Boundary);
    PlotScale = [Boundary.x / (2*Xmax), Boundary.y / (2*Ymax)];
    message("Plot scale: ", PlotScale);
    Points = scale(Path,PlotScale); // fill page, origin at center
    //—–
    // Produce G-Code
    feedrate(6000.0mm);
    safe_z = [-,-,25.0mm];
    plotz = -1.0mm;
    goto([0,0,10.0mm]);
    tracepath(Points, plotz);
    goto(safe_z);

  • MPCNC: GCMC Spirograph Shakedown

    The MPCNC instructions recommend running it for a while, taking it apart, then putting it back together, so all the parts have a chance to relax and get used to working together. To that end, I figured doing some full platform plots would run the rollers over the entire length of the rails:

    MPCNC - Full-platform Spirograph - first pass
    MPCNC – Full-platform Spirograph – first pass

    I taped three B-size sheets together, with an A-size sheet in the far right corner, into a 29×19 inch sheet to put borders around the MPCNC’s 28×18 inch work area. The tape is on the top surface to prevent embarrassing accidents where the pen snags on an edge, at the cost of blurry lines where the ink doesn’t stick quite right.

    The far left corner of the paper washes up on the tool length probe’s base, but the pen position turns out to be so repeatable (it should be!) you can swap them with gleeful abandon and get good results:

    MPCNC - Full-platform Spirograph - multicolor
    MPCNC – Full-platform Spirograph – multicolor

    The pen rumbles along at 12000 mm/min = 200 mm/s = 7.8 inch/s with no hint of wobblulation. Most likely, those big loops aren’t particularly challenging, although watching the big central assembly whip around a tight curve can be startling.

    I modified the pen holder for 3-point support, as the recess for the pen flange isn’t quite deep enough:

    MPCNC - Pen holder - 3 point grip
    MPCNC – Pen holder – 3 point grip

    Good old masking tape holds the pens securely enough for now.

    The glass plate I’d been using for B-size plots doesn’t cover the full area, but I’d set the Z axis limit switch to trip just before the bottom of the rails whacked into the glass. Extending the travel by 5 mm required a snippet of black tape:

    MPCNC - Z axis switch - table limit
    MPCNC – Z axis switch – table limit

    The patterns come from a scratch-built Spirograph generator, because I wanted to review what’s new in GCMC. More on the software tomorrow …

  • MPCNC: GCMC Configuration

    The default GCMC prolog(ue) spits out some G-Codes that GRBL doesn’t accept, requiring tweaks to the incantation.

    A first pass at a useful prolog, minus the offending codes:

    cat ~/.config/gcmc/prolog.gcmc 
    (prolog begins)
    G17 (XY plane)
    G21 (mm)
    G40 (no cutter comp)
    G49 (no tool length comp)
    G80 (no motion mode)
    G90 (abs distance)
    G94 (units per minute)
    (prolog ends)
    

    Including any of the usual “end of program” M-Codes in the epilog(ue) causes GRBL to pause before exiting the program, so leave only a placeholder:

    cat ~/.config/gcmc/epilog.gcmc 
    (epilog begins)
    (M2) (allow program to continue)
    (epilog ends)
    

    Having done a git clone into /opt/gcmc before building the program, the GCMC library routines live in:
    /opt/gcmc/library

    Limiting numeric values to two decimal places makes sense:
    -P 2

    With all that in hand, unleashing the compiler on an unsuspecting source file requires this jawbreaker:

    gcmc -P 2 -I /opt/gcmc/library \
    -G ~/.config/gcmc/prolog.gcmc \
    -g ~/.config/gcmc/epilog.gcmc \
    -o cycloids.ngc \
    cycloids.gcmc
    

    One might, of course, tuck all that into a little script, rather than depend on extracting it from the bash history as needed.

    The resulting G-Code file looks about right:

    head cycloids.ngc
    (prolog begins)
    G17 (XY plane)
    G21 (mm)
    G40 (no cutter comp)
    G49 (no tool length comp)
    G80 (no motion mode)
    G90 (abs distance)
    G94 (units per minute)
    (prolog ends)
    F2500.00
    [pi@MPCNC tmp]$ head -25 cycloids.ngc
    (prolog begins)
    G17 (XY plane)
    G21 (mm)
    G40 (no cutter comp)
    G49 (no tool length comp)
    G80 (no motion mode)
    G90 (abs distance)
    G94 (units per minute)
    (prolog ends)
    F2500.00
    G0 Z1.00
    (-- tracepath at Z=-1.00mm --)
    G0 X-145.50 Y-30.00
    G1 Z-1.00
    G1 X-145.51 Y-29.93
    
    ... vast snippage ...
    
    G1 X175.50 Y30.00
    G1 Z1.00
    G0 Z25.00
    (epilog begins)
    (M2)
    (epilog ends)
    

    Then it’s just a matter of tweaking cycloids.gcmc to make interesting things happen:

    GGMC Cycloids test patterns
    GGMC Cycloids test patterns
  • MPCNC: Re-Improved Endstop Switch Mount

    As part of entombing the endstop PCBs in epoxy, I tweaked the switch mounts to (optionally) eliminate the screw holes and (definitely) rationalize the spacings:

    MPCNC MB Endstop Mount - No screws
    MPCNC MB Endstop Mount – No screws

    The sectioned view shows the cable tie slot neatly centered between the bottom of the switch terminal pit and the EMT rail, now with plenty of meat above the cable tie latch recess. The guide ramp on the other side has a more-better position & angle, too.

    A trial fit before dabbing on the epoxy:

    MPCNC - Endstop Mount for epoxy coating - trial fit
    MPCNC – Endstop Mount for epoxy coating – trial fit

    The 3M black foam tape works wonderfully well!

    After the epoxy cures, it’s all good:

    MPCNC - Epoxy-coated Endstop - Installed
    MPCNC – Epoxy-coated Endstop – Installed

    The OpenSCAD source code as a GitHub Gist:

    // MPCNC Endstop Mount for Makerbot PCB on EMT tubing
    // Ed Nisley KE4ZNU – 2017-12-04
    /* [Build Options] */
    Layout = "Show"; // [Build, Show, Block]
    Holes = false; // holes for switch screws
    Section = true; // show internal details
    /* [Extrusion] */
    ThreadThick = 0.25; // [0.20, 0.25]
    ThreadWidth = 0.40; // [0.40]
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    /* [Hidden] */
    Protrusion = 0.01; // [0.01, 0.1]
    HoleWindage = 0.2;
    ID = 0;
    OD = 1;
    LENGTH = 2;
    /* [Sizes] */
    RailOD = 23.5; // actual rail OD
    SwitchHeight = 8.0; // switch PCB distance from rail OD
    Strap = [5.5,50,2.0]; // nylon strap securing block to rail
    StrapHead = [8.2,3.0,5.5]; // recess for strap ratchet head
    Screw = [2.0,3.6,7.0]; // thread dia, head OD, screw length
    HoleOffset = [2.5,19.0/2]; // PCB mounting holes from PCB edge, rail center
    SwitchClear = [6.0,15,3.0]; // clearance around switch pins
    SwitchOffset = [6.0,0]; // XY center of switch from holes
    StrapHeight = (SwitchHeight – SwitchClear[2])/2; // strap center from rail
    Block = [16.4,26.0,RailOD/2 + SwitchHeight]; // basic block shape
    //- Adjust hole diameter to make the size come out right
    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);
    }
    //- Shapes
    // Main block constructed centered on XY with Z=0 at top of rail
    module PCBBlock() {
    difference() {
    translate([-Block[0]/2,-Block[1]/2,-RailOD/2])
    cube(Block,center=false);
    translate([(SwitchOffset[0] + HoleOffset[0] – Block[0]/2),
    SwitchOffset[1],
    (SwitchHeight – SwitchClear[2]/2 + Protrusion/2)])
    cube(SwitchClear + [0,0,Protrusion],center=true);
    if (Holes)
    for (j=[-1,1])
    translate([HoleOffset[0] – Block[0]/2,j*HoleOffset[1],(Block[2]/2 – Screw[LENGTH])])
    rotate(180/6)
    if (true) // true = loose fit
    PolyCyl(Screw[ID],Screw[LENGTH] + Protrusion,6);
    else
    cylinder(d=Screw[ID],h=Screw[LENGTH] + Protrusion,$fn=6);
    translate([0,0,StrapHeight])
    cube(Strap,center=true);
    translate([0, // strap head recess
    (Block[1]/2 – StrapHead[1]/2 + Protrusion),
    StrapHeight – Strap[2]/2 + StrapHead[2]/2])
    cube(StrapHead + [0,Protrusion,0],center=true);
    StrapAngle = atan((StrapHeight + RailOD/4)/Strap[2]); // a reasonable angle
    echo(str("Strap Angle: ",StrapAngle));
    translate([0,-(Block[1]/2 – Strap[2]/(2*sin(StrapAngle))),StrapHeight])
    rotate([StrapAngle,0,0])
    translate([0,-Strap[1]/2,0])
    cube(Strap,center=true);
    if (Section)
    translate([Block[0]/2,0,0])
    cube(Block + [0,2*Protrusion,2*Block[2]],center=true);
    }
    }
    module Mount() {
    difference() {
    translate([0,0,RailOD/2])
    PCBBlock();
    rotate([0,90,0])
    cylinder(d=RailOD,h=3*Block[0],center=true);
    }
    }
    //- Build things
    if (Layout == "Show") {
    Mount();
    color("Yellow",0.5)
    rotate([0,90,0])
    cylinder(d=RailOD,h=3*Block[0],center=true);
    }
    if (Layout == "Block")
    PCBBlock();
    if (Layout == "Build")
    translate([0,0,Block[2]])
    rotate([180,0,0])
    Mount();
  • MPCNC: Endstop Mount, Now With Recess

    There being nothing like a new problem to take your mind off all your old problems, now there’s a cable tie latch recess:

    X min endstop - recessed cable tie latch
    X min endstop – recessed cable tie latch

    A sectioned view of the model shows the layout:

    MPCNC MB Endstop Mount - latch recess
    MPCNC MB Endstop Mount – latch recess

    On the other side, a ramp helps bend the tie toward the MPCNC rail:

    X min endstop - recessed strap
    X min endstop – recessed strap

    Which looks thusly in the realm of applied mathematics:

    MPCNC MB Endstop Mount - strap recess
    MPCNC MB Endstop Mount – strap recess

    I’ll leave the OpenSCAD code to your imagination, because the endstop block turns out to be a bit small for the recesses. Eventually, they need a dust cover and some cleanup.

    So, there!