StreamHandler - an OOP example

Based on the discussion from this thread:

Here's a practical example that might be very useful. It creates a class for parsing a Stream object like Serial and extracting and acting on commands therein. There are two types of commands, one calls a function with the full string (sans the start and end markers), and the other will update the value in an int variable.

In this example, if you send "<A>" or anything in < and > that starts with 'A' then it will call the aFunction with the string it parsed and print it. If you send 'I', 'J', or 'K' then it will parse the rest of the string with atoi for an int value and update the variable. "<I42>" will change the value of i to 42.

I'm going to come back and add some templates so the VariableCommand class will work with any type of variable. I broke this off of the earlier discussion so as not to pollute that thread with discussions of templates.

Example StreamHandler.ino

#include "StreamHandler.h"

// define some variables
int i = 2;
int j = 3;
int k = 4;

//define some functions to print them
void aFunction(char* str) {
  Serial.print("\n ** A function - ");
  Serial.println(str);
  Serial.print(" - prints i - ");
  Serial.println(i);
}
void bFunction(char* str) {
  Serial.print("\n ** B function - ");
  Serial.println(str);
  Serial.print(" - prints j - ");
  Serial.println(j);
}
void cFunction(char* str) {
  Serial.print("\n ** C function - ");
  Serial.println(str);
  Serial.print(" - prints k - ");
  Serial.println(k);
}

// define an array of function commands
// doesn't have to be an array but then they'd all need names
// you could also use new and create them on the heap.
FunctionCommand fcoms[] = {
  { 'A', aFunction },
  { 'B', bFunction },
  { 'C', cFunction }
};

// an array of variable commands
// same as above, doesn't have to be an array but this is easier.
VariableCommand vcoms[] = {
  { 'I', i },
  { 'J', j },
  { 'K', k }
};

// create a StreamHandler and connect to Serial
StreamHandler streamHandler(&Serial);

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("\n\n**** Starting StreamHandler.ino **** \n\n");
}

void loop() {
  streamHandler.run();  // run the stream handler
}

Header StreamHandler.h:

#ifndef STREAM_HANDLER_H
#define STREAM_HANDLER_H

#include "Arduino.h"

#ifndef STREAM_HANDLER_BUFFER_SIZE
#define STREAM_HANDLER_BUFFER_SIZE 64
#endif

/*
*
*    Base class for commands
*
*/
class Command {
private:
  char matchChar;
  static Command* first;
  Command* next = nullptr;

  boolean match(char* com) {
    return ((*com == matchChar));
  }
  Command();  // disallow default constructor
protected:
  virtual void handle(char* str){};
  Command(char c)
    : matchChar(c) {
    next = first;
    first = this;
  }

public:

  Command(const Command& other) = delete;
  Command& operator=(const Command& other) = delete;
  ~Command() {
    for (Command** pptr = &first; *pptr != nullptr; pptr = &((*pptr)->next)) {
      if (*pptr == this) {
        *pptr = next;
        break;
      }
    }
  }

  static void check(char* str);
};

/*
*
*    Commands that call a function
*    function must take a char* and return void
*/
class FunctionCommand : protected Command {
private:
  void (*func)(char*);
  FunctionCommand();  // disallow default constructor

protected:
  virtual void handle(char* str) {
    if (func) func(str);
  };

public:
  FunctionCommand(const FunctionCommand& other) = delete;
  FunctionCommand& operator=(const FunctionCommand& other) = delete;

  FunctionCommand(char c, void (*f)(char*))
    : Command(c), func(f){};
};


/*
*
*    Class for updating int variables
*
*/
class VariableCommand : protected Command {
private:
  int& var;
  VariableCommand();  // disallow default constructor
  int parse(char*);

protected:
  virtual void handle(char* str) {
    var = parse(str);
  }

public:
  VariableCommand(const VariableCommand& other) = delete;
  VariableCommand& operator=(const VariableCommand& other) = delete;

  VariableCommand(char c, int& v)
    : Command(c), var(v){};
};

/*
*
*    SreamHandler class
*    handles the actual parsing
*/

class StreamHandler {

private:
  char _SPbuffer[STREAM_HANDLER_BUFFER_SIZE];
  int index;

  Stream* in;
  char sop;
  char eop;

  boolean receiving = false;
  boolean greedy = false;

  void handleChar(char c);

public:

  StreamHandler(Stream* aIn, char aSop, char aEop)
    : index(0), in(aIn), sop(aSop), eop(aEop){};
  StreamHandler(Stream* aIn)
    : index(0), in(aIn), sop('<'), eop('>'){};
  void run();

  void setGreedy(bool);
  bool getGreedy();
};



#endif  //STREAM_HANDLER_H

Source StreamHandler.cpp

