Replace delay() with milllis()

Hi everyone! I have some questions about applying millis() function to my code

#include "MegunoLink.h"
#include "CommandHandler.h"

unsigned long s1 = 4;            // Variable time multiplier
unsigned long s2 = 1;
unsigned long s3 = 3;

byte v1 = 2;                     // Output pins
byte v2 = 3;
byte v3 = 4;

CommandHandler<3, 90, 32> SerialCommandHandler;

InterfacePanel MyPanel;

void setup()
{
  Serial.begin(9600);
  pinMode(v1, OUTPUT);
  pinMode(v2, OUTPUT);
  pinMode(v3, OUTPUT);
  SerialCommandHandler.AddCommand(F("Start"), Cmd_Start);

}

void Cmd_Start(CommandParameter &Parameters)
{
  Serial.println(F("the command is handled"));
  
   if (s1>0) {
   digitalWrite(v1, HIGH);
   delay(s1*1000);
   digitalWrite(v1, LOW);
   }
    if (s2>0) {
   digitalWrite(v2, HIGH);
   delay(s2*1000);
   digitalWrite(v2, LOW);
   }
    if (s3>0) {
   digitalWrite(v3, HIGH);
   delay(s3*1000);
   digitalWrite(v3, LOW);
   }
}
void loop()
{
   SerialCommandHandler.Process();
}

The code works perfectly as it should however I need ta add a analog sensor reading and other functions. and that won't work with delay(). Can someone please give some example on how to do that? The Blink Without Delay seems won't apply to my problem since it's running in loop. my function is executed only one time when it's called. and also it has multiple outputs and they need to execute in the right order. Any advice would be appreciated!

Welcome to the forum

Take a look at Using millis() for timing. A beginners guide - #5

Blink without delay endlessly repeats because the statement that restarts the timing is within the timing conditional code meaning that it restarts as soon as it ends.

To start the timing code only once or on command simply move the code to start the timing into another conditional statement.

/*
   Blink Once Without Delay()

   Perform an action for a period of time based on Input.

   Turns On a LED for 3 seconds when a character is recieved from the Serial Monitor 
   without using the delay() function.  This means that other code can run at the
   same time without being interrupted by the LED code.
*/

// All variables used with millis() must to be unsigned long
unsigned long interval = 3000;   // length of time in milliseconds for LED to remain on
unsigned long startMillis = 0;   // Stores the millis() value when the timer is started
unsigned long currentMillis = 0; // Stores the current time

int ledState = LOW; // Tracks the current state of the LED
int inByte = 0;     // incoming serial byte

void setup() {
  
  // Set the Builtin LED to Output
  pinMode(LED_BUILTIN, OUTPUT);
  
  // Start serial port at 9600 bps:
  Serial.begin(9600);

  while (!Serial) {
    ; // wait for serial port to connect.
  }

  // Print a message 
  Serial.println("Please Type in Any character and press Send to turn LED On");
  Serial.println("");

}

void loop() {
  // Record the current time
  unsigned long currentMillis = millis();

  // if we recieve a valid byte
  if (Serial.available() > 0) {
    inByte = Serial.read();  // Record the incoming byte
  }

  // If any data has been recieved
  if (inByte > 0) {
    startMillis = millis(); // Start/Restart the Timer by recording the starting time
    ledState = HIGH;        // Set ledState to turn on LED
    inByte = 0;             // Reset inByte to be ready for next loop

    // Print a message
    Serial.println("Byte recieved, The LED will now stay on for 3 sec.");
    Serial.print ("Starting Seconds = ");
    Serial.println(currentMillis / 1000); // Convert to Seconds before printing
    Serial.println("");
  }

  // If timer has expired - Note: Using subtration prevents problems at rollover
  if (currentMillis - startMillis >= interval) {
    
    // Only If LED is On - This prevents repeating these actions more than once  
    if (ledState == HIGH) {
      
      ledState = LOW; // Set ledState to turn off LED

      // Print a message
      Serial.println("The LED has now turned off");
      Serial.print ("Ending Seconds   = ");
      Serial.println(currentMillis / 1000); // Convert to Seconds before printing
      Serial.println("");
    }
  }

  // Turn On/Off LED according to ledState:
  digitalWrite(LED_BUILTIN, ledState);
}

Does it? What if a serial command arrives during your 8 second flashing sequence? If that is not a problem now, perhaps you don't need to use millis()?

Please explain why this sensor and your other functions are not compatible with delay().

We need to understand your answers so we can give you the most suitable advice, not something unnecessarily overcomplicated.

Hi. I will add a sensor and if the value exceeds I will have to stop the function. the sensor value will be displayed via serial. As i know both of these things will stop for the delay period. Or not? Maybe I have misunderstood something?

What function? You mean Cmd_Start()?

How often do you need to read the sensor?

Replacing delay() with millis() will be quite complex for you at this stage of your experience. It will probably require coding a state machine. But there may be a way to achieve this without that complexity. But I can't be sure without knowing more. So please try to answer all my questions and give more detailed answers.

The sensor value will be updated every 200ms. If the value exceeds I want to stop the Cmd_Start() with a serial command.

This Tutorial explains blocking code from non-blocking code in common sense with code.

