Arduino Forum

Using Arduino => Project Guidance => Topic started by: petedd on Sep 14, 2020, 06:35 am

Title: more with char arrays and class functions
Post by: petedd on Sep 14, 2020, 06:35 am
I am trying another bit of OOP with a class for reading and parsing serial input strings, such as GPS output.

I have non-OOP code to capture serial strings, save them, and parse them.  I am trying to move this to a Class so I can have multiple types of strings to capture, with different start characters or different end characters, or different delimiter characters, but one set of methods to handle them all.  Someone may have already done this, but I am trying to use this as a learning by doing exercise.

I want to be able to have multiple instances of the class where the char array size is one of the variables.  I have not found a way to set a char array while also setting the array size in a class.  For example, I want one instance of Messages, say Messages GPS to have an dataBufferSize of 80 characters and another, say, Messages IMU to have a dataBufferSize of 18 characters, as shown in the code below.  If I could do that, than the getString method could use the class member m_dataBuffer.  Instead, I have a local dataBuffer and am considering a poor workaround of copying the resultant string back to the class member m_dataBuffer.    I hate the idea of having multiple copies of the data using up RAM, even if temporarily. If I try to strcpy the local dataBuffer to the class member m_dataBuffer, I get mismatches of char and char* and complaints about non-static and const in all the variations I have tried. But that would be one workaround.

Really, what I want to do is is just have the getString function write directly to the respective m_databuffer, for example when I call GPS.getString, the captured string is written to GPS.m_dataBuffer and then I can do other things with that string (m_dataBuffer) with other Class methods.

Classes with char arrays continue to elude me...   Help to get me off center is appreciated.

Code: [Select]
[/
/*
    Demonstration of serial input string handler using Classes to
    allow for capture and parsing of different formats or serial messages

*/

class Messages {
  private:
    int         m_dataBufferSize;
    char       m_startChar;
    char       m_endChar;
    char       m_delimiters[];
  public:
    char       m_dataBuffer;
  private:
    bool       m_storeString;

  public:
    // single delimiter instance
    Messages(int DBS, char SC, char EC, char DL1)
    {
      m_dataBufferSize = DBS;
      m_startChar = SC;
      m_endChar   = EC;
      m_delimiters[0] = DL1;
    }

    // two delimter instance
    Messages(int DBS, char SC, char EC, char DL1, char DL2)
    {
      m_dataBufferSize = DBS;
      m_startChar = SC;
      m_endChar   = EC;
      m_delimiters[0] = DL1;
      m_delimiters[1] = DL2;
    }

    bool getSerialString() {
      static byte dataBufferIndex = 0;
      char dataBuffer[m_dataBufferSize + 1];  //HOW CAN I USE THE CLASS MEMBER m_dataBuffer HERE INSTEAD OF A LOCAL dataBuffer?
      while (Serial.available() > 0) {
        char incomingbyte = Serial.read();
        if (incomingbyte == m_startChar) {
          dataBufferIndex = 0;  //Initialize our dataBufferIndex variable
          m_storeString = true;
        }
        if (m_storeString) {
          //Let's check our index here, and abort if we're outside our buffer size
          //We use our define here so our buffer size can be easily modified
          if (dataBufferIndex == m_dataBufferSize) {
            //Oops, our index is pointing to an array element outside our buffer.
            dataBufferIndex = 0;
            break;
          }
          if (incomingbyte == m_endChar) {
            dataBuffer[dataBufferIndex] = 0; //null terminate the C string
            //m_storeString = false; could be declared here but not really necessary since it is set false again next time the function is called
            //Our data string is complete.  return true
            Serial.print("In getSerialString:  ");
            Serial.println(dataBuffer);
            // ??? HOW TO COPY THE LOCAL dataBuffer to m_dataBuffer???
            //strcpy(m_dataBuffer, dataBuffer); //ERROR invalid conversion from 'char' to 'char*' [-fpermissive]
            return true;
          }
          else {
            dataBuffer[dataBufferIndex++] = incomingbyte;
            dataBuffer[dataBufferIndex] = 0; //null terminate the C string
          }
        }
        else {
        }
      }
      //We've read in all the available Serial data, and don't have a valid string yet, so return false
      return false;
    }

    void outputString() {
      Serial.print("In outputString:  ");
      Serial.println(m_dataBuffer);
    }
};




//Invoke GPS and IMU instances
//Messages.InstanceName(int DBS, char SC, char EC, char DL1,(DL2) )
Messages GPS{80, '$', '\r', ','};
Messages IMU{18, '!', '\r', ':', ','};



void setup() {
  Serial.begin(115200);
  Serial.println("Serial port activated");

}
void loop() {
  if (GPS.getSerialString()) {
    Serial.println("got string");
    GPS.outputString();  // DOES NOT HAVE THE CAPTURED STRING
    while (true) {};
  }

}
code]

Title: Re: more with char arrays and class functions
Post by: horace on Sep 14, 2020, 07:11 am
I noted that you have not specified a size for the array
Code: [Select]
   char       m_delimiters[?];  //  specify size??