#include "StreamHandler.h"

Command* Command::first = nullptr;  // initialize list head

// static function to check all commands agains the given string
void Command::check(char* str) {
  for (Command* ptr = first; ptr != nullptr; ptr = ptr->next) {
    // Start markers should be stripped.
    if (ptr->match(str)) {
      ptr->handle(str);
      break;
    }
  }
}

// parses an integer from the string
int VariableCommand::parse(char* str) {
  return atoi(str + 1);  // skip command character
}

/*
*
*    StreamHandler methods
*
*/

void StreamHandler::run() {

  //	if (receivingRaw) {
  //		handleRawData();
  //	} else
  if (in->available()) {
    do {
      char c = in->read();
      handleChar(c);
    } while (in->available() && greedy);
  }
}

void StreamHandler::handleChar(char c) {

  if (c == sop) {
    receiving = true;
    index = 0;
    _SPbuffer[0] = 0;
  } else if (receiving) {
    if (c == eop) {
      receiving = false;
      Command::check(_SPbuffer);
    }
    _SPbuffer[index] = c;
    _SPbuffer[++index] = 0;

    if (index >= STREAM_HANDLER_BUFFER_SIZE - 1) {
      index--;
    }
  }
}

void StreamHandler::setGreedy(bool aBoo) {
  greedy = aBoo;
}

bool StreamHandler::getGreedy() {
  return greedy;
}


1 Like

What's a template?

Imagine we want to use that VariableCommand class for more than one type. Maybe we want to be able to do float too. We could write another class just like it with a slightly different name. But that would mean copying all of that code. Templates do that for us.

The important thing to remember about templates is that it will create completely different types. A VariableCommand<int> is going to be a completely different class from a VariableCommand<float>. They're not interchangeable and they can't go into an array together unless we created another type for them both to derive from.

I will leave it to the reader to check out any of the numerous resources on template syntax. Here's the new code. This time I added a command instance on the heap with new. Make sure you know what you're doing with new.

#include "StreamHandler.h"

// define some variables
int i = 2;
int j = 3;
int k = 4;
float m = 2.2;

//define some functions to print them
void aFunction(char* str) {
  Serial.print("\n ** A function - ");
  Serial.println(str);
  Serial.print(" - prints i - ");
  Serial.println(i);
}
void bFunction(char* str) {
  Serial.print("\n ** B function - ");
  Serial.println(str);
  Serial.print(" - prints j - ");
  Serial.println(j);
}
void cFunction(char* str) {
  Serial.print("\n ** C function - ");
  Serial.println(str);
  Serial.print(" - prints k - ");
  Serial.println(k);
}

void dFunction(char* str) {
  Serial.print("\n ** D function - ");
  Serial.println(str);
  Serial.print(" - prints m - ");
  Serial.println(m, 6);
}

// define an array of function commands
// doesn't have to be an array but then they'd all need names
// you could also use new and create them on the heap.
FunctionCommand fcoms[] = {
  { 'A', aFunction },
  { 'B', bFunction },
  { 'C', cFunction },
  { 'D', dFunction },
};

// an array of variable commands
// same as above, doesn't have to be an array but this is easier.
VariableCommand<int> vcoms[] = {
  { 'I', i },
  { 'J', j },
  { 'K', k },
};

// create a StreamHandler and connect to Serial
StreamHandler streamHandler(&Serial);

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("\n\n**** Starting StreamHandler.ino **** \n\n");
  // add commands on the heap
  new VariableCommand<float>('M', m);
}

void loop() {
  streamHandler.run();  // run the stream handler
}
#ifndef STREAM_HANDLER_H
#define STREAM_HANDLER_H

#include "Arduino.h"

#ifndef STREAM_HANDLER_BUFFER_SIZE
#define STREAM_HANDLER_BUFFER_SIZE 64
#endif

/*
*
*    Base class for commands
*
*/
class Command {
private:
  char matchChar;
  static Command* first;
  Command* next = nullptr;

  boolean match(char* com) {
    return ((*com == matchChar));
  }
  Command();  // disallow default constructor
protected:
  virtual void handle(char* str) {
    (void)str;
  };
  Command(char c)
    : matchChar(c) {
    next = first;
    first = this;
  }

public:

  Command(const Command& other) = delete;
  Command& operator=(const Command& other) = delete;
  ~Command() {
    for (Command** pptr = &first; *pptr != nullptr; pptr = &((*pptr)->next)) {
      if (*pptr == this) {
        *pptr = next;
        break;
      }
    }
  }

  static void check(char* str);
};

/*
*
*    Commands that call a function
*    function must take a char* and return void
*/
class FunctionCommand : public Command {
private:

