How to parse multiple variables in one string?

Hi all! I am new to Arduino and am trying to figure out how to parse a command that contains multiple settings such as this:

CommandName 4 10

CommandName = Switch case #
4 = DC motor to activate
10 = Leave DC motor on until push button is clicked 10 times.

I don't want to use commas to separate the variables. I would like to use spaces. Also, the code should know that the command is finished once it is sent. I'm assuming this has something to do with \n (new line).

Can anyone please provide me an example or point me to a direction on how to write this snippet of code?

The serial lnput basics tutorial may have information of interest. Example #5 shows how to use strtok() to parse data from a string.

Use c-strings, then strtok() to break it into separate parameters.

param 0 = CommandName
param 0 = 4
param 0 = 10
:::
Note - these will all be c-strings, soi you'll need atoi() and other relevant functions to convert the 'char'acter substrings into numeric data types (int, long etc)

You can switch() on param 0 'CommandName', act based on the second parameter '4', and perform the third '10' or whatever you need to achieve.

This is a really good opportuity to learn about char* (pointers) within a larger c-string.

My typical approach is to parse as it comes in.
Store the incoming characters in the buffer - you know what you can expect when.
So in this case the moment you receive "CommandName" you can start looking for the expected delimiter (the space), when that's received you start looking for the digits until you see the next delimiter (the next space), you process the received part, and you start looking for the next part.
Saves storing the whole string in memory. The buffer containing the "CommandName" string is of course reused to receive the following numbers.

Just receive 3 bytes.
"command name" is represented by 0 - 255
motor to activate is represented by 0 - 255

of button presses is represented by 0 - 255

Order of reception determines the meaning of each byte.

How many different commands do you have? Do the parameters vary depending on the command?

I wouldn't be surprised if you can shove that into a single byte. If that doesn't work use the first two bits of the byte to indicate what it is, the next 6 bits for the data - so you have a value range of 0-63. Hard to believe you have more commands or "button presses".

Another solution is to send an extra byte, value 255, as start token, followed by the other three (which of course see their range reduced to 0-254).

Thank you all for the help!

My goal is to have multiple DC motors and one push button. I can choose which motor I want to activate and until how many X button presses.

Example string commands:

M600 4 10 (Turn on motor #4 forward until 10 button presses) (M600 = "forward")
M400 3 16 (Turn on motor #3, in reverse and until 16 button presses) (M400 = "backward")

Mcodes would represent different switch cases or "mini-programs".

As I am new to Arduino and programming in general, can I ask that someone provide an example code? Otherwise it's hard for me to wrap my head around how to write the code to do this. I can modify it, but it's easiest for me to learn by example.

groundFungus:
The serial lnput basics tutorial may have information of interest. Example #5 shows how to use strtok() to parse data from a string.

I saw example #5 in that tutorial, but it uses <> to identify beginning and end. And it uses commas to separate commands. I would like to not use those as mentioned in the first post and instead use spaces and "new line" to understand that a full command has been submitted.

If someone can please provide an example, it would really help me understand how this code is supposed to be structured.

wildbill:
How many different commands do you have? Do the parameters vary depending on the command?

The goal is in the future to have the parameters vary depending on the command, yes. Like:

M200 150 = Set motor speed to 150 PWM for example. This is why I mentioned using switch cases.

@OP

Assume that you will receive this code: M600 4 10 from the InputBox of the Serial Monitor of the Arduino IDE. It is also assumed that the end of the said message string would be indicated by 'Newline' character to be set by the 'Line ending tab' of the Serial Monitor. The following codes may help you in decoding the information (motordirection, motorNumber, and numberPress) from the composite message.

char myData[10] = "";
char directionArray[5] = "";
int motorNumber;
int numberPress;
int i = 0;

void setup() 
{
  Serial.begin(9600);
}

void loop() 
{
  byte n = Serial.available();
  if( n!=0)  //character is recievd and it is in FIFO
  {
    char x = Serial.read(); //read the arrived character
    if(x != 0x0A)   //Newline character is not found '\n'
    {
      myData[i] = x;   //save in array M600 4 10
      //Serial.print(x);
      i++;
    }
    else
    {
      myData[4] = 0x00;  //insert NULL-byte
      strcpy(directionArray, myData); 
      Serial.println(directionArray);
      //----------------------------
      motorNumber = myData[5]-'0';
      Serial.println(motorNumber);
      //--------------------------
      myData[9] = 0x00;
      numberPress = atoi(myData+7);
      Serial.println(numberPress);
      while(1);
    }
  }
}

shai:
I saw example #5 in that tutorial, but it uses <> to identify beginning and end. And it uses commas to separate commands. I would like to not use those as mentioned in the first post and instead use spaces and "new line" to understand that a full command has been submitted.

What is stopping you from substituting your codes for the <,> codes? After all it's just a number.

GolamMostafa:
@OP

Assume that you will receive this code: M600 4 10 from the InputBox of the Serial Monitor of the Arduino IDE. It is also assumed that the end of the said message string would be indicated by 'Newline' character to be set by the 'Line ending tab' of the Serial Monitor. The following codes may help you in decoding the information (motordirection, motorNumber, and numberPress) from the composite message.

char myData[10] = "";

char directionArray[5] = "";
int motorNumber;
int numberPress;
int i = 0;

void setup()
{
  Serial.begin(9600);
}

void loop()
{
  byte n = Serial.available();
  if( n!=0)  //character is recievd and it is in FIFO
  {
    char x = Serial.read(); //read the arrived character
    if(x != 0x0A)  //Newline character is not found '\n'
    {
      myData[i] = x;  //save in array M600 4 10
      //Serial.print(x);
      i++;
    }
    else
    {
      myData[4] = 0x00;  //insert NULL-byte
      strcpy(directionArray, myData);
      Serial.println(directionArray);
      //----------------------------
      motorNumber = myData[5]-'0';
      Serial.println(motorNumber);
      //--------------------------
      myData[9] = 0x00;
      numberPress = atoi(myData+7);
      Serial.println(numberPress);
      while(1);
    }
  }
}

Thank you for this! Is it possible to explain line by line what this code does exactly? There's quite a bit I'm not familiar with here.

And it uses commas to separate commands. I would like to not use those as mentioned in the first post and instead use spaces

Replace the commas in the strtok() delimiter strings with a space

strtokIndx = strtok(tempChars,",");

To

strtokIndx = strtok(tempChars," ");

shai:
Thank you for this! Is it possible to explain line by line what this code does exactly? There's quite a bit I'm not familiar with here.

Your requested for an example. I have prepared this example and tested and then submitted for your use. Now, you are saying that some of the constructs of the codes are not friendly to you. Which particular one is no good to you?

GolamMostafa:
Your requested for an example. I have prepared this example and tested and then submitted for your use. Now, you are saying that some of the constructs of the codes are not friendly to you. Which particular one is no good to you?

Hi GolamMostafa,

I mean to say that since I am new to programming, it helps for me if the code is commented so I can learn what it is doing. I know this is asking for a bit of work as there's a few lines of code, but I would be really grateful! I'm mostly referring to everything under the loop(), as well as understanding what char myData[10] = ""; is? Is it to store the string before it is broken down?

I uploaded the sketch you posted, but nothing happens. Can you elaborate if I need to add some parts to this and which? I am guessing the switch case is missing here to begin with?

shai:
I uploaded the sketch you posted, but nothing happens. Can you elaborate if I need to add some parts to this and which? I am guessing the switch case is missing here to begin with?

OK! You please, carry out the following step; let me know the results and then I will try to meet your thirst.

  1. Upload the sketch that I have posted in your UNO/NANO/MEGA.

  2. Bring in the Serial Monitor at 9600 Bd.

  3. Set the 'Ine ending tab' (Fig-1 below) at 'Newline' option. This option sends '\n' (Newline charcater/Linefeed(LF); ASCII code = 0x0A; Fig-2 below) character at the end of your string that you are going to send down to UNO.
    SerialMonitor.png
    Figure-1: Serial Monitor of Arduino IDE


Figure-2: ASCII Chart for the charcaters of English Language and other Control Characters

  1. Bring in the Cursor at the InputBox of the Serial Monitor.

  2. Enter this string: M600 4 10 and then click on the Send Button of the Serial Monitor.

  3. Check that the following message has appeared in the OutputBox of the Serial Monitor.
    M600
    4
    10

  4. Check in my sketch that the information of Step-6 have been stored/saved into three separate variable and these are:
    char directionArray[5];
    int motorNumber;
    int numberPress;

  5. Now, you have the information with you and you can process/use them.

SerialMonitor.png

shai:
I'm mostly referring to everything under the loop(), as well as understanding what char myData[10] = ""; is? Is it to store the string before it is broken down?

myData is an array which can hold only 10 items/members and all these locations initially contain 0x00 which is a NULL-byte. The loading of 0x00 into all locations is done by placing ("" a pair of double quotes) at the RHS side of the assignment (=) operator.

When you place M600 4 10 in the InputBox of Serial Monitor with Newline option and click on the Send button, the following data bytes (these are ASCII codes of the charcaters of your string) are transferred (one byte at a time) to UNO.
4D36303020342031300A //10-byte : M 6 0 0 space 4 space 31 30 Newline

4D arrives at UNO; UNO receives it and saves in an unknown FIFO (first-in first-out) buffer. This is a very fast operation being carried out on interrupt basis at the background and beyond the visible knowledge of the user.

In the loop() function, you execute the following code to see if any data byte (here 4D; 1-byte) has been accumulated in the FIFO.

byte n = Serial.available(); //if n=0 means no data byte has arrived; but we know that 1-byte has come

So, we bring it from the FIFO and save it into the 1st location of the array named myData[]. That's why we have executed the following codes:

if(n !=0)
{
  char x = Serial.read();    //this code brings out the data byte (4D) from FIFO and puts into x
  myData[0] = x;             //4D is kept into 1st location of array. Observe that we should put a variable
                                     //in the index field so that we can save the next character 6 (0x36) into next
                                     //location of the array
 //myData[i] = x;           //i should be initially 0
 //i++;                          //increment i
}

Thank you so much for writing this out in detail! It makes it much easier for a non-programmer such as myself to understand! :slight_smile:

Before you posted this, I managed to get a fully working program, but it requires < > (I took it from Example #5 from the tutorial link posted earlier) and unfortunately I have to use letters like a, b, c, etc for the command switch case type. Like so:

<a 4 6>
a = case
4 = motor
6 = until 6 clicks

Here is my code that I hacked together from snippets online. I'm sure there's better ways of writing it, so feel free to point out how I can improve it!

I will definitely look in more detail how you did it and incorporate it since your program is able to do it without < > and mix letters with numbers like M600 whereas I was only able to use letters like 'a'

/*************************************************************************
 Name: AutoFeeders.ino
 Created: 3/25/2019
 Author: Shai

 Description: Pick and place Auto Feeder controller
**************************************************************************/

/*******************************************************
*                       INCLUDES
********************************************************/

#include "pins.h"

/*******************************************************
*                       DEFINES
********************************************************/
// Variables will change:
int encoderCounter = 0;   // counter for the number of button presses
int buttonState = 0;         // current state of the button
int lastButtonState = 0;     // previous state of the button
const byte numChars = 32;
char receivedChars[numChars];
char tempChars[numChars];        // temporary array for use when parsing

      // variables to hold the parsed data
char command[numChars] = {0};
int feederNumber = 0;
int feederDistance = 0;
// float floatFromPC = 0.0;

boolean newData = false;

/*******************************************************
*                       SETUP
********************************************************/


//String inData;

void setup()
{
 Serial.begin(SERIAL_BAUD);

 // initialize the button pin as a input:
  pinMode(buttonPin, INPUT);
  // initialize the motor PWM pin as an output:
  pinMode(mosfetPin1, OUTPUT);
  pinMode(mosfetPin2, OUTPUT);
  // initialize serial communication:
  Serial.begin(SERIAL_BAUD);
  //Set Mosfet off to begin with for safety
  digitalWrite(mosfetPin1, LOW);
  digitalWrite(mosfetPin2, LOW);

 /*
 pinMode(speedPin, OUTPUT);
 setSpeedMotor(speedPin,100);
 */

 Serial.println("Ready! Type <command feederNumber feederDistance>");
}

/*******************************************************
*                    ENCODER COUNTER
********************************************************/

void loop()
{
 recvWithStartEndMarkers();
 if (newData == true) {
 strcpy(tempChars, receivedChars);
 // this temporary copy is necessary to protect the original data
 //   because strtok() used in parseData() replaces the commas with \0
 parseData();
 showParsedData();
 newData = false;
 }

 // read the pushbutton input pin:
 buttonState = digitalRead(buttonPin);

// compare the buttonState to its previous state
 if (buttonState != lastButtonState) {
 // if the state has changed, increment the counter
 if (buttonState == HIGH) {
 // if the current state is HIGH then the button went from off to on:
 encoderCounter++;
 Serial.println("Encoder pad detected");
 Serial.print("Detection number: ");
 Serial.println(encoderCounter);
 } else {
 // if the current state is LOW then the button went from on to off:
 Serial.println("Encoder pad disengaged");
 }
 // Delay a little bit to avoid bouncing
 delay(50);
 // save the current state as the last state, for next time through the loop
 lastButtonState = buttonState;
 }

  // If encoderCounter reaches over 4, shut off ALL mosfets
 if (encoderCounter >feederDistance) {
 digitalWrite(mosfetPin1, LOW);
 }

 if (encoderCounter >feederDistance) {
 digitalWrite(mosfetPin2, LOW);
 }
}

void recvWithStartEndMarkers() {
    static boolean recvInProgress = false;
    static byte ndx = 0;
    char startMarker = '<';
    char endMarker = '>';
    char rc;

    while (Serial.available() > 0 && newData == false) {
        rc = Serial.read();

        if (recvInProgress == true) {
            if (rc != endMarker) {
                receivedChars[ndx] = rc;
                ndx++;
                if (ndx >= numChars) {
                    ndx = numChars - 1;
                }
            }
            else {
                receivedChars[ndx] = '\0'; // terminate the string
                recvInProgress = false;
                ndx = 0;
                newData = true;
            }
        }

        else if (rc == startMarker) {
            recvInProgress = true;
        }
    }
}

 //============

void parseData() {      // split the data into its parts

  char * strtokIndx; // this is used by strtok() as an index


 strtokIndx = strtok(tempChars," ");      // get the first part - the string
  strcpy(command, strtokIndx); // copy it to command

  strtokIndx = strtok(NULL, " "); // this continues where the previous call left off
  feederNumber = atoi(strtokIndx);     // convert this part to an integer

  strtokIndx = strtok(NULL, " "); // this continues where the previous call left off
  feederDistance = atoi(strtokIndx);     // convert this part to an integer

  //strtokIndx = strtok(NULL, " ");
  //floatFromPC = atof(strtokIndx);     // convert this part to a float

 switch(tolower(command[0])) { //do switch on first char

 case 'a': {
 encoderCounter = 0; // Reset encoderCounter
 if(feederNumber == mosfetPin1){
 digitalWrite(mosfetPin1, HIGH);
 }
 else if(feederNumber == mosfetPin2){
 digitalWrite(mosfetPin2, HIGH);
 }
  Serial.println("Feeder %feederNumber turned on");
 }
 break;
 }

}

 //============

void showParsedData() {
  Serial.print("Command: ");
  Serial.println(command);
  Serial.print("Feeder Number: ");
  Serial.println(feederNumber);
  Serial.print("Feeder Distance: ");
  Serial.println(feederDistance);
}

and attached is my pins file (note that a lot of stuff defined there isn't used yet, but gives you a glimpse into what I am planning to do as I build on this program. Code was over character limit to paste here.

pins.h (1.88 KB)

If you are okay with single-letter commands you can use a state machine to read commands without buffering or using string.h functions at all.

Nick Gammon shows how in the State Machine example of his read serial without blocking tutorial.

Another way of processing incoming data, without blocking, is to set up a "state machine". Effectively this means looking at each byte in the input stream, and handling it depending on the current state.

As an example, say you had this coming into the serial port:

R4500S80G3

Where Rnnn is RPM, Snnnn is speed, and Gnnnn is the gear setting.

The state machine below switches state when it gets a letter "R", "S" or "G". Otherwise it processes incoming digits by multiplying the previous result by 10, and adding in the new one.

When switching states, if first handles the previous state. So for example, after getting R4500 when the "S" arrives, we call the ProcessRPM function, passing it 4500.

This has the advantage of handling long messages without even needing any buffer, thus saving RAM. You can also process message as soon as the state changes, rather than waiting for end-of-line.

// Example state machine reading serial input
// Author: Nick Gammon
// Date: 17 December 2011

// the possible states of the state-machine
typedef enum {  NONE, GOT_R, GOT_S, GOT_G } states;

// current state-machine state
states state = NONE;
// current partial number
unsigned int currentValue;

void setup ()
{
  Serial.begin (115200);
  state = NONE;
}  // end of setup

void processRPM (const unsigned int value)
{
  // do something with RPM 
  Serial.print ("RPM = ");
  Serial.println (value);
} // end of processRPM

void processSpeed (const unsigned int value)
{
  // do something with speed 
  Serial.print ("Speed = ");
  Serial.println (value);
} // end of processSpeed

void processGear (const unsigned int value)
{
  // do something with gear 
  Serial.print ("Gear = ");
  Serial.println (value);  
} // end of processGear

void handlePreviousState ()
{
  switch (state)
  {
  case GOT_R:
    processRPM (currentValue);
    break;
  case GOT_S:
    processSpeed (currentValue);
    break;
  case GOT_G:
    processGear (currentValue);
    break;
  }  // end of switch  

  currentValue = 0; 
}  // end of handlePreviousState

void processIncomingByte (const byte c)
{
  if (isdigit (c))
  {
    currentValue *= 10;
    currentValue += c - '0';
  }  // end of digit
  else 
  {

    // The end of the number signals a state change
    handlePreviousState ();

    // set the new state, if we recognize it
    switch (c)
    {
    case 'R':
      state = GOT_R;
      break;
    case 'S':
      state = GOT_S;
      break;
    case 'G':
      state = GOT_G;
      break;
    default:
      state = NONE;
      break;
    }  // end of switch on incoming byte
  } // end of not digit  
  
} // end of processIncomingByte

void loop ()
{
  while (Serial.available ())
    processIncomingByte (Serial.read ());

  // do other stuff in loop as required
  
}  // end of loop

I do have a serial keyword match function and data table layout for more than single letter commands that finds a match or no-match to text a letter at a time with no buffering (if it's a match, I have the letters already and if it's not it's an error) and every matched word is matched and number known when the delimiter (space, comma, newline, end-of-file) is read with time to do something with the info before the next command begins to arrive. --- You get non-stop on-the-fly parse and lex (match) in one go, it easily keeps up with 115200 baud. The match function is not so big, the data table and supporting arrays stay in flash and yeah it took a bit to write but has a neat table-crawl algorithm. It can handle 1000's of words on an Uno.

Still, the single-letter command interpreter that Nick wrote is more likely in your grasp to modify and learn much in doing.

GolamMostafa:
OK! You please, carry out the following step; let me know the results and then I will try to meet your thirst.

  1. Upload the sketch that I have posted in your UNO/NANO/MEGA.

  2. Bring in the Serial Monitor at 9600 Bd.

  3. Set the 'Ine ending tab' (Fig-1 below) at 'Newline' option. This option sends '\n' (Newline charcater/Linefeed(LF); ASCII code = 0x0A; Fig-2 below) character at the end of your string that you are going to send down to UNO.
    SerialMonitor.png
    Figure-1: Serial Monitor of Arduino IDE


Figure-2: ASCII Chart for the charcaters of English Language and other Control Characters

  1. Bring in the Cursor at the InputBox of the Serial Monitor.

  2. Enter this string: M600 4 10 and then click on the Send Button of the Serial Monitor.

  3. Check that the following message has appeared in the OutputBox of the Serial Monitor.
    M600
    4
    10

  4. Check in my sketch that the information of Step-6 have been stored/saved into three separate variable and these are:
    char directionArray[5];
    int motorNumber;
    int numberPress;

  5. Now, you have the information with you and you can process/use them.

So I did this and the first time it worked and returned M600 4 10. If I tried typing this exact same text again or any modification of the numbers, it would not print it back into the monitor. Any idea why? I can only send the command once, even if it's not "M600 4 10"