Having trouble understanding how to convert from delay to millis

Hey, y'all. I'm fresh to the forum but I've been reading through all kinds of posts here and on other forums. I'm still having the hardest time trying to figure out how to ditch the delays and switch to using millis. I've been spending hours pouring through the Adafruit multi-tasking tutorial and through the NeoPatterns tutorial. I can get the neopatterns example to work, but I can't get it to function the way I need it to. The examples show it kicking off with a pattern and then switching patterns with a button press. I'm however trying to start off with all leds off then running a pattern when triggered by PIR. Basically I'm trying to get two 16bit rings to run colorwipe in opposite directions at the same time, which means I need to ditch the delay codes. I'd greatly appreciate if someone could take a look at this and help me figure out how to convert it. I've left out my coding for DFPlayerMini since it's mostly irrelevant here at the moment.

#include <Adafruit_NeoPixel.h>
#include "SoftwareSerial.h"
#include "DFRobotDFPlayerMini.h"

#define DEBUG //serial prints

Adafruit_NeoPixel EYE_l = Adafruit_NeoPixel(16, 11, NEO_GRB + NEO_KHZ800);   
Adafruit_NeoPixel EYE_r = Adafruit_NeoPixel(16, 12, NEO_GRB + NEO_KHZ800);   

static const uint8_t PIN_MP3_TX = 2; // Connects to module's RX
static const uint8_t PIN_MP3_RX = 3; // Connects to module's TX
SoftwareSerial mySoftwareSerial(3, 2); // RX, TX



int PIRmotion1 = 10; // choose the input pin (for PIR sensor)
int PIRmotion2 = 9;
int PIRstate = LOW; // we start, assuming no motion detected
int PIRval1 = 0; // variable for reading the pin status
int PIRval2 = 0; // variable for reading the pin status



void colorWipe_l(uint32_t c, uint8_t wait) {
  for(uint16_t i=0; i<EYE_l.numPixels(); i++) {
    EYE_l.setPixelColor(i, c);
    EYE_l.show();
    delay(wait);
  }
}

void reverseColorWipe_r(uint32_t c, uint8_t wait) {
  for(int16_t i=(EYE_r.numPixels()-1); i>=0; i--) {
    EYE_r.setPixelColor(i, c);
    EYE_r.show();
    delay(wait);
  }
}

void mirroredColorWipe(uint32_t c, uint8_t wait) {
  for(int16_t i=(EYE_r.numPixels()-1); i>=0; i--) {
    EYE_r.setPixelColor(i, c);
    EYE_r.show();
    delay(wait);
  }
    for(uint16_t i=0; i<EYE_l.numPixels(); i++) {
    EYE_l.setPixelColor(i, c);
    EYE_l.show();
    delay(wait);
  }
}

void lightsUp(){
  colorWipe_l(EYE_l.Color(0,255,0), 22);
  reverseColorWipe_r(EYE_r.Color(0,255,0), 22);
  //delay(6666);         // wait for a second
}

void lightsOut(){
  mirroredColorWipe((0,0,0),22);
  delay(666);
}

void showBoth(){
  EYE_l.show();
  EYE_r.show();
}


void setup() {
  // put your setup code here, to run once:
  EYE_l.begin(); //starts the NeoPixel sketch
  EYE_r.begin(); //starts the NeoPixel sketch
  showBoth();
  EYE_l.setBrightness(66);
  EYE_r.setBrightness(66);
  
  Serial.begin(9600);
  pinMode(PIRmotion1, INPUT); // declare sensor as input
  pinMode(PIRmotion2, INPUT); // declare sensor as input
  pinMode(11, OUTPUT);//define arduino pin
  pinMode(12, OUTPUT);//define arduino pin
  Serial.println("startup");

}

void loop() {
  // put your main code here, to run repeatedly: 
  PIRval1 = digitalRead(PIRmotion1);
  PIRval2 = digitalRead(PIRmotion2);
  if(PIRval1 == HIGH || PIRval2 == HIGH)
  {
      lightsUp();
      if(PIRstate == LOW)
      {
        Serial.println(F("Motinon Detected!"));
        PIRstate = HIGH; //update to HIGH
        delay(6000);   
      }
  } 

  else{
    lightsOut();
      if(PIRstate == HIGH)
      {
        Serial.println(F("Motion stopped!"));
        PIRstate = LOW;
        }
    delay(6000);
  }
}

Hello olafzbeardo

Welcome to the world's best Arduino forum ever.

You can use the delay() function when running a single task on the Arduino.
Mixing the delay() function with a millis() based timer leads to unstable system behaviour.

If you have serval tasks with a dedicated timing behaiviour it is usefull to have time slices based on millis() function.

Take the BlinkWithOutDelay example of the IDE and use this example to design your timer() function .

This timer() function might provide the following methodes:

  • start();
  • stop(),
  • event();
  • isRunning();

