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

Using and tweaking a Makergear M2 3D printer

  • 3D Printer Design Conversation: Part 4

    Continued musings about building a large-format 3D printer …

    (Continued from yesterday)

    taking your challenge and am starting by cloning the M2

    That gives you an existence theorem: you know exactly what you want to end up with.

    AFAICT, few of the M2’s parts bear standardized numbers you can simply order from a reputable seller. Makergear knows what it’s buying (obviously!), but they’re under no obligation to help out: you must reverse engineer the requirements, find a suitable part, find a supplier, then buy one item.

    Let me know how that works out for cost & performance; “cost” should include a nonzero value for your time and “performance” should have numbers you can verify. I (obviously) think the build will be a dead loss on both counts (*), but good data will be interesting.

    (*) Albeit useful for educational purposes, which I’ve used to justify many absurd projectst!

    How the heck do you read out the current (estimated, obviously) X Y Z position absolute to the machine coordinates?

    Perhaps M114 or M117?

    My overall list may be helpful, although the RepRap Marlin reference has more detail on their command set:

    https://softsolder.com/2013/03/14/g-code-and-m-code-grand-master-list/

    The LinuxCNC (and, perhaps, Machinekit) G-Code languages give you access to built-in variables and extend G-Code into a true scripting language. Marlin evolved differently and doesn’t support that sort of thing.

    G-Code is pretty much a write-only language, but you can do some interesting things:

    https://softsolder.com/2013/07/18/makergear-m2-cnc-platform-corner-clips/

    I use the gcmc compiler whenever I can for actual CNC machining:

    https://softsolder.com/2014/02/21/can-opener-gear-rebuild/

    Works for me, anyhow, although I don’t do much CNC these days.

    move my nozzle up .01 at a time

    Stiction / microstep errors / command resolution prevent that:

    Makergear M2 Z-axis Backlash Numbers

    The only way to measure the nozzle position is to measure a finished part with a known height, because any variation comes from the first layer offset. That’s if you have Z=0 at the platform, of course, rather than whatever offset you get by defining Z=0 at some random height based on jamming business cards / feeler gages / special Japanese rolling papers under the snout. [ptui & similar remarks]

    For example:

    https://softsolder.com/2015/09/14/makergear-m2-platform-stability/

    You need numbers. Lots of numbers. [grin]

    strip basic tools out of the control interface

    Yet another reason I don’t use S3D: that “Simplify” thing gets in the way of my obsessive need for control.

    (Continues tomorrow)

  • 3D Printer Design Conversation: Part 3

    More musings in response to questions about building a large-format 3D printer.

    (Continued from yesterday)

    make a direct clone of the M2. No thinking required.

    The present-day M2 has survived four years of rather fierce Darwininan winnowing, so it’s a much better thought-out product than, ahem, you may think just by looking at it.

    To build a one-off duplicate, you’ll spend as much money collecting the parts as you would to just buy another M2 and start printing.

    Should you buy cheap parts to save money, without considering the requirements, you’ll get, say, the same Z-axis motor Makergear used on the original M2, the complete faceplant of Thing-O-Matic electronics, or crap from eBay described as being kinda-sorta what you want.

    Sometimes crap from eBay can be educational, of course:

    https://softsolder.com/2013/01/24/hall-effect-sensors-from-ebay-variations-on-a-specification/

    I encourage thinking, particularly with numbers, because it leads to understanding, rather than being surprised by the results.

    increase the rigidity of the X and Y axis

    In round numbers, deflection varies as the fourth power of length: enlarge a frame member by 50% and it becomes five times bendier. If your design simply scales up the frame, it won’t hold the tolerances required to produce a good object.

    https://en.wikipedia.org/wiki/Euler%E2%80%93Bernoulli_beam_theory

    If you add more mass (“stiffening”) to the Y axis, then the Z axis motor (probably) can’t accelerate the new load upward with the original firmware settings and the Y axis motor may have trouble, too. Perhaps you should measure the as-built torque to support your design:

    https://softsolder.com/2013/07/02/makergear-m2-better-z-axis-motor-calculations/

    Reduce the acceleration and lower the print speed? Use bigger motors (if you can find a Z motor with the correct leadscrew) and lose vertical space? Make the frame taller and lose stiffness? Use two Z motors (like the RepRap Mendels) and get overconstrained vertical guides? Try building a kinematic slide and lose positioning accuracy? Your choice!

    If your intent is to print more parts at once, buy more M2 printers, which will not only be cheaper, but also give you more throughput, lower the cost of inevitable failures, good redundancy, and generally produce better results. Some of the folks on the forum run a dozen M2s building production parts; they’re not looking for bigger print volumes to wreck more parts at once.

    Conversely, if your intent is to learn how to build a printer, then, by all means, think about the design, run the numbers, collect the parts, then proceed. It sounds like a great project with plenty of opportunity for learning; don’t let me discourage you from proceeding!

    However, I’ll be singularly unhelpful with specific advice, because I’m not the guy building the printer. You must think carefully about what you want to achieve, figure out how to get there, and make it happen.

    To a large extent, searching my blog with appropriate keywords will tell you exactly what I think about 3D printing, generally with numbers to back up the conclusions. Get out your calculator, fire up your pencil, and get started!

    (Continues tomorrow)

  • 3D Printer Design Conversation: Part 2

    Wherein I continue dumping my responses to a large-format 3D printer project …

    (Continued from yesterday)

    What do you mean by 12 hour mean time to failure

    In round numbers, the cries of anguish on the M2 forum seem to increase as parts require more than a dozen hours from start to finish; while you can print things that require 48 hours, that’s not the way to bet. There are more ways for things to go wrong than for them to go right, given the rather rickety collection of software & firmware making everything happen, plus the gummy nature of squeezing hot plastic into precise heaps.

    Most of the time, it works fine.

    much cheaper hardened polished rod system that the taz 6 uses?

    Unless they’re doing something non-obvious to make a kinematic assembly, two rods on four hard mounts with four one-degree-of-freedom slides will be severely overconstrained and, I expect, a continuing hunk o’ trouble:

    https://softsolder.com/2011/02/04/thing-o-matic-x-and-z-axis-rod-alignment/

    FWIW, linear slides don’t eliminate the need for a rigid and well-aligned frame. Even the slab atop an M2 can deform by more than 0.1 mm under belt tension, which is enough to wreck the nozzle-to-platform alignment across the length of the X axis.

    “Arduino-class firmware (Marlin, et. al.) is a dead end” Why is that?

    Marlin is a dead end: they’re trying to jam hard real time motor control, soft real time command parsing, and non real time UI control into an 8 bit microcontroller teleported from the mid 90s. AVR microncontrollers worked really well up through the Cupcake and have held back printer design & performance ever since.

    Which inexpensive all in one board would you go with

    Machinekit on a Beaglebone seems to be the least awful of the current alternatives, but I haven’t examined the field recently enough to have a valid opinion. You’ll find plenty of proprietary “solutions” out there, none of which I’d be interested in.

    Am I wrong?

    I think so, but, then, I may be wrong, too. [grin]

    It’s incredibly easy to slap together a bunch of parts that look like they should become a 3D printer. It’s remarkably difficult to engineer a reliable, stable, accurate device that actually produces dependable results.

    Mooching design cues and parts from here & there doesn’t get you to the goal; if it did, Kickstarter wouldn’t be a graveyard of cheap 3D printer projects.

    design a very rigid system for cheap

    If it’s for your personal satisfaction, have at it, but a one-off large-format printer won’t be any cheaper than, say, a Taz 6. Some diligent searching will uncover any number of homebrew printer projects along the lines of what you’re considering; learning from their mistakes will certainly be edifying.

    Anything is possible, but if you want to end up with a state of the art machine, you must begin with numbers showing how & why it actually meets the requirements. 3D printing now operates at accuracies, speeds, and controls comparable to CNC machines, with corresponding structural demands. There’s a reason high-end CNC machines aren’t made of sheet metal and don’t use 8 bit microcontrollers.

    You might want to start at the beginning of my blog and read through my adventures with the Thing-O-Matic, which will explain why I’m such a curmudgeon …

    (Continues tomorrow)

  • 3D Printer Design Conversation: Part 1

    I recently engaged in a wide-ranging email exchange with a guy planning to scratch-build a large-format 3D printer. He figured it would be a straightforward exercise and asked for some advice; I may be more cynical that he expected.

    Over the next few days, I’ll dump my side of the conversation so I can refer to it in other contexts. I’ve left his side of the conversation as the short quotes that prompted my replies, but you can probably infer what he was thinking.

    He’s well-acquainted with CNC machining and recently added a Makergear M2 to his collection …

    I’m hooked.

    All of sudden, you realize what you’ve been missing!

    In round numbers, I’ve been designing & printing one “thing” every week for the last five years. Granted, my “things” look a lot like brackets, because they go into other shop projects, but 3D printing is how I make nearly all the shapes I formerly bashed from metal.

    I loves me my 3D printer!

    an open source design with AFFORDABLE, EASILY ACCESSIBLE parts with a build platform of at least 150% X/Y volume of the MakerGear

    Some years ago, I had the same general idea. Then I bought an M2 (replacing my Thing-O-Matic), considered LinuxCNC / Machinekit for motion control, and realized there wasn’t much point; I didn’t want to devote far too much time & effort to solving an already solved problem.

    A larger build volume doesn’t buy you as much as you think, while imposing far too many hard constraints. Basically, good-resolution extruders run at 2 to 10 mm³/s, so large objects require print times beyond the 12-hour MTTF of the “printing system”: something will go wrong often enough to drive you mad.

    Bonus: plastic’s thermal coefficient guarantees bed adhesion problems. Using high-traction materials (PEI / hairspray / whatever) introduces problems in the other direction. There’s a limit to how big you can make things before they either don’t stick or stick too hard.

    Some the fundamental design problems that nobody recognizes until far too late in their design:

    • nozzle-to-platform accuracy < ±0.05 mm
    • XY axis speeds 30 mm/s to 500 mm/s
    • Z axis stiction & backlash < 0.1 mm
    • filament drive with excellent retraction control / speed
    • bed adhesion vs. part removal vs. Z accuracy
    • Arduino-class firmware (Marlin, et. al.) is a dead end
    • Windows is crap in any part of a machine-control problem

    Those are hard requirements. At a minimum, your design must satisfy all of them: miss any one and you’re not in the game. It’s easy to build a cheap and crappy fused-filament 3D printer (see Kickstarter), but exceedingly difficult to build one at the state of the art (see patent litigation).

    The M2 descends from the original RepRap design, with the Y axis slinging far too much mass back & forth. That kills nozzle-to-platform accuracy, introduces temperature instability, and soaks up bench space. On the other paw, look at the problems Makerbot (not Makergear) had with their direct-drive extruder on an XY platform; getting that right requires nontrivial engineering

    Bowden filament drives have improved, but really can’t provide enough retraction control / speed. Delta printers always use Bowden drives, because they can’t sling a direct-drive extruder with enough XYZ speed & accuracy. Bowden on an XY platform has the worst of both worlds: bad retraction and difficult mechanical design.

    I think the M2 occupies a sweet spot in 3D printer design: excellent results without excessive complexity or expense. It’s not perfect, but good enough.

    But, then, I’m a known curmudgeon …

    (Continues tomorrow)

  • Vacuum Tube Lights: Duodecar Rebuild

    You’ll recall the LED atop the 21HB5A tube failed, shortly after replacing the bottom LED and rewiring the ersatz plate lead, which led me to rebuild the whole thing with SK6812 RGBW LEDs. So I printed all the plastic parts again, because the duodecar tube socket’s pin circle can fit into a hard drive platter’s unmodified 25 mm hole, then drilled another platter to suit:

    Duodecar disk drilling
    Duodecar disk drilling

    The hole under the drill fits the 3.5 mm stereo socket for the ersatz plate lead, so it’s bigger than before.

    I’ve switched from Arduino Pro Minis with a separate USB converter to Arduino Nanos with an on-board CH340 USB chip, because the fake FTDI chips on the converters are a continuing aggravation:

    21HB5A base - interior
    21HB5A base – interior

    Adding those wire slots to the sockets definitely helps tidy things up; the wires no longer need a crude cable tie anchoring them to the socket mounting screws.

    I wanted to drive the LEDs from the A7 pin, rather than the A3 pin I’d been using on the Pro Minis, to keep the wires closer together, but it turns out that A6 and A7 can’t become digital output pins. So I used A5, although I may come to regret the backward incompatibility.

    In any event, the 21HB5A tube looks spiffy with its new LEDs in full effect:

    21HB5A with RBGBW LEDs - cyan violet phase
    21HB5A with RBGBW LEDs – cyan violet phase

    I dialed the white LED PWM down to 32, making the colors somewhat pastel, rather than washed-out.

    The Arduino source code as a GitHub Gist:

    // Neopixel mood lighting for vacuum tubes
    // Ed Nisley – KE4ANU – June 2016
    // September 2016 – Add Morse library and blinkiness
    // October 2016 – Set random colors at cycle end
    // March 2017 – RGBW SK6812 LEDs
    #include <Adafruit_NeoPixel.h>
    #include <morse.h>
    #include <Entropy.h>
    //———-
    // Pin assignments
    const byte PIN_NEO = A5; // DO – data out to first Neopixel
    const byte PIN_HEARTBEAT = 13; // DO – Arduino LED
    #define PIN_MORSE 12
    //———-
    // Constants
    // number of pixels
    #define PIXELS 2
    // index of the Morse output pixel and how fast it sends
    boolean Send_Morse = false;
    #define PIXEL_MORSE (PIXELS – 1)
    #define MORSE_WPM 10
    // lag between adjacent pixel, degrees of slowest period
    #define PIXELPHASE 45
    // update LEDs only this many ms apart (minus loop() overhead)
    #define UPDATEINTERVAL 50ul
    #define UPDATEMS (UPDATEINTERVAL – 1ul)
    // number of steps per cycle, before applying prime factors
    #define RESOLUTION 500
    //———-
    // Globals
    // instantiate the Neopixel buffer array
    Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIXELS, PIN_NEO, NEO_GRBW + NEO_KHZ800);
    uint32_t FullWhite = strip.Color(255,255,255,255);
    uint32_t FullOff = strip.Color(0,0,0,0);
    uint32_t MorseColor;
    struct pixcolor_t {
    unsigned int Prime;
    unsigned int NumSteps;
    unsigned int Step;
    float StepSize;
    float Phase;
    byte MaxPWM;
    };
    unsigned int PlatterSteps;
    byte PrimeList[] = {3,5,7,13,19,29};
    // colors in each LED
    enum pixcolors {RED, GREEN, BLUE, WHITE, PIXELSIZE};
    struct pixcolor_t Pixels[PIXELSIZE]; // all the data for each pixel color intensity
    uint32_t UniColor;
    unsigned long MillisNow;
    unsigned long MillisThen;
    // Morse code
    char * MorseText = " cq cq cq de ke4znu";
    LEDMorseSender Morse(PIN_MORSE, (float)MORSE_WPM);
    uint8_t PrevMorse, ThisMorse;
    //– Figure PWM based on current state
    byte StepColor(byte Color, float Phi) {
    byte Value;
    Value = (Pixels[Color].MaxPWM / 2.0) * (1.0 + sin(Pixels[Color].Step * Pixels[Color].StepSize + Phi));
    // Value = (Value) ? Value : Pixels[Color].MaxPWM; // flash at dimmest points for debug
    return Value;
    }
    //– Select three unique primes for the color generator function
    // Then compute all the step parameters based on those values
    void SetColorGenerators(void) {
    Pixels[RED].Prime = PrimeList[random(sizeof(PrimeList))];
    do {
    Pixels[GREEN].Prime = PrimeList[random(sizeof(PrimeList))];
    } while (Pixels[RED].Prime == Pixels[GREEN].Prime);
    do {
    Pixels[BLUE].Prime = PrimeList[random(sizeof(PrimeList))];
    } while (Pixels[BLUE].Prime == Pixels[RED].Prime ||
    Pixels[BLUE].Prime == Pixels[GREEN].Prime);
    do {
    Pixels[WHITE].Prime = PrimeList[random(sizeof(PrimeList))];
    } while (Pixels[WHITE].Prime == Pixels[RED].Prime ||
    Pixels[WHITE].Prime == Pixels[GREEN].Prime ||
    Pixels[WHITE].Prime == Pixels[BLUE].Prime);
    printf("Primes: %d %d %d %d\r\n",Pixels[RED].Prime,Pixels[GREEN].Prime,Pixels[BLUE].Prime,Pixels[WHITE].Prime);
    Pixels[RED].MaxPWM = 255;
    Pixels[GREEN].MaxPWM = 255;
    Pixels[BLUE].MaxPWM = 255;
    Pixels[WHITE].MaxPWM = 32;
    unsigned int PhaseSteps = (unsigned int) ((PIXELPHASE / 360.0) *
    RESOLUTION * (unsigned int) max(max(max(Pixels[RED].Prime,Pixels[GREEN].Prime),Pixels[BLUE].Prime),Pixels[WHITE].Prime));
    printf("Pixel phase offset: %d deg = %d steps\r\n",(int)PIXELPHASE,PhaseSteps);
    for (byte c=0; c < PIXELSIZE; c++) {
    Pixels[c].NumSteps = RESOLUTION * Pixels[c].Prime; // steps per cycle
    Pixels[c].StepSize = TWO_PI / Pixels[c].NumSteps; // radians per step
    Pixels[c].Step = random(Pixels[c].NumSteps); // current step
    Pixels[c].Phase = PhaseSteps * Pixels[c].StepSize;; // phase in radians for this color
    printf(" c: %d Steps: %d Init: %d Phase: %d deg",c,Pixels[c].NumSteps,Pixels[c].Step,(int)(Pixels[c].Phase * 360.0 / TWO_PI));
    printf(" PWM: %d\r\n",Pixels[c].MaxPWM);
    }
    }
    //– Helper routine for printf()
    int s_putc(char c, FILE *t) {
    Serial.write(c);
    }
    //——————
    // Set the mood
    void setup() {
    pinMode(PIN_HEARTBEAT,OUTPUT);
    digitalWrite(PIN_HEARTBEAT,LOW); // show we arrived
    Serial.begin(57600);
    fdevopen(&s_putc,0); // set up serial output for printf()
    printf("Vacuum Tube Mood Light – RGBW\r\nEd Nisley – KE4ZNU – March 2017\r\n");
    Entropy.initialize(); // start up entropy collector
    // set up pixels
    strip.begin();
    strip.show();
    // lamp test: a brilliant white flash
    printf("Lamp test: flash white\r\n");
    for (byte i=0; i<5 ; i++) {
    for (int j=0; j < strip.numPixels(); j++) { // fill LEDs with white
    strip.setPixelColor(j,FullWhite);
    }
    strip.show();
    delay(500);
    for (int j=0; j < strip.numPixels(); j++) { // fill LEDs with black
    strip.setPixelColor(j,FullOff);
    }
    strip.show();
    delay(500);
    }
    // get an actual random number
    uint32_t rn = Entropy.random();
    printf("Random seed: %08lx\r\n",rn);
    randomSeed(rn);
    // set up the color generators
    SetColorGenerators();
    // set up Morse generator
    Morse.setup();
    Morse.setMessage(String(MorseText));
    MorseColor = strip.Color(255,random(32,64),random(16),0);
    PrevMorse = ThisMorse = digitalRead(PIN_MORSE);
    printf("Morse enabled: %d at %d wpm color: %08lx\n [%s]\r\n",Send_Morse,MORSE_WPM,MorseColor,MorseText);
    MillisNow = MillisThen = millis();
    }
    //——————
    // Run the mood
    void loop() {
    if (!Morse.continueSending()) {
    printf("Restarting Morse message\r\n");
    Morse.startSending();
    }
    ThisMorse = digitalRead(PIN_MORSE);
    MillisNow = millis();
    if (((MillisNow – MillisThen) >= UPDATEMS) || // time for color change?
    (PrevMorse != ThisMorse)) { // Morse output bit changed?
    digitalWrite(PIN_HEARTBEAT,HIGH);
    if (Send_Morse && ThisMorse) { // if Morse output high, overlay flash
    strip.setPixelColor(PIXEL_MORSE,MorseColor);
    }
    PrevMorse = ThisMorse;
    strip.show(); // send out precomputed colors
    boolean CycleRun = false; // check to see if all cycles have ended
    for (byte c=0; c < PIXELSIZE; c++) { // compute next increment for each color
    if (++Pixels[c].Step >= Pixels[c].NumSteps) {
    Pixels[c].Step = 0;
    printf("Cycle %d steps %d at %8ld delta %ld ms\r\n",c,Pixels[c].NumSteps,MillisNow,(MillisNow – MillisThen));
    }
    else {
    CycleRun = true; // this color is still cycling
    }
    }
    // If all cycles have completed, reset the color generators
    if (!CycleRun) {
    printf("All cycles ended: setting new color generator values\r\n");
    SetColorGenerators();
    }
    for (int i=0; i < strip.numPixels(); i++) { // for each pixel
    byte Value[PIXELSIZE];
    for (byte c=0; c < PIXELSIZE; c++) { // … for each color
    Value[c] = (Pixels[c].MaxPWM / 2.0) * (1.0 + sin(Pixels[c].Step * Pixels[c].StepSize – i*Pixels[c].Phase));
    }
    UniColor = strip.Color(Value[RED],Value[GREEN],Value[BLUE],Value[WHITE]);
    strip.setPixelColor(i,UniColor);
    }
    MillisThen = MillisNow;
    digitalWrite(PIN_HEARTBEAT,LOW);
    }
    }
    view raw TubeMorse.ino hosted with ❤ by GitHub
  • Check Your Zero

    A recent OpenSCAD mailing list discussion started with an observation that the dimensions of printed parts were wildly different from the numeric values used in the OpenSCAD program that created the STL. Various folks suggested possible errors, examined the source and STL files to no avail, and were generally baffled.

    Finally, a photo conclusively demonstrating the problem arrived:

    Caliper - digital vs. analog scale
    Caliper – digital vs. analog scale

    Note the difference between the digital readout and the analog scale printed on the body.

    Turns out it’s his first digital caliper: he simply didn’t realize you must close the jaws and press the ZERO button before making any measurements.

    We’ve all been that guy. Right?

    FWIW, our Larval Engineer can probably still hear me intoning “Check your zero” every time she picks up a caliper or turns on a multimeter. Perhaps she’ll think fondly of me, some day. [grin]

  • SK2812 RGBW LED: Test Fixture

    [Edit: The SK2812 in the title and elsewhere should be SK6812. If I change the title, then all the other links break. So it goes.]

    An envelope of RGBW LEDs, allegedly with SK6812 controllers, arrived from halfway around the planet:

    SK2812RGBW LEDs - as received
    SK2812RGBW LEDs – as received

    The yellow phosphor sauce poured atop the blue LED on the left that makes it glow white leaves the upper loop of two wire bonds sticking out, but I can’t fault ’em for that. The overall build quality looks better than the ill-fated WS2812 LEDs, although it’s hard to tell by looking.

    I conjured a test stand from the vasty digital deep by tweaking the WS2812 mount:

    SK6812 LED Array Test Fixture - Slic3r preview
    SK6812 LED Array Test Fixture – Slic3r preview

    Wiring up a 5×5 panel went as before:

    SK2812RGBW test fixture - rear
    SK2812RGBW test fixture – rear

    The array test code adds another pixel channel and runs another raised sine wave with another random period, accomplished without much hackage.

    With the warm-white LED at full throttle (MaxPWM = 255), the panel tends toward the pallid end of HSV space:

    SK2812RGBW test fixture - front - W PWM255
    SK2812RGBW test fixture – front – W PWM255

    Dialing the white MaxPWM back to 32 crisps things a bit:

    SK2812RGBW test fixture - front - W PWM32
    SK2812RGBW test fixture – front – W PWM32

    Of course, the RGBW data stream isn’t compatible with the RGB data stream, so vacuum tubes with SK6812 chips require a slightly different driver and I can’t mix the two chips on a single tube.

    The Arduino source code as a GitHub Gist:

    // SK6812 RGBW LED array exerciser
    // Ed Nisley – KE4ANU – February 2017
    #include <Adafruit_NeoPixel.h>
    //———-
    // Pin assignments
    const byte PIN_NEO = A3; // DO – data out to first Neopixel
    const byte PIN_HEARTBEAT = 13; // DO – Arduino LED
    //———-
    // Constants
    #define UPDATEINTERVAL 20ul
    const unsigned long UpdateMS = UPDATEINTERVAL – 1ul; // update LEDs only this many ms apart minus loop() overhead
    // number of steps per cycle, before applying prime factors
    #define RESOLUTION 100
    // phase difference between LEDs for slowest color
    #define BASEPHASE (PI/16.0)
    // LEDs in each row
    #define NUMCOLS 5
    // number of rows
    #define NUMROWS 5
    #define NUMPIXELS (NUMCOLS * NUMROWS)
    #define PINDEX(row,col) (row*NUMCOLS + col)
    //———-
    // Globals
    // instantiate the Neopixel buffer array
    Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUMPIXELS, PIN_NEO, NEO_GRBW + NEO_KHZ800);
    uint32_t FullWhite = strip.Color(255,255,255,255);
    uint32_t FullOff = strip.Color(0,0,0,0);
    struct pixcolor_t {
    byte Prime;
    unsigned int NumSteps;
    unsigned int Step;
    float StepSize;
    float TubePhase;
    byte MaxPWM;
    };
    // colors in each LED
    enum pixcolors {RED, GREEN, BLUE, WHITE, PIXELSIZE};
    struct pixcolor_t Pixels[PIXELSIZE]; // all the data for each pixel color intensity
    unsigned long MillisNow;
    unsigned long MillisThen;
    //– Figure PWM based on current state
    byte StepColor(byte Color, float Phi) {
    byte Value;
    Value = (Pixels[Color].MaxPWM / 2.0) * (1.0 + sin(Pixels[Color].Step * Pixels[Color].StepSize + Phi));
    // Value = (Value) ? Value : Pixels[Color].MaxPWM; // flash at dimmest points
    // printf("C: %d Phi: %d Value: %d\r\n",Color,(int)(Phi*180.0/PI),Value);
    return Value;
    }
    //– Helper routine for printf()
    int s_putc(char c, FILE *t) {
    Serial.write(c);
    }
    //——————
    // Set the mood
    void setup() {
    pinMode(PIN_HEARTBEAT,OUTPUT);
    digitalWrite(PIN_HEARTBEAT,LOW); // show we arrived
    Serial.begin(57600);
    fdevopen(&s_putc,0); // set up serial output for printf()
    printf("WS2812 / SK6812 array exerciser\r\nEd Nisley – KE4ZNU – February 2017\r\n");
    /// set up Neopixels
    strip.begin();
    strip.show();
    // lamp test: run a brilliant white dot along the length of the strip
    printf("Lamp test: walking white\r\n");
    strip.setPixelColor(0,FullWhite);
    strip.show();
    delay(250);
    for (int i=1; i<NUMPIXELS; i++) {
    digitalWrite(PIN_HEARTBEAT,HIGH);
    strip.setPixelColor(i-1,FullOff);
    strip.setPixelColor(i,FullWhite);
    strip.show();
    digitalWrite(PIN_HEARTBEAT,LOW);
    delay(250);
    }
    strip.setPixelColor(NUMPIXELS – 1,FullOff);
    strip.show();
    delay(250);
    // fill the array, row by row
    printf(" … fill\r\n");
    for (int i=NUMROWS-1; i>=0; i–) { // for each row
    digitalWrite(PIN_HEARTBEAT,HIGH);
    for (int j=NUMCOLS-1; j>=0 ; j–) {
    strip.setPixelColor(PINDEX(i,j),FullWhite);
    strip.show();
    delay(100);
    }
    digitalWrite(PIN_HEARTBEAT,LOW);
    }
    // clear to black, column by column
    printf(" … clear\r\n");
    for (int j=NUMCOLS-1; j>=0; j–) { // for each column
    digitalWrite(PIN_HEARTBEAT,HIGH);
    for (int i=NUMROWS-1; i>=0; i–) {
    strip.setPixelColor(PINDEX(i,j),FullOff);
    strip.show();
    delay(100);
    }
    digitalWrite(PIN_HEARTBEAT,LOW);
    }
    delay(1000);
    // set up the color generators
    MillisNow = MillisThen = millis();
    printf("First random number: %ld\r\n",random(10));
    Pixels[RED].Prime = 3;
    Pixels[GREEN].Prime = 5;
    Pixels[BLUE].Prime = 7;
    Pixels[WHITE].Prime = 11;
    printf("Primes: (%d,%d,%d,%d)\r\n",
    Pixels[RED].Prime,Pixels[GREEN].Prime,Pixels[BLUE].Prime,Pixels[WHITE].Prime);
    unsigned int PixelSteps = (unsigned int) ((BASEPHASE / TWO_PI) *
    RESOLUTION * (unsigned int) max(max(max(Pixels[RED].Prime,Pixels[GREEN].Prime),Pixels[BLUE].Prime),Pixels[WHITE].Prime));
    printf("Pixel phase offset: %d deg = %d steps\r\n",(int)(BASEPHASE*(360.0/TWO_PI)),PixelSteps);
    Pixels[RED].MaxPWM = 255;
    Pixels[GREEN].MaxPWM = 255;
    Pixels[BLUE].MaxPWM = 255;
    Pixels[WHITE].MaxPWM = 32;
    for (byte c=0; c < PIXELSIZE; c++) {
    Pixels[c].NumSteps = RESOLUTION * (unsigned int) Pixels[c].Prime;
    Pixels[c].Step = (3*Pixels[c].NumSteps)/4;
    Pixels[c].StepSize = TWO_PI / Pixels[c].NumSteps; // in radians per step
    Pixels[c].TubePhase = PixelSteps * Pixels[c].StepSize; // radians per tube
    printf("c: %d Steps: %5d Init: %5d",c,Pixels[c].NumSteps,Pixels[c].Step);
    printf(" PWM: %3d Phi %3d deg\r\n",Pixels[c].MaxPWM,(int)(Pixels[c].TubePhase*(360.0/TWO_PI)));
    }
    }
    //——————
    // Run the mood
    void loop() {
    MillisNow = millis();
    if ((MillisNow – MillisThen) > UpdateMS) {
    digitalWrite(PIN_HEARTBEAT,HIGH);
    unsigned int AllSteps = 0;
    for (byte c=0; c < PIXELSIZE; c++) { // step to next increment in each color
    if (++Pixels[c].Step >= Pixels[c].NumSteps) {
    Pixels[c].Step = 0;
    printf("Color %d steps %5d at %8ld delta %ld ms\r\n",c,Pixels[c].NumSteps,MillisNow,(MillisNow – MillisThen));
    }
    AllSteps += Pixels[c].Step; // will be zero only when all wrap at once
    }
    if (0 == AllSteps) {
    printf("Grand cycle at: %ld\r\n",MillisNow);
    }
    for (int k=0; k < NUMPIXELS; k++) { // for each pixel
    byte Value[PIXELSIZE];
    for (byte c=0; c < PIXELSIZE; c++) { // … for each color
    Value[c] = StepColor(c,-k*Pixels[c].TubePhase); // figure new PWM value
    // Value[c] = (c == RED && Value[c] == 0) ? Pixels[c].MaxPWM : Value[c]; // flash highlight for tracking
    }
    uint32_t UniColor = strip.Color(Value[RED],Value[GREEN],Value[BLUE],Value[WHITE]);
    strip.setPixelColor(k,UniColor);
    }
    strip.show();
    MillisThen = MillisNow;
    digitalWrite(PIN_HEARTBEAT,LOW);
    }
    }