be careful using Arduino Strings they  cause problems (https://hackingmajenkoblog.wordpress.com/2016/02/04/the-evils-of-arduino-strings/)
you could allocate memory dynamically (http://arduino.land/FAQ/content/4/26/en/how-to-use-dynamic-memory.htm)
Title: Re: more with char arrays and class functions
Post by: petedd on Sep 14, 2020, 04:42 pm
I noted that you have not specified a size for the array
Code: [Select]
    char       m_delimiters[?];  //  specify size??
be careful using Arduino Strings they  cause problems (https://hackingmajenkoblog.wordpress.com/2016/02/04/the-evils-of-arduino-strings/)
you could allocate memory dynamically (http://arduino.land/FAQ/content/4/26/en/how-to-use-dynamic-memory.htm)
Actually, that part of works fine.  The problem I am having is regarding the m_dataBuffer. 
Title: Re: more with char arrays and class functions
Post by: horace on Sep 14, 2020, 04:53 pm
should m_dataBuffer be a pointer to char?
Code: [Select]
char *      m_dataBuffer;
Title: Re: more with char arrays and class functions
Post by: petedd on Sep 14, 2020, 07:41 pm
should m_dataBuffer be a pointer to char?
Code: [Select]
char *      m_dataBuffer;
That seems to work for a string literal but does not let me build the string one character at a time using array indexing.  Maybe if I maintain the local dataBuffer and then try to do a strcpy to it...  but then I get a corrupted string.

Code: [Select]

/*
    Demonstation of serial input string handler using Classes to
    allow for capture and parsing of different formats or serial messages

    1.1 uses delimiters[]
    1.2EX with char*      m_dataBuffer {"12345678901234567890123456789012345678901234567890123456789012345678901234567890"};
*/

class Messages {
  public:
  int          m_dataBufferSize;
  private:
    char       m_startChar;
    char       m_endChar;
    char       m_delimiters[];
  public:
    char*      m_dataBuffer {"12345678901234567890123456789012345678901234567890123456789012345678901234567890"};
    //                        
    bool       m_storeString;

  public:
    // single delimiter instance
    Messages(int DBS, char SC, char EC, char DL1)
    {
      m_dataBufferSize = DBS;
      m_startChar = SC;
      m_endChar   = EC;
      m_delimiters[0] = DL1;
    }

    // two delimter instance
    Messages(int DBS, char SC, char EC, char DL1, char DL2)
    {
      m_dataBufferSize = DBS;
      m_startChar = SC;
      m_endChar   = EC;
      m_delimiters[0] = DL1;
      m_delimiters[1] = DL2;
    }

    bool getSerialString() {
      static byte dataBufferIndex = 0;
      char dataBuffer[m_dataBufferSize + 1];
      while (Serial.available() > 0) {
        char incomingbyte = Serial.read();
        if (incomingbyte == m_startChar) {
          dataBufferIndex = 0;  //Initialize our dataBufferIndex variable
          m_storeString = true;
        }
        if (m_storeString) {
          //Let's check our index here, and abort if we're outside our buffer size
          //We use our define here so our buffer size can be easily modified
          if (dataBufferIndex == m_dataBufferSize) {
            //Oops, our index is pointing to an array element outside our buffer.
            dataBufferIndex = 0;
            break;
          }
          if (incomingbyte == m_endChar) {
            //dataBuffer[dataBufferIndex] = 0; //null terminate the C string
            dataBuffer[dataBufferIndex] = '\n'; //null terminate the C string
            //m_storeString = false; could be declared here but not really necessary since it is set false again next time the function is called
            //Our data string is complete.  return true
            Serial.print("Exiting getString.  dataBuffer: ");
            Serial.println(dataBuffer);
            m_dataBuffer = dataBuffer;
            return true;
          }
          else {
            dataBuffer[dataBufferIndex++] = incomingbyte;
            dataBuffer[dataBufferIndex] = 0; //null terminate the C string
          }
        }
        else {
        }
      }
      //We've read in all the available Serial data, and don't have a valid string yet, so return false
      return false;
    }

};




//Invoke GPS and IMU instances
//Messages.InstanceName(int DBS, char SC, char EC, char DL1,(DL2) )
Messages GPS{80, '$', '\r', ','};
Messages IMU{18, '!', '\r', ':', ','};



void setup() {
  Serial.begin(115200);
  Serial.println("Serial port activated");

}
void loop() {
  Serial.print("GPS.m_dataBuffer starting: ");
  Serial.println(GPS.m_dataBuffer);
  if (GPS.getSerialString()) {
    Serial.println("got string");
    Serial.print("GPS.m_dataBuffer: ");
    Serial.println(GPS.m_dataBuffer);
    while (true) {};
  }

}



INPUT:

$GPGSA,A,3,07,02,26,27,09,04,15,,,,,,1.8,1.0,1.5*33


OUTPUT:

GPS.m_dataBuffer starting: 3456789012345678901234567890123456789012345678901234567890
Exiting getString.  dataBuffer: $GPGSA,A,3,07,02,26,27,09,04,15,,,,,,1.8,1.0,1.5*33

got string
GPS.m_dataBuffer: $GPGSA,A,3,07,02,26,27,09,04,15,,,,,⸮⸮⸮
Title: Re: more with char arrays and class functions
Post by: petedd on Sep 14, 2020, 08:57 pm
okay gang, here is a version that works but I think setting the m_dataBuffer this way is ugly and does not allow for different sized m_dataBuffer for each invocation of the class (that is, say, 80 characters for GPS and 18 characters for IMU).

Still working on this...

Code: [Select]


/*
    Demonstration of serial input string handler using Classes to
    allow for capture and parsing of different formats or serial messages

    1.1 uses delimiters[]
    1.21ex This version sets the m_dataBuffer to an 80 character literal string and works but does not allow me to change the size of the
             buffers based on the m_dataBufferSize.
*/

class Messages {
  public:
    int        m_dataBufferSize;
  private:
    char       m_startChar;
    char       m_endChar;
    char       m_delimiters[];
  public:
    char*      m_dataBuffer {"12345678901234567890123456789012345678901234567890123456789012345678901234567890"}; //UGLY!!
    bool       m_storeString;

  public:
    // single delimiter instance
    Messages(int DBS, char SC, char EC, char DL1)
    {
      m_dataBufferSize = DBS;
      m_startChar = SC;
      m_endChar   = EC;
      m_delimiters[0] = DL1;
    }

    // two delimter instance
    Messages(int DBS, char SC, char EC, char DL1, char DL2)
    {
      m_dataBufferSize = DBS;
      m_startChar = SC;
      m_endChar   = EC;
      m_delimiters[0] = DL1;
      m_delimiters[1] = DL2;
    }

    bool getSerialString() {
      static byte dataBufferIndex = 0;
      while (Serial.available() > 0) {
        char incomingbyte = Serial.read();
        if (incomingbyte == m_startChar) {
          dataBufferIndex = 0;  //Initialize our dataBufferIndex variable
          m_storeString = true;
        }
        if (m_storeString) {
          //Let's check our index here, and abort if we're outside our buffer size
          if (dataBufferIndex == m_dataBufferSize) {
            //Oops, our index is pointing to an array element outside our buffer.
            dataBufferIndex = 0;
            break;
          }
          if (incomingbyte == m_endChar) {
            m_dataBuffer[dataBufferIndex] = 0; //null terminate the C string
            //m_storeString = false; could be declared here but not really necessary since it is set false again next time the function is called
            //Our data string is complete.  return true
            Serial.print("Exiting getString.  m_dataBuffer: ");
            Serial.println(m_dataBuffer);
            return true;
          }
          else {
            m_dataBuffer[dataBufferIndex++] = incomingbyte;
            m_dataBuffer[dataBufferIndex] = 0; //null terminate the C string
          }
        }
        else {
        }
      }
      //We've read in all the available Serial data, and don't have a valid string yet, so return false
      return false;
    }

};




//Invoke GPS and IMU instances
//Messages.InstanceName(int DBS, char SC, char EC, char DL1,(DL2) )
Messages GPS{80, '$', '\r', ','};
Messages IMU{18, '!', '\r', ':', ','};

bool flag = true;

void setup() {
  Serial.begin(115200);
  Serial.println("Serial port activated");
  delay(200);

}
void loop() {


  if (flag) {
    Serial.print("GPS.m_dataBuffer starting: ");
    Serial.println(GPS.m_dataBuffer);
    flag = false;
  }

  if (GPS.getSerialString()) {
    Serial.println("got string");
    Serial.print("GPS.m_dataBuffer: ");
    Serial.println(GPS.m_dataBuffer);
    while (true) {};
  }

}
Title: Re: more with char arrays and class functions
Post by: PieterP on Sep 14, 2020, 09:25 pm
Actually, that part of works fine.  The problem I am having is regarding the m_dataBuffer. 
Depending on the version of GCC you're using that's either a zero-length array, or a flexible array member.
Both are GNU extensions, and you don't want to have to deal with either of them.

When I compile your class with the latest version of the AVR core (GCC 7.3), I get the error:
Code: [Select]
sketch:12:29: error: flexible array member 'Messages::m_delimiters' not at end of 'class Messages'
     char       m_delimiters[];
                             ^

So I assume you're using an older version of GCC, which interprets your m_delimiters member as an array with size zero.
In that case you're writing completely out of bounds in your constructor, m_delimiters is an array of length zero, there is no "first element":
Code: [Select]
Messages(int DBS, char SC, char EC, char DL1, char DL2)
    {
      m_dataBufferSize = DBS;
      m_startChar = SC;
      m_endChar   = EC;
      m_delimiters[0] = DL1; // <--- m_delimiters[0] does not exist
      m_delimiters[1] = DL2; // <--- m_delimiters[1] does not exist
    }

You're just overwriting memory that's not part of m_delimiters (most likely the memory of the other members of your struct).


Take a step back and think about ownership: someone has to own the memory of the string you store. Either the instance of your "Messages" class owns the string, or someone else does.

If someone else owns the memory, you can run into lifetime issues. For example:

Code: [Select]
struct Message {
  const char *message; // This just points to a string that's owned by someone else
};

Message numberToMessage(int number) {
  String str(number);
  return Message{str.c_str()};
} // Oops: str is deleted here, and the Message still contains a dangling pointer to it

In this example, the memory that stores the string is owned by the local str variable. The Message only stores a pointer to it. When the str variable is deleted, the pointer is no longer valid, because the string it points to is gone.

You can work around lifetimes issues like that, but it's cumbersome, and requires you as the programmer to be extremely aware of the lifetimes of all of your strings and messages.

The alternative is to make the Message itself own its string.
You cannot have C-style arrays with a dynamic size. The size of an array must be known at compile-time. So you'll have to create a buffer that's large enough so your longest message will fit, but not too large that it'll waste too much memory.

For example:
Code: [Select]
struct Message {
  char buffer[16] = {}; // maximum message length is 15 characters + null terminator
  Message(const char *message) {
    strncpy(buffer, message, sizeof(buffer) - 1);
  }
};

Message numberToMessage(int number) {
  String str(number);
  return Message{str.c_str()};
} // str is deleted here, but Message made a copy in the buffer that it owns


Always allocating memory for the longest possible message size is wasteful. The example above always takes 16 bytes to store a message, even if the message is empty.

As an alternative, you could allocate the buffer dynamically. This is what the String class does, for example. The Message still owns its string buffer, but it's now allocated on the heap instead of directly inside of the struct.

For example:

Code: [Select]
struct Message {
  String buffer;
  Message(const char *message) : buffer(message) {}
};

Message numberToMessage(int number) {
  String str(number);
  return Message{str.c_str()};
} // str is deleted here, but Message made a copy in the buffer that it owns

This is much easier to manage, but opens a whole new can of worms: Heap fragmentation. Dynamic allocations, especially when using strings that can have different lengths, can cause the heap memory to become fragmented. When this happens, there are many small sections of free memory, but no large contiguous sections. Allocating large amounts of memory becomes impossible and your program crashes. Desktop computers have a ton of RAM, a memory management unit an operating system to prevent this, but your Arduino has a couple of kilobytes of RAM and runs on the bare metal, so heap fragmentation is a huge issue.
Dynamically allocated strings can be non-deterministic, so you cannot always know beforehand whether heap fragmentation will occur. That's why dynamic allocations are often forbidden in critical software.

Pieter
Title: Re: more with char arrays and class functions
Post by: petedd on Sep 14, 2020, 09:53 pm
Pieter,
Thank you again!
This is a true and valuable lesson and I appreciate you taking the time to lay it out.  I will fix the m_delimiters[] declaration.
Title: Re: more with char arrays and class functions
Post by: petedd on Sep 14, 2020, 10:15 pm
Further... towards solving the m_dataBuffer creation with different sized buffers, I created another armstrong method wherein I see the m_dataBuffer in the constructor with a string literal.  Not pretty, but better than just setting all instantiations of the class to the maximum buffer size of the largest message type.  This is the first block of code below.

I still would like to come up with a way to dynamically build the m_dataBuffer seed.  I have tried this using a method (class function) initBuffer, that snippet shown in the second code block below.  This builds a fakeBuffer character array and then attempts to set m_dataBuffer to this array.  That does not work (the results of what actually copies is shown in the comments of the code).  I feel I am close here, but putting the value of the character array into the char * m_dataBuffer eludes me.   Ideas?

Code: [Select]
/*
    Demonstration of serial input string handler using Classes to
    allow for capture and parsing of different formats or serial messages

    1.1 uses delimiters[]
    1.21 This version sets the m_dataBuffer to an 80 character literal string and works but does not allow me to change the size of the
             buffers based on the m_dataBufferSize.
    1.22 -delimiters[] declaration changed to delimiters[3] and constructors updated to include 1, 2 , and 3 delimiters
         -uses string literal in constructor to seed m_dataBuffer to maximum size
*/

class Messages {
  public:
    int        m_dataBufferSize;
  private:
    char       m_startChar;
    char       m_endChar;
    char       m_delimiters[3];
  public:
    char*      m_dataBuffer;
    bool       m_storeString;

  public:
    // single delimiter instance
    Messages(int DBS, char SC, char EC, char DL0, char* DB)
    {
      m_dataBufferSize = DBS;
      m_startChar = SC;
      m_endChar   = EC;
      m_delimiters[0] = DL0;
      m_dataBuffer= DB;  // DB should be a string literal "aflsf" the length of DBS
    }

    // two delimter instance
    Messages(int DBS, char SC, char EC, char DL0, char DL1, char* DB)
    {
      m_dataBufferSize = DBS;
      m_startChar = SC;
      m_endChar   = EC;
      m_delimiters[0] = DL0;
      m_delimiters[1] = DL1;
      m_dataBuffer= DB;  // DB should be a string literal "aflsf" the length of DBS
    }

    // three delimter instance
    Messages(int DBS, char SC, char EC, char DL0, char DL1, char DL2, char* DB)
    {
      m_dataBufferSize = DBS;
      m_startChar = SC;
      m_endChar   = EC;
      m_delimiters[0] = DL0;
      m_delimiters[1] = DL1;
      m_delimiters[2] = DL2;
      m_dataBuffer= DB;  // DB should be a string literal "aflsf" the length of DBS
    }

    bool getSerialString() {
      static byte dataBufferIndex = 0;
      while (Serial.available() > 0) {
        char incomingbyte = Serial.read();
        if (incomingbyte == m_startChar) {
          dataBufferIndex = 0;  //Initialize our dataBufferIndex variable
          m_storeString = true;
        }
        if (m_storeString) {
          //Let's check our index here, and abort if we're outside our buffer size
          if (dataBufferIndex == m_dataBufferSize) {
            //Oops, our index is pointing to an array element outside our buffer.
            dataBufferIndex = 0;
            break;
          }
          if (incomingbyte == m_endChar) {
            m_dataBuffer[dataBufferIndex] = 0; //null terminate the C string
            //m_storeString = false; could be declared here but not really necessary since it is set false again next time the function is called
            //Our data string is complete.  return true
            Serial.print("Exiting getString.  m_dataBuffer: ");
            Serial.println(m_dataBuffer);
            return true;
          }
          else {
            m_dataBuffer[dataBufferIndex++] = incomingbyte;
            m_dataBuffer[dataBufferIndex] = 0; //null terminate the C string
          }
        }
        else {
        }
      }
      //We've read in all the available Serial data, and don't have a valid string yet, so return false
      return false;
    }

};




//Invoke GPS and IMU instances
//Messages.InstanceName(int DBS, char SC, char EC, char DL1,(DL2) )
Messages GPS{80, '$', '\r', ',', "12345678901234567890123456789012345678901234567890123456789012345678901234567890"};
Messages IMU{18, '!', '\r', ':', ',', "123456789012345678"};

bool flag = true;

void setup() {
  Serial.begin(115200);
  Serial.println("Serial port activated");
  delay(200);

}
void loop() {


  if (flag) {
    Serial.print("GPS.m_dataBuffer starting: ");
    Serial.println(GPS.m_dataBuffer);
    flag = false;
  }

  if (GPS.getSerialString()) {
    Serial.println("got string");
    Serial.print("GPS.m_dataBuffer: ");
    Serial.println(GPS.m_dataBuffer);
    while (true) {};
  }

}




Code: [Select]


    void initBuffer() {
      char fakeBuffer[m_dataBufferSize+1];
      for(int i = 0;i<m_dataBufferSize;++i){
        fakeBuffer[i] = '2'; // just fill with '2's as placeholders          
      }
      fakeBuffer[m_dataBufferSize] = 0;  // null character on end
      Serial.print("fakeBuffer: ");
      Serial.println(fakeBuffer);
      m_dataBuffer = (char*)fakeBuffer;  //THIS DOES NOT WORK - THE COMPLETE STRING IS NOT TRANSFERED.
      // this char array: 22222222222222222222222222222222222222222222222222222222222222222222222222222222
      //         becomes: 22222222222222222222222222222222222222222⸮⸮⸮=>
      
    }

};

Title: Re: more with char arrays and class functions
Post by: PieterP on Sep 15, 2020, 12:24 am
You've fallen into the lifetime trap. The lifetime of "fakeBuffer" ends at the end of the "initBuffer()" method. After that, the m_dataBuffer is dangling.

You're still using variable length arrays, which is a GNU extension and a bad idea. All array sizes should be compile-time constants.

Code: [Select]
Messages IMU{18, '!', '\r', ':', ',', "123456789012345678"};

That's invalid as well. You cannot pass a string literal as "char *" and then write to it. String literals are read-only.
Title: Re: more with char arrays and class functions
Post by: petedd on Sep 15, 2020, 01:14 am
Let me try to parse that...
Quote
You've fallen into the lifetime trap. The lifetime of "fakeBuffer" ends at the end of the "initBuffer()" method. After that, the m_dataBuffer is dangling.
1) I intended fakeBuffer to be a local variable which would die when exiting initBuffer()
2) I wanted to then move the contents of fakeBuffer into m_dataBuffer
3) I have instead moved a pointer to fakeBuffer into m_dataBuffer (correct?)
4) Since fakeBuffer is ephemeral, I don't end up with the right content when I try to work with m_dataBuffer later. (correct?)

