Function vs object approach

Hey all,

I am using a Nextion display in my project; and I found two ways of sending data to the display.

The first approach is to use functions:

void terminate_packet() {
	Serial.write(0xFF);
	Serial.write(0xFF);
	Serial.write(0xFF);
}

void setColor(char const* name, unsigned color)
{
	Serial.print(name);
	Serial.print(".color=");
	Serial.print(color);
	
	terminate_packet();
}

void setText(char const* name, char const* text)
{
	Serial.print(name);
	Serial.print(".txt=\"");
	Serial.print(text);
	Serial.print("\"");
	
	terminate_packet();
}

The problem with this approach is that we cannot check for compatibility of the Nextion component and function (for example sending text to a Nextion component that has no text).

The other approach is to use classes:

void terminate_packet() {
	Serial.write(0xFF);
	Serial.write(0xFF);
	Serial.write(0xFF);
}

class Base {
	protected:
		char const* name;
		
		Base(char const* n) : name{n} {}
};

class Color : virtual public Base {
	using Base::Base;
	
	public:
		void setColor(unsigned color) {
			Serial.print(name);
			Serial.print(".color=");
			Serial.print(color);				
			terminate_packet();
		}
};

class Text : public virtual Base {
	using Base::Base;

	public:
		void setText(char const* text) {
			Serial.print(name);
			Serial.print(".txt=\"");
			Serial.print(text);
			Serial.print("\"");	
			terminate_packet();
		}
};

class NexButton : public Color , public Text, public Font {
	
	public:
		NexButton(char const* n) : Base(n), Color(n), Text(n), Font{n} {}
};

I prefer the object approach as it is more intuitive and type safe; however my issue with it is that the objects are created and destroyed in each loop cycle; which I presume would have significant overhead. How could I only create the objects once when needed and destroy them once we no longer need them (like switching pages on the Nextion, create and destroy component objects once per page switch). How could I achieve this without using static or global variables?

Thanks.

You don't create and destroy the Serial OOP stuff every time through loop() what makes you think you have to do that?

-jim lee

ningaman151:
The problem with this approach is that we cannot check for compatibility of the Nextion component and function (for example sending text to a Nextion component that has no text).

Is someone going to power the device off, swap displays, then power the device on?

ningaman151:
I prefer the object approach as it is more intuitive and type safe

i'm curious why you think the object approach is more intuitive or what makes it more intuituve?

and what makes it "type safe"? why would that be important

What? Why would I do that?

gcjr:
i'm curious why you think the object approach is more intuitive or what makes it more

Because it makes the link between an Object on the Arduino and the Nextion components; as if the Nextion component is in the Arduino.

gcjr:
and what makes it "type safe"? why would that be important

Let me give you an example:
Say we have a class NexButton that inherits from Color and Text. We make an instance of that class, and we call the methods we gained from inheriting Color and Text. All good for now. Now if we try to call methods from the Font class we get an error; as NexButton doesn't inherit from Font. This way we can make sure we're not sending illegal data to the display. It also makes it easier to debug as with the function approach you could send illegal data and not be aware of it.

appreciate the explanation, but they don't make sense to me. i believe well written object oriented code could be implemented more understandably. i don't believe the abstraction is necessary on a project that fits on an arduino.

jimLee:
You don't create and destroy the Serial OOP stuff every time through loop() what makes you think you have to do that?

-jim lee

loop() {
	NexButton button_1{"b0"}; //object is created at the beginning of the loop
	
	button_1.setText("button text");
} //button_1 object is destroyed

ningaman151:

loop() {
NexButton button_1{"b0"}; //object is created at the beginning of the loop

button_1.setText("button text");

} //button_1 object is destroyed

Just because you're doing it wrong does not mean it HAS to be done wrong. Make the Nextion objects global. and they will exist for the life of the program. loop() then only needs to change whatever needs to change on each pass of loop().

Look at how it's done in ANY of the Nextion example applications.

RayLivingston:
Just because you're doing it wrong does not mean it HAS to be done wrong. Make the Nextion objects global. and they will exist for the life of the program. loop() then only needs to change whatever needs to change on each pass of loop().

