How to parse multiple values received in serial

Hi people,

I am doing a modding project: Controlling my desktop PC's RGB led strips and fans from my phone. I already tried to control leds and fans locally and done successfully (With help of a fan hub). I plan to use HC-06 bluetooth transreceiver for this.

There's only one obstable remaining: receiving multiple commands. I did a similar project before. Controlling an RGB led connected to Arduino and this was the code I used for it:

const int redpin = 3;
const int greenpin = 4;
const int bluepin = 5;
String readString;

void setup() {
  Serial.begin(9600);
  pinMode(redpin, OUTPUT); 
  pinMode(bluepin, OUTPUT); 
  pinMode(greenpin, OUTPUT); 
}

void loop() {
  while (Serial.available()) {
    delay(3);  
    char c = Serial.read();
    readString += c; 
  }

  if (readString.length() >0) {
    Serial.println(readString);
    
    if (readString == "blue")     
    {
      analogWrite(bluepin, 0);
      analogWrite(redpin, 255);
      analogWrite(greenpin, 255);
    }
        if (readString == "red")     
    {
      analogWrite(bluepin, 255);
      analogWrite(redpin, 0);
      analogWrite(greenpin, 255);
    }
        if (readString == "green")     
    {
      analogWrite(bluepin, 255);
      analogWrite(redpin, 255);
      analogWrite(greenpin, 0);
    }

But it only allows me to receive single information at a time. As I said before I'd like to control both fan speed and led color. I'm planning to make a slider on the app for fan speed (valued between 0-255) and the app should send some data like this:

blue
255

or

blue.255

But I do not know how to seperate these two values from each other. Could you please help me?
Thanks in advance.

You could get the whole string (e.g. "blue.255") and compare the substring up to the dot with the keywords red, blue, green as you do now. The rest of the string after the dot could be converted to an integer.

But why use three colour keyword? You could parse a format like 255.0.0.255 where the first three number are the RGB levels and the last one the fan.

Or you could plan ahead and adopt a format like keyword.value, e.g.:

led.blue
fan.255

so that you could add more controls in the future.[/code]

tuxduino:
You could get the whole string (e.g. "blue.255") and compare the substring up to the dot with the keywords red, blue, green as you do now. The rest of the string after the dot could be converted to an integer.

But why use three colour keyword? You could parse a format like 255.0.0.255 where the first three number are the RGB levels and the last one the fan.

Or you could plan ahead and adopt a format like keyword.value, e.g.:

led.blue

fan.255




so that you could add more controls in the future.[/code]

Yes, you are right. I should use the RGB number values instead.

But I still do not how to parse and save them.

Allright, this is my first try with the some Google search and reading. BT connection works. But I get all incorrect values.

String readString; //main captured String 
String fan;
String pump;
String red;
String green;
String blue;

int ind1; 
int ind2;
int ind3;
int ind4;
int ind5;


void setup() {
  // put your setup code here, to run once:
pinMode(11,OUTPUT);
pinMode(10,OUTPUT);
pinMode(9,OUTPUT);
pinMode(6,OUTPUT);
pinMode(5,OUTPUT);
Serial.begin(9600);
}

void loop() {
  // put your main code here, to run repeatedly:
  
 if (Serial.available())  {
    char c = Serial.read();  //gets one byte from serial buffer
    if (c == '*') {
      //do stuff

      ind1 = readString.indexOf(',');
      fan = readString.substring(0, ind1);
      ind2 = readString.indexOf(',', ind1+1 );
      pump = readString.substring(ind1+1, ind2+1); 
      ind3 = readString.indexOf(',', ind2+1 );
      red = readString.substring(ind2+1, ind3+1);
      ind4 = readString.indexOf(',', ind3+1 );
      green = readString.substring(ind3+1, ind4+1);
      ind5 = readString.indexOf(',', ind4+1 );
      blue = readString.substring(ind4+1);

int fanr = round(fan.toInt());
int pumpr = round(pump.toInt());
int redr = round(red.toInt());
int greenr = round(green.toInt());
int bluer = round(blue.toInt());
      
analogWrite(11,redr);
analogWrite(10,greenr);
analogWrite(11,bluer);
//analogWrite(6,fanr);
//analogWrite(5,pumpr);


Serial.println(redr);
Serial.println(greenr);
Serial.println(bluer);

      readString=""; //clears variable for new input
      fan="";
      pump="";
      red="";
      green="";
      blue="";
      
  }
   else {     
      readString += c; //makes the string readString
  }
 }
}

I'll never claim to be an expert at reading serial nor the arduino; however, I'll give you a jump start in offering a tid bit of digestible info I wish I knew when I was starting out. As you progress, you'll pick up better methods.

That said, we'll start out with a small refinement by creating a function called 'Parse'; the ',' is read and discarded. Next, we'll pass our character string to this function. Once we start the function, we'll use 'switch case' to determine which data entry was passed. Once processed, we increase 'member' for the next data member. Since a comma doesn't exist for the last data member, we'll use the '>' to exit to the Parse function.

Condensed example:

char c;
int member;
int fanr;
int pumpr;
String data;
char start = '<';
char End   = '>';
char delimiter = ',';


void loop() {

  member = 0;
  data = "";
    //incoming serial looks like this    <fandata,pumpdata,etc>
  while (Serial.available() > 0)  {
    c = Serial.read();  //gets one byte from serial buffer
    if (c == delimiter or c == End) {
      Parse(data);
    }else{
      if (c != start){data += c;}  //build string
    }
  }
}  

void Parse(String charString) {

  switch (member) {

      case 0:  // member = 0 so case = fan
        fan = String(charString);
        int fanr = round(fan.toInt());
        analogWrite(6, fanr);
        Serial.println(fanr);
        member++;
        c  = '\0'; // clear
        break;

      case 1: //member = 1 so case = pump
        pump = String(charString);
        int pumpr = round(pump.toInt());
        analogWrite(5, pumpr);
        Serial.println(pumpr);
        member++;
        break;


        // etc, etc

      }

  }

Thank you very much. Looks promising. But...

As far as I know there's no "switch case ()" on Arduino but "switch ()". So I fixed it.

I also get "fan" was not declared in this scope error when tried to run your code. Declaring it as int or String gave me errors so I do not know how to fix this.

Good catch; bad habit on my part.
As for the build errors, I only displayed a snippet to offer an alternative method. There are also errors caused by "int pumpr = round(pump.toInt());" in each case.

Do you know what causes this? I don't see any reason. Shouldn't it be converted from String to int without issue?

I commented out the offenders in the simplistic switch case example and your entire code built fine; you'll have to re-post your new code.

Sorry, I understand your question now; variables cannot be declared in the case statement.

Have a look at the examples in Serial Input Basics - simple reliable ways to receive data. There is also a parse example to illustrate how to extract numbers from the received text.

The technique in the 3rd example will be most reliable. Send your data like this <255, 123, 117, 23>

And it is not a good idea to use the String (capital S) class on an Arduino as it can cause memory corruption in the small memory on an Arduino. Just use cstrings - char arrays terminated with 0.

...R

Robin2:
Have a look at the examples in Serial Input Basics - simple reliable ways to receive data. There is also a parse example to illustrate how to extract numbers from the received text.

The technique in the 3rd example will be most reliable. Send your data like this <255, 123, 117, 23>

And it is not a good idea to use the String (capital S) class on an Arduino as it can cause memory corruption in the small memory on an Arduino. Just use cstrings - char arrays terminated with 0.

...R

Hi,

I managed to do a successful and correct reading with your method but now I can't save the values in the receivedChars array to my variables.

This is the code I use. I assumed the values are stored in the array like this and that's how I should access them:

red = receivedChars[0];
green = receivedChars[1]; 
blue = receivedChars[2];
fan = receivedChars[3];
pump = receivedChars[4];

analogWrite(11,red);
analogWrite(10,green);
analogWrite(9,blue);
analogWrite(6,fan);
analogWrite(5,pump);

But all I end up with are random numbers. I guess these are random and caused because I could not access to the array properly.

You MUST post the complete program.

...R

Robin2:
You MUST post the complete program.

...R

Of course.

const byte numChars = 32;
char receivedChars[numChars];
int fan;
int pump;
int red;
int green;
int blue;

boolean newData = false;

void setup() {

TCCR0B = TCCR0B & B11111000 | B00000001;    // Modifying Arduino's PWM signals to get rid of PWM noise caused with case fans.

pinMode(11,OUTPUT);
pinMode(10,OUTPUT);
pinMode(9,OUTPUT);
pinMode(6,OUTPUT);
pinMode(5,OUTPUT);
    Serial.begin(9600);
    Serial.println("<Arduino is ready>");
}

void loop() {
    recvWithStartEndMarkers();
    showNewData();

red = receivedChars[0];
green = receivedChars[1]; 
blue = receivedChars[2];
fan = receivedChars[3];
pump = receivedChars[4];

Serial.println(" ");
Serial.println(red);
Serial.println(green);
Serial.println(blue);
Serial.println(fan);
Serial.println(pump);
Serial.println(" ");

analogWrite(11,red);
analogWrite(10,green);
analogWrite(9,blue);
analogWrite(6,fan);
analogWrite(5,pump);

}  

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 showNewData() {
    if (newData == true) {
        Serial.print("This just in ... ");
        Serial.println(receivedChars);




        
        newData = false;
    }
}

Now, what do you see when the function showNewData() is called - please post a copy of some of the output.

...R

This just in ... 61, 84, 99, 117, 130

The values are correct, these are slider positions that I send via my smartphone.

That is the sort of thing I expected to see. Alas, you are completely misinterpreting the concept when you use

red = receivedChars[0];
green = receivedChars[1];
// etc

What you have posted 61, 84, 99, 117, 130 represents the characters (not the byte values) in the positions 0 to 19 in receivedChars - if I have counted correctly. Note that 61 comprises two characters and 117 comprises 3 characters.

For example receivedChars[0] will have the value 54 which is the Ascii code for '6' and receivedChars[2] will have the code for a comma (I think it is 44).

You need to use the concepts in my parse example to split the groups of characters using the commas as separator and then use the atoi() function to convert each group (for example "117") into an int.

...R

What is the format of the data you are sending?

Looks like you're only looking for "<" and ">" but no comma.

Also, you are using single bytes as integers:

    red = receivedChars[0];
    green = receivedChars[1];
    blue = receivedChars[2];
    fan = receivedChars[3];
    pump = receivedChars[4];

which probably would yield e.g. red == 65 if you sent ""

I've actually tried it:
by sending

<ABCDE>

I got this in the console:

This just in ... ABCDE
 
65
66
67
68
69

I've modified the loop() function so that it only prints something when new data is available:

void loop() {
    recvWithStartEndMarkers();

    if (newData) {
        Serial.print("Received: ");
        Serial.println(receivedChars);

        red = receivedChars[0];
        green = receivedChars[1];
        blue = receivedChars[2];
        fan = receivedChars[3];
        pump = receivedChars[4];

        newData = false;

        Serial.println(" ");
        Serial.println(red);
        Serial.println(green);
        Serial.println(blue);
        Serial.println(fan);
        Serial.println(pump);
        Serial.println(" ");

        analogWrite(11,red);
        analogWrite(10,green);
        analogWrite(9,blue);
        analogWrite(6,fan);
        analogWrite(5,pump);
    }

Since you are parsing all integers, your life is easier using sscanf() like this:

char* data = "Data:255,0,255,125,125;";
struct Vars{
  int red;
  int green;
  int blue;
  int pump;
  int fan;
};

Vars myVars;

void setup() 
{
  Serial.begin(9600);
  delay(100);
  Serial.println(data);
  sscanf(data, "Data:%d,%d,%d,%d,%d;", &myVars.red, &myVars.green, &myVars.blue, &myVars.pump, &myVars.fan);
  Serial.println(myVars.red);
  Serial.println(myVars.green);
  Serial.println(myVars.blue);
  Serial.println(myVars.pump);
  Serial.println(myVars.fan);
}

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

}