  void (*func)(char*);
  FunctionCommand();  // disallow default constructor

protected:

  virtual void handle(char* str) {
    if (func) func(str);
  };

public:

  FunctionCommand(char c, void (*f)(char*))
    : Command(c), func(f){};
};


/*
*
*    Class for updating variables
*
*/

template<class T>
class VariableCommand : public Command {
private:

  T& var;
  VariableCommand();  // disallow default constructor
  T parse(char*);

protected:

  virtual void handle(char* str) {
    var = parse(str);
  }

public:

  VariableCommand(char c, T& v)
    : Command(c), var(v){};
};


/*
*
*    SreamHandler class
*    handles the actual parsing
*/

class StreamHandler {

private:

  char _SPbuffer[STREAM_HANDLER_BUFFER_SIZE];
  int index;

  Stream* in;
  char sop;
  char eop;

  boolean receiving = false;
  boolean greedy = false;

  void handleChar(char c);

public:

  StreamHandler(Stream* aIn, char aSop, char aEop)
    : index(0), in(aIn), sop(aSop), eop(aEop){};
  StreamHandler(Stream* aIn)
    : index(0), in(aIn), sop('<'), eop('>'){};
  void run();

  void setGreedy(bool);
  bool getGreedy();
};



#endif  //STREAM_HANDLER_H
#include "StreamHandler.h"

Command* Command::first = nullptr;  // initialize list head

// static function to check all commands agains the given string
void Command::check(char* str) {
  for (Command* ptr = first; ptr != nullptr; ptr = ptr->next) {
    // Start markers should be stripped.
    if (ptr->match(str)) {
      ptr->handle(str);
      break;
    }
  }
}

// parses an integer from the string
template<>
int VariableCommand<int>::parse(char* str) {
  return atoi(str + 1);  // skip command character
}
// parses an double from the string
template<>
float VariableCommand<float>::parse(char* str) {
  return atof(str + 1);  // skip command character
}

/*
*
*    StreamHandler methods
*
*/

void StreamHandler::run() {

  //	if (receivingRaw) {
  //		handleRawData();
  //	} else
  if (in->available()) {
    do {
      char c = in->read();
      handleChar(c);
    } while (in->available() && greedy);
  }
}

void StreamHandler::handleChar(char c) {

  if (c == sop) {
    receiving = true;
    index = 0;
    _SPbuffer[0] = 0;
  } else if (receiving) {
    if (c == eop) {
      receiving = false;
      Command::check(_SPbuffer);
    }
    _SPbuffer[index] = c;
    _SPbuffer[++index] = 0;

    if (index >= STREAM_HANDLER_BUFFER_SIZE - 1) {
      index--;
    }
  }
}

void StreamHandler::setGreedy(bool aBoo) {
  greedy = aBoo;
}

bool StreamHandler::getGreedy() {
  return greedy;
}

1 Like

The cool thing about a template is that in order to work with a new type, I only need to define any functions that are special for that type.

Here's an example that also includes uint8_t. No changes have been made to the header or source. The .ino is defining a function at the top that will serve as the parse function for VariableCommand<uint8_t>.

#include "StreamHandler.h"

// parses an double from the string
template<>
uint8_t VariableCommand<uint8_t>::parse(char* str) {
  return atoi(str + 1);  // skip command character
}

// define some variables
int i = 2;
int j = 3;
int k = 4;
float m = 2.2;
uint8_t n = 6;

//define some functions to print them
void aFunction(char* str) {
  Serial.print("\n ** A function - ");
  Serial.println(str);
  Serial.print(" - prints i - ");
  Serial.println(i);
}
void bFunction(char* str) {
  Serial.print("\n ** B function - ");
  Serial.println(str);
  Serial.print(" - prints j - ");
  Serial.println(j);
}
void cFunction(char* str) {
  Serial.print("\n ** C function - ");
  Serial.println(str);
  Serial.print(" - prints k - ");
  Serial.println(k);
}

void dFunction(char* str) {
  Serial.print("\n ** D function - ");
  Serial.println(str);
  Serial.print(" - prints m - ");
  Serial.println(m, 6);
}

void eFunction(char* str) {
  Serial.print("\n ** E function - ");
  Serial.println(str);
  Serial.print(" - prints n - ");
  Serial.println(n);
}
// define an array of function commands
// doesn't have to be an array but then they'd all need names
// you could also use new and create them on the heap.
FunctionCommand fcoms[] = {
  { 'A', aFunction },
  { 'B', bFunction },
  { 'C', cFunction },
  { 'D', dFunction },
  { 'E', eFunction },
};

// an array of variable commands
// same as above, doesn't have to be an array but this is easier.
VariableCommand<int> vcoms[] = {
  { 'I', i },
  { 'J', j },
  { 'K', k },
};

