Non-blocking receiving of commands over serial interface using the SafeString-library [code example]

This is another small tutorial that shall demonstrate how to receive commands over a serial interface in a non-blocking manner using the SafeString-library

If you are in a hurry to throw together code as fast as possible jump to posting # 2

non-blocking means the code has a fast running function loop() that is able to

  • check for button-presses
  • check for IO-pin changing their state
  • read in sensors
  • etc

and in parallel listen to the serial interface all the time
and in case all characters of a command are received execute the code dedicated / connected to this command

Especially if you are a newcomer:
If you have questions about the demo-code just ask the questions.
There are no better experts than newcomers to judge and improve demo-codes that shall explain something.

If you have ideas for improvment or variants post your ideas.

The first code-version has some additional features. using functions and using non-blocking timing.

here is the demo-code with intensive comments

// SafeStringReader_Cmds.ino
//
// Example of NON-Blocking read commmands from the Arduino Monitor input and acts on them
// the available commands are start stop
// See the SafeStringReader_CmdsTimed.ino for an example using a struct to hold the commands and their functions
//
// Commands are delimited by space dot comma NL or CR
// If you set the Arduino Monitor to No line ending then the last command will be ignored until it is terminated by a space or ,
//  Use the settings Newline or Carrage Return or Both NL & CR
//
// These commands can be picked out of a line of user input
// start  stop
// The input line can be as long as you like 100's of Kb long, but only two small buffers need to parse the commands
//
// download and install the SafeString library from
// www.forward.com.au/pfod/ArduinoProgramming/SafeString/index.html

// overview about the functionality of this code:

// inside function loop() four things are done:
// 1. count up a variable infinitely to demonstrate the fast running loop

// 2. Blink the onboard LED to demonstrate non-blocking timing

// 3. Check for commands send over the serial interface 
//    and in case a command is received analyse the command and execute 
//    the function related to the command

// 4. print the value of the counter-variable once every second to
//    demontsrate the fast running loop

#include "SafeStringReader.h"

// create an myInputReader instance of SafeStringReader class
// that will handle commands upto 5 chars long
// delimited by space, comma or CarrageReturn or NewLine
// the createSafeStringReader( ) macro creates both the SafeStringReader (myInputReader) and the necessary SafeString that holds input chars until a delimiter is found
// args are (ReaderInstanceName, expectedMaxCmdLength, delimiters)


// create the "object" with name myInputReader that does read from the serial interface
// maximum length of accepted inputcommands is 5 characteras
// allowed delimiters are:
// space: the " "
// comma: ,
// carriage return:  \r
// new line:         \n
createSafeStringReader(myInputReader, 5, " ,\r\n");

boolean running = true;
unsigned long loopCounter = 0;
unsigned long allwaysCounter = 100000000;



// easy to use helper-function for non-blocking timing
boolean TimePeriodIsOver (unsigned long &startOfPeriod, unsigned long TimePeriod) {
  unsigned long currentMillis  = millis();
  if ( currentMillis - startOfPeriod >= TimePeriod ) {
    // more time than TimePeriod has elapsed since last time if-condition was true
    startOfPeriod = currentMillis; // a new period starts right here so set new starttime
    return true;
  }
  else return false;            // actual TimePeriod is NOT yet over
}

unsigned long MyTestTimer =  0;                   // Timer-variables MUST be of type unsigned long
const byte    OnBoard_LED = 13;


void BlinkHeartBeatLED(int IO_Pin, int BlinkPeriod) {
  static unsigned long MyBlinkTimer;
  pinMode(IO_Pin, OUTPUT);

  if ( TimePeriodIsOver(MyBlinkTimer, BlinkPeriod) ) {
    digitalWrite(IO_Pin, !digitalRead(IO_Pin) );
  }
}


void setup() {
  Serial.begin(115200);
  Serial.println( F("Setup-Start") );
  Serial.println();
  Serial.println(F(" SafeString-Reader-Demo"));
  Serial.println(F(" Commands are stop start"));
  Serial.println(F(" Set the Arduino IDE monitor to Newline, or Carriage return or Both NL & CR"));
  Serial.println(F(" See the SafeStringReader_CmdsTimed.ino for an example using a struct to hold the commands and their functions."));

  SafeString::setOutput(Serial); // enable error messages and SafeString.debug() output to be sent to Serial
  if (running) {
    Serial.println(F(" Counter Started"));
  }
  myInputReader.connect(Serial); // where SafeStringReader will read from in this demo the standard Serial
  myInputReader.echoOn(); // echo back all input, by default echo is off
}



void handleStartCmd() {
  running = true;
  Serial.println();
  Serial.print(F("> start at Counter:"));
  Serial.println(loopCounter);
}

void handleStopCmd() {
  running = false;
  Serial.println();
  Serial.print(F("> stop at Counter:"));
  Serial.println(loopCounter);
}