Years ago I wrote examples to show how to un-delay code with delays and make it sketch-combine compatible. The actual conversion is cut-and-dry techniques that let the non-delay code lines (what ran a GSM in one case.. 13 steps of it worked before, it works after too) copy straight through and run like no other code is in the sketch.

The Tutorial should explain enough to make sense of this ton-of-comments undelay-combine-sketches example.

[code]

// CombineSketchesDemo 2022 by GoForSmoke @ Arduino.cc Forum
// Free for use, May 23, 2022 by GFS. Compiled on Arduino 2.1.0.5
// Free to post anywhere complete and as is.

// This sketch shows a general method to get rid of delays in code.
// You could upgrade code that delays to combine into this sketch.

// .. adding delays in loop cases
// revisions with forum help:
// dlloyd --  added state enums May 11. <--- changed as needed.

////////////////////////////////////////////////////////////////////////////

// This example/demo takes 3 sketches that use delay() and COMBINE THEM
// by using simple techniques the sketch demonstrates.
// Plus there's a user stop/go that violates how the 3 together work.

// Code that does not sit or loop in one spot to wait, 
// does not block other code from executing during a wait.
// In 1 ms, code can use or lose 16000 cpu cycles. So don't block!

// The 3 delay-sketches void loop() code goes into 3 functions that run as tasks. 
// Arduino void loop() runs the same functions over and over, these functions don't wait
// but instead check time and if time's not up then run the next function.
// if ( Serial.available() ) is just such a thing, not waiting around for data.

// Every delay() has a built-in timer. I replace it with a timer that only runs when set.
// The timer is first in the function so that the function can return until timeout.
// The timer only runs when the time to wait is set > 0, else the function continues.

// Where a delay was removed, the time is set and the timer runs for the next many loops.
// When the time is done, the time to wait set = 0. What was a delay() sets the timer. 

// This demo also addresses loops inside of void loop(), which can hog cycles terribly.
// It also contains a state machine, a code tool of value beyond what the demo does.
// A State Machine is code written in steps of what to do according to what's been done.
// A state variable holds what to do next time the machine runs.
// perhaps the 1st state waits for input and when it gets it the code changes state..
// to run what to do with the input. State Machines can run inside of state machines.

// task StopGo. --  Block - unBlock on a keystroke to stop serial monitor 

// task LoopCounter  --  lets you know how often void loop() ran last second.
// LoopCounter value
#define microsInOneSecond 1000000UL

// task PrintNumbers --  prints from printNum to setNum, pnWait ms apart
// PrintNumbers variables
unsigned long pnStart;
unsigned long pnWait = 1000UL; 
int setNum = 60;
int printNum = 0;

// task BlinkPattern --  blinks led13
// BlinkPattern variables
const byte ledPin = 13;
unsigned long blinkStart;
unsigned long blinkWait; 
byte indexMax = 12;
byte index;

enum blinkStates {BLINK_1_ON, BLINK_1_OFF, BLINK_2_ON, BLINK_2_OFF, BLINK_3_ON, BLINK_3_OFF};
blinkStates blinkStep; // state tracking for BlinkPattern() below


void setup()
{
  Serial.begin( 115200 );
  Serial.println( F( "\n\n\n  Combine Sketches Demo, free by GoForSmoke" ));
  Serial.println( F( "This sketch shows how to combine sketches.\n" ));
  Serial.println( F( "Press Enter to toggle monitor scrolling." ));

  pinMode( ledPin, OUTPUT );
  
  digitalWrite( ledPin, HIGH );  // 2 secs before the scroll starts
  delay( 500 );
  digitalWrite( ledPin, LOW );
  delay( 500 );
  digitalWrite( ledPin, HIGH );
  delay( 500 );
  digitalWrite( ledPin, LOW );
  delay( 500 );

  blinkStep = BLINK_1_ON;  // actual value is 0
};


/* LoopCounter -- void loop() code to count loops per second
*
*  delay( 1000 );
*  Serial.println( "1" );
*/

void LoopCounter() // tells the average response speed of void loop()
{ // inside a function, static variables keep their value from run to run
  static unsigned long count, countStartMicros; // only this function sees these

  count++; // adds 1 to count after any use in an expression, here it just adds 1.
  if ( micros() - countStartMicros >= microsInOneSecond ) // 1 second
  {
    countStartMicros += microsInOneSecond; // for a regular second
    Serial.println( count ); // 32-bit binary into decimal text = many micros
    count = 0; // don't forget to reset the counter 
  }
}


/* PrintNumbers -- void loop() code to count from one value to another with wait between.
*
* for ( printNum = 0; printNum <= setNum; printNum++ )
* {
*   Serial.print( F( "Number " ));
*   Serial.print( printNum );
*   Serial.print( F( "   Time " ));
*   Serial.println( millis() );
*   delay( pnWait );
* }
*/

// this task runs once. suppose that StopGo made it start again?

void PrintNumbers()  // a loop with a delay in it becomes...
{
  if ( setNum < 1 )  return; // how to turn this task off, setNum = 0;
  
  // This repeat timer replaces delay()  
  // start of repeat timer
  if ( millis() - pnStart < pnWait )  // wait is not over
  {
    return; // instead of blocking, the undelayed function returns
  }

  Serial.print( F( "Number " ));
  Serial.print( printNum );
  Serial.print( F( "   Time " ));
  Serial.println( millis() );
  
  pnStart += pnWait; // starting 1 sec after last start, not the same as = millis()

  if ( ++printNum >= setNum )  
  { 
    setNum = printNum = 0;
  }
}


