Start a timer when event occurs, stop it if the event stops before timer ends?

Hi,

I'm trying to use millis() as a delay/timer which is triggered after an event happens.

I measure the weight on a sensor, when it is <125 I want to start a 7 second timer, and continue to test
. . . if at anytime during the timer weight >125 then stop the timer.

If at the end of the timer weight is still <125 then do an action.

My millis() code is attached, my problem is that the timer starts when the Arduino is reset and counts the total time - I cant figure out how to make it start only when weight is <125 . . . and then stop when weight is >125.

void loop() {
 
 if (BeerWeight < 125) {                               // check for timer trigger, if <125 check weight again
    currentMillis = millis();                          // if >125 stop and reset timer                            
    if(currentMillis - startMillis >= intervalDrink) {
    startMillis = currentMillis;
    
    isFull();                                          // function to check BeerWeight

    if (BeerWeight < 125) {                            // check BeerWeight again, if still <125 
    isEmpty();                                         // run function isEmpty (this activates an alarm)
   }
  }
}

Any suggestions much appreciated.

cul
billd

billd0139ET:

void loop() {

if (BeerWeight < 125) {                              // check for timer trigger, if <125 check weight again
    currentMillis = millis();                          // if >125 stop and reset timer                           
    if(currentMillis - startMillis >= intervalDrink) {
    startMillis = currentMillis;

The line currentMillis = millis() should be before the IF statement - like this

void loop() {
     currentMillis = millis();   
     if (BeerWeight < 125 and timerStarted == false) {      
        lowWeightStartMillis = currentMillis();
        timerStarted = true;  // so start value is not updated next time around
        if(currentMillis - lowWeightStartMillis >= intervalDrink) {
                // do whatever needs to be done
       }

The demo Several Things at a Time illustrates the use of millis() to manage timing. It may help with understanding the technique.

...R

If you need to stop and start a millis() based timing operation then make its running dependant on a boolean variable. Time only when its value is true. Stop/start the timing by changing its value

Thnx for the quick reply, much appreciated.

I added your suggestion and added some serial prints for debugging.

Even with beerweight approx zero it never gets into the millis() if() section.

void loop()
{
  timer.run();
  percentBeer();
  
  currentMillis = millis();
  if (BeerWeight < 125 && timerStarted == false) 
  {
    lowWeightStartMillis = currentMillis;
    timerStarted = true;                 // so start value is not updated next time around
    Serial.println("");
    Serial.println("Timer . . .");
    Serial.println(currentMillis);
    Serial.println(lowWeightStartMillis);
    //Serial.println("Timer . . .");
    Serial.println("");
   
    if(currentMillis - lowWeightStartMillis >= intervalDrink) {
    
    if (BeerWeight < 125)
    {
     Serial.println("");
     Serial.println("Alert . . .");
     Serial.println("");
     isEmpty();                             // function to sound buzzer
     }
   }
  }
}

Serial Monitor capture attached. The loop enters the first if(), lowWeightStartMillis and currentMillis are always the same, and timerStarted is always true.

At some point lowWeightStartMillis and currentMillis need to be different, and timerStarted reset back to false?

thnx
billd

BeerWeightCapture01.JPG

billd0139ET:
I added your suggestion and added some serial prints for debugging.

You need to post your complete program. The problem is usually in the part we can't see. :slight_smile:

...R

Thnx for your suggestions, got me in the right direction! Now resolved and working as expected.

I had to add a few tests for beerWeight, then set timerStarted as true or false depending on the position within the nested if() structures. Its probably not the most elegant code but it works and I availed delay() which was my primary goal.

void loop()
{
  timer.run();
  percentBeer();
  
  currentMillis = millis();
  if (BeerWeight < 125)
  {
    if (timerStarted == false)
    {
    lowWeightStartMillis = currentMillis;
    timerStarted = true;                 // so start value is not updated next time around
    }
    Serial.println("");
    Serial.println("Timer . . .");
    Serial.println(currentMillis);
    Serial.println(lowWeightStartMillis);
    Serial.println(timerStarted);
    Serial.println("");
   
    if(currentMillis - lowWeightStartMillis >= intervalDrink) {
    if (BeerWeight < 125)
    {
     Serial.println("");
     Serial.println(". . . Alert . . .");
     Serial.println(currentMillis - lowWeightStartMillis);
     isEmpty();                            // buzzer alert when empty
     timerStarted = false;                 // so start value reset after alarm
     Serial.println(timerStarted);
     }
    }
   }
if (BeerWeight >= 125)
   {
    timerStarted = false;                  // so start value reset if weight is >125 before alarm is needed, stop timer
   }
}

Thnx again for your help - your first tip about having currentMillis outside the first if() got me started in the right direction.
cul
billd

When I want a 1-shot timer, I use (StartTime != 0) to indicate that the timer is running.

unsigned long StartTime = 0;

void StartTimer() {
    StartTime = millis();
}

void loop() {
    if (StartTime)   // Since 0==false, this is the same as (StartTime != 0)
    {
        // The timer is running.  Check for stuff.
        unsigned long elapsedTime = StartTime - millis();
        if (elapsedTime >= MaxDuration) 
        {
            // The timer has ended!  Do the end thing!
            StartTime = 0;
        }
    }
}
    if (StartTime)   // Since 0==false, this is the same as (StartTime != 0)

Wouldn't

   if(StartTime != 0)

have been less typing?

johnwasser:
When I want a 1-shot timer, I use (StartTime != 0) to indicate that the timer is running.

When I want to make timers more manageable, I use this MillisTimer class:

/********************** MillisTimer Class Definition **********************/
constexpr uint8_t MAX_TIMER_INSTANCES = 6;

class MillisTimer {
  using funcPtr = void(*)(void);
  public:
    MillisTimer(uint32_t duration, funcPtr action = nullptr, bool autoRepeat = true) : interval(duration), callback(action), repeat(autoRepeat) {
      instances[numInstances++] = this;
    }
    static void update(void) {
      uint32_t currentMillis = millis();
      for (size_t i = 0; i < numInstances; i++) {
        if (currentMillis - instances[i]->lastMillis >= instances[i]->interval and instances[i]->state == TIMER_RUNNING) {
          instances[i]->lastMillis = currentMillis;
          if (!instances[i]->repeat) {
            instances[i]->state = TIMER_STOPPED;
          }
          if (instances[i]->callback) {
            instances[i]->callback();
          }
        }
      }
    }
    bool isRunning(void) {
      return state == TIMER_RUNNING;
    }
    void start(void) {
      lastMillis = millis();
      state = TIMER_RUNNING;
    }
    void stop(void) {
      state = TIMER_STOPPED;
    }
    void setRepeat(void) {
      repeat = true;
    }
    void setOneShot(void) {
      repeat = false;
    }
    static size_t getTimerCount(void) {
      return numInstances;
    }

  private:
    enum {
      TIMER_STOPPED,
      TIMER_RUNNING,
    }state = TIMER_STOPPED;
    uint32_t interval;
    funcPtr callback;
    bool repeat;
    uint32_t lastMillis;
    static size_t numInstances;
    static MillisTimer* instances[MAX_TIMER_INSTANCES];
};
size_t MillisTimer::numInstances = 0;
MillisTimer* MillisTimer::instances[MAX_TIMER_INSTANCES] = {nullptr};

/******************** END MillisTimer Class Definition ********************/
void toggleLed(void);

// you can use a funtion pointer as done here:
MillisTimer ledTimer(250, toggleLed);  // lack of 3rd argument indicates timer repeats (default)
// or you can just define a lambda like done here
MillisTimer serialTimer(10000, [](){
    Serial.println("Serial Timer Expired"); 
    if (ledTimer.isRunning()) {
      ledTimer.stop();  // this timer will stop the led timer after 60 seconds; comment this line to allow LED to blink indefinately
      digitalWrite(13, LOW);  // sets led to OFF
    } else {
      ledTimer.start();
    }
  }, true);  // true explicitly indicates timer repeats
//  }, false);  // false indicates timer does not repeat

void setup() {
  Serial.begin(9600);
  pinMode(13, OUTPUT);
  Serial.print(F("Timers in use:\t"));
  Serial.println(MillisTimer::getTimerCount());
  ledTimer.start();
  serialTimer.start();
}

void loop() {
  MillisTimer::update();
}

void toggleLed(void) {
  digitalWrite(13, !digitalRead(13));
}

then starting, restarting, and stopping timers becomes trivial.

Put it in a library and it really cleans up your code:

.ino

#include "MillisTimer.h"

void toggleLed(void);

// you can use a funtion pointer as done here:
MillisTimer ledTimer(250, toggleLed);  // lack of 3rd argument indicates timer repeats (default)
// or you can just define a lambda like done here
MillisTimer serialTimer(10000, [](){
    Serial.println("Serial Timer Expired"); 
    if (ledTimer.isRunning()) {
      ledTimer.stop();  // this timer will stop the led timer after 60 seconds; comment this line to allow LED to blink indefinately
      digitalWrite(13, LOW);  // sets led to OFF
    } else {
      ledTimer.start(1000);  // you can set a new interval using start()
    }
  }, true);  // true explicitly indicates timer repeats
//  }, false);  // false indicates timer does not repeat

void setup() {
  Serial.begin(9600);
  pinMode(13, OUTPUT);
  Serial.print(F("Timers in use:\t"));
  Serial.println(MillisTimer::getTimerCount());
  ledTimer.start();
  ledTimer.start();
  serialTimer.start();
}

void loop() {
  MillisTimer::update();
}

void toggleLed(void) {
  digitalWrite(13, !digitalRead(13));
}

header

#ifndef MILLISTIMER_H
#define MILLISTIMER_H

#include "Arduino.h"

constexpr uint8_t MAX_TIMER_INSTANCES = 5;

class MillisTimer {
  using funcPtr = void(*)(void);
  public:
    MillisTimer(uint32_t duration, funcPtr action = nullptr, bool autoRepeat = true);
    static void update(void);
    bool isRunning(void);
    void start(void);
    void start(uint32_t newInterval);
    void stop(void);
    void stopAll(void);
    void setInterval(uint32_t newInterval);
    void setRepeat(void);
    void setOneShot(void);
    static size_t getTimerCount(void);
    
  private:
    enum {
      TIMER_STOPPED,
      TIMER_RUNNING,
    }state = TIMER_STOPPED;
    uint32_t interval;
    funcPtr callback;
    bool repeat;
    uint32_t lastMillis;
    static size_t numInstances;
    static MillisTimer* instances[MAX_TIMER_INSTANCES];
};

#endif

implementation file:

#include "MillisTimer.h"

size_t MillisTimer::numInstances = 0;
MillisTimer* MillisTimer::instances[MAX_TIMER_INSTANCES] = {nullptr};

MillisTimer::MillisTimer(uint32_t duration, funcPtr action, bool autoRepeat) : interval(duration), callback(action), repeat(autoRepeat) {
  instances[numInstances++] = this;
}

void MillisTimer::update(void) {
  uint32_t currentMillis = millis();
  for (size_t i = 0; i < numInstances; i++) {
    if (currentMillis - instances[i]->lastMillis >= instances[i]->interval and instances[i]->state == TIMER_RUNNING) {
      instances[i]->lastMillis = currentMillis;
      if (!instances[i]->repeat) {
        instances[i]->state = TIMER_STOPPED;
      }
      if (instances[i]->callback) {
        instances[i]->callback();
      }
    }
  }
}

bool MillisTimer::isRunning(void) {
  return state == TIMER_RUNNING;
}

void MillisTimer::start(void) {
  lastMillis = millis();
  state = TIMER_RUNNING;
}

void MillisTimer::start(uint32_t newInterval) {
  interval = newInterval;
  lastMillis = millis();
  state = TIMER_RUNNING;
}

void MillisTimer::stop(void) {
  state = TIMER_STOPPED;
}

void MillisTimer::stopAll(void) {
  for (size_t i = 0; i < numInstances; i++) {
    instances[i]->stop();
  }
}

void MillisTimer::setInterval(uint32_t newInterval) {
  interval = newInterval;
}

void MillisTimer::setRepeat(void) {
  repeat = true;
}

void MillisTimer::setOneShot(void) {
  repeat = false;
}

size_t MillisTimer::getTimerCount(void) {
  return numInstances;
}
1 Like

PaulS:

    if (StartTime)   // Since 0==false, this is the same as (StartTime != 0)

Wouldn't

   if(StartTime != 0)

have been less typing?

Yes, but when writing example code for beginners I like to add comments to explain some of the shortcuts I am using so they can learn. In code for myself or other experienced C++ programmers I would write:

    if (StartTime)

(which is even less typing)

OK then, how do you handle millis() rollover? What if StartTime really is 0 ?