So, is there no solution to having a Class wherein I can have different sized char arrays, with their sizes defined by the constructor?  (Again, I have lots of workarounds, but I am trying to use this as a learning exercise).
Title: Re: more with char arrays and class functions
Post by: gfvalvo on Sep 15, 2020, 02:49 am
You can have you class constructor dynamically create an char array that's owned by the object using the 'new' operator. It's true that dynamic allocation could cause problems on a small memory processor. So, be sure the class's destructor properly releases the memory. Also, you need to trap and properly handle the situation where there's not enough memory available and 'new' returns nullptr. Also, carefully think about how your copy constructor and operator= function will work. Should they even be allowed?

EDIT - Other Issues with Your Code:
* If you use dynamic allocation as mentioned above, there's no need to supply a "string literal in constructor to seed m_dataBuffer to maximum size"

* Also, you have 3 overloaded constructors to allow different numbers of "delimiters". But, once the object is instantiated, you have no idea of which constructor was used or how many "delimiters" it has. I'd add a member variable that's set to the proper value by each of the constructors.

* Why is 'm_dataBufferSize' an int? Do you ever expect a buffer of negative length? If not, use an unsigned type like uint8_t or uint16_t.
Title: Re: more with char arrays and class functions
Post by: GoForSmoke on Sep 15, 2020, 04:13 am
I got here late, i can see. What I do is put a char * in the class and point it to a char array elsewhere.

I have a single-pass parse&lex. It gets the chars 1 at a time as they arrive and finds the first string in an alpha-sorted array of  char arrays that matches *so far* and if/when it doesn't, moves to the next until it does or there -is- no match. By the time the delimiter at the end of the word is reached, match or no-match status is known and if a match, the word number is known. In the time it takes to buffer a word, this way also has it identified.

added: oh hang on, you're not trying to match the text to saved text so no lex.
 

Title: Re: more with char arrays and class functions
Post by: petedd on Sep 15, 2020, 05:03 am
I got here late, i can see. What I do is put a char * in the class and point it to a char array elsewhere.


Thank you.  I'm not sure what you mean by "point it to a char array elsewhere".  How does one do that?  Would you have a code snippet you could share or insert an example into my Class code above?

Thanks again.
Title: Re: more with char arrays and class functions
Post by: petedd on Sep 15, 2020, 05:11 am
You can have you class constructor dynamically create an char array that's owned by the object using the 'new' operator. It's true that dynamic allocation could cause problems on a small memory processor. So, be

* Also, you have 3 overloaded constructors to allow different numbers of "delimiters". But, once the object is instantiated, you have no idea of which constructor was used or how many "delimiters" it has. I'd add a member variable that's set to the proper value by each of the constructors.

* Why is 'm_dataBufferSize' an int? Do you ever expect a buffer of negative length? If not, use an unsigned type like uint8_t or uint16_t.

Thank you.  To answer these two questions/points:

* In this case, I will be using strtok() to parse the fields.  Having the delimiters in an array, the delimiter field of strtok only needs to know the name of the array, so I believe I can get away with a member variable for the number of delimiters.  The unused array entries should be null but if I find that is not the case, I can fix that also.

* Odd that... I have m_dataBufferSize as a uint8_t in the very first version of the code and some error led me to change it to an int.  I will change it back and assume I won't have any issues with that.  Thanks.
Title: Re: more with char arrays and class functions
Post by: gfvalvo on Sep 15, 2020, 12:40 pm
The unused array entries should be null but if I find that is not the case, I can fix that also.
That will only be true when for objects of your class that are instantiated as global (and perhaps static-local) variables. Ones that are instantiated dynamically or as automatic-local variables will have random data in the array. So, yes, you should fix it by explicitly initialing all elements of the array to 0.

However, the main point of my post was that you can specify the size of the data buffer character array with a parameter to the constructor (or even a begin() method) by using dynamic allocation. But, you need to mindful of memory management.
Title: Re: more with char arrays and class functions
Post by: petedd on Sep 15, 2020, 04:59 pm
Got it.  Thanks.  I avoid dynamic allocation on microprocessors whenever I can.
Title: Re: more with char arrays and class functions
Post by: gfvalvo on Sep 15, 2020, 07:21 pm
I avoid dynamic allocation on microprocessors whenever I can.
In that case, it seems like the only robust way of achieving your stated goal of having "different sized char arrays, with their sizes defined by the constructor" is to use templates. Of course, that requires that the size must be known at compile-time. Also, objects with different-sized arrays created this way will be different data types.
Title: Re: more with char arrays and class functions
Post by: petedd on Sep 15, 2020, 11:06 pm
If anyone is interested, here is my final demonstration code for capturing, parsing, and displaying multiple string message formats using Class-based OOP.   Here I demo it with one port and alternately capture one message string type and then the other.   If one were using a chip with multiple serial ports, the input of each string type could be assigned to a dedicated port and share the code.