/* BlinkPattern -- void loop() code to blink led13 using delay()
 * 
 * digitalWrite( ledPin, HIGH );   --  BLINK_1_ON
 * Serial.print( F( "BLINK_1_ON, time " ));
 * Serial.println( millis());
 * delay( 500 );
 * digitalWrite( ledPin, LOW );    --  BLINK_1_OFF
 * Serial.print( F( "BLINK_1_OFF, time " ));
 * Serial.println( millis());
 * delay( 500 );
 * for ( i = 0; i < 12; i++ )
 * (
 *   digitalWrite( ledPin, HIGH );   --  BLINK_2_ON
 *   Serial.print( F( "BLINK_2_ON, time " ));
 *   Serial.println( millis());
 *   delay( 250 );
 *   digitalWrite( ledPin, LOW );    --  BLINK_2_OFF
 *   Serial.print( F( "BLINK_2_OFF, time " ));
 *   Serial.println( millis());
 *   delay( 250 );
 * }
 * digitalWrite( ledPin, HIGH );   --  BLINK_3_ON
 * Serial.print( F( "BLINK_3_ON, time " ));
 * Serial.println( millis());
 * delay( 1000 );
 * digitalWrite( ledPin, LOW );    --  BLINK_3_OFF
 * Serial.print( F( "BLINK_3_ON, time " ));
 * Serial.println( millis());
 * delay( 1000 );
 */

void BlinkPattern()  // does the same as above without delay()
{
  // This one-shot timer replaces every delay() removed in one spot.  
  // start of one-shot timer
  if ( blinkWait > 0 ) // one-shot timer only runs when set
  {
    if ( millis() - blinkStart < blinkWait )
    {
      return; // instead of blocking, the undelayed function returns
    }
    else
    {
      blinkWait = 0; // time's up! turn off the timer and run the blinkStep case
    }
  }
  // end of one-shot timer

  // here each case has a timed wait but cases could change Step on pin or serial events.
  switch( blinkStep )  // runs the case numbered in blinkStep
  {
    case BLINK_1_ON :
    digitalWrite( ledPin, HIGH );
    Serial.print( F( "BLINK_1_ON, time " ));
    Serial.println( blinkStart = millis()); // able to set a var to a value I pass to function
    blinkWait = 500; // for the next half second, this function will return on entry.
    blinkStep = BLINK_1_OFF;   // when the switch-case runs again it will be case 1 that runs
    break; // exit switch-case

    case BLINK_1_OFF :
    digitalWrite( ledPin, LOW );
    Serial.print( F( "BLINK_1_OFF, time " ));
    Serial.println( blinkStart = millis());
    blinkWait = 500;
    blinkStep = BLINK_2_ON;
    break;

    case BLINK_2_ON :
    digitalWrite( ledPin, HIGH );
    Serial.print( F( "BLINK_2_ON, time " ));
    Serial.println( blinkStart = millis());
    blinkWait = 250;
    blinkStep = BLINK_2_OFF;
    break;

    case BLINK_2_OFF :
    digitalWrite( ledPin, LOW );
    Serial.print( F( "BLINK_2_OFF, time " ));
    Serial.println( blinkStart = millis());
    blinkWait = 250;
    // ******  this replaces the for-loop in non-blocking code.  ******
    if ( index++ < indexMax ) // index gets incremented after the compare
    {
      blinkStep = BLINK_2_ON;
    }
    else
    {
      index = 0;
      blinkStep = BLINK_3_ON;
    }  // end of how to for-loop in a state machine without blocking execution.
    break;

    case BLINK_3_ON :
    digitalWrite( ledPin, HIGH );
    Serial.print( F( "BLINK_3_ON, time " ));
    Serial.println( blinkStart = millis());
    blinkWait = 1000;
    blinkStep = BLINK_3_OFF;
    break;

    case BLINK_3_OFF :
    digitalWrite( ledPin, LOW );
    Serial.print( F( "BLINK_3_OFF, time " ));
    Serial.println( blinkStart = millis());
    blinkWait = 1000;
    blinkStep = BLINK_1_ON; // start again
    break;
  }
}


void StopGo()  // user keyboard block / unblock 
{
  if ( Serial.available() )
  {
    while ( Serial.available() )  // clearing the buffer
    {
      Serial.read();
      delay(1); // don't care
    }
  }
  else
  {
    return;
  }
  
  while ( ! Serial.available() ); // waiting for Go, ! is logical NOT
  
  while ( Serial.available() )  // clearing the buffer
  {
    Serial.read();
    delay(1); // don't care
  }
}

void loop()  // runs over and over, see how often
{            
  LoopCounter(); // the function runs as a task, the optimizer will inline the code.
  PrintNumbers();
  BlinkPattern();
  StopGo();
}
[/code]

look this over


const byte PinV1 = 10;                     // Output pins
const byte PinV2 = 11;
const byte PinV3 = 12;

enum { Off = HIGH, On = LOW };

void sm ();
void somethingElse ();

struct Timer {
    void (*func) (void);
    unsigned long msecPeriod;
    unsigned long msec0;
}
tmr [] {
    { sm },
    { somethingElse, 5000 },
};
const int Ntimer = sizeof(tmr)/sizeof(Timer);
unsigned long msec;