Have a nice day and enjoy coding in C++.

The function millis() can be an event timer to make an event happen at a certain time. If you want RED LED to light every 200 ms, GREEN LED to light every 500ms and BLUE LED to light every 1000ms, your timeline would look like this:

 000ms .
 100ms .
 200ms RED
 300ms .
 400ms RED
 500ms GRN
 600ms RED
 700ms .
 800ms RED
 900ms .
1000ms RED GRN BLU
(repeat)

Use your timeline and the "blink without delay" tutorial in the Arduino Documents to make some blinking lights.

An example...

Going to be honest here, arbitrary timing on an Arduino isn't fun. This is an intermediate to advanced project.

Let's start with your code for the colorWipe_l() function. The way it's written, it increments the counter, lights up the next led, waits for delay(wait) to be over. Then, because the for loop hasn't finished yet, it keeps repeating that code until every led is lit. Only then does it return to main() and let the rest of the code run. That's an example of blocking code, nothing else can run while waiting for it to finish.

What you need to do is rewrite the whole thing without using for() or delay(). Instead your code needs to light up the first led, note down the time in millis(), note which led it's up to, and then continue with the rest of the program. Next time around it needs to check to see IF millis() minus the delay period is greater than the value of millis that you noted down before. IF it is, turn on the next led, note the time in millis again, note which led is now on, then continue with the rest of the program. IF not, do nothing and carry on with the rest of the code. Notice how that's all IF statements.

You'll need to replace every delay with that sort of code. Each one will need it's own variables to save the time in millis() and where it's up to.

So I guess my issue is more so with understanding how to call the neopatterns that are setup with millis() the same way I would call the colorwipe functions I have shown above. The above code works perfect, except of course that the rings do their colorwipes one after the other instead of at the same time. I'm trying to modify the example part 3 of Adafruit's multitasking tutorial and for the life of me can't figure out how to get it to start with leds off, call a colorwipe to green when motion is detected, and to colorwipe back to off when motion stops.



// Pattern types supported:
enum  pattern { NONE, RAINBOW_CYCLE, THEATER_CHASE, COLOR_WIPE, SCANNER, FADE };
// Patern directions supported:
enum  direction { FORWARD, REVERSE };

// NeoPattern Class - derived from the Adafruit_NeoPixel class
class NeoPatterns : public Adafruit_NeoPixel
{
    public:

    // Member Variables:  
    pattern  ActivePattern;  // which pattern is running
    direction Direction;     // direction to run the pattern
    
    unsigned long Interval;   // milliseconds between updates
    unsigned long lastUpdate; // last update of position
    
    uint32_t Color1, Color2;  // What colors are in use
    uint16_t TotalSteps;  // total number of steps in the pattern
    uint16_t Index;  // current step within the pattern
    
    void (*OnComplete)();  // Callback on completion of pattern
    
    // Constructor - calls base-class constructor to initialize strip
    NeoPatterns(uint16_t pixels, uint8_t pin, uint8_t type, void (*callback)())
    :Adafruit_NeoPixel(pixels, pin, type)
    {
        OnComplete = callback;
    }
    
    // Update the pattern
    void Update()
    {
        if((millis() - lastUpdate) > Interval) // time to update
        {
            lastUpdate = millis();
            switch(ActivePattern)
            {
                case RAINBOW_CYCLE:
                    RainbowCycleUpdate();
                    break;
                case THEATER_CHASE:
                    TheaterChaseUpdate();
                    break;
                case COLOR_WIPE:
                    ColorWipeUpdate();
                    break;
                case SCANNER:
                    ScannerUpdate();
                    break;
                case FADE:
                    FadeUpdate();
                    break;
                default:
                    break;
            }
        }
    }
  
    // Increment the Index and reset at the end
    void Increment()
    {
        if (Direction == FORWARD)
        {
           Index++;
           if (Index >= TotalSteps)
            {
                Index = 0;
                if (OnComplete != NULL)
                {
                    OnComplete(); // call the comlpetion callback
                }
            }
        }
        else // Direction == REVERSE
        {
            --Index;
            if (Index <= 0)
            {
                Index = TotalSteps-1;
                if (OnComplete != NULL)
                {
                    OnComplete(); // call the comlpetion callback
                }
            }
        }
    }
    
    // Reverse pattern direction
    void Reverse()
    {
        if (Direction == FORWARD)
        {
            Direction = REVERSE;
            Index = TotalSteps-1;
        }
        else
        {
            Direction = FORWARD;
            Index = 0;
        }
    }
    
    // Initialize for a ColorWipe
    void ColorWipe(uint32_t color, uint8_t interval, direction dir = FORWARD)
    {
        ActivePattern = COLOR_WIPE;
        Interval = interval;
        TotalSteps = numPixels();
        Color1 = color;
        Index = 0;
        Direction = dir;
    }
    