THANK YOU TO ALL WHO HELPED ME GET ON TRACK HERE!!

Code: [Select]

/*
    Demonstration of serial input string handler using Classes to
    allow for capture and parsing of different formats or serial messages

    This is  based on the non-Class examples in the (classy nonetheless) tutorials by J Haskell found at:
      http://jhaskellsblog.blogspot.com/2011/05/serial-comm-fundamentals-on-arduino.html  and
      http://jhaskellsblog.blogspot.com/2011/06/parsing-quick-guide-to-strtok-there-are.html

    Here GPS and IMU strings are handled.  "Parsing" here is just the breaking out and printing of string fields.
    Further usage of the data could be done with additional coding to capture the values as for use later as intergers, floats, char*, etc

    The GPS module might be this one: http://www.sparkfun.com/products/465
    The IMU might be this one: http://www.sparkfun.com/products/9623

    Here are some test strings you can copy and paste into the Serial Monitor
    NOTE: Be sure to set the Serial Monitor to add either CR or both NL/CR

       $GPGGA,161229.487,3723.2475,N,12158.3416,W,1,07,1.0,9.0,M,,,,0000*18
       $GPGLL,3723.2475,N,12158.3416,W,161229.487,A*2C
       $GPGSA,A,3,07,02,26,27,09,04,15,,,,,,1.8,1.0,1.5*33

       !ANG:320,33,191
       !ANG:0,320,90
       !ANG:0,0,0

    While this example uses only one serial port, one could assign GPS input to one port and IMU input to another and add a member variable
    to the Messages class for the port to use (eg: byte m_inputPort;  where, for example, GPS input is found serial port 1 and IMU input is
    found on serial port 2) and then use different ports in the getString method based on the passing of m_inputPort.  By keeping all variables (
    except incomingbyte as member variables, the code is reentrant in that multiple strings can be captured, each from a separate port.

    The Messages Class is set-up here to accept one, two, or three field delimiters in the constructor for each instance.  Here we use two for
    the GPS and three for the IMU strings.

//   Copyright (c) 2020 by Peter Dubler
//              This program is free software: you can redistribute it and/or modify
//              it under the terms of the GNU General Public License as published by
//              the Free Software Foundation, either version 3 of the License, or
//              (at your option) any later version.
//     
//              This program is distributed in the hope that it will be useful,
//              but WITHOUT ANY WARRANTY; without even the implied warranty of
//              MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//              GNU General Public License for more details.
//     
//              The GNU General Public License
//              may be found here: <http://www.gnu.org/licenses/>.

*/

class Messages {
  public:
    uint8_t    m_dataBufferSize;
  private:
    char       m_startChar;
    char       m_endChar;
    char       m_delimiters[3] {0, 0, 0};  // set default values to null character
    char*      m_dataBuffer;  // if you want to access the dataBuffer (as GPS.m_dataBuffer for example)with non-class functions, change this to public:
                              // m_dataBuffer is "seeded" with a maximum-sized (for the string to be captured) string literal in the constructor
                              //   this assures stability of the data fields.
    bool       m_storeString;
    uint8_t    m_dataBufferIndex;

  public:
    // here we will create overloads of the constructor specifications for one, two, or three field delimiters
    // single delimiter instance
    Messages(uint8_t DBS, char SC, char EC, char DL0, char* DB)
    {
      m_dataBufferSize = DBS;
      m_startChar = SC;
      m_endChar   = EC;
      m_delimiters[0] = DL0;
      m_dataBuffer = DB; // DB should be a string literal "abc..." the length of DBS
    }

    // two delimter instance
    Messages(uint8_t DBS, char SC, char EC, char DL0, char DL1, char* DB)
    {
      m_dataBufferSize = DBS;
      m_startChar = SC;
      m_endChar   = EC;
      m_delimiters[0] = DL0;
      m_delimiters[1] = DL1;
      m_dataBuffer = DB; // DB should be a string literal "abc..." the length of DBS
    }

    // three delimter instance
    Messages(uint8_t DBS, char SC, char EC, char DL0, char DL1, char DL2, char* DB)
    {
      m_dataBufferSize = DBS;
      m_startChar = SC;
      m_endChar   = EC;
      m_delimiters[0] = DL0;
      m_delimiters[1] = DL1;
      m_delimiters[2] = DL2;
      m_dataBuffer = DB; // DB should be a string literal "abc..." the length of DBS
    }

    bool getSerialString() {  // Captures serial input starting with m_startChar and ending with m_endChar, upto a length of m_dataBufferSize.
                              // Returns true when full string has been captured.
      //static byte dataBufferIndex = 0;
      while (Serial.available() > 0) {
        char incomingbyte = Serial.read();
        if (incomingbyte == m_startChar) {
          m_dataBufferIndex = 0;  //Initialize our dataBufferIndex variable
          m_storeString = true;
        }
        if (m_storeString) {
          //Let's check our index here, and abort if we're outside our buffer size
          if (m_dataBufferIndex == m_dataBufferSize) {
            //Oops, our index is pointing to an array element outside our buffer.
            m_dataBufferIndex = 0;
            m_storeString = false; //reset flag so that next time through method will check for m_startChar
            break;
          }
          if (incomingbyte == m_endChar) {
            m_dataBuffer[m_dataBufferIndex] = 0; //null terminate the C string
            m_storeString = false; //reset flag so that next time through method will check for m_startChar
            //Our data string is complete.  return true
            return true;
          }
          else {
            m_dataBuffer[m_dataBufferIndex++] = incomingbyte;
            m_dataBuffer[m_dataBufferIndex] = 0; //null terminate the C string
          }
        }
        else {
        }
      }
      //We've read in all the available Serial data, and don't have a valid string yet, so return false
      return false;
    }

    void parseString() {
      char* valPosition;
      //This initializes strtok with our string to tokenize
      valPosition = strtok(m_dataBuffer, m_delimiters);

      while (valPosition != NULL) {
        Serial.print(valPosition);  Serial.print("  ");
        //Here we pass in a NULL value, which tells strtok to continue working with the previous string
        valPosition = strtok(NULL, m_delimiters);
      }
      Serial.println('\n');//Serial.println();
    }

};

#define GPSbufferSeed "12345678901234567890123456789012345678901234567890123456789012345678901234567890"  // 80 character string literal
#define IMUbufferSeed "123456789012345678"  // 18 character string literal


//Invoke GPS and IMU instances
//Messages InstanceName(int DBS, char SC, char EC, char DL1,(DL2),(DL3), char * bufferSeed) )
Messages GPS{80, '$', '\r', ',', '$', GPSbufferSeed};
Messages IMU{18, '!', '\r', ':', ',', '!', IMUbufferSeed};



void setup() {
  Serial.begin(115200);
  Serial.println("Serial port activated");
  delay(200);

}
void loop() {  // Ask for, receive, parse, and display parsed GPS string, then IMU string, and repeat
  Serial.println("Waiting for GPS string");
  bool gotString = false;
  while (!gotString) {
    if (GPS.getSerialString()) {
      Serial.print("got GPS string: ");
      GPS.parseString();
      gotString = true;
    }
  }

  Serial.println("Waiting for IMU string");
  gotString = false;
  while (!gotString) {
    if (IMU.getSerialString()) {
      Serial.print("got IMU string: ");
      IMU.parseString();
      gotString = true;
    }
  }
}

Title: Re: more with char arrays and class functions
Post by: gfvalvo on Sep 16, 2020, 01:36 am
I assume you've seen the following compiler warnings from your code (if not, turn up the warning level in the Arduino IDE):
Code: [Select]


C:\Users\GFV\AppData\Local\Temp\arduino_modified_sketch_756953\sketch_sep15a.ino:155:52: warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]

 Messages GPS{80, '$', '\r', ',', '$', GPSbufferSeed};

                                                    ^

C:\Users\GFV\AppData\Local\Temp\arduino_modified_sketch_756953\sketch_sep15a.ino:156:57: warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]

 Messages IMU{18, '!', '\r', ':', ',', '!', IMUbufferSeed};

                                                         ^


I believe it was mentioned in one of your other threads that writing to memory occupied by a string literal is a really bad thing. It still is.

Why not just do this:
Code: [Select]
char GPSbufferSeed[80];
char IMUbufferSeed[18];


//Invoke GPS and IMU instances
//Messages InstanceName(int DBS, char SC, char EC, char DL1,(DL2),(DL3), char * bufferSeed) )
Messages GPS{sizeof(GPSbufferSeed), '$', '\r', ',', '$', GPSbufferSeed};
Messages IMU{sizeof(IMUbufferSeed), '!', '\r', ':', ',', '!', IMUbufferSeed};


Either way, the code violates the OOP tenant of Encapsulation. Since the buffer is handled by the class, it should be encapsulated in the the class instead of being supplied externally.

Also, the code is hardwired to only work with the Serial object. Better to pass a reference to a Stream object into the constructor. That way the class could work with any object whose class inherits from Stream (HardwareSerial, usb_serial_class, SoftwareSerial, etc).
Title: Re: more with char arrays and class functions
Post by: GoForSmoke on Sep 16, 2020, 06:24 am
Thank you.  I'm not sure what you mean by "point it to a char array elsewhere".  How does one do that?  Would you have a code snippet you could share or insert an example into my Class code above?