void ManageSerialCommands() {

  if (myInputReader.read()) { // check if all characters of a command including delimiter are received
    if (myInputReader == "start") {
      handleStartCmd();
    }

    else if (myInputReader == "stop") {
      handleStopCmd();
    } // else ignore unrecognized command
  } // else no delimited command yet

  // rest of code here is executed while the user typing in commands
  if (running) {
    loopCounter++;
    if ((loopCounter % 100000) == 0) {
      // print the current counter each time it is a multiple of 100000
      Serial.print(F("Counter:"));
      Serial.println(loopCounter);
    }
  }
}

void loop() {
  
  allwaysCounter++; // does what its name says allways counting up more and more demonstrating the non-blocking character of the code

  BlinkHeartBeatLED(OnBoard_LED, 250); // demontrates non-blocking timing

  ManageSerialCommands(); // call this non-blocking function in every iteration of loop

  if ( TimePeriodIsOver(MyTestTimer, 1000) ) {// check if 1000 milliseconds have passed by 
      // once every 1000 milliseconds execute code below
    if (running) { // only in case variable running has value true
      Serial.print(F("allwaysCounter:"));
      Serial.println(allwaysCounter);
    }
  }

}

best regards Stefan

most stripped down version of the demo-code

// derived from SafeStringReader_Cmds.ino
//
#include "SafeStringReader.h"

createSafeStringReader(myInputReader, 5, " ,\r\n");

boolean running = true;
unsigned long loopCounter = 0;

void setup() {
  Serial.begin(115200);
  Serial.println( F("Setup-Start") );
  Serial.println();
  Serial.println(F(" SafeString-Reader-Demo"));
  Serial.println(F(" Commands are stop start"));
  Serial.println(F(" Set the Arduino IDE monitor to Newline, or Carriage return or Both NL & CR"));
  Serial.println(F(" See the SafeStringReader_CmdsTimed.ino for an example using a struct to hold the commands and their functions."));

  SafeString::setOutput(Serial); // enable error messages and SafeString.debug() output to be sent to Serial
  if (running) {
    Serial.println(F(" Counter Started"));
  }
  myInputReader.connect(Serial); // where SafeStringReader will read from in this demo the standard Serial
  myInputReader.echoOn(); // echo back all input, by default echo is off
}


void handleStartCmd() {
  running = true;
  Serial.println();
  Serial.print(F("> start at Counter:"));
  Serial.println(loopCounter);
}

void handleStopCmd() {
  running = false;
  Serial.println();
  Serial.print(F("> stop at Counter:"));
  Serial.println(loopCounter);
}


void loop() {
  
  if (myInputReader.read()) { // check if all characters of a command including delimiter are received
    if (myInputReader == "start") {
      handleStartCmd();
    }

    else if (myInputReader == "stop") {
      handleStopCmd();
    } // else ignore unrecognized command
  } // else no delimited command yet

  // rest of code here is executed while the user typing in commands
  if (running) {
    loopCounter++;
    if ((loopCounter % 100000) == 0) {
      // print the current counter each time it is a multiple of 100000
      Serial.print(F("Counter:"));
      Serial.println(loopCounter);
    }
  }
}

My view is that If you are a newcomer you should not use the SafeString monster until you have a full understanding of cStrings and know what you are doing. (And then maybe you decide not to)

The right place to start for handling the Serial port asynchronously is to study Several Things at a Time, Serial Input Basics and Planning and Implementing a Program to name a few

(As well as the common C functions from stdlib.h and string.h )

1 Like

This is like learning tayloring for wearing a jeans.

The learning curve for all the SafeString classes, constraints etc is steeper and learning the fundamentals of programming and the language is never wasted. (You get what SafeString does because you understand what’s behind).

If you only want to “wear jeans” and don’t mind the holes at the knees the. Just use the String class. It will kinda work until it doesn’t

again compared to the realworld example of Jeans:
the SafeString-library is like knee-protectors and using highly robust Tyvek instead of cotton while still the material is breathable to wick away moisture.

The comparison has its limits. There is no skill difference in putting on a pair of trousers regardless of its quality.

Designing and building one from fabric is probably different indeed (haute couture is art). But in both cases you need basic skills like sewing, cutting, assembling, ironing and practice.

Managing fixed size buffers without overflow is part of the basic skills a programmer should get.

If you don’t want to be bothered by the fixed size limit and the mandatory associated handling the memory overflow then using the String class is your best alternative as it will grow for you until it can’t.

(The only thing you get with safestring is that the string operation won’t crash - but you still need to catch the not enough space error otherwise your code will assume the operation was a success and will behave erratically - so it’s not different than handling fixed size buffers)

Anyway - thx for posting an example - keep on.

1 Like

What is special about this print, that it doesn't get the F macro?

@StefanL38
In my opinion all example code should be split into separate functions with only calls to those functions in loop() and nothing else. Newbies have a habit of making long, complicated and unreadable code all buried inside loop(), giving them examples of dividing code up into manageable chunks in separate functions with meaningful names will help them improve their skills.

1 Like