// create a StreamHandler and connect to Serial
StreamHandler streamHandler(&Serial);

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("\n\n**** Starting StreamHandler.ino **** \n\n");
  // add commands on the heap
  new VariableCommand<float>('M', m);
  new VariableCommand<uint8_t>('N', n);
}

void loop() {
  streamHandler.run();  // run the stream handler
}

I send: <A_Hello><I42><A><D><M3.141592><D><E><N127><E>

and output is:

**** Starting StreamHandler.ino **** 



 ** A function - A_Hello
 - prints i - 2

 ** A function - A
 - prints i - 42

 ** D function - D
 - prints m - 2.200000

 ** D function - D
 - prints m - 3.141592

 ** E function - E
 - prints n - 6

 ** E function - E
 - prints n - 127

may be you could use const char * instead of char * in the parameters definition as you don't modify the c-string

1 Like

const correctness has never been my strong suit. Something I should study better.

Let's talk about where the const goes.

The base class matchChar shouldn't be allowed to change as that would crash my whole idea. match can take a const char* since it only checks and doesn't modify anything, but handle shouldn't be const. I imagine that functions called by handle may well use strtok or some other thing that modifies the buffer. That's an intended use.

So the static function check can't be const because it calls handle with the same argument.

In the FunctionCommand class
void (*func)(char*);
could be const since there's no allowance for changing the pointer to something else. But I'm not sure where to put the const there. Does it go between the * and func?

The argument can't be const though for the same reason handle can't.

In the VariableCommand class
T& var could be a const reference, but obviously not a const variable. Again, not 100% sure where to put the const. I think it's T& const var; Is that right?

parse likewise shouldn't be const or take a const argument for the same reasons as handle. Again it is the end of the chain and it is intended that the string may be modified here.

In StreamHandler, handleChar could have a const argument.

Am I missing any?

Haven't look through all your code's details, but ...

Make your life easy, go with a C++ alias and avoid the syntactic jumble of C-style function pointer defintions:

    //void (*func)(char*);
    using functPtr = void (*)(char *);
    const functPtr func;

That's straightforward, there's only one type of C++ reference ... a reference to a "thing". That "thing" can be const or non-const. But once bound to the "thing", the reference variable itself can never be changed during its lifetime to bind to different "thing". So it's const by definition. So:

template <typename T>
class theClass {
  public:
    theClass(const T &v) : var(v) {}

  private:
    const T &var;
};

Oh but I spent so much time and pulled out so much hair learning how to do that.

1 Like

It's for the parameter, so it would be like void (*func)(const char*);

if the intended use is to allow for strtok() then of course don't use const.

My remark was coming from the fact that some libraries accept have functions like
bool parse(char * message) {...}
and don't modify the message and when you try to call them with parse("x=42"); then on AVR it does not care too much and you get a warning that a const char * (the string literal) cannot be transformed into a char * but it still compiles whilst on the ESP32 the compiler will complain and handle the warning as a bug and fail to compile. This forces you to declare your string in a buffer which eats up RAM when it could happily have been into the flash memory.

The original question was ambiguous as to which pointer was intended not to be changed. I read it as he didn't want the function pointer's value itself to be changed.

Hence, my answer:

Yes - unsure what @Delta_G meant

I meant like @gfvalvo wrote.

The parameter will be the buffer containing the message. I don't want to have to make another copy so I expect that to be modified by whatever function ends up being the end user of it.

I was thinking that I shouldn't let a user change the function pointer itself, but why not? Maybe something changes and you want to call a different command in response to a given string. So I guess neither one really needs to be const.

Living on the edge...

Ok, I understand this now. The reference can never change to another variable, so there's no need to const that part. If I use const like this, then I can't alter the value of var and that's the whole purpose of the code. So const is out here.

OK got it. Then const is not such a good idea in the end :slight_smile:

Still playing with this, mainly because I actually need it for a couple of projects. Trying to make a common trope in my programming into a reusable library.

I decided that there might be different lists of commands for different Streams. So I moved the linked list into the StreamHandler class. Now each Stream can be handled by a different StreamHandler and have a different and even overlapping set of commands.

I renamed the VariableCommand to VariableUpdater because it just made more sense.

I also gave StreamHandler some factory functions to build commands on the heap.

StreamHandler.ino
#include "StreamHandler.h"

// parses an double from the string
template<>
uint8_t VariableUpdater<uint8_t>::parse(char* str) {
  return atoi(str + 1);  // skip command character
}

// define some variables
int i = 2;
int j = 3;
int k = 4;
float m = 2.2;
uint8_t n = 6;