Thanks again.
in pseudocode then?
it's been too long since I used C++

class Whatever
{
  char * textbuff;
  int      otherdata;
}

char str00[] = "12345678";
char str01[] = "1234567";
char str02[] = "123456";
char str03[] = "12345";
char str04[] = "1234";
char str05[] = "123";
char str06[] = "12";
char str07[] = "1";

Whatever textobject();

textobject.textbuff = str04;

The name of every array is a pointer to the 0 element of that array. A 2D array is a pointer to an array of pointers, etc.

I learned C first in the 80's. It fits small computers, was great on the Z80.
Title: Re: more with char arrays and class functions
Post by: GoForSmoke on Sep 16, 2020, 07:20 am
Thank you.  I'm not sure what you mean by "point it to a char array elsewhere".  How does one do that?  Would you have a code snippet you could share or insert an example into my Class code above?

Thanks again.
If you store your delimiters in flash, the elsewhere you point to uses PROGMEM to write and read. EEPROM has its own library but an address is an address if you get the street right, pointers are an address that the compiler treats according to data type in source but the 2 bytes in an AVR pointer are offset from 0 addresses no matter which memory type.


If you set a parser up in a serial stream, at 250000 baud you have 640 cycles between each char arriving to check delimiters. If the delimiters are < all other legal input chars that could happen in very few cycles, possibly 1.

You have a "free" 64-byte buffer as long as it's going to print on Serial. There is no need to assemble text in a buffer just to print it. Print each item in turn and try not to overfill the Serial output buffer as it is easy to print 64 more chars before the bits from the first clear the TX register.
Title: Re: more with char arrays and class functions
Post by: PieterP on Sep 16, 2020, 05:56 pm
Whatever textobject();
I know you intended it as pseudocode, but it's too close to C++ not to say anything about it :)

Whatever textobject(); is the declaration of a function textobject that takes no arguments and returns an object of type Whatever.
If you want to declare a variable textobject of type Whatever, you want Whatever textobject; (without the parentheses).

A 2D array is a pointer to an array of pointers, etc.
That's not true, a 2D array is not the same as an array of pointers. Multidimensional arrays are stored contiguously in memory. There are no pointers involved at all.

If you do want your multidimensional array of type T to decay to a pointer, you'll still end up with a pointer to T, not pointer to pointer to T. (This is not usually done, because you lose the stride or leading dimensions of your multidimensional array. Unlike one-dimensional arrays, multidimensional arrays don't decay to pointers implicitly.)

For example: https://godbolt.org/z/qj1cE6 (https://godbolt.org/z/qj1cE6)

An array of pointers behaves completely differently, is not necessarily laid out contiguously in memory, and requires an extra indirection: https://godbolt.org/z/e5n7qd (https://godbolt.org/z/e5n7qd)
Title: Re: more with char arrays and class functions
Post by: petedd on Sep 16, 2020, 09:42 pm
I assume you've seen the following compiler warnings from your code (if not, turn up the warning level in the Arduino IDE):


Thanks.  Great input.  I will look into using a stream object for input... one more thing to learn.
Title: Re: more with char arrays and class functions
Post by: petedd on Sep 16, 2020, 09:50 pm
Either way, the code violates the OOP tenant of Encapsulation. Since the buffer is handled by the class, it should be encapsulated in the the class instead of being supplied externally.

Also, the code is hardwired to only work with the Serial object. Better to pass a reference to a Stream object into the constructor. That way the class could work with any object whose class inherits from Stream (HardwareSerial, usb_serial_class, SoftwareSerial, etc).

gfvalvo,

By the way, do you have a favorite book, reference, tutorial, etc. that might cover Encapsulation and Stream objects which you might recommend?  (I have lots that I am reading, but perhaps you know of something better).  Thanks again.
Title: Re: more with char arrays and class functions
Post by: gfvalvo on Sep 16, 2020, 10:21 pm
By the way, do you have a favorite book, reference, tutorial, etc. that might cover Encapsulation and Stream objects which you might recommend?  (I have lots that I am reading, but perhaps you know of something better).
Nothing specific, different topics.

Encapsulation is a general OOP concept. As I'm sure you've found there is voluminous information and references for OOP out there. Perhaps too much :)

The Stream class is Arduino-specific. There's a chain of class Inheritance (another OOP concept):

Print: An abstract class that knows how to output numerical values, c-strings, String objects, etc as a serial of ASCII characters. However, it doesn't know how to actually "print" to any specific hardware.

Stream: An abstract class that inherits from Print. It adds the ability to input ASCII characters. But, it also doesn't know how to work with specific hardware.

HardwareSerial, usb_serial_class, SoftwareSerial, etc: Classes that inherit from Stream. They are concrete classes (you can instantiate objects with them) that know how to work with specific hardware to do ASCII input and outputs.

Also, some concrete classes (such as those for LCD displays) inherit directly from Print as they involve output only, no input.
Title: Re: more with char arrays and class functions
Post by: petedd on Sep 16, 2020, 10:34 pm
The Stream class is Arduino-specific. There's a chain of class Inheritance (another OOP concept):

Print: An abstract class that knows how to output numerical values, c-strings, String objects, etc as a serial of ASCII characters. However, it doesn't know how to actually "print" to any specific hardware.

Stream: An abstract class that inherits from Print. It adds the ability to input ASCII characters. But, it also doesn't know how to work with specific hardware.

HardwareSerial, usb_serial_class, SoftwareSerial, etc: Classes that inherit from Stream. They are concrete classes (you can instantiate objects with them) that know how to work with specific hardware to do ASCII input and outputs.

Also, some concrete classes (such as those for LCD displays) inherit directly from Print as they involve output only, no input.

Thanks again.

1) I thought I was trying to encapsulate everything so having an external array which is not private breaks that.  Still looking for a way to create different-sized char arrays as private objects in my Messages Class.

2) This Streams concept sounds just like one layer in what we called a protocol stack back when I learned this over 40 years ago.  Pretty straightforward.
Title: Re: more with char arrays and class functions
Post by: gfvalvo on Sep 16, 2020, 11:15 pm
Still looking for a way to create different-sized char arrays as private objects in my Messages Class.
As I said, if you don't want to use dynamic allocation, then templates may be the only robust way. Example below. I didn't implement your actual message class because I didn't want to deal with providing input to parse. But, it shows all the concepts I recommended.
Code: [Select]
template<size_t N>
class Messages {
  public:
    Messages(Stream &str) : charStream(str), m_dataBufferSize(N) {
    }
    void setBuffer(const char *inString);
    void printBuffer();
    const char *getBufferPointer();
    size_t getStringLen();
    size_t getBufferSize();

  private:
    Stream &charStream;
    size_t m_dataBufferSize;
    char m_dataBuffer[N + 1] = {'\0'}; // Make sure there's always a null character at end of buffer
};


template<size_t N>
void Messages<N>::setBuffer(const char *inString) {
  strncpy(m_dataBuffer, inString, m_dataBufferSize);
}

template<size_t N>
void Messages<N>::printBuffer() {
  charStream.print(m_dataBuffer);
}

template<size_t N>
size_t Messages<N>::getBufferSize() {
  return (m_dataBufferSize);
}

template<size_t N>
size_t Messages<N>::getStringLen() {
  return strlen(m_dataBuffer);
}

template<size_t N>
const char * Messages<N>::getBufferPointer() {
  return m_dataBuffer;
}


Messages<80> GPSbufferSeed(Serial);
Messages<18> IMUbufferSeed(Serial);

void setup() {
  const char *ptr;

  Serial.begin(115200);
  delay(1000);

  Serial.print("GPSbufferSeed Buffer Size = ");
  Serial.println(GPSbufferSeed.getBufferSize());
  Serial.print("GPSbufferSeed String Size = ");
  Serial.println(GPSbufferSeed.getStringLen());
  GPSbufferSeed.setBuffer("Hello World");
  Serial.print("GPSbufferSeed Buffer String = ");
  GPSbufferSeed.printBuffer();
  Serial.println();
  ptr = GPSbufferSeed.getBufferPointer();
  Serial.print("GPSbufferSeed Buffer String = ");
  Serial.println(ptr);
  Serial.print("GPSbufferSeed String Size = ");
  Serial.println(GPSbufferSeed.getStringLen());

  Serial.println();
  Serial.println();

  Serial.print("IMUbufferSeed Buffer Size = ");
  Serial.println(IMUbufferSeed.getBufferSize());
  Serial.print("IMUbufferSeed String Size = ");
  Serial.println(IMUbufferSeed.getStringLen());
  IMUbufferSeed.setBuffer("Test 123");
  Serial.print("IMUbufferSeed Buffer String = ");
  IMUbufferSeed.printBuffer();
  Serial.println();
  ptr = IMUbufferSeed.getBufferPointer();
  Serial.print("IMUbufferSeed Buffer String = ");
  Serial.println(ptr);
  Serial.print("IMUbufferSeed String Size = ");
  Serial.println(IMUbufferSeed.getStringLen());
}

void loop() {
}
Title: Re: more with char arrays and class functions
Post by: petedd on Sep 17, 2020, 12:46 am
Code: [Select]
template<size_t N>
class Messages {
  public:
    Messages(Stream &str) : charStream(str), m_dataBufferSize(N) {
    }
    
...

template<size_t N>
void Messages<N>::printBuffer() {
  charStream.print(m_dataBuffer);
}


}

Thanks glvalvo!

These are two breakthroughs for me.  1) I have used templates before but was not aware I could use a template this way to specify a value instead of specifying a type.  This is brilliant and I can really see the value here.   2) Passing the stream specifier this way is also brilliant as it allows me to specify different i/o streams. 