enum { T_SM, T_SE };

char s [90];

// -----------------------------------------------------------------------------
int  state = 0;

void sm ()
{
    switch (state) {
    case 0:
        digitalWrite (PinV1, On);
        tmr [T_SM].msecPeriod = 4000;
        tmr [T_SM].msec0      = msec;
        state ++;
        break;

    case 1:
        digitalWrite (PinV1, Off);
        digitalWrite (PinV2, On);
        tmr [T_SM].msecPeriod = 1000;
        state ++;
        break;

    case 2:
        digitalWrite (PinV2, Off);
        digitalWrite (PinV3, On);
        tmr [T_SM].msecPeriod = 2000;
        state ++;
        break;

    case 3:
        digitalWrite (PinV3, Off);
        tmr [T_SM].msecPeriod = 0;
        state      = 0;
        break;
    }
}

// -----------------------------------------------------------------------------
void somethingElse ()
{
    Serial.println (__func__);
}

// -----------------------------------------------------------------------------
void loop ()
{
    msec = millis ();

    for (int n = 0; n < Ntimer; n++)  {
        if (tmr[n].msecPeriod && (msec - tmr[n].msec0 >= tmr[n].msecPeriod))  {
            tmr [n].msec0 = msec;
            tmr [n].func ();
        }
    }

    if (Serial.available ()) {
        char c = Serial.read ();
        if (0 == state)
            sm ();
    }
}

// -----------------------------------------------------------------------------
void setup ()
{
    Serial.begin (9600);

    pinMode (PinV1, OUTPUT);
    pinMode (PinV2, OUTPUT);
    pinMode (PinV3, OUTPUT);

    digitalWrite (PinV1, Off);
    digitalWrite (PinV2, Off);
    digitalWrite (PinV3, Off);
}

This example is easier to just use before reading the code, it shows the difference between the same actions done with delay or with timer code.

With delay, everything happens in serial, one AFTER the other.

With timers, everything happens in parallel.. as if each task runs by itself because It Does!

Be sure to know how to Pause/Unpause and switch between modes. The code itself shows many parts that all work together in Timer Mode and even show how many times void loop() has run every second — a number that may take a while to grasp… 10’s of times per millisecond is a little hard to accept at first.

[code]
// DelayOrNoDelayWithLoopCount V1.1 by GoForSmoke  6/21/25 --
// replacing jumper for mode selection with serial commands
// cleaned up a lot of variable names and comments
// make sure that your Serial Monitor only sends \n or \r end of line!
// DelayOrNoDelayWithLoopCount V1 by GoForSmoke  6/20/25 --
// LoopCount sketch lines added to run like the Status Blink as a parallel Task
// Serial Monitor output improved and more verbose -- Uno speed up to 64K range.
// DelayOrNoDelay V1 by GoForSmoke  6/3/25 -- some changes to...
// DualActionDelayMillis v1.1 by GoForSmoke 11/18/24 -- made for Uno R3
// expect: ground pin 7 to run delay mode, not grounded runs timer mode
// expect: enter key in Serial Monitor to pause action, unpause action
// code revised 2/7/25 -- now using arrays and function calls + status led

// note that Arduino millis is +/-1 and that printing takes time as well!

// line taken from my 2024 NoBlockLoopCounterLT sketch.
extern volatile unsigned long timer0_millis; // might be faster than millis()
// end line taken


const byte statusBlinkPin = 13; // Uno board LED pin13 to run as status led
byte statusBlinkState = 0;  // led13 0=OFF, not-0=ON
unsigned long statusBlinkStart;
const unsigned int  statusBlinkInterval = 500;

const byte tasks = 2;
byte mode = 1; // 0 runs delay mode, 1 runs time mode
const unsigned long interval[ tasks ] = { 3000000, 700000 };   // for 2 timings, both modes wait time
unsigned long start[ tasks ];                            // for 2 timings, timer mode
byte started[ tasks ] = { 0, 0 };
byte task = 0;

byte pause; // paused if 1


void usage()
{
  Serial.println( F( "\n\n    Dual Action Delay Millis  2/7/25 \n" ));  // now shows version
  Serial.println( F( "    Enter D or d in Serial monitor to run delay mode." ));
  Serial.println( F( "    Enter T or t in Serial monitor to run time mode." ));
  Serial.println( F( "    Enter P or p in Serial monitor to pause data scrolling." ));
  Serial.println( F( "    While paused, P, p, or Enter will end pause." ));
  Serial.println( F( "    Loop count will print, status led13 blink, in Time Mode only." ));
}

// function taken from my 2024 NoBlockLoopCounterLT sketch.
void LoopCounter() // tells the average response speed of void loop()
{ // inside a function, static variables keep their value from run to run
  static unsigned long count; // only this function sees this
  static bool lastBit10Set; // only this function sees this
  word millis16 = timer0_millis;

  count++; // adds 1 to count after any use in an expression, here it just adds 1.

  bool currentBit10Set = millis16 & 0x0400; // leverage integral to bool implicit promotion
  if (currentBit10Set != lastBit10Set) // 1 second
  {
    //    Serial.print( millis16 ); // 16-bit binary into decimal text, many micros
    //    Serial.write('\t');
    Serial.print( F( "Loops " )); // added for demo, not from the taken code
    Serial.println( count ); // 32-bit binary into decimal text, load of cycles!
    count = 0; // don't forget to reset the counter
    lastBit10Set = currentBit10Set;
  }
}
// end function taken