//define some functions to print them
void aFunction(char* str) {
  Serial.print("\n ** A function - ");
  Serial.println(str);
  Serial.print(" - prints i - ");
  Serial.println(i);
}
void bFunction(char* str) {
  Serial.print("\n ** B function - ");
  Serial.println(str);
  Serial.print(" - prints j - ");
  Serial.println(j);
}
void cFunction(char* str) {
  Serial.print("\n ** C function - ");
  Serial.println(str);
  Serial.print(" - prints k - ");
  Serial.println(k);
}

void dFunction(char* str) {
  Serial.print("\n ** D function - ");
  Serial.println(str);
  Serial.print(" - prints m - ");
  Serial.println(m, 6);
}

void eFunction(char* str) {
  Serial.print("\n ** E function - ");
  Serial.println(str);
  Serial.print(" - prints n - ");
  Serial.println(n);
}

// test String   <A_Hello><I42><A><D><M3.141592><D><E><N127><E>
// create a StreamHandler and connect to Serial
StreamHandler streamHandler(&Serial);

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("\n\n**** Starting StreamHandler.ino **** \n\n");
  // add commands
  streamHandler.addFunctionCommand('A', aFunction);
  streamHandler.addFunctionCommand('B', bFunction);
  streamHandler.addFunctionCommand('C', cFunction);
  streamHandler.addFunctionCommand('D', dFunction);
  streamHandler.addFunctionCommand('E', eFunction);

  streamHandler.addVariableUpdater('I', i);
  streamHandler.addVariableUpdater('J', j);
  streamHandler.addVariableUpdater('K', k);

  streamHandler.addVariableUpdater('M', m);
  streamHandler.addVariableUpdater('N', n);
}

void loop() {
  streamHandler.run();  // run the stream handler
}
StreamHandler.h
#ifndef STREAM_HANDLER_H
#define STREAM_HANDLER_H

#include "Arduino.h"

#ifndef STREAM_HANDLER_BUFFER_SIZE
#define STREAM_HANDLER_BUFFER_SIZE 64
#endif

typedef void (*ComFuncPtr)(char*);

/*
*
*    Base class for commands
*
*/
class Command {
  friend class StreamHandler;
private:
  const char matchChar;
  Command* next = nullptr;

  boolean match(char com) {
    return ((com == matchChar));
  }
  Command();  // disallow default constructor
protected:
  virtual void handle(char* str) {
    (void)str;
  };
  Command(char c)
    : matchChar(c) {}

public:

  Command(const Command& other) = delete;
  Command& operator=(const Command& other) = delete;
  ~Command() {}
};

/*
*
*    Commands that call a function
*    function must take a char* and return void
*/
class FunctionCommand : public Command {
private:

  ComFuncPtr func;
  FunctionCommand();  // disallow default constructor

protected:

  virtual void handle(char* str) {
    if (func) func(str);
  };

public:

  FunctionCommand(char c, ComFuncPtr f)
    : Command(c), func(f){};
};


/*
*
*    Class for updating variables
*
*/

template<class T>
class VariableUpdater : public Command {
private:

  T& var;
  VariableUpdater();  // disallow default constructor
  T parse(char*);

protected:

  virtual void handle(char* str) {
    var = parse(str);
  }

public:

  VariableUpdater(char c, T& v)
    : Command(c), var(v){};
};


/*
*
*    SreamHandler class
*    handles the actual parsing
*/

class StreamHandler {

private:

  char _SHbuffer[STREAM_HANDLER_BUFFER_SIZE];
  int index;

  Stream* in;
  char sop;
  char eop;

  boolean receiving = false;
  boolean greedy = false;

  void handleChar(char c);

  Command* first;

public:

  StreamHandler(Stream* aIn, char aSop, char aEop)
    : index(0), in(aIn), sop(aSop), eop(aEop){};
  StreamHandler(Stream* aIn)
    : index(0), in(aIn), sop('<'), eop('>'){};
  void run();

  void setGreedy(bool);
  bool getGreedy();

  void addCommand(Command*);
  void addFunctionCommand(char, ComFuncPtr);
  template<class T>
  void addVariableUpdater(char, T&);
  boolean commandExists(char);
  void checkCommands();
};

template<class T>
void StreamHandler::addVariableUpdater(char c, T& v) {
  if (!commandExists(c)) {
    VariableUpdater<T>* updater = new VariableUpdater<T>(c, v);
    addCommand(updater);
  }
}



#endif  //STREAM_HANDLER_H
StreamHandler.cpp
#include "StreamHandler.h"


// parses an integer from the string
template<>
int VariableUpdater<int>::parse(char* str) {
  return atoi(str + 1);  // skip command character
}
// parses an double from the string
template<>
float VariableUpdater<float>::parse(char* str) {
  return atof(str + 1);  // skip command character
}