Look at how it's done in ANY of the Nextion example applications.

I specifically said I don't want to use global or static variables. But I guess there's no other way. Functions method it is then.

ningaman151:
I specifically said I don't want to use global or static variables.

Refusing to use global or static variables, where they are entirely appropriate and correct, is completely illogical...

RayLivingston:
Refusing to use global or static variables, where they are entirely appropriate and correct, is completely illogical...

To preserve ram.

You're not accomplishing that. In fact, just the opposite, especially if you create and destroy all the Nextion objects on every pass of loop(). Dynamic allocation consumes MORE RAM than static allocation, because in addition to the RAM occupied by the object itself, EVERY dynamically-allocated object created will have a memory block descriptor, typically 4-8 bytes of RAM (depending on the platform), attached to it, to track the size and location of that memory descriptor. So that restriction will use MORE RAM, not LESS, in addition to making the entire program slower, and just less usable.

RayLivingston:
You're not accomplishing that. In fact, just the opposite, especially if you create and destroy all the Nextion objects on every pass of loop(). Dynamic allocation consumes MORE RAM than static allocation, because in addition to the RAM occupied by the object itself, EVERY dynamically-allocated object created will have a memory block descriptor, typically 4-8 bytes of RAM (depending on the platform), attached to it, to track the size and location of that memory descriptor. So that restriction will use MORE RAM, not LESS, in addition to making the entire program slower, and just less usable.

I was talking about the use of global and static variables in general. Especially when compared to the function method. I know that the loop method is bad.

ningaman151:
What? Why would I do that?

In which case your code should never include virtual methods.

You can accomplish what you want through conditional compilation and static methods.

I think I partly understand what you mean. Can you please provide an example?

RayLivingston:
Dynamic allocation consumes MORE RAM than static allocation...

Heap allocations? Yes.

Stack allocations, such as the snippet shown? No.

without using static or global variables

I tried writing a program once where I couldn't use the = sign. That didn't turn out very well either.

Look, the important part is typically to solve the problem at hand, not "borrow trouble" by solving problems that don't yet, and may never exist. Even if you run out of RAM, its a LOT easier to grab something with more RAM than trying to use weird half understood constraints and black magic beforehand.

-jim lee

This pattern works reasonably well...

#if ! DISPLAY_SUPPORTS_COLOR
class DisplayHelper
{
  public:
    void setColor(unsigned color) 
    {
      // Ignored because the display is monochrome.
      // Suppress the warning.
      (void)(color);
    }
};
#endif

#if DISPLAY_SUPPORTS_COLOR
class DisplayHelper
{
  public:
    void setColor(unsigned color) 
    {
      // Do something useful here.
      // Suppress the warning until color is actually used.
      (void)(color);
    }
};
#endif

class NexButton
{
  public:
    NexButton(DisplayHelper& helper)
      : helper(helper)
    {
        // other constructor stuff
    }
  private:
    // Use helper to adjust the display.  It's responsible for doing the display specific things.
    DisplayHelper& helper;
};

void setup(void)
{
}

void loop(void)
{
}

Given the source code adjustments made by the IDE it's less painful to put things like DisplayHelper in separate header + source files.

I prefer this minor difference...

class DisplayHelperMonochrome
{
  public:
    void setColor(unsigned color) 
    {
      // Ignored because the display is monochrome.
      // Suppress the warning.
      (void)(color);
    }
};

class DisplayHelperColor
{
  public:
    void setColor(unsigned color) 
    {
      // Do something useful here.
      // Suppress the warning until color is actually used.
      (void)(color);
    }
};

#if DISPLAY_SUPPORTS_COLOR
typedef DisplayHelperColor DisplayHelper;
#else
typedef DisplayHelperMonochrome DisplayHelper;
#endif

class NexButton
{
  public:
    NexButton(DisplayHelper& helper)
      : helper(helper)
    {
        // other constructor stuff
    }
  private:
    // Use helper to adjust the display.  It's responsible for doing the display specific things.
    DisplayHelper& helper;
};

void setup(void)
{
}

void loop(void)
{
}

Having access to both classes makes unit testing easier.