    // Update the Color Wipe Pattern
    void ColorWipeUpdate()
    {
        setPixelColor(Index, Color1);
        show();
        Increment();
    }

void Ring1Complete();
void Ring2Complete();

// Define some NeoPatterns for the two rings
//  as well as some completion routines
NeoPatterns Ring1(16, 11, NEO_GRB + NEO_KHZ800, &Ring1Complete);
NeoPatterns Ring2(16, 12, NEO_GRB + NEO_KHZ800, &Ring2Complete);

void LightsUp()
{
  Ring1.ColorWipe((0,255,0),66,FORWARD);
  Ring2.ColorWipe((0,255,0),66,REVERSE);
}

void LightsOut()
{
  Ring1.ActivePattern = COLOR_WIPE;
  Ring1.Color1 = (0,0,0);
  Ring2.ColorWipe((0,0,0),66,FORWARD);


}

// Initialize everything and prepare to start
void setup()
{
  Serial.begin(9600);
  pinMode(PIRmotion1, INPUT); // declare sensor as input
  pinMode(PIRmotion2, INPUT); // declare sensor as input
  pinMode(11, OUTPUT);//define arduino pin
  pinMode(12, OUTPUT);//define arduino pin
  Serial.println("startup");

    
 // Initialize all the pixelStrips
 Ring1.begin();
 Ring2.begin();
}

// Main loop
void loop()
{
    // Update the rings.
  
    
  
    PIRval1 = digitalRead(PIRmotion1);
    PIRval2 = digitalRead(PIRmotion2);
    if(PIRval1 == HIGH || PIRval2 == HIGH)
    {
      Ring1.ColorWipe((0,255,0),66,FORWARD);
      Ring2.ColorWipe((0,255,0),66,REVERSE);
      Ring1.Update();
      Ring2.Update();  
      if(PIRstate == LOW)
      {
        Serial.println(F("Motion Detected!"));
        PIRstate = HIGH; //update to HIGH   
      }
    }

    else // Back to normal operation
    {
     LightsOut();
     if(PIRstate == HIGH)
      {
        Serial.println("Motion stopped!");
        PIRstate = LOW;
      }
    }    
}

//------------------------------------------------------------
//Completion Routines - get called on completion of a pattern
//------------------------------------------------------------

// Ring1 Completion Callback
void Ring1Complete()
{
      Ring1.Reverse();
}

// Ring 2 Completion Callback
void Ring2Complete()
{
  Ring2.Reverse();
}

as you can see from my alternate code I just posted, I'm really trying to figure out how to start off with no leds on and thus no active pattern, then run the patterns when either of the pir sensors triggers from motion detected, then to run a colorwipe to lights off when motion has stopped.

You can use x.clear()

The list of functions:

begin()
updateLength()
updateType()
show()
delay_ns()
setPin()
setPixelColor()
fill()
ColorHSV()
getPixelColor()
setBrightness()
getBrightness()
clear()
gamma32()

From the library:

Changing from blocking to non-blocking is not that complicated once you know how to approach it

To keep track of time, you need a variable to remember the last time that something happened.
You also need a variable to keep track of the pixel that you want to update.

void colorWipe_l(uint32_t c, uint8_t wait) {
  // last time that a pixel was updated
  static uint32_t lastUpdateTime;
  // pixel to update (was i in for-loop)
  static uint16_t cnt;
  

  for(uint16_t i=0; i<EYE_l.numPixels(); i++) {
    EYE_l.setPixelColor(i, c);
    EYE_l.show();
    delay(wait);
  }
}

You can now take the for-loop out

void colorWipe_l(uint32_t c, uint8_t wait) {
  // last time that a pixel was updated
  static uint32_t lastUpdateTime;
  // pixel to update (was i in for-loop)
  static uint16_t cnt;
  
  EYE_l.setPixelColor(cnt, c);
  EYE_l.show();
  delay(wait);

  // update the count
  cnt++;
  // if we've reached the end
  if(cnt == numPixels)
  {
    cnt = 0;
  }
}

And in the last step implement the timing

bool colorWipe_l(uint32_t c, uint8_t wait) {
  // last time that a pixel was updated
  static uint32_t lastUpdateTime;
  // pixel to update (was i in for-loop)
  static uint16_t cnt;

  if(millis() - lastUpdateTime >= wait)
  {
    // remember the last time that a pixel was updated
    lastUpdateTime = millis();
    EYE_l.setPixelColor(cnt, c);
    EYE_l.show();

    // update the count
    cnt++;
    // if we've reached the end
    if(cnt == numPixels)
    {
      cnt = 0;
      return true;
    }
  }
  return false;
}

You will have to call colorWipe() regularly from loop().

Note that the last version returns a bool to indicate if one complete cycle was done.

void loop()
{
  if (colorWipe(....) == true) // for you to fill in the arguments
  {
    Serial.println(F("colorWipe() cycle complete"));
  }
}

Code not compiled.