A simple Led Blinking class based C++ Program for beginners

(Note from JML - moved to general forum, when text ready and in perfect shape, submit for tutorials)

1. C language still exists, and I have been using it to write programs limited to 200–500 lines, targeted for low-flash-memory AVR microcontrollers such as the ATmega328P on the UNO R3 board.

2. C++ is approximately equal to "C + few more additions (the classes and others)". There are reasons why Bjarne Stroustrup thought of developing C++ which is highly an augmented version of C and yet he did not name it D Pogramming Laguage.

3. Using C, we write progam in "Procedural Method" in which the executing steps come one-after-another. In C, the variables/data are seperated from the functions which operate on data.

4. C++ allows creating sketches using the so called classes; where, the variables/data and the operating methods/functions remain together offering some kind of protection -- a feature that is not available in C.

5. A beginner always start with C. Once, he becomes proficient in C, he tries to convert his C-based programs/sketches into C++ to see the benefits of C++.

C++ motivates a programmer to treat hardware as real objects and then apply methods to modify their properties and behaviors. For example: you have a fully inflated balloon which will burst if you push a pin into it. Here:

"inflated ballonn" is an object,
"push a pin into it" is a method (C++ vocabulary)
"burst" is a eaction (output)

6. Example of a Procedural Program using C to blink onboard led-L of UNO R3 board.

#define ledL 13

void setup() 
{
   Serial.begin(9600);
   pinMode(ledL, OUTPUT);
}

void loop() 
{
    digitalWrite(Built_inLed, HIGH);
    delay(2000);
    digitalWrite(Built_inLed, LOW);
    delay(1000);
 }

7. Let us convert the sketch of Step-6 using C++ Language constructs.
(1) List the variable(s)

  • DPin number with which led-L is conneted
  • Diection of DPin line
  • On time of led-L
  • Off time of led-L

(2) List the methods (functions)

  • ioDirection()
  • ledOn()
  • ledOff()
  • timeDelay()

8. Before writing a class based sketch for the Procedural sketch of Step-6, let us get familiar with various fields of a typpical "Class Structure" (Fig-1) that is created using class keyword.


Fgure-1:

9. Now let us see how I have creted below a simple "class structure" using class keyword. I have followed the template (Fig-1) of Step-8. (I always use three spaces for indentation.)

class SimpleLED
{
   private:
      byte DPin;
      byte direction;
      unsigned long onTime;
      unsigned long offTime;

   public:
      SimpleLED(byte pin, byte dir, unsigned long onT, unsigned long offT);
      void ioDirection();
      void ledOn();
      void ledOff();
      void timeDelay(unsigned long delayTime);
};

SimpleLED ledL(13, OUTPUT, 2000, 1000);   //object ledL is created 

Note-1: Only member functions are allowed to access (performing read/write operations) on the private variables.

Note-2: Any function outside even the loop() function that is outside of class has no right to access any private variables of that class.

Note-3: Any function outside the class has the right o access the member functions.

Note-4: Member functions can access each other.

Note-5: Relationship among object, method, member operator, and function (Fig-2):

image
Figure-2:

Q: Note that the number of variables (4) agrees with Step-7 except that number of methods is 5. Why is there one more method (SimpleLed()) that has been added?
A: It is the rule of C++ that it adds one constructor() function with the same name as the "Class name" is. The constructor() function is automatically called upon for execution when an object is created as we will see in Step-10.

10. Now, we have to write defining codes for the methods and then integrate them with the setup() and loop() functions of Arduio Sketch.
(1) Object creation/declaration

SimpleLED ledL(13, OUTPUT, 2000, 1000);   //object ledL is created 

(2) Defning codes of SimpleLED() method

SimpleLED::SimpleLED(byte pin, byte dir, unsigned long onT, unsigned long offT)
{
   DPin      = pin;
   direction = dir;
   onTime    = onT;
   offTime   = offT;
}

Note-1: The name of the construction() function is always be the name of the class name. the constructor function does not return any value and not even void.

Note-2: The value of the first argument (13) of the led1 object is passed to the pin parameter of the SimpleLED() constructor, and from there it is assigned to the private member variable DPin. The value 13 cannot be assigned directly to a private variable; it must be passed through a public method/member function (the constructor). The same applies to the other arguments: OUTPUT, 2000, and 1000 of the object.

(3) Defining codes for ioDirecton() method.

void SimpleLED::ioDirection() //:: says ioDirection() belongs to SimpleLed class
{
  pinMode(DPin, direction);   //pinMode(13, OUTPUT);
}