Again, many thanks.
Pete
Title: Re: more with char arrays and class functions
Post by: petedd on Sep 17, 2020, 10:13 pm
passing Stream and using template for sizing the buffers saved 78 bytes of program memory and the same amount of dynamic memory (RAM).   Thanks again gfvalvo, PieterP and others!

here is the version 3.0 code.

Code: [Select]

/*
    Demonstration of serial input string handler using Classes to
    allow for capture and parsing of different formats or serial messages

    This is  based on the non-Class examples in the (classy nonetheless) tutorials by J Haskell found at:
      http://jhaskellsblog.blogspot.com/2011/05/serial-comm-fundamentals-on-arduino.html  and
      http://jhaskellsblog.blogspot.com/2011/06/parsing-quick-guide-to-strtok-there-are.html

    Here GPS and IMU strings are handled.  "Parsing" here is just the breaking out and printing of string fields.
    Further usage of the data could be done with additional coding to capture the values as for use later as intergers, floats, char*, etc

    The GPS module might be this one: http://www.sparkfun.com/products/465
    The IMU might be this one: http://www.sparkfun.com/products/9623

    Here are some test strings you can copy and paste into the Serial Monitor
    NOTE: Be sure to set the Serial Monitor to add either CR or both NL/CR

       $GPGGA,161229.487,3723.2475,N,12158.3416,W,1,07,1.0,9.0,M,,,,0000*18
       $GPGLL,3723.2475,N,12158.3416,W,161229.487,A*2C
       $GPGSA,A,3,07,02,26,27,09,04,15,,,,,,1.8,1.0,1.5*33

       !ANG:320,33,191
       !ANG:0,320,90
       !ANG:0,0,0

    While this example uses only one serial port, one could assign GPS input to one port and IMU input to another by specifying different Streams for the ports
    to use By keeping all variables (except incomingbyte as member variables, the code is reentrant in that multiple strings can be captured, each from
    a separate port (Stream).

    The Messages Class is set-up here to accept one, two, or three field delimiters in the constructor for each instance.  Here we use two delimters for
    the GPS and three delimiters for the IMU strings.

  //   Copyright (c) 2020 by Peter Dubler
  //              This program is free software: you can redistribute it and/or modify
  //              it under the terms of the GNU General Public License as published by
  //              the Free Software Foundation, either version 3 of the License, or
  //              (at your option) any later version.
  //
  //              This program is distributed in the hope that it will be useful,
  //              but WITHOUT ANY WARRANTY; without even the implied warranty of
  //              MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  //              GNU General Public License for more details.
  //
  //              The GNU General Public License
  //              may be found here: <http://www.gnu.org/licenses/>.

                3.0 using Class templates - uses 78 fewer bytes of each program memory and dynamic memory (RAM) as compared to prior version

*/
template<size_t N>
class Messages {
  private:
    char       m_startChar;
    char       m_endChar;
    char       m_delimiters[3] {0, 0, 0};  // set default values to null character
    bool       m_storeString;
    uint8_t    m_dataBufferIndex;
    Stream     &charStream;
    size_t     m_dataBufferSize;
    char       m_dataBuffer[N + 1] = {'\0'}; // Make sure there's always a null character at end of buffer
  public:
    // here we will create overloads of the constructor specifications for one, two, or three field delimiters
    // single delimiter instance
    Messages(Stream &str, char SC, char EC, char DL0)
      : charStream(str), m_dataBufferSize(N),  m_startChar(SC), m_endChar(EC) {
      m_delimiters[0] = DL0;
    }

    // two delimiter instance
    Messages(Stream &str, char SC, char EC, char DL0, char DL1)
      : charStream(str), m_dataBufferSize(N),  m_startChar(SC), m_endChar(EC) {
      m_delimiters[0] = DL0;
      m_delimiters[1] = DL1;
    }

    // three delimiter instance
    Messages(Stream &str, char SC, char EC, char DL0, char DL1, char DL2)
      : charStream(str), m_dataBufferSize(N),  m_startChar(SC), m_endChar(EC) {
      m_delimiters[0] = DL0;
      m_delimiters[1] = DL1;
      m_delimiters[2] = DL2;
    }

    bool getString();
    void parseString();
};

template<size_t N>
bool Messages<N>::getString() {  // Captures serial input starting with m_startChar and ending with m_endChar, upto a length of m_dataBufferSize.
  // Returns true when full string has been captured.
  while (charStream.available() > 0) {
    char incomingbyte = charStream.read();
    if (incomingbyte == m_startChar) {
      m_dataBufferIndex = 0;  //Initialize our dataBufferIndex variable
      m_storeString = true;
    }
    if (m_storeString) {
      //Let's check our index here, and abort if we're outside our buffer size
      if (m_dataBufferIndex == m_dataBufferSize) {
        //Oops, our index is pointing to an array element outside our buffer.
        m_dataBufferIndex = 0;
        m_storeString = false; //reset flag so that next time through method will check for m_startChar
        break;
      }
      if (incomingbyte == m_endChar) {
        m_dataBuffer[m_dataBufferIndex] = 0; //null terminate the C string
        m_storeString = false; //reset flag so that next time through method will check for m_startChar
        //Our data string is complete.  return true
        return true;
      }
      else {
        m_dataBuffer[m_dataBufferIndex++] = incomingbyte;
        m_dataBuffer[m_dataBufferIndex] = 0; //null terminate the C string
      }
    }
  }
  //We've read in all the available Serial data, and don't have a valid string yet, so return false
  return false;
}

template<size_t N>
void Messages<N>::parseString() {
  char* valPosition;
  //This initializes strtok with our string to tokenize
  valPosition = strtok(m_dataBuffer, m_delimiters);

  while (valPosition != NULL) {
    charStream.print(valPosition);  charStream.print("  ");
    //Here we pass in a NULL value, which tells strtok to continue working with the previous string
    valPosition = strtok(NULL, m_delimiters);
  }
  charStream.println('\n');
}


// Messages constructors
//  <bufferSize> Stream,  SC,  EC , DL0, DL1, DL2
//        |         |     |    |     |    |    |
//        V         V     V    V     V    V    V
Messages<80> GPS(Serial, '$', '\r', ',', '$');
Messages<18> IMU(Serial, '!', '\r', ':', ',', '!');



void setup() {
  Serial.begin(115200);
  Serial.println("Serial port activated");
  delay(200);
}

void loop() {  // Ask for, receive, parse, and display parsed GPS string, then IMU string, and repeat
  Serial.println("Waiting for GPS string");
  bool gotString = false;
  while (!gotString) {
    if (GPS.getString()) {
      Serial.print("got GPS string: ");
      GPS.parseString();
      gotString = true;
    }
  }

  Serial.println("Waiting for IMU string");
  gotString = false;
  while (!gotString) {
    if (IMU.getString()) {
      Serial.print("got IMU string: ");
      IMU.parseString();
      gotString = true;
    }
  }
}
Title: Re: more with char arrays and class functions
Post by: GoForSmoke on Sep 20, 2020, 03:07 am
I got to find out that this version IDE typecasts more retentively than my old one.

Title: Re: more with char arrays and class functions
Post by: petedd on Oct 07, 2020, 08:40 pm
Well, as usual, I am trying to take things further and hit a road block.  I clearly am missing a fundamental here...

We ended up with some great code for creating different-sized char arrays (buffers) and setting different streams for some class objects.   Now I want to do something else where all I need to do is create different-sized char arrays and will not need to set the streams.  Here is the code that creates the template class (focus on the first few lines)...

Code: [Select]
template<size_t N>
class Messages {
  public:
    Messages(Stream &str) : charStream(str), m_dataBufferSize(N) {
    }
    void setBuffer(const char *inString);
    void printBuffer();
    const char *getBufferPointer();
    size_t getStringLen();
    size_t getBufferSize();

  private:
    Stream &charStream;
    size_t m_dataBufferSize;
    char m_dataBuffer[N + 1] = {'\0'}; // Make sure there's always a null character at end of buffer
};


template<size_t N>
void Messages<N>::setBuffer(const char *inString) {
  strncpy(m_dataBuffer, inString, m_dataBufferSize);
}

template<size_t N>
void Messages<N>::printBuffer() {
  charStream.print(m_dataBuffer);
}

template<size_t N>
size_t Messages<N>::getBufferSize() {
  return (m_dataBufferSize);
}

template<size_t N>
size_t Messages<N>::getStringLen() {
  return strlen(m_dataBuffer);
}

template<size_t N>
const char * Messages<N>::getBufferPointer() {
  return m_dataBuffer;
}


Messages<80> GPSbufferSeed(Serial);
Messages<18> IMUbufferSeed(Serial);

void setup() {
  const char *ptr;

  Serial.begin(115200);
  delay(1000);

  Serial.print("GPSbufferSeed Buffer Size = ");
  Serial.println(GPSbufferSeed.getBufferSize());
  Serial.print("GPSbufferSeed String Size = ");
  Serial.println(GPSbufferSeed.getStringLen());
  GPSbufferSeed.setBuffer("Hello World");
  Serial.print("GPSbufferSeed Buffer String = ");
  GPSbufferSeed.printBuffer();
  Serial.println();
  ptr = GPSbufferSeed.getBufferPointer();
  Serial.print("GPSbufferSeed Buffer String = ");
  Serial.println(ptr);
  Serial.print("GPSbufferSeed String Size = ");
  Serial.println(GPSbufferSeed.getStringLen());

  Serial.println();
  Serial.println();

  Serial.print("IMUbufferSeed Buffer Size = ");
  Serial.println(IMUbufferSeed.getBufferSize());
  Serial.print("IMUbufferSeed String Size = ");
  Serial.println(IMUbufferSeed.getStringLen());
  IMUbufferSeed.setBuffer("Test 123");
  Serial.print("IMUbufferSeed Buffer String = ");
  IMUbufferSeed.printBuffer();
  Serial.println();
  ptr = IMUbufferSeed.getBufferPointer();
  Serial.print("IMUbufferSeed Buffer String = ");
  Serial.println(ptr);
  Serial.print("IMUbufferSeed String Size = ");
  Serial.println(IMUbufferSeed.getStringLen());
}