void setup()
{
  Serial.begin( 115200 ); // run serial fast to clear the output buffer fast
  // set Serial Monitor to match

  pinMode( statusBlinkPin, OUTPUT ); // LOW by default
  statusBlinkState = 0;
  statusBlinkStart = millis();

  usage();
}


void SerialEntry()  // change mode or stop the scrolling to allow highlight and copy (ctrl-C) for paste (ctrl-V)
{
  char ch;
  if ( Serial.available())
  {
    switch ( ch )
    {
      case 'D' :  // set delay mode
      case 'd' :
        mode = 0;
        break;

      case 'T' :  // set time mode
      case 't' :
        mode = 1;
        break;

      case 'P' :  // pause toggle
      case 'p' :
        if ( pause == 1 ) pause = 0;
        else              pause = 2; // since after the P there's a \n
        break;

      case '\n' :     // make sure that your Serial Monitor only sends \n or \r end of line!
      case '\r' :
      if ( pause > 0 ) pause--;
    }
    started[ 0 ] = started[ 1 ] = 0; // re-init timer mode
  }
}

// this code came from an older sketch
void statusBlinker()  // will not run properly in delay mode
{
  if ( millis() - statusBlinkStart >= statusBlinkInterval )
  {
    statusBlinkState = !statusBlinkState;
    digitalWrite( statusBlinkPin, statusBlinkState );
    statusBlinkStart += statusBlinkInterval;
  }
}
// end older sketch code

void delayModeTiming( )
{
  Serial.print( F( "Delay " ));
  Serial.print( task );
  Serial.write( ' ' );
  Serial.print( interval[ task ] / 1000 );
  Serial.print( F( " ms  now: " ));
  Serial.println( millis());

  delayMicroseconds( interval[ task ] );

  Serial.print( F( "End Delay " ));
  Serial.print( task );
  Serial.print( F( " End time " ));
  Serial.println( millis());
}

void timerModeTiming( ) // uses micros timing for closer precision, but shows millis time!
{
  if ( started[ task ] == 0 ) // initialize timer only prints info line at the start
  {
    started[ task ] = 1;  // locked
    Serial.print( F( "Timer " ));
    Serial.print( task );
    Serial.print( F( " Wait " ));
    Serial.print( interval[ task ] / 1000 );
    Serial.print( F( " ms  now: " ));
    Serial.println( millis());
    start[ task ] = micros(); // loaded
    return;
  }

  if ( micros() - start[ task ] >= ( interval[ task ] ))
  {
    started[ task ] = 0;
    Serial.print( F( "Timer " ));
    Serial.print( task );
    Serial.print( F( " End time " ));
    Serial.println( millis());
  }
}

void loop()
{
  if ( mode == 1 ) // time mode
  {
    LoopCounter(); // the function runs as a task.
  }

  if ( mode == 1 ) // run time mode
  {
    timerModeTiming( );
  }
  else
  {
    delayModeTiming( );
  }

  // running 1 timer per loop, there could be many so just 1 per
  task = !task; // ! changes 0 to 1, ! changes 1 to 0, ! is NOT
  // started[ task ] and interval[ task ] control the action

  if ( mode == 1 ) // run status blinker
  {
    statusBlinker();
  }

  SerialEntry();
}
[/code]

Here is a template for all timed events...

unsigned long timer, timeout = 200; // event timer for 200ms

void setup() {
  Serial.begin(115200);
}

void loop() {
  if (millis() - timer > timeout) { // verify event time slot
    timer = millis(); // reset timer for next event
    Serial.print(1); // send a command or call a function
  }
}

Here is an example multiple timed events, each with separate timeouts...

Here is an example of multiple timed events combined with a state machine...

Another :wink:

#define every(dt) \
    for (static uint32_t t = millis(), now = 0; \
         (now = millis()) - t >= (dt); \
         t = now)
void loop() {
    every(1000){
        // do something
    }
}

Eventually (for a precise period)...

#define every(dt) \
    for (static uint32_t t = millis(), now = 0; \
         (now = millis()) - t >= (dt); \
         t += (dt))

This tutorial might help you with your task: Process Serial Commands with an Arduino - Getting started - MegunoLink

with a state machine ?

#include "MegunoLink.h"
#include "CommandHandler.h"

unsigned long s1 = 4;
unsigned long s2 = 1;
unsigned long s3 = 3;

byte v1 = 2;
byte v2 = 3;
byte v3 = 4;

CommandHandler<3, 90, 32> SerialCommandHandler;

InterfacePanel MyPanel;

enum OutputState { IDLE, ON, OFF };

struct OutputControl {
  byte pin;
  unsigned long duration;
  OutputState state;
  unsigned long startTime;
};

OutputControl outputs[3];

void Cmd_Start(CommandParameter &Parameters) {
  Serial.println(F("the command is handled"));
  for (int i = 0; i < 3; i++) {
    if (outputs[i].duration > 0) {
      outputs[i].state = ON;
      outputs[i].startTime = millis();
      digitalWrite(outputs[i].pin, HIGH);
    }
  }
}