Note-1: The double colon (::) is called "Scope Resolution Operator." It syas to the compiler that the method to its right (ioDirection()) belongs to the class that is located at its left (SimpleLED).

(4) Defining codes for ledOn() method.

void SimpleLED::ledOn()
{
  digitalWrite(DPin, HIGH);
}

(5) Defining codes for ledOff() method.

void SimpleLED::ledOff()
{
  digitalWrite(DPin, LOW);
}

(6) Defining codes for timeDelay() method.

void SimpleLED::timeDelay(unsigned long delayTime)
{
  delay(delayTime);
}

11. The Sketch

//-- place code to declare class structure
//-- place code to declare object

void setup()
{
    ledL.ioDirection();
}

void loop()
{
    ledL.ledOn()
    ledL.timeDelay(argOn);//argOn must be evaluated to 2000 (onTime variable)
    ledL.ledOff();
    ledL.timeDelay(argOff);//argOff must be evaluated to 1000 (offTime variable)
}

//-- place code to define SimpleLED() method
//-- place code to defiine ioDirection() method
//-- place code to define ledOn() method
//-- place code to define ledOff() method
//--place code to define timeDelay() method

Note-1: The argOn argument must be evaluated to onTime (2000 ms). The onTime is a private variable. So, it must be accessed via a member function like unsigned long getOnTime(). This type of function is known as getter function.

Note-2: Similarly to assign the value of the offTime (1000) variable to argOff argument, the member function unsigned long getOffTime() is needed.

Note-3: As a result, the class structure of Step-9 will take the following form:

class SimpleLED
{
   private:
      byte DPin;
      byte direction;
      unsigned long onTime;
      unsigned long offTime;

   public:
      SimpleLED(byte pin, byte dir, unsigned long onT, unsigned long offT);
      void ioDirection();
      void ledOn();
      void ledOff();
      void timeDelay(unsigned long delayTime);

      unsigned long getOnTime();     //getter function
      unsigned long getOffTime();
};

Note-4: Defining codes for getOnTime() method

unsigned long SimpleLED::getOnTime()
{
  return onTime;   //2000 ms goes to argOn variable of calling function
}

Note-5: Defining codes for getOffTime() method

unsigned long SimpleLED::getOffTime()
{
  return offTime;
}

12. The final integrated sketch:

class SimpleLED
{
  private:
    byte DPin;
    byte direction;
    unsigned long onTime;
    unsigned long offTime;

  public:
    SimpleLED(byte pin, byte dir, unsigned long onT, unsigned long offT);
    void ioDirection();
    void ledOn();
    void ledOff();
    void timeDelay(unsigned long delayTime);

    unsigned long getOnTime(); //getter declaration
    unsigned long getOffTime();
};

SimpleLED ledL(13, OUTPUT, 2000, 1000);  // DPin 13, OUTPUT, 2000/1000ms ON/OFF

void setup()
{
  ledL.ioDirection();
}

void loop()
{
  ledL.ledOn();
  unsigned long argOn = ledL.getOnTime();
  ledL.timeDelay(argOn);//(ledL.getOnTime());
  
  ledL.ledOff();
  unsigned long argOff = ledL.getOffTime();
  ledL.timeDelay(argOff);//(ledL.getOffTime());
}

SimpleLED::SimpleLED(byte pin, byte dir, unsigned long onT, unsigned long offT)
{
  DPin      = pin;
  direction = dir;
  onTime    = onT;
  offTime   = offT;
}

void SimpleLED::ioDirection()
{
  pinMode(DPin, direction);
}

void SimpleLED::ledOn()
{
  digitalWrite(DPin, HIGH);
}

void SimpleLED::ledOff()
{
  digitalWrite(DPin, LOW);
}

void SimpleLED::timeDelay(unsigned long delayTime)
{
  delay(delayTime);
}

unsigned long SimpleLED::getOnTime()
{
  return onTime;
}

unsigned long SimpleLED::getOffTime()
{
  return offTime;
}

13. I have uploaded the Sketch of Step-12 into Arduino UNO R3 and have seen that that the onboard led-L is blinking with 2 sec onTime and 1 sec offTime.

14. Exercise
Re-write Sketch of Step-12 to get onTime/offTime from the Serial Monitor and then pass them to the class.

I would say the balloon is the object and inlfated is one of its attribute (as could also be balloon color, pressure, ...)

Not sure about that one...

burst the ballon could be a method (similar as push a pin) or could be an attribute (a burst balloon can't be inflated anymore, it's broken)

  • My preference is to place the class functions in the class.
    Some exceptions do this exist.



right to access

write program

balloon

typical

Arduino

says

You need to ask yourself why would this be part of the LED class ? Does a LED wait ?