void loop() {
}


Now what if I want to take the stream setting out of this part:

Code: [Select]
template<size_t N>
class Messages {
  public:
    Messages(Stream &str) : charStream(str), m_dataBufferSize(N) {
    }


So I want to just set the objects up for varying m_dataBufferSize   So I try this:

Code: [Select]
template<size_t N>
class Messages {
  public:
    Messages m_dataBufferSize(N) {
    }

//..

};


and this as the constructor, for example:

Code: [Select]

Messages<80> GPSbufferSeed();


and I get the following compile-time error:  'N' is not a type
Pointing to this line:

   Messages m_dataBufferSize(N) {

I've tried a variety of variants and can't hit on the right code for this.  Clearly I am missing something very fundamental here and am eager to learn... after over two hours of working on this one little point...

Any pointers to what I should be studying to better understand this would be very appreciated.
Title: Re: more with char arrays and class functions
Post by: petedd on Oct 07, 2020, 09:42 pm
Well, I got my own answer... I found this tutorial...I did not know I was looking for "non-type parameters"

https://www.learncpp.com/cpp-tutorial/134-template-non-type-parameters/

and then I wrote this demonstration code.

Code: [Select]


template <size_t arraysize> // size is the non-type parameter
class MakeArray
{
  public:
    MakeArray () {
      m_arraysize = arraysize;
    }
    void SetArray(const char *InputString);
    void PrintArray();

  private:
    size_t m_arraysize;
    char m_array[arraysize + 1] = {'\0'}; // Make sure there's always a null character at end of array
};



template <size_t arraysize>
void MakeArray<arraysize>::SetArray(const char *InputString) {
  strncpy(m_array, InputString, m_arraysize);
}

template <size_t arraysize>
void MakeArray<arraysize>::PrintArray() {
  Serial.println(m_array);
}

//create objects
MakeArray<5> SmallArray;
MakeArray<20> LargeArray;

void setup() {

  SmallArray.SetArray("12345");
  LargeArray.SetArray("12345678901234567890");

  Serial.begin(115200);
  delay(1000);
  Serial.println("Serial ready");

  SmallArray.PrintArray();
  LargeArray.PrintArray();
}

void loop() {
}
Title: Re: more with char arrays and class functions
Post by: PieterP on Oct 07, 2020, 09:48 pm
A better answer, more in line with what you had before, would be
Code: [Select]
template <size_t ArraySize>
class MakeArray {
  public:
    MakeArray() : m_arraysize(ArraySize) {}
  private:
    size_t m_arraysize;
};

Or even better, use default member initialization, so you don't forget to initialize it when adding more constructors in the future:

Code: [Select]
template <size_t ArraySize>
class MakeArray {
  public:
    // no user-defined constructor here
  private:
    size_t m_arraysize = ArraySize;
};


That being said, I see no reason why you would store it as a member variable in the first place, you already have access to the template parameter. By saving it as a copy in a member variable, you're just wasting 4 bytes of RAM for each instance of your class.
Title: Re: more with char arrays and class functions
Post by: petedd on Oct 07, 2020, 11:00 pm
Or even better, use default member initialization, so you don't forget to initialize it when adding more constructors in the future:


Hi Pieter,

Good point as usual regarding m_arraysize.   I agree that I can get away without the m_arraysize member variable and I fixed that.  Thank you.

With regards to your comment about initializing (above), I think I might be encountering that as I try to move from my example code to my real code.   I have tried to reduce the problem down to the following snippet.   What is happening is that I am getting an "expected initializer before '<' token" error when I try to define the UpdateDisplayField and SetNewValue methods outside of the class declaration.   Compiles fine if I delete these function definitions.

Code: [Select]




//FieldNumber

#define fnAV 1  // Alternator Volts
#define fnBV 2  // Battery Volts

//Justify
#define RJ 1 //Right
#define LJ 2 //Left
#define CJ 3 //Center

//Column (X) variable is number of pixels
#define screen2ColAlt     120
#define screen2ColBat     220

//Top row (Y) is 0
#define Y_Pixels_12pt  21  //without descenders, Theight is 18, with descenders (used in lower case letters), tHeight is 23
#define Y_Pixels_18pt  31  //without descenders, Theight is 26, with descenders (used in lower case letters), tHeight is 34

#define screen2RowTop       1 * Y_Pixels_18pt
#define screen2RowVolt      2 * Y_Pixels_18pt
#define screen2RowAmp       3 * Y_Pixels_18pt

// Class
template <size_t arraysize>
class LCDfield {
  public:
    //constructors
    //LCDfield(uint8_t object, int x,  int y, const GFXfont *Font, uint8_t just)
    LCDfield( int x,  int y, int Font, uint8_t just)
    {
      m_X = x;
      m_Y = y;
      m_Font = Font;
      m_Just = just;
      m_arraySize = arraysize;
    }

    void SetNewValue(const char* newValue);
    void UpdateDisplayField();

  private:
    size_t        m_arraySize;
    char          m_newValue[arraysize + 1] = {'\0'}; // Make sure there is always a null character at the end
    int           m_X, m_Y; // This is the cursor reference position (for example, the center of the string if using Center Justify)
    int           m_X1, m_Y1;  // Used for position of fillRect to write over last string printed
   // const         GFXfont *m_Font;
    int     m_Font;
    uint8_t       m_Just; // Specifies the text Justification (position relative to m_X, m_Y, as Right, Left, or Center)
    int           m_LLX, m_LLY; //corrected cursor position for the start of the string - Proportional Fonts use Lower Left for origin of string
    unsigned int  m_Twidth, m_Theight;
}; // don't forget the semicolon at the end of the class

template <size_t arraysize>
void LCDField<arraysize>::SetNewValue(const char* newValue) {
  strncpy(m_newValue, newValue, m_arraySize);
}


template <size_t arraysize>
void LCDField<arraysize>::UpdateDisplayField(void) {
  Serial.println(m_newValue);  //this is just dummy code
}//Update()


//LCDfield Screen 2 Objects                                             (Optional)
//construct instances  column,           row,               Font       Justification
LCDfield <5>fAV{       screen2ColAlt,    screen2RowVolt,   27,          LJ  };
LCDfield <5>fBV{       screen2ColBat,    screen2RowVolt,   28,         LJ  };


void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:

}


Since this code closely follows the working demonstration code I posted earlier and subsequent versions of that code in which I passed variables to the methods which I defined outside of the class declaration, I am at a loss as to why I am throwing this error.  (I imagine the problem is obvious to you...)
Title: Re: more with char arrays and class functions
Post by: PieterP on Oct 07, 2020, 11:37 pm
LCDfield != LCDField


Also, macros are evil. Never use them to define constants, only use them for conditional compilation. Use constants or enumerations instead:

Code: [Select]
const unsigned int screen2ColAlt = 120;
Or
Code: [Select]
constexpr unsigned int screen2ColAlt = 120;

Code: [Select]
enum Field {
  fnAV = 1,  // Alternator Volts
  fnBV = 2,  // Battery Volts
};

Preferably, use an enum class, so the enumerators are scoped and more strongly typed:

Code: [Select]
enum class Field {
  fnAV = 1,  // Alternator Volts
  fnBV = 2,  // Battery Volts
};

// In your code, use Field::fnAV,
// and pass them using the Field type instead of int

Do the same for the other macros.

Title: Re: more with char arrays and class functions
Post by: petedd on Oct 07, 2020, 11:54 pm
Doh!   I can't believe I missed that.  LCDfield/LCDField...  I need to take more breaks...

I always appreciate your insights Pieter.  Could you tell me more on why macros "are evil"?  Do I burn any more or less memory space by using constants and enumerated instead?

(40+ years ago, at Georgia Tech, I did undergrad, grad, and post grad classes and projects and had access to some of the best development tools of that time... even one or two 8" floppy disks to store my work with Motorola 6800 series (not 68000) microprocessors.  The Data General machines I learned driver design on had a few K of magnetic core memory, if that, and everything was done in assembly code.  We had to switch flick the code we wrote into the front panel in order to enter the drivers we wrote for a paper tape reader so we could advance to using an ASR-33 to type our code to paper tape...  it all taught me to be very stingy with memory and I continue to try to learn the most efficient approaches.   Also, there are so many ways to set up class members in C++... I try to use the most blatant for easier readability (for me) but want to get better at using other approaches.)
Title: Re: more with char arrays and class functions
Post by: PieterP on Oct 08, 2020, 12:12 am
I always appreciate your insights Pieter.  Could you tell me more on why macros "are evil"?
Because macros are completely invisible to the compiler, they are just "stupid" text search and replace operations that happen before your code is passed to the compiler. This means that there are no checks, no type system, etc.

It also makes it hard for the programmer to see what's going on, having to expand possibly multiple layers of macros in your head. It's easy to forget to add parentheses around the operands or results, etc.

Finally, macros aren't scoped. If two libraries happen to use the same macro name, everything breaks, even if the macro is only used at a local scope or inside of a namespace block. This is a consequence of macros being invisible to the compiler.
If you use constants, they follow the scope and namespace rules of the language, and they won't clash if two constants in a different scope or namespace happen to have the same name.

Quote
Do I burn any more or less memory space by using constants and enumerated instead?
Macros will not save memory. Modern compilers are smart, if you're not using a constant, it'll optimize it out, it doesn't matter if it's a macro or a const/constexpr variable.
Title: Re: more with char arrays and class functions
Post by: petedd on Oct 08, 2020, 01:08 am
Thanks for that very good explanation.
Title: Re: more with char arrays and class functions
Post by: petedd on Oct 08, 2020, 05:57 am
So now things are looking up but I have another problem.  I construct my template class objects and then I construct a list of those objects (*Screen2Objects) so I can step though each of the objects in a for loop later.  I am not sure how to specify the non-type field (arraysize) in this case.  The compiler seems to want the arraysize for Screen2Objects to match each of the objects but since there are several sizes, that is not going to happen.   Is there a wildcard way to do this?  (only the relevant lines are shown here)

Code: [Select]

template <size_t arraysize>
class LCDfield {

  public:
    //constructors
    //constructor with justification field
    LCDfield( int x,  int y, const GFXfont *Font, uint8_t just=LJ)
    {
      m_X = x;
      m_Y = y;
      m_Font = Font;
      m_Just = just;
    }
 
    
    void SetNewValue(const char* newValue);
    void UpdateDisplayField();

  private:
    char          m_newValue[arraysize + 1] = {'\0'}; // Make sure there is always a null character at the end
    int           m_X, m_Y; // This is the cursor reference position (for example, the center of the string if using Center Justify)
    int           m_X1, m_Y1;  // Used for position of fillRect to write over last string printed
    const         GFXfont *m_Font;
    uint8_t       m_Just; // Specifies the text Justification (position relative to m_X, m_Y, as Right, Left, or Center)
    int           m_LLX, m_LLY; //corrected cursor position for the start of the string - Proportional Fonts use Lower Left for origin of string
    unsigned int  m_Twidth, m_Theight;
}; // don't forget the semicolon at the end of the class

//...

/create objects

LCDfield <5>fAV{       screen2ColAlt,    screen2RowVolt,   &FreeSans18pt7b,  LJ  };
LCDfield <5>fBV{       screen2ColBat,    screen2RowVolt,   &FreeSans18pt7b,  LJ  };
LCDfield <3>fAA{       screen2ColAlt,    screen2RowAmp,    &FreeSans18pt7b,  LJ  };
LCDfield <3>fBA{       screen2ColBat,    screen2RowAmp,    &FreeSans18pt7b,  LJ  };
LCDfield <4>fAT{       screen2ColAlt,    screen2RowTemp,   &FreeSans18pt7b,  LJ  };
LCDfield <4>fBT{       screen2ColBat,    screen2RowTemp,   &FreeSans18pt7b,  LJ  };
LCDfield <2>fCD{       screen2ColCount,  screen2RowCount,  &FreeSans18pt7b,  LJ  };
LCDfield <20>fCS{      screen2ColState,  screen2RowState,  &FreeSans18pt7b,  LJ  };
LCDfield <4>fPW{       screen2ColPWM,    screen2RowPWM,    &FreeSans18pt7b,  LJ  };
LCDfield <5>fSB{       screen2ColScuba,  screen2RowScuba,  &FreeSans18pt7b,  RJ  };
LCDfield <20>fFL{      screen2ColState,  screen2RowScuba,  &FreeSans18pt7b,  LJ  };

//List of Screen2 objects - does not include fault code field fFL
LCDfield *Screen2Objects[] = {&fAV, &fBV, &fAA, &fBA, &fAT, &fBT, &fCD, &fCS, &fPW, &fSB};
Title: Re: more with char arrays and class functions
Post by: petedd on Oct 09, 2020, 08:21 pm
Well, researching further, with the valuable clue of using a interface class, I found the following link, adopted the third answer there, and came up with the following code. https://stackoverflow.com/questions/9961329/c-iterating-through-objects-to-call-the-same-method


This saved me 500 bytes of dynamic memory, almost half of what I was using before adopting the non-type parameter for arraysize.


Paying it forward, and hoping others might benefit, here is the code I ended up with (enough shown to demonstrate one use of the for loop (and there are plenty others in my code).


Just for the record, this was not at all obvious (to me, at least) and, if you compare to the code above, you can see that using the absrtact interface and the virtual functions really changed the syntax of the constructors and other aspects of the class statements.


Code: [Select]



// Class
class IScreens {
  public:
virtual void SetNewValue(const char* newValue) = 0;
virtual void UpdateDisplayField() = 0;
}; // don't forget the semicolon at the end of the class


template <size_t arraysize>
class LCDfield : public IScreens {
  public:
//constructor
LCDfield( uint16_t x,  uint16_t y, const GFXfont *Font, uint8_t just = LJ)
{
  m_X = x;
  m_Y = y;
  m_Font = Font;
  m_Just = just;
}


virtual void SetNewValue(const char* newValue);
virtual void UpdateDisplayField();


  private:
char   m_newValue[arraysize + 1] = {'\0'}; // Make sure there is always a null character at the end
int    m_X, m_Y; // This is the cursor reference position (for example, the center of the string if using Center Justify)
int    m_X1, m_Y1;  // Used for position of fillRect to write over last string printed
const GFXfont *m_Font;
uint8_t    m_Just; // Specifies the text Justification (position relative to m_X, m_Y, as Right, Left, or Center)
int    m_LLX, m_LLY; //corrected cursor position for the start of the string - Proportional Fonts use Lower Left for origin of string
unsigned int  m_Twidth, m_Theight;
}; // don't forget the semicolon at the end of the class


template <size_t arraysize>
void LCDfield<arraysize>::SetNewValue(const char* newValue) {
  strncpy(m_newValue, newValue, arraysize);
}


template <size_t arraysize>
void LCDfield<arraysize>::UpdateDisplayField(void) {
   Serial.println(m_newValue);  // Just a dummy function
}//DisplayField(void)




//LCDfield Screen 2 Objects (Optional)
//construct instances  column,    row,    Font    Justification
IScreens* fAV = new LCDfield <5> {    screen2ColAlt, screen2RowVolt,   &FreeSans18pt7b,  LJ  };
IScreens* fBV = new LCDfield <5> {    screen2ColBat, screen2RowVolt,   &FreeSans18pt7b,  LJ  };
IScreens* fAA = new LCDfield <3> {    screen2ColAlt, screen2RowAmp, &FreeSans18pt7b,  LJ  };
IScreens* fBA = new LCDfield <3> {    screen2ColBat, screen2RowAmp, &FreeSans18pt7b,  LJ  };
IScreens* fAT = new LCDfield <4> {    screen2ColAlt, screen2RowTemp,   &FreeSans18pt7b,  LJ  };
IScreens* fBT = new LCDfield <4> {    screen2ColBat, screen2RowTemp,   &FreeSans18pt7b,  LJ  };
IScreens* fCD = new LCDfield <2> {    screen2ColCount,  screen2RowCount,  &FreeSans18pt7b,  LJ  };
IScreens* fCS = new LCDfield <20> {   screen2ColState,  screen2RowState,  &FreeSans18pt7b,  LJ  };
IScreens* fPW = new LCDfield <4> {    screen2ColPWM, screen2RowPWM, &FreeSans18pt7b,  LJ  };
IScreens* fSB = new LCDfield <5> {    screen2ColScuba,  screen2RowScuba,  &FreeSans18pt7b,  RJ  };
IScreens* fFL = new LCDfield <20> {   screen2ColState,  screen2RowScuba,  &FreeSans18pt7b,  LJ  };


//List of Screen2 objects - does not include fault code field
IScreens* const Screen2Objects[] = {fAV, fBV, fAA, fBA, fAT, fBT, fCD, fCS, fPW, fSB};


//....


// Update screen2 fields
 for (size_t i = 0; i < sizeof Screen2Objects / sizeof Screen2Objects[0]; i++) {
Screen2Objects[i]->UpdateDisplayField();  //refresh using existing m_newValue data
 }


Title: Re: more with char arrays and class functions
Post by: PieterP on Oct 09, 2020, 08:46 pm
This saved me 500 bytes of dynamic memory, almost half of what I was using before adopting the non-type parameter for arraysize.
You didn't save 500 bytes of dynamic memory, you hid it from the IDE, because it is dynamically allocated at runtime. Your code will use more memory than before, the IDE simply doesn't report it. (The IDE only reports the sizes of the .bss and .data sections. Local variables allocated on the stack and memory allocated on the heap using new are not reported.)

There's no need to allocate your LCDfields dynamically. 
Code: [Select]
LCDfield <5> fAV = {   screen2ColAlt, screen2RowVolt,   &FreeSans18pt7b,  LJ  };
LCDfield <5> fBV = {   screen2ColBat, screen2RowVolt,   &FreeSans18pt7b,  LJ  };
...
IScreens* const Screen2Objects[] = {&fAV, &fBV, ...};

You should avoid using new in C++ code, use smart pointers (https://docs.microsoft.com/en-us/cpp/cpp/smart-pointers-modern-cpp) if you can.
Title: Re: more with char arrays and class functions
Post by: petedd on Oct 09, 2020, 09:56 pm
Points made.  Thank you.  A little bit saved since the m_newValue char arrays are not all 20 char long anymore.  Also, dropping the dynamic allocation reduced the program memory by 1300 bytes just now.  Did not expect that big a change.