/*
*
*    StreamHandler methods
*
*/

void StreamHandler::run() {

  //	if (receivingRaw) {
  //		handleRawData();
  //	} else
  if (in->available()) {
    do {
      char c = in->read();
      handleChar(c);
    } while (in->available() && greedy);
  }
}

void StreamHandler::handleChar(const char c) {

  if (c == sop) {
    receiving = true;
    index = 0;
    _SHbuffer[0] = 0;
  } else if (receiving) {
    if (c == eop) {
      receiving = false;
      checkCommands();
    }
    _SHbuffer[index] = c;
    _SHbuffer[++index] = 0;

    if (index >= STREAM_HANDLER_BUFFER_SIZE - 1) {
      index--;
    }
  }
}

void StreamHandler::setGreedy(bool aBoo) {
  greedy = aBoo;
}

bool StreamHandler::getGreedy() {
  return greedy;
}

void StreamHandler::addCommand(Command* com) {
  if (!commandExists(com->matchChar)) {
    com->next = first;
    first = com;
  }
}

void StreamHandler::addFunctionCommand(char c, ComFuncPtr f) {
  if (!commandExists(c)) {
    FunctionCommand* command = new FunctionCommand(c, f);
    addCommand(command);
  }
}

boolean StreamHandler::commandExists(char c) {
  for (Command* ptr = first; ptr != nullptr; ptr = ptr->next) {
    if (ptr->match(c)) {
      return true;
    }
  }
  return false;
}

void StreamHandler::checkCommands() {
  for (Command* ptr = first; ptr != nullptr; ptr = ptr->next) {
    // Start markers should be stripped.  Command char should be first element
    if (ptr->match(*_SHbuffer)) {
      ptr->handle(_SHbuffer);
      break;
    }
  }
}

I compromised and went with a typedef.

Next thing to add is a way to respond. I added an extra buffer in StreamHandler and pass it to handle which now takes two char*. If it's not needed like FunctionCommand then it just ignores it. If needed like in VariableUpdater or the new ReturnFunction, then the response is stored there and StreamHandler takes care of sending it back to the Stream.

I also figured to make two Stream pointers for the in and out going data in case they go different places.

Now VariableUpdater sends back a message with the parsed value, so the sender can check that it was received and parsed correctly. Or display it in my particular use case.

Then next thing I need for my particular project is a way to handle messages going back the other way. Things that aren't a response to a command.

StreamHandler.ino

#include "StreamHandler.h"

// parses an double from the string
template<>
uint8_t VariableUpdater<uint8_t>::parse(char* str) {
  return atoi(str + 1);  // skip command character
}
template<>
void VariableUpdater<uint8_t>::display(char* ret) {
  snprintf(ret, STREAM_HANDLER_BUFFER_SIZE, "<%c%u>", matchChar, var);
}

// define some variables
int i = 2;
int j = 3;
int k = 4;
float m = 2.2;
uint8_t n = 6;

//define some functions to print them
void aFunction(char* str, char* ret) {
  Serial.print("\n ** A function - ");
  Serial.println(str);
  snprintf(ret, STREAM_HANDLER_BUFFER_SIZE, "matched 'A' printed i %d", i);
}
void bFunction(char* str, char* ret) {
  Serial.print("\n ** B function - ");
  Serial.println(str);
  snprintf(ret, STREAM_HANDLER_BUFFER_SIZE, "matched 'B' printed j %d", j);
}
void cFunction(char* str) {
  Serial.print("\n ** C function - ");
  Serial.println(str);
  Serial.print(" - prints k - ");
  Serial.println(k);
}

void dFunction(char* str) {
  Serial.print("\n ** D function - ");
  Serial.println(str);
  Serial.print(" - prints m - ");
  Serial.println(m, 6);
}

void eFunction(char* str) {
  Serial.print("\n ** E function - ");
  Serial.println(str);
  Serial.print(" - prints n - ");
  Serial.println(n);
}

void def(char* str, char* ret) {
  strncpy(ret, "Default Handler", STREAM_HANDLER_BUFFER_SIZE);
  Serial.println(str);
}

// test String   <A_Hello><I42><A><D><M3.141592><D><E><N127><E>
// create a StreamHandler and connect to Serial
StreamHandler streamHandler(&Serial, &Serial);

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("\n\n**** Starting StreamHandler.ino **** \n\n");
  // add commands
  streamHandler.addReturnCommand('A', aFunction);
  streamHandler.addReturnCommand('B', bFunction);

  streamHandler.addFunctionCommand('C', cFunction);
  streamHandler.addFunctionCommand('D', dFunction);
  streamHandler.addFunctionCommand('E', eFunction);

  streamHandler.addVariableUpdater('I', i);
  streamHandler.addVariableUpdater('J', j, false);  // doesn't send return back
  streamHandler.addVariableUpdater('K', k);

  streamHandler.addVariableUpdater('M', m);
  streamHandler.addVariableUpdater('N', n);

  streamHandler.setDefaultHandler(def);
}