void setup() {
  Serial.begin(9600);

  outputs[0] = { v1, s1 * 1000, IDLE, 0 };
  outputs[1] = { v2, s2 * 1000, IDLE, 0 };
  outputs[2] = { v3, s3 * 1000, IDLE, 0 };

  for (int i = 0; i < 3; i++) {
    pinMode(outputs[i].pin, OUTPUT);
    digitalWrite(outputs[i].pin, LOW);
  }

  SerialCommandHandler.AddCommand(F("Start"), Cmd_Start);
}



void loop() {
  SerialCommandHandler.Process();

  unsigned long currentMillis = millis();
  for (int i = 0; i < 3; i++) {
    if (outputs[i].state == ON && currentMillis - outputs[i].startTime >= outputs[i].duration) {
      digitalWrite(outputs[i].pin, LOW);
      outputs[i].state = OFF;
    }
  }
}

HI @k2tm !

Here a version that uses arrays to hold the multiplier and pin data and uses booleans to control the led functions. In addition there is a function “doSomethingElseAllTheTime()” that handles a separate led to demonstrate the non blocking functionality.

The commands “Start” and “Stop” are implemented.

Feel free to have a look:

https://wokwi.com/projects/451598397933631489

Sketch
/*
  Forum: https://forum.arduino.cc/t/replace-delay-with-milllis/1421640
  Wokwi: https://wokwi.com/projects/451598397933631489

  2025/12/28
  ec2021

  Example using arrays and boolean variables to control the function handleLeds()

  SerialCommand library used for this example:
  https://www.arduinolibraries.info/libraries/serial-command-advanced


*/

#include <SerialCommand.h>

SerialCommand SerialCommandHandler;

constexpr int noOfItems = 3;
constexpr unsigned long multiplier[noOfItems] {4, 1, 3};
constexpr byte pin[noOfItems] {2, 3, 4};

boolean startCmdReceived = false;
boolean stopCmdReceived = false;

void setup()
{
  Serial.begin(115200);
  SerialCommandHandler.addCommand("Start", Cmd_Start);
  SerialCommandHandler.addCommand("Stop", Cmd_Stop);
  SerialCommandHandler.setDefaultHandler(unrecognized);
  Serial.println("Begin");
  for (int i = 0; i < noOfItems; i++) {
    pinMode(pin[i], OUTPUT);
  }
}

void loop()
{
  SerialCommandHandler.readSerial();
  handleLeds();
  doSomethingElseAllTheTime(500);
}

// Commands Start and Stop
// set boolean variables to true
// that enable the required functionality

void Cmd_Start()
{
  Serial.println(F("The Start command is handled"));
  startCmdReceived = true;
}

void Cmd_Stop()
{
  Serial.println(F("The Stop command is handled"));
  stopCmdReceived = true;
}

// Invalid commands leave a message
void unrecognized(const char *command) {
  Serial.println("Invalid command ..");
}

// The function handleLeds()
// handles Start and Stop command
// On Stop command
//    * all leds are switched off
//    * the local static variables are set to their start values
//    * the booleans that control handleLeds() are set to false
// On Start command
//   * the items are handled according to the index itemNo
//   * in the first loop the item is switched on and the recent time is stored
//   * in further loops, when the delay time has expired, everything is prepared
//     for the next item
//   * if all items have been handled, itemNo is set to 0 and startCmdReceived
//     is cleared (set to false)


void handleLeds() {
  static byte itemNo = 0;
  static unsigned long switchOnTime = 0;
  static boolean isSwitchedOff = true;
  if (stopCmdReceived) {
    for (int i = 0; i < noOfItems; i++) {
      digitalWrite(pin[i], LOW);
    }
    itemNo = 0;
    startCmdReceived = false;
    isSwitchedOff = true;
    stopCmdReceived = false;
    return;  // Not really necessary in this case as the rest of the function depends on startCmdReceived = true
  }
  if (startCmdReceived) {
    if (isSwitchedOff) {
      digitalWrite(pin[itemNo], HIGH);
      isSwitchedOff = false;
      switchOnTime = millis();
    }
    if (millis() - switchOnTime > multiplier[itemNo] * 1000) {
      digitalWrite(pin[itemNo], LOW);
      isSwitchedOff = true;
      itemNo++;
    }
    if (itemNo >= noOfItems) {
      itemNo = 0;
      startCmdReceived = false;
    }
  }
}

// The following function blinks the blue led
// to verify that handleLeds() is not blocking
// loop()

void doSomethingElseAllTheTime(unsigned long interval) {
  constexpr byte bluePin {5};
  static unsigned long lastChange = 0;
  if (lastChange == 0) pinMode(bluePin, OUTPUT);
  if (millis() - lastChange > interval) {
    lastChange = millis();
    byte state = digitalRead(bluePin);
    digitalWrite(bluePin, !state);
  }
}

Just be aware that the CommandHandler used in the Wokwi script is different from the one you use in your sketch ... However it will not be difficult to adjust the affected lines.

Good luck and have fun!
ec2021

Thanks to you all for helping. Sorry for my late response. @ec2021 Your code works excellent. The only thing I could not solve yet is that I have variables in the output and in time multipliers and I wanted to change them with serial command. So I removed the "const". But when I send the serial command with the new values they don't change. Looks like the handleLeds() function reads the values only once. Any ideas?
Thank you!

