more with char arrays and class functions

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.

[/
/*
    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]

I noted that you have not specified a size for the array

    char       m_delimiters[?];  //  specify size??

be careful using Arduino Strings they cause problems
you could allocate memory dynamically

horace:
I noted that you have not specified a size for the array

    char       m_delimiters[?];  //  specify size??

be careful using Arduino Strings they cause problems
you could allocate memory dynamically

Actually, that part of works fine. The problem I am having is regarding the m_dataBuffer.

should m_dataBuffer be a pointer to char?

char *      m_dataBuffer;

horace:
should m_dataBuffer be a pointer to char?

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.

/*
    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, '

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,,,,,⸮⸮⸮, '\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,,,,,⸮⸮⸮

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...

/*
    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) {};
 }

}

petedd:
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:

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":

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:

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:

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:

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

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.

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?

/*
    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, '
    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⸮⸮⸮=>
      
    }

};

, '\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) {};
 }

}


§DISCOURSE_HOISTED_CODE_1§

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.

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.

Let me try to parse that...

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).

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.

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.

GoForSmoke:
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.

gfvalvo:
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.

petedd:
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.

Got it. Thanks. I avoid dynamic allocation on microprocessors whenever I can.

petedd:
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.

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!!

/*
    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;
    }
  }
}

I assume you've seen the following compiler warnings from your code (if not, turn up the warning level in the Arduino IDE):

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, '

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:

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), '

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).
, '\r', ',', '


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:

§DISCOURSE_HOISTED_CODE_1§


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).
, 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:

§_DISCOURSE_HOISTED_CODE_1_§

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).
, '\r', ',', '


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).
, '\r', ',', '

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:

§_DISCOURSE_HOISTED_CODE_1_§

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).
, 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:

§DISCOURSE_HOISTED_CODE_1§


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).
, 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).
, '\r', ',', '


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:

§DISCOURSE_HOISTED_CODE_1§


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).
, 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:

§_DISCOURSE_HOISTED_CODE_1_§

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).