Arduino OOP: How do I invoke a method inside a class from another class in a lib

Good afternoon dear Forum friends,

Simple question: Inside the loop() method, I want to use a variable updated inside a Class, by means of reading its updated contents from inside other Classes. All these Classes reside inside a Library. How do I do that?
Need clear code example on best way to do this:)

//library definition
//class initialization
//setup (Serial comms)

loop(){

         data.listen();

         process_A.doTaskA1();
         process_A.doTaskA2();
         process_B.doTaskB1();
         process_B.doTaskB2();

}

My expanded Example:

I want to check Serial line in case a msg has arrived.

  1. I create a Class called DATA, a Class called PROCESS_A, and a Class called Process_B
  2. I create the .cpp and the .h structure to call them from a library added to a .ino file
  3. At the beginning of the loop() the program executes data.listen();
  4. Once data.listen() has been executed, the other 2 classes are invoked
  5. The PROCESS_A/PROCESS_B Classes will read the variable "serialData" residing inside Class DATA using an accesor function (getMsgStatus()) and will update variables or execute actions inside them accordingly.

I have not succeeded in Step 5, always get an error 'getMsgStatus()' was not declared in this scope', meaning inside the PROCESS_A/PROCESS_B Classes, even if I declared it public in the DATA Class. I guess I need to declare the usage of an external variable inside the PROCESS_A / PROCESS_B Class but the syntax/structure escapes me. C++ Noob OOP hardship I guess.

Let me know your comments. Structure is below:

.h

#ifndef TARGET_LIB_H
#define TARGET_LIB_H

#include <Arduino.h>

class DATA{

          private: String serialData;

          public:
                   void listen();
                   String getMsgStatus();
};

class PROCESS_A{

          private: String rBuffer = "";

          public:
                   void doTask_A1();
                   void doTask_A2();
};

class PROCESS_B{
 
          private: String rBuffer = "";

          public:
                   void doTask_B1();
                   void doTask_B2();
};

#endif

.cpp

#include "Target_Lib.h"

void DATA::listen(){

       if(Serial.available() > 0)
              {
                Serial.print("Listening... ");
                serialData = Serial.readStringUntil('\n');
                Serial.println(serialData);
              }
              else
                {
                  ;
                }
}


String DATA::getMsgStatus(){                      //getter for Data Class
                    
                   return(serialData);
}



void PROCESS_A::doTask_A1(){

                   rBuffer = data.getMsgStatus();     //read var in Class 'DATA' by means of getter method

                   Serial.println("Task A1 executed. Value captured: " + rBuffer);     
}

void PROCESS_A::doTask_A2(){

                   rBuffer = data.getMsgStatus();
                   Serial.println("Task A2 executed. Value captured: " + rBuffer);
}

void PROCESS_B::doTask_B1(){

                   rBuffer = data.getMsgStatus();
                   Serial.println("Task B1 executed. Value captured: " + rBuffer);      
}

void PROCESS_B::doTask_B2(){

                   rBuffer = data.getMsgStatus();
                   Serial.println("Task B2 executed. Value captured: " + rBuffer);                  
}

.ino

#include "Target_Lib.h"

DATA data;
PROCESS_A process_A;
PROCESS_B process_B;

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

void loop() { 
                   data.listen();
                   process_A.doTask_A1();
                   process_A.doTask_A2();
                   process_B.doTask_B1();
                   process_B.doTask_B2();

                   delay(1000);
}

thanks in advance for your tips and support.

regards
-EZ

I guess you can’t do this:

rBuffer = getMsgStatus() ; //read var in Class 'DATA' by means of getter method

with out stating explicitly which instance of the class DATA you are using when you invoke its method getMsgStatus().

6v6gt:
I guess you can’t do this:

rBuffer = getMsgStatus() ; //read var in Class 'DATA' by means of getter method

with out stating explicitly which instance of the class DATA you are using when you invoke its method getMsgStatus().

Thanks for noting that 6v6gt, I modified the .cpp and added the data instance from DATA Class, but it still gives me the error: 'data' was not declared in this scope at compile time, in every class where this method is mentioned.

try putting this:

extern DATA data ;

following the #include statement at the top of Target_Lib.cpp

@edzamper, the code inside your PROCESS_A and PROCESS_B classes has no idea that you've created an instance of the DATA class named data. And, even if it did work, you're unnecessarily restricting these classes to work ONLY with an instance named "data". What if you had two instances of the DATA class? Would you create classes PROCESS_C and PROCESS_D? That's not very OOP!!!