Could you post the code you used and also explain what you type in and expect?

Just follow @J-M-L 's suggestion...

In the meantime I added some commands that show how to change the values of e.g. an integer, and unsigned long and a boolean variable via SerialCommandHandler:

On Wokwi: https://wokwi.com/projects/453605316898722817

/*
  Forum: https://forum.arduino.cc/t/replace-delay-with-milllis/1421640
  Wokwi: https://wokwi.com/projects/453605316898722817

  Based on previous version:
         https://wokwi.com/projects/451598397933631489

  2026/01/19
  ec2021

  Example using arrays and boolean variables to control the function handleLeds()

  Added variables varInt, varBool and blinkInterval and commands to change their values

  SerialCommand library used for this example:
  https://www.arduinolibraries.info/libraries/serial-command-advanced


*/

#include <SerialCommand.h>

SerialCommand SerialCommandHandler;

constexpr int noOfItems = 3;
constexpr unsigned long multiplier[noOfItems] {4, 1, 3};
constexpr byte pin[noOfItems] {2, 3, 4};

boolean startCmdReceived = false;
boolean stopCmdReceived = false;

int varInt = 0;
boolean varBool  = false;
unsigned long blinkInterval = 500;

void setup()
{
  Serial.begin(115200);
  SerialCommandHandler.addCommand("Start", Cmd_Start);
  SerialCommandHandler.addCommand("Stop", Cmd_Stop);
  SerialCommandHandler.addCommand("Int=", Cmd_Int);
  SerialCommandHandler.addCommand("Bool=", Cmd_Bool);
  SerialCommandHandler.addCommand("Blink=", Cmd_BlinkInterval);
  SerialCommandHandler.setDefaultHandler(unrecognized);
  Serial.println("Begin");
  for (int i = 0; i < noOfItems; i++) {
    pinMode(pin[i], OUTPUT);
  }
}

void loop()
{
  SerialCommandHandler.readSerial();
  handleLeds();
  doSomethingElseAllTheTime(blinkInterval);
}

// Commands Start and Stop
// set boolean variables to true
// that enable the required functionality

void Cmd_Start()
{
  Serial.println(F("The Start command is handled"));
  startCmdReceived = true;
}

void Cmd_Stop()
{
  Serial.println(F("The Stop command is handled"));
  stopCmdReceived = true;
}

void Cmd_Int()
{
  char *arg;
  arg = SerialCommandHandler.next();
  if (arg != NULL) {
    varInt = atoi(arg);    // Converts a char string to an integer
    Serial.print("varInt = ");
    Serial.println(varInt);
  }
}

void Cmd_BlinkInterval()
{
  char *arg;
  arg = SerialCommandHandler.next();
  if (arg != NULL) {
    blinkInterval = atol(arg);    // Converts a char string to a (signed) long int 
    Serial.print("Blink Interval = ");
    Serial.println(blinkInterval);
  }
}

void Cmd_Bool()
{
  char *arg;
  arg = SerialCommandHandler.next();
  if (arg != NULL) {
    if (strstr(arg, "true") != NULL) varBool = true;
    if (strstr(arg, "false") != NULL) varBool = false;
    Serial.print("varBool = ");
    Serial.print(arg);
    Serial.print(" ");
    Serial.println(varBool ? "true" : "false");
  }
}


// Invalid commands leave a message
void unrecognized(const char *command) {
  Serial.println("Invalid command ..");
}

// The function handleLeds()
// handles Start and Stop command
// On Stop command
//    * all leds are switched off
//    * the local static variables are set to their start values
//    * the booleans that control handleLeds() are set to false
// On Start command
//   * the items are handled according to the index itemNo
//   * in the first loop the item is switched on and the recent time is stored
//   * in further loops, when the delay time has expired, everything is prepared
//     for the next item
//   * if all items have been handled, itemNo is set to 0 and startCmdReceived
//     is cleared (set to false)


void handleLeds() {
  static byte itemNo = 0;
  static unsigned long switchOnTime = 0;
  static boolean isSwitchedOff = true;
  if (stopCmdReceived) {
    for (int i = 0; i < noOfItems; i++) {
      digitalWrite(pin[i], LOW);
    }
    itemNo = 0;
    startCmdReceived = false;
    isSwitchedOff = true;
    stopCmdReceived = false;
    return;  // Not really necessary in this case as the rest of the function depends on startCmdReceived = true
  }
  if (startCmdReceived) {
    if (isSwitchedOff) {
      digitalWrite(pin[itemNo], HIGH);
      isSwitchedOff = false;
      switchOnTime = millis();
    }
    if (millis() - switchOnTime > multiplier[itemNo] * 1000) {
      digitalWrite(pin[itemNo], LOW);
      isSwitchedOff = true;
      itemNo++;
    }
    if (itemNo >= noOfItems) {
      itemNo = 0;
      startCmdReceived = false;
    }
  }
}

// The following function blinks the blue led
// to verify that handleLeds() is not blocking
// loop()

void doSomethingElseAllTheTime(unsigned long interval) {
  constexpr byte bluePin {5};
  static unsigned long lastChange = 0;
  if (lastChange == 0) pinMode(bluePin, OUTPUT);
  if (millis() - lastChange > interval) {
    lastChange = millis();
    byte state = digitalRead(bluePin);
    digitalWrite(bluePin, !state);
  }
}