void loop() {
  streamHandler.run();  // run the stream handler
}

StreamHandler.h

#ifndef STREAM_HANDLER_H
#define STREAM_HANDLER_H

#include "Arduino.h"

#ifndef STREAM_HANDLER_BUFFER_SIZE
#define STREAM_HANDLER_BUFFER_SIZE 64
#endif

#ifndef DEFAULT_SOP
#define DEFAULT_SOP '<'
#endif

#ifndef DEFAULT_EOP
#define DEFAULT_EOP '>'
#endif

#ifndef DEFAULT_VU_ECHO
#define DEFAULT_VU_ECHO true
#endif

typedef void (*ComFuncPtr)(char*);
typedef void (*RetFuncPtr)(char*, char*);

/*
*
*    Base class for commands
*
*/
class Command {
  friend class StreamHandler;
private:
  Command* next = nullptr;

  boolean match(char com) {
    return ((com == matchChar));
  }
  Command();  // disallow default constructor
protected:
  const char matchChar;
  virtual void handle(char* str, char* ret) = 0;
  Command(char c)
    : matchChar(c) {}

public:

  Command(const Command& other) = delete;
  Command& operator=(const Command& other) = delete;
  ~Command() {}
};

/*
*
*    Commands that call a function
*    function must take a char* and return void
*/
class FunctionCommand : public Command {
private:

  ComFuncPtr func;
  FunctionCommand();  // disallow default constructor

protected:

  virtual void handle(char* str, char* ret) {
    (void)ret;
    if (func) func(str);
  };

public:

  FunctionCommand(char c, ComFuncPtr f)
    : Command(c), func(f){};
};

/*
*
*    Commands that call a function and send back a return
*    function must take a char* for the command and a char* to put the return in
*    and return void
*/
class ReturnCommand : public Command {
private:

  RetFuncPtr func;
  ReturnCommand();  // disallow default constructor

protected:

  virtual void handle(char* str, char* ret) {
    if (func) func(str, ret);
  };

public:

  ReturnCommand(char c, RetFuncPtr f)
    : Command(c), func(f){};
};

/*
*
*    Class for updating variables
*
*/

template<class T>
class VariableUpdater : public Command {
private:

  T& var;
  VariableUpdater();  // disallow default constructor
  T parse(char*);
  void display(char*);
  bool echo;

protected:

  virtual void handle(char* str, char* ret) {
    var = parse(str);
    if (echo) {
      display(ret);
    }
  }

public:

  VariableUpdater(char c, T& v, bool e)
    : Command(c), var(v), echo(e){};
  VariableUpdater(char c, T& v)
    : VariableUpdater(c, v, DEFAULT_VU_ECHO){};
};


/*
*
*    SreamHandler class
*    handles the actual parsing
*/

class StreamHandler {

private:

  char inBuffer[STREAM_HANDLER_BUFFER_SIZE];
  char outBuffer[STREAM_HANDLER_BUFFER_SIZE];
  int index;

  Stream* in;
  Stream* out;
  char sop;
  char eop;

  boolean receiving = false;
  int greedy = 0;

  void handleChar(char c);

  void (*defaultHandler)(char*, char*) = nullptr;

  Command* first;

  boolean commandExists(char);
  void checkCommands();
  void sendOutBuffer();

public:
  StreamHandler(Stream* aIn, Stream* aOut, char aSop, char aEop)
    : index(0), in(aIn), out(aOut), sop(aSop), eop(aEop){};
  StreamHandler(Stream* aIn, char aSop, char aEop)
    : StreamHandler(aIn, nullptr, aSop, aEop){};
  StreamHandler(Stream* aIn, Stream* aOut)
    : StreamHandler(aIn, aOut, DEFAULT_SOP, DEFAULT_EOP){};
  StreamHandler(Stream* aIn)
    : StreamHandler(aIn, nullptr){};

  void run();

  void setGreedy(int);
  int getGreedy();

  void setDefaultHandler(void (*)(char*, char*));

  void addCommand(Command*);
  void addFunctionCommand(char, ComFuncPtr);
  void addReturnCommand(char, RetFuncPtr);
  template<class T>
  void addVariableUpdater(char, T&, bool = true);
};

template<class T>
void StreamHandler::addVariableUpdater(char c, T& v, bool e) {
  if (!commandExists(c)) {
    VariableUpdater<T>* updater = new VariableUpdater<T>(c, v, e);
    addCommand(updater);
  }
}