Here is a little application of the non-blocking serial receiving
You can send the commands ON or OFF to the arduino which wil switch on/off the onboard-LED
For switching on / off a state-machine is used
additionally the code uses non-blocking timing for switching off the LED after 10 seconds

#include "SafeStringReader.h"

createSafeStringReader(myInputReader, 5, " ,\r\n");

const byte LED_pin = 13;

const byte fsm_idling         = 1;
const byte fsm_switchLEDOn    = 2;
const byte fsm_waitWithLEDOn  = 3;
const byte fsm_SwitchLEDOFF   = 4;

byte myStateVar;

unsigned long myWaitTimer;

const unsigned long LedOnTime = 10000;

// easy to use helper-function for non-blocking timing
boolean TimePeriodIsOver (unsigned long &startOfPeriod, unsigned long TimePeriod) {
  unsigned long currentMillis  = millis();
  if ( currentMillis - startOfPeriod >= TimePeriod ) {
    // more time than TimePeriod has elapsed since last time if-condition was true
    startOfPeriod = currentMillis; // a new period starts right here so set new starttime
    return true;
  }
  else return false;            // actual TimePeriod is NOT yet over
}

void ManageSerialCommands() {

  if (myInputReader.read()) { // check if all characters of a command including delimiter are received
    if (myInputReader == "ON") {
      myStateVar = fsm_switchLEDOn;
    }

    if (myInputReader == "OFF") {
      myStateVar = fsm_SwitchLEDOFF;
    }
  }
}


void setup() {
  pinMode(LED_pin, OUTPUT); // no comment needed because of SELF-explaining name  led
  Serial.begin(115200);
  Serial.println( F("Setup-Start") );
  Serial.println( F("enter command ON or OFF to switch LED") );

  SafeString::setOutput(Serial); // enable error messages and SafeString.debug() output to be sent to Serial
  myInputReader.connect(Serial); // where SafeStringReader will read from in this demo the standard Serial
  myInputReader.echoOn(); // echo back all input, by default echo is off
  myStateVar = fsm_idling;
}


void FSM() {

  switch (myStateVar) {

    case fsm_idling:
      // just do sit and wait for serial commands
      break; // immidiately jump down to end of switch

    case fsm_switchLEDOn:
      digitalWrite(LED_pin, HIGH);
      myWaitTimer = millis(); // initialise Timer-variable with actual value of millis()
      myStateVar = fsm_waitWithLEDOn;
      break; // immidiately jump down to end of switch

    case fsm_waitWithLEDOn:
      if ( TimePeriodIsOver(myWaitTimer, LedOnTime) ) { // check if waitingtime is over
        // if number of milliseconds stored in LedOnTime have passed by
        Serial.println( F("waiting time over chnage to switching LED off") );
        myStateVar = fsm_SwitchLEDOFF;
      }
      break; // immidiately jump down to end of switch

    case fsm_SwitchLEDOFF:
      digitalWrite(LED_pin, LOW);
      Serial.println( F("LED switched off") );
      Serial.println( F("enter command ON or OFF to switch LED") );
      myStateVar = fsm_idling;
      break; // immidiately jump down to end of switch

    default:
      myStateVar = fsm_idling;
      break; // immidiately jump down to end of switch
  }
}


void loop() {
  ManageSerialCommands();
  FSM();
}

best regards Stefan

I love enums

I know that you love enums. For didactical reasons I did not use enum

"didactic" adjective, intended to educate.

Why would you deliberately intend not to educate?

OK I was not precise enough.

For didactically reduction I decided to not use enum
Which means I want to reduce the number of new things presented

1 Like

you did not present the SafeString nor the SafeStringReader :slight_smile:

BTW you are missing a break in one of your cases

    case fsm_SwitchLEDOFF:
      digitalWrite(LED_pin, LOW);
      Serial.println( F("LED switched off") );
      Serial.println( F("enter command ON or OFF to switch LED") );
      myStateVar = fsm_idling;

    default:
      myStateVar = fsm_idling;
      break;

if the fall through is intentional (it would be OK) then a comment might be welcome or better document in code

    case fsm_SwitchLEDOFF:
      digitalWrite(LED_pin, LOW);
      Serial.println( F("LED switched off") );
      Serial.println( F("enter command ON or OFF to switch LED") );
      myStateVar = fsm_idling;
      [[gnu::fallthrough]]; // C++11 and C++14
    //  [[fallthrough]]; // C++17 and above

    default:
      myStateVar = fsm_idling;
      break;

of course there is no need to repeat myStateVar = fsm_idling; then

It was not intenionally. Thank you very much for pointing on it. I will add the break;
best regards Stefan

an else might also be beneficial in between the two if in ManageSerialCommands()

    if (myInputReader == "ON") {
      myStateVar = fsm_switchLEDOn;
    } else if (myInputReader == "OFF") {
      myStateVar = fsm_SwitchLEDOFF;
    }

and probably (as we discussed in another thread)

    startOfPeriod = currentMillis; // a new period starts right here so set new starttime

is not needed in this case since the FSM handles the myWaitTimer when necessary

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.