Check out the example on https://github.com/shyd/Arduino-SerialCommand/blob/master/examples/SerialCommandExample/SerialCommandExample.ino

to see how to handle C strings or two and more arguments in a command line.

In the Wokwi sketch the commands are

Start
Stop
Blink= 100
Blink= 2000
Blink= 500  
Int= -20
Bool= false
Bool= true

I'm using 3 commands. "!Start/r" "!Stop/r" "!SetValues 1 1 1 4 3 2/r"

/*
  Forum: https://forum.arduino.cc/t/replace-delay-with-milllis/1421640
  Wokwi: https://wokwi.com/projects/451598397933631489

  2025/12/28
  ec2021

  Example using arrays and boolean variables to control the function handleLeds()

  SerialCommand library used for this example:
  https://www.arduinolibraries.info/libraries/serial-command-advanced


*/

#include "MegunoLink.h"
#include "CommandHandler.h"

CommandHandler<5, 92, 34> SerialCommandHandler;

InterfacePanel MyPanel;

unsigned long s1 = 3;
unsigned long s2 = 1;
unsigned long s3 = 4;

int v1 = 2;
int v2 = 4;
int v3 = 3;

constexpr int noOfItems = 3;
unsigned long multiplier[noOfItems] {s1, s2, s3};
int pin[noOfItems] {v1, v2, v3};

boolean startCmdReceived = false;
boolean stopCmdReceived = false;



// Commands Start and Stop
// set boolean variables to true
// that enable the required functionality

void Cmd_Start()
{
  Serial.println(F("The Start command is handled"));
  startCmdReceived = true;
}

void Cmd_Stop()
{
  Serial.println(F("The Stop command is handled"));
  stopCmdReceived = true;
}

void Cmd_SetValues(CommandParameter &Parameters)
{
  s1 = Parameters.NextParameterAsUnsignedLong();
  s2 = Parameters.NextParameterAsUnsignedLong();
  s3 = Parameters.NextParameterAsUnsignedLong();
  v1 = Parameters.NextParameterAsInteger();
  v2 = Parameters.NextParameterAsInteger();
  v3 = Parameters.NextParameterAsInteger();
  Serial.println(F("The SetValues command is handled"));
}

// Invalid commands leave a message
void unrecognized(const char *command) {
  Serial.println("Invalid command ..");
}

// The function handleLeds()
// handles Start and Stop command
// On Stop command
//    * all leds are switched off
//    * the local static variables are set to their start values
//    * the booleans that control handleLeds() are set to false
// On Start command
//   * the items are handled according to the index itemNo
//   * in the first loop the item is switched on and the recent time is stored
//   * in further loops, when the delay time has expired, everything is prepared
//     for the next item
//   * if all items have been handled, itemNo is set to 0 and startCmdReceived
//     is cleared (set to false)


void handleLeds() {
  static byte itemNo = 0;
  static unsigned long switchOnTime = 0;
  static boolean isSwitchedOff = true;
  if (stopCmdReceived) {
    for (int i = 0; i < noOfItems; i++) {
      digitalWrite(pin[i], LOW);
    }
    itemNo = 0;
    startCmdReceived = false;
    isSwitchedOff = true;
    stopCmdReceived = false;
    return;  // Not really necessary in this case as the rest of the function depends on startCmdReceived = true
  }
  if (startCmdReceived) {
    if (isSwitchedOff) {
      digitalWrite(pin[itemNo], HIGH);
      isSwitchedOff = false;
      switchOnTime = millis();
    }
    if (millis() - switchOnTime > multiplier[itemNo] * 1000) {
      digitalWrite(pin[itemNo], LOW);
      isSwitchedOff = true;
      itemNo++;
    }
    if (itemNo >= noOfItems) {
      itemNo = 0;
      startCmdReceived = false;
    }
  }
}

// The following function blinks the blue led
// to verify that handleLeds() is not blocking
// loop()

void doSomethingElseAllTheTime(unsigned long interval) {
  constexpr byte bluePin {LED_BUILTIN};
  static unsigned long lastChange = 0;
  if (lastChange == 0) pinMode(bluePin, OUTPUT);
  if (millis() - lastChange > interval) {
    lastChange = millis();
    byte state = digitalRead(bluePin);
    digitalWrite(bluePin, !state);
  }
}
void setup()
{
  Serial.begin(9600);
  SerialCommandHandler.AddCommand(F("Start"), Cmd_Start);
  SerialCommandHandler.AddCommand(F("SetValues"), Cmd_SetValues);
  SerialCommandHandler.AddCommand(F("Stop"), Cmd_Stop);
  Serial.println("Begin");
  for (int i = 0; i < noOfItems; i++) {
    pinMode(pin[i], OUTPUT);
  }
}

void loop()
{
  SerialCommandHandler.Process();
  handleLeds();
  doSomethingElseAllTheTime(500);
}

The set values command contains the values of s1 s2 s3 v1 v2 v3 in order.

I'm using a different CommandHandler on Wokwi as I could not get the MegunoLink Command Handler to accept manual Serial input ...

Can you give some further information:

  • Do the commands Start and Stop work as intended?
  • What happens if you use SetValues? Just nothing?