You should have the functions in PROCESS_A and PROCESS_B accept a pointer (or reference) to a DATA instance as a parameter. Then the call in the main program would be something like:

process_A.doTask_A1(data);

Or even make the members of DATA static since it anyway encapsulates Serial, so multiple instances don't really make much sense.

6v6gt:
try putting this:

extern DATA data ;

following the #include statement at the top of Target_Lib.cpp

I placed the code and it does compile, but it doesn't show any data in the Serial screen, nor accepts any data written back. I don't think the .cpp is the right place to instantiate a user defined class.
Any other suggestions?

Thanks

gfvalvo:
@edzamper, the code inside your PROCESS_A and PROCESS_B classes has no idea that you've created an instance of the DATA class named data. And, even if it did work, you're unnecessarily restricting these classes to work ONLY with an instance named "data". What if you had two instances of the DATA class? Would you create classes PROCESS_C and PROCESS_D? That's not very OOP!!!

You should have the functions in PROCESS_A and PROCESS_B accept a pointer (or reference) to a DATA instance as a parameter. Then the call in the main program would be something like:

process_A.doTask_A1(data);

OK, thanks for pointing that out. I want to instantiate in the .ino the way a normal user will do. However, I do NOT want to pass a pointer or reference as a parameter through the methods. I want a way to instantiate the class and make sure other classes can read the vars directly. It would even be preferable to do all the process inside the .cpp as a Class. Do I need to declare the DATA class as static? I'm not sure how to proceed on this.

1 Like

edzamper:
I placed the code and it does compile, but it doesn't show any data in the Serial screen, nor accepts any data written back. I don't think the .cpp is the right place to instantiate a user defined class.
Any other suggestions?

Thanks

You may have another problem. How are you generating the serial data that you are expecting to see and what pins are you using on the Arduino ? Try a minimal non-class sketch to read from a Serial input and write simultaneously to the Serial console, maybe also using also a softwareSerial instance.

If I add the external declaration of data to the .cpp file,

extern DATA data ;   // This is a declaration, not an instantiation. The instantiation, 
                     // that is the creation of the object data, is done in the .ino

then add this line to the .cpp file, I get a result in the Serial monitor:

void DATA::listen(){
       Serial.println("in DATA::listen() ") ;    << this appears in the serial monitor
       . . .

edzamper:
OK, thanks for pointing that out. I want to instantiate in the .ino the way a normal user will do. However, I do NOT want to pass a pointer or reference as a parameter through the methods. ......

Those two contradictory positions result in a non-OOP situation -- as mentioned. Why do you want to give the main code the ability instantiate an object in the .ino but then restrict the class to operating on only a single (fixed) instance of that class? What guarantee is there that the .ino will instantiate an object with the proper name? Answer -- NONE.

If you really only want one "instance" there are better ways of doing it. You could make all class data and functions 'static' and then reference them via the class's scope resolution operator. If you went this route, you could also declare an extern instance of the class in the .h but never define it. That would allow you to access the class via the ojbectName.member notation that's more comfortable to newbies. With all members being static, you should make the class's default constructor 'private'. That will prevent any actual objects from being instantiated.

Before going further, perhaps you should learn a little more about the basics of OOP and think about what problem you're trying to solve.

gfvalvo:
If you really only want once "instance" there are better ways of doing it. You could make all class data and functions 'static' and then reference them via the class's scope resolution operator. If you went this route, you could also declare an extern instance of the class in the .h but never define it. That would allow you to access the class via the ojbectName.member notation that's more comfortable to newbies. With all members being static, you should make the class's default constructor 'private'. That will prevent any actual objects from being instantiated.

As simple example:

MyClass.h:

#ifndef MY_CLASS
#define MY_CLASS

#include <Arduino.h>

class MyClass {
  public:
    static void gatherData();
    static uint8_t getResult();

  private:
    MyClass();
    static uint8_t savedData;
};

extern MyClass myObject;

#endif

MyClass.cpp:

#include "MyClass.h"

uint8_t MyClass::savedData;

void MyClass::gatherData() {
  savedData = random(0, 256);
}

uint8_t MyClass::getResult() {
  return savedData;
}

.ino File:

#include "MyClass.h"

void setup() {
  Serial.begin(115200);
  delay(1000);

  // Using scope resolution operator:
  MyClass::gatherData();
  Serial.println(MyClass::getResult());

  // Using object that's declared but not defined:
  myObject.gatherData();
  Serial.println(myObject.getResult());
}

void loop() {
}

gfvalvo:
As simple example:

MyClass.h:

#ifndef MY_CLASS

#define MY_CLASS

#include <Arduino.h>

class MyClass {
 public:
   static void gatherData();
   static uint8_t getResult();

private:
   MyClass();
   static uint8_t savedData;
};

extern MyClass myObject;

#endif





[u]**MyClass.cpp:**[/u]


#include "MyClass.h"

uint8_t MyClass::savedData;

void MyClass::gatherData() {
 savedData = random(0, 256);
}

uint8_t MyClass::getResult() {
 return savedData;
}




[u]**.ino File:**[/u]


#include "MyClass.h"

void setup() {
 Serial.begin(115200);
 delay(1000);

// Using scope resolution operator:
 MyClass::gatherData();
 Serial.println(MyClass::getResult());

// Using object that's declared but not defined:
 myObject.gatherData();
 Serial.println(myObject.getResult());
}

void loop() {
}

Hi gfValvo, In the example you are providing, it's still not clear to me how can I get the value to be read from inside the methods in the classes contained in the library. I am attaching a visual example of what I'm trying to achieve. If I declare the members of the class DATA as static, I get error "multiple definition of DATA::SerialData" at compile time.

calling a var inside a method inside a class from within another class inside a lib.pdf (81 KB)

As I already explained your wish to instantiate the object in your main code is contradictory to your refusal to pass a pointer or reference to it as an argument into the other methods. The two positions are more or less mutually exclusive. Why are you clinging to this notion so stubbornly?

IMO, your two best options are:

  1. Don't instantiate the object in your main code and use the static member techniques that I showed in my previous post.

OR

  1. Do instantiate the object in your main code and pass a pointer or reference as a parameter to the other methods.

If you get compiler errors, post the code which caused them, together with the exact error message.

I see at least two errors in the .h / .cpp section in your pdf file:

  1. listen() is declared as void in the .cpp file and static String in the .h file
  2. serialData in the .h file appears also outside the class definition.

6v6gt:
If you get compiler errors, post the code which caused them, together with the exact error message.

I see at least two errors in the .h / .cpp section in your pdf file:

  1. listen() is declared as void in the .cpp file and static String in the .h file
  2. serialData in the .h file appears also outside the class definition.

Evening @gfvalvo & @6v6qt I have been pondering your answers, I do apologize as I was copying the code manually from a different computer. Indeed there is a parsing error in the code. I corrected it and also initialized the static variable I use to retrieve the Serial message, but did so in the .cpp instead of the .h file and now everything works corrrectly. Here is the code as it is compiled right now:

.h

#ifndef TARGET_LIB_H
#define TARGET_LIB_H

#include <Arduino.h>

class DATA{

          private: static String serialData;

          public:
                   static void listen();
                   static String getMsgStatus();
};

class PROCESS_A{

          private: String rBuffer = "";

          public:
                   void doTask_A1();
                   void doTask_A2();
};

class PROCESS_B{
 
          private: String rBuffer = "";

          public:
                   void doTask_B1();
                   void doTask_B2();
};

#endif

.cpp

#include "Target_Lib.h"
//extern DATA data;                  //<-- Not required

void DATA::listen(){

       if(Serial.available() > 0)
              {
                Serial.print("Listening... ");
                serialData = Serial.readStringUntil('\n');
                Serial.println(serialData);
              }
              else
                {
                  Serial.println("No data...");
                }
}
String DATA::serialData;         //instantiated the serialData var here, as it is declared static inside the class

String DATA::getMsgStatus(){                      //getter for Data Class
                    
                   return(DATA::serialData);      //apply class' scope resolutor inside the return method
}



void PROCESS_A::doTask_A1(){

                   rBuffer = DATA::getMsgStatus();
                   
                   Serial.println("Task A1 executed. Value captured: " + rBuffer);    
}

void PROCESS_A::doTask_A2(){

                   rBuffer = DATA::getMsgStatus();

                   Serial.println("Task A2 executed. Value captured: " + rBuffer);
}

void PROCESS_B::doTask_B1(){

                   rBuffer = DATA::getMsgStatus();
                   
                   Serial.println("Task B1 executed. Value captured: " + rBuffer);    
}

void PROCESS_B::doTask_B2(){

                   rBuffer = DATA::getMsgStatus();
                   
                   Serial.println("Task B2 executed. Value captured: " + rBuffer);                 
}

.ino

#include "Target_Lib.h"

PROCESS_A process_A;
PROCESS_B process_B;

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

void loop() { 
                   DATA::listen();
                   process_A.doTask_A1();
                   process_A.doTask_A2();
                   process_B.doTask_B1();
                   process_B.doTask_B2();

                   delay(1000);
}

The output is as follows:

22:08:16.727 -> No data...
22:08:16.727 -> Task A1 executed. Value captured: 
22:08:16.727 -> Task A2 executed. Value captured: 
22:08:16.727 -> Task B1 executed. Value captured: 
22:08:16.727 -> Task B2 executed. Value captured: 
22:08:17.711 -> Listening... hello

22:08:17.711 -> Task A1 executed. Value captured: hello

22:08:17.711 -> Task A2 executed. Value captured: hello

22:08:17.711 -> Task B1 executed. Value captured: hello

22:08:17.711 -> Task B2 executed. Value captured: hello

As you can see, using the serial terminal, whenever I write to the controller (hello), it will read the Serial message and then make it available to the functions (methods) inside the .cpp by storing the msg in the String "serialData". I can be sure now of 2 things: 1)the message will be read and stored at the beginning of each loop scan, up to a '/n' ocurrence and, 2) all possible instantiated methods the user may have in the .ino file will have the same opportunity to read from the "serialData" var exactly the same message BEFORE the scan returns to the top and provides a chance to the Serial method to update the message.