You got the ledOn and ledOff method, but a led can blink... that should be the method to write in the exercise (and come to discover the discussion about blocking or non blocking code)

I am not arguing with you but to interact to have my understanding better.

In OOP/C++ programming, we apply a method to an object to observe an outcome. If a deflated balloon is treated as an object, then where do we apply the method (push the pin) to observe a burst?

You have done the great job of a reviewer. Thanks for it. I am doing the corrections.

Ballon would be the class.
a Ballon has attributes (color, internal pressure, state).
state could be for example inflated, deflated, burst

side note: usually you don't develop the tutorial right in this section? See the pinned notes

If you would like to propose the addition of a tutorial you wrote, follow these instructions:

  1. Publish your tutorial topic under a relevant normal forum category.
  2. The community may now make some suggestions for improvements. You can edit the tutorial contents by clicking the 🖉 icon at the bottom of the post.
  3. When the tutorial is ready to submit, click the ":black_flag: Flag" button at the bottom of the topic page.
    A "Thanks for helping to keep our community civil!" dialog will open.
  4. Select the "Something Else" radio button in the dialog.
  5. Write a request for the moderators to move the topic to the tutorials category:

I am proposing this tutorial be moved to the tutorials category.

  1. Click the ":email: Message" button in the dialog.

The moderators will evaluate your tutorial and decide whether it is appropriate for inclusion in the curated tutorials.

Thanks for your contribution