#endif  //STREAM_HANDLER_H

StreamHandler.cpp

#include "StreamHandler.h"


// parses an integer from the string
template<>
int VariableUpdater<int>::parse(char* str) {
  return atoi(str + 1);  // skip command character
}
// parses an double from the string
template<>
float VariableUpdater<float>::parse(char* str) {
  return atof(str + 1);  // skip command character
}

template<>
void VariableUpdater<int>::display(char* ret) {
  snprintf(ret, STREAM_HANDLER_BUFFER_SIZE, "<%c%d>", matchChar, var);
}

template<>
void VariableUpdater<float>::display(char* ret) {
  char buf[16];
  dtostrf(var, 2, 2, buf);
  snprintf(ret, STREAM_HANDLER_BUFFER_SIZE, "<%c%s>", matchChar, buf);
}

/*
*
*    StreamHandler methods
*
*/

void StreamHandler::run() {

  //	if (receivingRaw) {
  //		handleRawData();
  //	} else
  if (in->available()) {
    int counter = greedy;
    do {
      char c = in->read();
      handleChar(c);
    } while (in->available() && counter--);
  }
}

void StreamHandler::handleChar(const char c) {

  if (c == sop) {
    receiving = true;
    index = 0;
    inBuffer[0] = 0;
  } else if (receiving) {
    if (c == eop) {
      receiving = false;
      checkCommands();
    } else {
      inBuffer[index] = c;
      inBuffer[++index] = 0;
    }

    if (index >= STREAM_HANDLER_BUFFER_SIZE - 1) {
      index--;
    }
  }
}

void StreamHandler::setGreedy(int g) {
  greedy = g;
}

int StreamHandler::getGreedy() {
  return greedy;
}

void StreamHandler::setDefaultHandler(void (*h)(char*, char*)) {
  defaultHandler = h;
}

void StreamHandler::addCommand(Command* com) {
  if (!commandExists(com->matchChar)) {
    com->next = first;
    first = com;
  }
}

void StreamHandler::addFunctionCommand(char c, ComFuncPtr f) {
  if (!commandExists(c)) {
    FunctionCommand* command = new FunctionCommand(c, f);
    addCommand(command);
  }
}

void StreamHandler::addReturnCommand(char c, RetFuncPtr f) {
  if (!commandExists(c)) {
    ReturnCommand* command = new ReturnCommand(c, f);
    addCommand(command);
  }
}

boolean StreamHandler::commandExists(char c) {
  for (Command* ptr = first; ptr != nullptr; ptr = ptr->next) {
    if (ptr->match(c)) {
      return true;
    }
  }
  return false;
}

void StreamHandler::checkCommands() {
  bool found = false;
  for (Command* ptr = first; ptr != nullptr; ptr = ptr->next) {
    // Start markers should be stripped.  Command char should be first element
    if (ptr->match(*inBuffer)) {
      found = true;
      ptr->handle(inBuffer, outBuffer);
      sendOutBuffer();
      break;
    }
  }
  if (defaultHandler && !found) {
    defaultHandler(inBuffer, outBuffer);
    sendOutBuffer();
  }
}

void StreamHandler::sendOutBuffer() {
  if (out && strlen(outBuffer) > 0) {
    out->write(outBuffer, strlen(outBuffer));
  }
  outBuffer[0] = 0;
}

Seems the comment is wrong there

What about adding error detection? Using strtol() would be better than atoi() to catch a mismatch

Never trust the comments. :slight_smile: It used to be a double. I was playing with templates and auto type promotion and it got changed somewhere.

I'll be honest, I'm building this more for my use than anything else. I have a few specific projects that have spaghetti of nested switch cases to parse commands out of a serial stream. I wanted to have something reusable where I can enforce a protocol (like markers) from the inside.

I also wanted to clean my OOP claws a little. It's @dougp 's fault. He got me onto it. Realize that right now I'm replacing two functions that span a couple of hundred lines with 6 or 7 pages of code. I may end up just calling this an exercise by the time it's over with.

Indeed. Once I get the OOP part worked out I'll dress up those parsing function.

Notice that I marked those parsers as weak. The end use case would involve re-implementing those in my code to define the protocol for that piece of code. For instance, I might want to use Hex or something in one project but not another.

What I'd like to figure out is how to somehow alias something like int into two different types I could have different parsers for. If I try to typedef int to something else then the template still just sees it as int.

The other thing that is definitely coming is the ability to parse raw bytes instead of ascii. That means sending a length with the packet since you can't guarantee the end marker won't show up in some raw data.

Ok

As parsing various formats is frequent I could foresee an option to have the equivalent of sscanf() where you pass the format and the pointers to storage and have this part of the “library”