You also can see that there is no instantiation of any sort on the .ino file, but I do execute the call to the Serial read method in .cpp using the class scope resolution operator (proposed by @gfvalvo) :

DATA::listen();

I also don't need the
extern DATA data; instantiation in .cpp, so I believe, at this point, that the OOP state is preserved. Would you guys agree? Let me know.

thanks
-EZ

It's a bit late, but why not just have the two process classes inherit from the data class? It would make things a ton easier.

Note that any members of the data class that are currently "private" would need to be changed to "protected" or "public".

Power_Broker:
It's a bit late, but why not just have the two process classes inherit from the data class? It would make things a ton easier.

Note that any members of the data class that are currently "private" would need to be changed to "protected" or "public".

Public vs Protected in C++ with Examples - GeeksforGeeks

Thanks for your interest @Power_Broker. I am not too familiar with inheritance so I struggle with the concept. I'll give the document a read; however, can you structure the provided method above with the inheritance approach you suggest? I'd be more than happy to use simplified structures / methods to achieve the desired result if you can point me in the right direction with the example.

Regards
-EZ

After further looking into your code, I don't think inheritance will help. A better method, would be to actually instantiate a "DATA" class and use its public members in conjunction with the "process" classes.

Here's how:

Header:

#pragma once
#include <Arduino.h>



class DATA
{
public:
	void begin(Stream& stream=Serial);
	void listen();
	String getMsgStatus();

private:
	Stream* _serial;
	String serialData;
};



class PROCESS_A
{
public:
	void doTask_A1(String rBuffer);
	void doTask_A2(String rBuffer);
};



class PROCESS_B
{
public:
	void doTask_B1(String rBuffer);
	void doTask_B2(String rBuffer);
};

CPP:

#include "Target_Lib.h"



void DATA::begin(Stream& stream)
{
	_serial = &stream;
}



void DATA::listen()
{
	if (_serial->available() > 0)
	{
		_serial->print("Listening... ");
		serialData = _serial->readStringUntil('\n');
		_serial->println(serialData);
	}
	else
		_serial->println("No data...");
}



String DATA::getMsgStatus()
{
	return serialData;
}



void PROCESS_A::doTask_A1(String rBuffer)
{
	Serial.println("Task A1 executed. Value captured: " + rBuffer);
}



void PROCESS_A::doTask_A2(String rBuffer)
{
	Serial.println("Task A2 executed. Value captured: " + rBuffer);
}



void PROCESS_B::doTask_B1(String rBuffer)
{
	Serial.println("Task B1 executed. Value captured: " + rBuffer);
}