A thought provoking issue. I have just put all the functions in the class without thinking if they are needed or not. Putting timeDelay() function in the class has pushed me a lot to find the way of passing the private onTime/offTime variables in the delay() function. (I don't practice much C++ programming. I have made this tutorial for my EEE students.)

What to answer against your question -- Does a LED wait? It is the MCU that waits (being blocked) until the OnTime/OffTime expires.

I will try to appply these concepts in C++ Programming.

My view is that the LED does not wait. one approach would be to get the onTime or off time and call delay yourself in the main code

➜

void loop()
{
  ledL.ledOn();
  delay(ledL.getOnTime()); 
  ledL.ledOff();
  delay(ledL.getOffTime());
}
  • I asked ChatGPT to improve the tutorial, here is what it came up with.



1. Background: C and C++ in Embedded Systems

  1. C language still exists and is widely used in embedded systems. I regularly use C to write programs of about 200–500 lines, targeted for low–flash-memory AVR microcontrollers, such as the ATmega328P used on the Arduino UNO R3.
  2. C++ can be viewed as “C with additional features”, most notably classes and object-oriented programming. Bjarne Stroustrup designed C++ as an extension of C, not as a completely new language. This is why it was named C++ rather than something entirely different.
  3. In C, programs are written using the procedural programming paradigm, where execution proceeds step by step.
  • Data (variables) and the functions that operate on that data are separate.
  • There is no built-in mechanism to bind data and behavior together.
  1. C++ introduces classes, which allow:
  • Data (variables) and methods (functions) to be grouped together
  • Data protection through access specifiers (private, public)
    These features are not available in pure C.
  1. Beginners typically start with C. Once they become comfortable, they often migrate to C++ to take advantage of abstraction, encapsulation, and code reusability.

2. Thinking in Objects (C++ Perspective)

C++ encourages programmers to treat hardware as real-world objects.

Example (Balloon analogy):

  • Inflated balloon → Object
  • Pushing a pin → Method
  • Bursting → Reaction (output)

This same idea can be applied to hardware such as LEDs, motors, and sensors.


3. Procedural C-Style Arduino Program (Blink LED)

Example: Blink the onboard LED (LED-L) on Arduino UNO R3

#define ledL 13

void setup()
{
  pinMode(ledL, OUTPUT);
}

void loop()
{
  digitalWrite(ledL, HIGH);
  delay(2000);

  digitalWrite(ledL, LOW);
  delay(1000);
}

This program is procedural:

  • No abstraction
  • No grouping of data and behavior
  • Logic is spread across setup() and loop()

4. Planning a C++ Class-Based Design

Step 1: Identify Variables

  • Digital pin number connected to LED-L
  • Pin direction
  • LED ON time
  • LED OFF time

Step 2: Identify Methods

  • ioDirection()
  • ledOn()
  • ledOff()
  • timeDelay()

5. Basic Class Structure

A typical C++ class contains:

  • Private data members
  • Public member functions
  • A constructor

6. Defining the SimpleLED Class

class SimpleLED
{
  private:
    byte DPin;
    byte direction;
    unsigned long onTime;
    unsigned long offTime;

  public:
    SimpleLED(byte pin, byte dir, unsigned long onT, unsigned long offT);
    void ioDirection();
    void ledOn();
    void ledOff();
    void timeDelay(unsigned long delayTime);
};

Creating an Object

SimpleLED ledL(13, OUTPUT, 2000, 1000);

Important Notes

  • Private variables can be accessed only by member functions
  • Functions outside the class (including loop()) cannot access private data directly
  • Member functions can access each other
  • The constructor is called automatically when the object is created

7. Constructor Definition

SimpleLED::SimpleLED(byte pin, byte dir, unsigned long onT, unsigned long offT)
{
  DPin      = pin;
  direction = dir;
  onTime    = onT;
  offTime   = offT;
}

Key points:

  • Constructor name = class name
  • No return type (not even void)
  • Used to initialize private variables

8. Member Function Definitions

Set Pin Direction

void SimpleLED::ioDirection()
{
  pinMode(DPin, direction);
}

Turn LED ON

void SimpleLED::ledOn()
{
  digitalWrite(DPin, HIGH);
}

Turn LED OFF

void SimpleLED::ledOff()
{
  digitalWrite(DPin, LOW);
}

Delay Function

void SimpleLED::timeDelay(unsigned long delayTime)
{
  delay(delayTime);
}

9. Need for Getter Functions

Because onTime and offTime are private, they must be accessed through getter functions.

Updated Class Declaration

class SimpleLED
{
  private:
    byte DPin;
    byte direction;
    unsigned long onTime;
    unsigned long offTime;

  public:
    SimpleLED(byte pin, byte dir, unsigned long onT, unsigned long offT);
    void ioDirection();
    void ledOn();
    void ledOff();
    void timeDelay(unsigned long delayTime);

    unsigned long getOnTime();
    unsigned long getOffTime();
};

Getter Definitions

unsigned long SimpleLED::getOnTime()
{
  return onTime;
}

unsigned long SimpleLED::getOffTime()
{
  return offTime;
}

10. Final Integrated Sketch

class SimpleLED
{
  private:
    byte DPin;
    byte direction;
    unsigned long onTime;
    unsigned long offTime;

  public:
    SimpleLED(byte pin, byte dir, unsigned long onT, unsigned long offT);
    void ioDirection();
    void ledOn();
    void ledOff();
    void timeDelay(unsigned long delayTime);
    unsigned long getOnTime();
    unsigned long getOffTime();
};

SimpleLED ledL(13, OUTPUT, 2000, 1000);

void setup()
{
  ledL.ioDirection();
}

void loop()
{
  ledL.ledOn();
  ledL.timeDelay(ledL.getOnTime());

  ledL.ledOff();
  ledL.timeDelay(ledL.getOffTime());
}

SimpleLED::SimpleLED(byte pin, byte dir, unsigned long onT, unsigned long offT)
{
  DPin      = pin;
  direction = dir;
  onTime    = onT;
  offTime   = offT;
}

void SimpleLED::ioDirection()
{
  pinMode(DPin, direction);
}

void SimpleLED::ledOn()
{
  digitalWrite(DPin, HIGH);
}

void SimpleLED::ledOff()
{
  digitalWrite(DPin, LOW);
}

void SimpleLED::timeDelay(unsigned long delayTime)
{
  delay(delayTime);
}

unsigned long SimpleLED::getOnTime()
{
  return onTime;
}

unsigned long SimpleLED::getOffTime()
{
  return offTime;
}

11. Result

After uploading this sketch to the Arduino UNO R3, the onboard LED-L blinks with:

  • 2 seconds ON
  • 1 second OFF

12. Exercise

Modify the sketch so that:

  • onTime and offTime are entered via the Serial Monitor
  • The values are passed to the SimpleLED class dynamically

Fine! Then I don't need the timeDelay() function. This is a good point for class room discussion.

Sometimes, students have problems to understand the concept of passing function as an argument. So, I would prefer to write:

unsigned long argOn = ledL.getOnTime();  //argOn = onTime = 2000
delay(argOn);

you could have a look at this discussion with C style or OO style

Flashing multiple LEDs at the same time

@J-M-L has provided constructive and academic criticism explaining why this function should not be placed inside the class; whereas, ChatGPT has remained silent on this point.

Readers will clearly notice that the tutorial has been prepared by a non-native human being from the style of the prose. On the other hand, AI writes prose in an unbelievable correct way.

  • Next year it will catch this . . .

:wink:

  • Here is something I offered a while back.
    It was meant to serve a different purpose.
    It might be of interest:

//
//================================================^================================================
//                                N e w   U s e r   S k e l e t o n
//================================================^================================================
//
//  URL reference:
//
//  Your Name
//
//  Version    YY/MM/DD    Comments
//  =======    ========    ========================================================================
//  1.00       24/01/23    Running code
//  1.10       25/08/27    Added comments.
//
//
//  ================================================
//  New users, are urged to follow the guidelines below.
//  - Before writing any code:
//       Write a preamble of "everything" you want your project to do, be exact.
//       Make a preliminary schematic of your project's component interconnections.
//       You can use programs like KiCAD or EasyEDA to make computer aided schematics, hand drawn is good too.
//       In the Arduino IDE Preferences, set "Compiler warnings: All".
//       Never make circuit connections when circuit power is connected.
//       When writing code, suggest new users adopt a similar sketch format as seen here.
//  - Use white space to make your code easier to read and follow. Write your sketches in an organized style.
//  - Add "meaningful comments" to your lines of code. Do not just repeat what's happening in the code.
//  - Place braces { and } on lines by themselves.
//  - Do not place more than 1 line of code on a line.
//  - For documentation, use a variable instead of HIGH/LOW or true/false.
//       Example:     const byte LED_ON = HIGH;
//                    digitalWrite(heartbeatLED, LED_ON);
//  - Avoid using magic numbers; instead, assign a variable name to the number, use that variable in the sketch.
//       Example:     if(analogRead(potentiometer) >= 512) {...}
//       Instead use: const int maxLevel = 512;
//                    if(analogRead(potentiometer) >= maxLevel) {...}
//  - Every now and then, format your sketch by pressing <CTRL><T> or <CMD><T>, make backup copies.
//  - Do not make huge changes all at once. Let the compiler confirm you have no errors as changes are made.
//  - Your sketch should be fast and responsive. Code should be written using non-blocking coding techniques:
//       Avoid using delay(...) as it stops normal code execution during the delay period.
//       When using while() and do(), avoid long blocking intervals.
//  - To help you to move around your sketch quickly, highlight the below "unique" character string found in header lines.
//       Example:  ====^====
//       Press <CTRL><F> (Find).
//       Click Find/Previous again and again until you get to the area you want.
//
//  Notes:
//  - See the accompanying PDF Flowchart.
//  - The sketch below demonstrates how to make "simple", semi-automated, non-blocking TIMER objects, and Input objects.
//  - De-bouncing of slow mechanical switches while looking for a switch change in state, is presented.
//  - A very simple State Machine is briefly demonstrated.
//
//  The Circuit:  (See the accompanying schematic.)                              Internal
//  The “redSwitch" is connected between Arduino pin D2 and GND (0V).      +5V---[20 to 50k]---Pin---[Switch]---GND
//  The "relaySwitch" is connected between pin D3 and GND.
//  3 LEDs are connected to pins 11, 12, and 13; each has a 220R series resistor.     Pin---[220R]---A[LED]K---GND
//  We have a 10k potentiometer wiper connected to A0, with the outer legs connected between 5V and GND.
//  The potentiometer controls the brightness of the LED on pin 11 using PWM.
//  When the "redSwitch" is pressed, the LED on pin 12 turns ON for 2 seconds, then turns OFF.
//  When the "relaySwitch" is released, the relay on D8 toggles ON or OFF state.
//  The LED on pin 13 toggles every 500ms. This gives us a rough indication if there is code blocking occurring.
//
//

//
//================================================^================================================
//Library area:
//
//#include <someLibrary.h>


//================================================
//Constants/Macros:
//
const byte LED_ON               = HIGH;    //PIN---[220R]---A[LED]K---GND
const byte LED_OFF              = LOW;

const byte RELAY_ON             = HIGH;
const byte RELAY_OFF            = LOW;

const bool ENABLED              = true;
const bool DISABLED             = false;

//========================
const unsigned long ONE_SECOND  = 1000ul;               //milliseconds
const unsigned long ONE_MINUTE  = (ONE_SECOND * 60ul);  //60 seconds in one minute.
const unsigned long ONE_HOUR    = (ONE_MINUTE * 60ul);  //60 minutes in one hour.
const unsigned long ONE_DAY     = (ONE_HOUR * 24ul);    //24 hours in one day.

//========================
//On pin D13 of an Arduino UNO (Atmega328), produce a 126ns pulse, used for oscilloscope timing.
#define PULSE126D13        cli(); PINB = bit(PINB5); asm("nop\n"); PINB = bit(PINB5); sei()


//                                            G P I O s   
//================================================^================================================
//

//Analogs
//================================================
//
const byte potentiometer           = A0;

//INPUTS
//================================================
//
const byte switchOne               = 2;
const byte switchTwo               = 3;

//OUTPUTS
//================================================
//
const byte relay                   = 8;
const byte pwmLED                  = 11;
const byte redLED                  = 12;
const byte heartbeatLED            = 13;


//                                       S T R U C T U R E S
//================================================^================================================
//
//This sketch intentionally avoids discussing advanced structures and classes.
//
//This struct (structure) automates the process of making simple "Switch Input Objects".
//The struct below has 4 data members and 1 member function.
//
struct createSwitchInput
{
  byte pin;                         //.pin           The GPIO pin this switch is connected to.
  byte pressedState;                //.pressedState  The pin state when the switch is pressed.
  byte lastState;                   //.lastState     The "state" the switch was last in.
  unsigned long pressTime;          //.pressedTime   The time when the switch was pressed.

  //Constructor
  createSwitchInput(byte _pin, byte _pressedState)
  {
    pin = _pin;
    pressedState = _pressedState;

    lastState = digitalRead(pin);
    pressTime = millis();

    //Do we need to enable the internal pull-up resistor ?
    if (pressedState == LOW)
    {
      pinMode(pin, INPUT_PULLUP);
    }
  }

};  //END of   struct

//========================
//Create and initialize our "Switch Input Objects".
//Example: 
//createSwitchInput startSwitch     = {7, LOW};
//Creates the "startSwitch"  object. This switch is connected to pin 7, when pressed, the input is LOW.
//We have access to: startSwitch.pin, startSwitch.pressedState, startSwitch.lastState, and startSwitch.pressedTime
//
createSwitchInput   redSwitch       = {switchOne, LOW};   //.pin .pressedState
createSwitchInput   relaySwitch     = {switchTwo, LOW};   //.pin .pressedState


//
//================================================
//This struct (structure) automates the process of making simple "Non-blocking TIMER Objects".
//The struct below has 3 data members.
//
struct createTIMER
{
  unsigned long startTime;          //.startTime     The time when this TIMER started.
  unsigned long interval;           //.interval      The time when this TIMER expires.
  bool runFlag;                     //.runFlag       A Flag we can use to enable or disable the TIMER from running.

}; //END of   struct

//========================
//Create and initialize our "Non-blocking TIMER Objects".
//Note: millis() returns the time in milliseconds, micros()the time in microseconds, since the Arduino was turned ON.
//Example: createTIMER laser        = {micros(), 500ul, ENABLED};
//Creates the "laser" TIMER object.   micros() based TIMER, 500us interval, TIMER is enabled.
//We have access to: laser.startTime, laser.interval, and laser.runFlag
//
//millis() based TIMERs
createTIMER heartbeat               = {millis(), 500ul, ENABLED};    //Used to regularly toggle the heartbeat LED.
createTIMER checkSwitch             = {millis(), 50ul, ENABLED};     //Used to regularly scan our switches.
createTIMER checkAnalog             = {millis(), 100ul, ENABLED};    //Used to regularly scan our analog inputs.
createTIMER printer                 = {millis(), 1000ul, ENABLED};   //Used to regularly print to the serial monitor.
createTIMER stateTimer              = {millis(), 1000ul, DISABLED};  //Used for timing inside our State Machine.

//micros() based TIMERs
createTIMER machine                 = {micros(), 1000ul, ENABLED};   //Used to regularly service our State Machine.


//                                    S t a t e   M a c h i n e
//================================================^================================================
//
//Create a new "data type" called STATES.
//Define the named constants of all the "states" in our State Machine.
enum STATES : byte
{
  // 0       1       2       3       4
  STARTUP, STATE1, STATE2, STATE3, STATE4
};

//Create the variable "mState" of type STATES.
//At power up time, we are in the "STARTUP" state.
STATES mState = STARTUP;


//VARIABLES
//================================================
//
byte potentiometerValuePWM;
int potentiometerValue;

const unsigned long shortPressTime = 500ul;
const unsigned long longPressTime  = 2000ul;


//                                           s e t u p ( )
//================================================^================================================
//
void setup()
{
  //With todays hardware, we can use a fast baud rate.
  Serial.begin(115200);

  //========================
  //Initialize GPIOs
  digitalWrite(heartbeatLED, LED_OFF);      // Pin---[220R]---A[LED]K---GND
  pinMode(heartbeatLED, OUTPUT);

  digitalWrite(redLED, LED_OFF);
  pinMode(redLED, OUTPUT);

  digitalWrite(pwmLED, LED_OFF);
  pinMode(pwmLED, OUTPUT);

  digitalWrite(relay, RELAY_OFF);
  pinMode(relay, OUTPUT);

} //END of   setup()


//                                            l o o p ( )
//================================================^================================================
//
void loop()
{
  //========================================================================
  //We can run a test to see how much time it takes loop() to run.
  //As things stand, in this example, loop() takes ~24us to run.
  //Comment/uncomment the next 3 lines as needed.
  //
  //  static unsigned long startTime;
  //  Serial.println(micros() - startTime);
  //  startTime = micros();
  //

  //  PULSE126D13;    //On pin D13 of an Arduino UNO, produce a 126ns pulse, used for oscilloscope timing.

  //========================================================================  T I M E R  heartbeat
  //This is a millis() based TIMER.
  //Is it time to toggle the heartbeat LED ?
  if (millis() - heartbeat.startTime >= heartbeat.interval)
  {
    //Restart this TIMER.
    heartbeat.startTime = millis();

    //This LED gives us a rough indication if there is code blocking.
    //This LED should toggle every 500ms, if it doesn't toggle or if it stutters, we should look for blocking code.
    //If the LED is ON turn it OFF, if the LED is OFF turn it ON.
    digitalWrite(heartbeatLED, !digitalRead(heartbeatLED));
    
    //We could have used the "Ternary" operator instead:
    //digitalWrite(heartbeatLED, digitalRead(heartbeatLED) == LED_ON ? LED_OFF : LED_ON);
  }

  //========================================================================  T I M E R  checkSwitch
  //This is a millis() based TIMER.
  //Is it time to scan our switches ?
  if (millis() - checkSwitch.startTime >= checkSwitch.interval)
  {
    //Restart this TIMER.
    checkSwitch.startTime = millis();

    checkSwitches();
  }

  //========================================================================  T I M E R  checkAnalog
  //This is a millis() based TIMER.
  //Is it time to scan our analogs ?
  if (millis() - checkAnalog.startTime >= checkAnalog.interval)
  {
    //Restart this TIMER.
    checkAnalog.startTime = millis();

    checkAnalogs();
  }

  //========================================================================  T I M E R  printer
  //This is a millis() based TIMER.
  //Is it time to print to the serial monitor ?
  if (millis() - printer.startTime >= printer.interval)
  {
    //Restart this TIMER.
    printer.startTime = millis();

    //For demonstration purposes, print the potentiometer reading and send it to the pwmLED.
    Serial.println(potentiometerValue);
  }

  //========================================================================  T I M E R  machine
  //This is a micros() based TIMER.
  //Is it time to service the State Machine ?
  if (micros() - machine.startTime >= machine.interval)
  {
    //Restart this TIMER.
    machine.startTime = micros();

    checkMachine();
  }


  //================================================
  //       Other non-blocking code goes here
  //================================================


} //END of   loop()


//                                    c h e c k M a c h i n e ( )
//================================================^================================================
//
//We sit in one of the States in this State Machine.
//As events happen, we might move from one State to another State.
//The TIMER "stateTimer" if used, can trigger iterations to occur; this continues until some event happens
//and we then move to a different State.
//
void checkMachine()
{
  //================================================
  //Service the current "state".
  switch (mState)
  {
    //========================
    case STARTUP:
      {
        //Do startup stuff.
      }
      break;

    //========================
    case STATE1:
      {
        //We stay in STATE1 until the "stateTimer" expires; in this example, we then go back to the STARTUP State.
        //This is a millis() based TIMER.
        //If this TIMER is enabled, did this TIMER expire ?
        if (stateTimer.runFlag == ENABLED && millis() - stateTimer.startTime >= stateTimer.interval)
        {
          digitalWrite(redLED, LED_OFF);

          //We no longer need this TIMER, we can now disable it.
          stateTimer.runFlag = DISABLED;

          //Next state.
          mState = STARTUP;
        }
      }
      break;

    //========================
    case STATE2:
      {
        //Do something.
      }
      break;

    //========================
    case STATE3:
      {
        //Do something.
      }
      break;

    //========================
    case STATE4:
      {
        //Do something.
      }
      break;

  } //END of  switch/case

} //END of   checkMachine()


//                                   c h e c k S w i t c h e s ( )
//================================================^================================================
//
//Scan all switches.
//Look for a switch change in state.
//If there is a change in state, execute the appropriate code.
//
void checkSwitches()
{
  byte currentState;

  //========================================================================  redSwitch
  currentState = digitalRead(redSwitch.pin);

  //Is there a change in this switch's state ?
  if (redSwitch.lastState != currentState)
  {
    //Update to this new state.
    redSwitch.lastState = currentState;

    //================================================
    //Is this switch pressed ?
    if (redSwitch.lastState == redSwitch.pressedState)
    {
      //Record the time when the switch is pressed.
      redSwitch.pressTime = millis();

      //Serial.println("The \"redSwitch\" switch is pressed.\n");
      //Using the F() macro we can save SRAM memory by placing the message in Flash memory.
      Serial.println (F("The \"redSwitch\" switch is pressed.\n"));

      digitalWrite(redLED, LED_ON);

      //Set this TIMER's interval to two (2) seconds.
      stateTimer.interval = 2000ul;

      //Restart this TIMER.
      stateTimer.startTime = millis();

      //Enable this TIMER.
      stateTimer.runFlag = ENABLED;

      //Next state.
      mState = STATE1;
    }

    //================================================
    //This switch was released.
    else
    {
      Serial.print(F("The switch was pressed for "));
      Serial.print(millis() - redSwitch.pressTime);
      Serial.println(F("ms."));

      //========================  Short Press
      //Was this a short press ?
      if (millis() - redSwitch.pressTime <= shortPressTime)
      {
        //Do something.

        Serial.println(F("Short Press.\n"));
      }

      //========================  Long Press
      //Was this a long press ?
      else if (millis() - redSwitch.pressTime >= longPressTime)
      {
        //Do something.

        Serial.println(F("Long Press.\n"));
      }

      //========================  Regular Press
      //This was a regular press.
      else
      {
        //Do something.

        Serial.println(F("Regular Press.\n"));
      }
    }

  } //END of   redSwitch

  //========================================================================  relaySwitch
  currentState = digitalRead(relaySwitch.pin);

  //Is there a change in this switch's state ?
  if (relaySwitch.lastState != currentState)
  {
    //Update to the new switch state.
    relaySwitch.lastState = currentState;

    //================================================
    //Is this switch pressed ?
    if (relaySwitch.lastState == relaySwitch.pressedState)
    {
      //Nothing to do.
    }

    //================================================
    //This switch was released.
    else
    {
      //The relay is toggled "when" the switch is released.
      //Toggle the relay.
      digitalWrite(relay, digitalRead(relay) == RELAY_ON ? RELAY_OFF : RELAY_ON);

      Serial.println(F("The relay state has toggled."));
    }

  } //END of   relaySwitch

  //========================================================================  Next Switch

} //END of   checkSwitches()


//                                    c h e c k A n a l o g s ( )
//================================================^================================================
//
//Update our analog input readings.
//
void checkAnalogs()
{
  //========================================================================  potentiometer
  //Read the voltage on this analog pin.
  potentiometerValue = analogRead(potentiometer);

  //Convert to a value between 0 and 255 so we can use the value in a analogWrite(...) situation.
  potentiometerValuePWM = potentiometerValue >> 2;  //Divides by 4

  //Update the pwmLED.
  analogWrite(pwmLED, potentiometerValuePWM);

  //========================================================================  Next analog

} //END of   checkAnalogs()


//                                              E N D
//================================================^================================================
//

My preference is to keep the class body clean withot populating it with the defining codes for the methods/functions.

I asked chatGPT (sharing also the interface of your class)

does it make sense in an OO way to have the method timeDelay() which just waits ?

chatGPT said:

In a strict object-oriented design, a method whose sole responsibility is to block execution by waiting does not conceptually belong to a SimpleLED object. Waiting is a control-flow or scheduling concern, not an intrinsic behavior or state of an LED. Including timeDelay() couples the LED abstraction to a particular execution model and makes the class less reusable and less testable.

It is generally preferable for the LED class to expose state and behavior such as on, off, and timing parameters, while the calling code or a higher-level controller handles delays or non-blocking timing. In embedded contexts, this also avoids blocking designs and allows better composition with other components.

but in order to ask, you need to understand OOP... That's the challenge with AI and LLM. Great help if you know what you are doing, poor help if you don't know how to question some of the results...

That's why it is said that machine will never/ever be able to defeat man.

@GolamMostafa
Why not let the LED class control its state itself?
If you define the on and off durations in the constructor, the object should be able to “help” itself. That is the way object abstraction should work, the user should not be bothered when to set the LED on or off.

class SimpleLED
{
  public:
  SimpleLED(pin, state, onDuration, offDuration);
  state update();  //  returns new state of the LED.

  private:
  void whatEverNeeded();
  uint32_t lastToggle;
}

//  then in code I would only need to

void loop()
{
  myLED.update();
  // other code here.
}

The update() function could use the “blink without delay” way of programming to be non blocking.

Check my heartBeat class - GitHub - RobTillaart/HeartBeat: Arduino library for a HeartBeat with frequency and duty cycle. - as it implements effectively just a blinking led.

My 2 cents.