void PROCESS_B::doTask_B2(String rBuffer)
{
	Serial.println("Task B2 executed. Value captured: " + rBuffer);
}

Sketch:

#include "Target_Lib.h"

DATA data;
PROCESS_A process_A;
PROCESS_B process_B;

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

void loop()
{
  data.listen();
  process_A.doTask_A1(data.getMsgStatus());
  process_A.doTask_A2(data.getMsgStatus());
  process_B.doTask_B1(data.getMsgStatus());
  process_B.doTask_B2(data.getMsgStatus());

  delay(1000);
}

Power_Broker:
After further looking into your code, I don't think inheritance will help. A better method, would be to actually instantiate a "DATA" class and use its public members in conjunction with the "process" classes.

Here's how:

Header:

#pragma once

#include <Arduino.h>

class DATA
{
public:
void begin(Stream& stream=Serial);
void listen();
String getMsgStatus();

private:
Stream* _serial;
String serialData;
};

class PROCESS_A
{
public:
void doTask_A1(String rBuffer);
void doTask_A2(String rBuffer);
};

class PROCESS_B
{
public:
void doTask_B1(String rBuffer);
void doTask_B2(String rBuffer);
};





CPP:


#include "Target_Lib.h"

void DATA::begin(Stream& stream)
{
_serial = &stream;
}

void DATA::listen()
{
if (_serial->available() > 0)
{
_serial->print("Listening... ");
serialData = _serial->readStringUntil('\n');
_serial->println(serialData);
}
else
_serial->println("No data...");
}

String DATA::getMsgStatus()
{
return serialData;
}

void PROCESS_A::doTask_A1(String rBuffer)
{
Serial.println("Task A1 executed. Value captured: " + rBuffer);
}

void PROCESS_A::doTask_A2(String rBuffer)
{
Serial.println("Task A2 executed. Value captured: " + rBuffer);
}

void PROCESS_B::doTask_B1(String rBuffer)
{
Serial.println("Task B1 executed. Value captured: " + rBuffer);
}

void PROCESS_B::doTask_B2(String rBuffer)
{
Serial.println("Task B2 executed. Value captured: " + rBuffer);
}






Sketch:


#include "Target_Lib.h"

DATA data;
PROCESS_A process_A;
PROCESS_B process_B;

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

void loop()
{
 data.listen();
 process_A.doTask_A1(data.getMsgStatus());
 process_A.doTask_A2(data.getMsgStatus());
 process_B.doTask_B1(data.getMsgStatus());
 process_B.doTask_B2(data.getMsgStatus());

delay(1000);
}

Hi Power_Broker. Thanks for the alternative structure. Although this option looks attractive, it doesn't fit my purpose because I don't want to pass parameters as arguments in the functions. In this case the parameters are passed in the .ino

  process_A.doTask_A1(data.getMsgStatus());
  process_A.doTask_A2(data.getMsgStatus());
  process_B.doTask_B1(data.getMsgStatus());
  process_B.doTask_B2(data.getMsgStatus());

While in the original code the getMsgStatus() is invoked directly inside the .cpp methods using the scope resolution operator. That also ensures that no argument is passed in the function calls inside the .cpp, as the method is called using the scope resolution directly.
Notice it is used also in the return of the getMsgStatus()

String DATA::getMsgStatus(){return(DATA::serialData);}

because the serialData variable is declared static in the listen() function.

The main reason I don't want to pass parameters or methods as args in the .ino is because the final user of the program will have a defined set of methods that will require 10-12 arguments each, and adding an extra call for the getMsgStatus into the args set is basically throwing a little wrench in the gearbox calling for trouble.
I like the way you handle the Serial stream using pointers though. I will take a careful look and try to replace in my final code.

Thanks again for your valuable insight!

-EZ

edzamper:
The main reason I don't want to pass parameters or methods as args in the .ino is because the final user of the program will have a defined set of methods that will require 10-12 arguments each, and adding an extra call for the getMsgStatus into the args set is basically throwing a little wrench in the gearbox calling for trouble.

Sounds like an XY Problem. The problem isn't passing a string to the process methods, it's the design of the rest of the code. Remember that passing structs as a parameter is an option and default arguments might help as well.

Regardless of what your library is supposed to do, though, it's asking for more trouble to have multiple classes try to process data from the same serial port without deconfliction. Having a proper interface for each serial port and farming out the processing based on that interface is a more robust solution.