Read data packet

Hi,

I have a project using SDS 11 particulate matter sensor. It uses UART protocol to communicate with my Arduino Uno (packet format: AA C0 PM25_Low PM25_High PM10_Low PM10_High 0 0 CRC AB). The datasheet specifies that

PM2.5 value: PM2.5 (μg /m3) = ((PM2.5 High byte *256) +PM2.5 low byte)/10
PM10 value: PM10 (μg /m3 ) = ((PM10 high byte*256) + PM10 low byte)/10

I’ve done the coding, but when I did a comparation test with another sensor (a more reliable DustTrak 8520), the output data is off about -70ug and wasn’t quite linear. This is my code:

float Pm25 = 0;
float Pm10 = 0;
unsigned char Pm25IsNew = 0;

void ProcessSerialData()
{
  uint8_t mData = 0;
  uint8_t i = 0;
  uint8_t mPkt[10] = {0};
  uint8_t mCheck = 0;
while (Serial.available() > 0) 
  {  
     mData = Serial.read();
     if(mData == 0xAA)
     {
        mPkt[0] =  mData;
        mData = Serial.read();
        if(mData == 0xc0)
        {
          mPkt[1] =  mData;
          mCheck = 0;
          for(i=0;i < 6;i++)
          {
             mPkt[i+2] = Serial.read();
             delay(2);
             mCheck += mPkt[i+2];
          }
          mPkt[8] = Serial.read();
          delay(1);
          mPkt[9] = Serial.read();
          if(mCheck == mPkt[8])
          {
            Serial.flush();
            //Serial.write(mPkt,10);


            Pm25 = (uint16_t)mPkt[2] | (uint16_t)mPkt[3]<<8);
            Pm10 = (uint16_t)mPkt[4] | (uint16_t)mPkt[5]<<8);
            if(Pm25 > 9999)
             Pm25 = 9999;
            if(Pm10 > 9999)
             Pm10 = 9999;            
            Pm25IsNew = 1;
             return;

Is there something I do wrong, or there are better ways to get the data? Thanks! :slight_smile:

Haven't taken a very detailed look, but so far it looks ok to me. To be sure, why don't you link the datasheet and post your WHOLE sketch.

Hi Power_Broker, thanks for the reply

Here’s link to the datasheet, and the sketch

#include <Wire.h>
#include <RTClib.h>
RTC_DS1307 rtc;

float Pm25 = 0;
float Pm10 = 0;
unsigned char Pm25IsNew = 0;

void ProcessSerialData()
{
  uint8_t mData = 0;
  uint8_t i = 0;
  uint8_t mPkt[10] = {0};
  uint8_t mCheck = 0;
while (Serial.available() > 0) 
  {  
      mData = Serial.read(); delay(2);
    if(mData == 0xAA)
     {
  delay(500);
        mPkt[0] =  mData;
        mData = Serial.read();
        if(mData == 0xc0)
        {
          mPkt[1] =  mData;
          mCheck = 0;
          for(i=0;i < 6;i++)
          {
             mPkt[i+2] = Serial.read();
             delay(2);
             mCheck += mPkt[i+2];
          }
          mPkt[8] = Serial.read();
          delay(1);
    mPkt[9] = Serial.read();
          if(mCheck == mPkt[8])
          {
            Serial.flush();
            //Serial.write(mPkt,10);

            Pm25 = (uint16_t)mPkt[2] | (uint16_t)(mPkt[3]<<8);
            Pm10 = (uint16_t)mPkt[4] | (uint16_t)(mPkt[5]<<8);
            if(Pm25 > 9999)
             Pm25 = 9999;
            if(Pm10 > 9999)
             Pm10 = 9999;            
            //get one good packet
            Pm25IsNew = 1;
             return;
          }
        }      
     }
   } 
}

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

void loop() {

     ProcessSerialData();
     
     DateTime now = rtc.now();
     
    Serial.print(now.year(), DEC);
    Serial.print('/');
    Serial.print(now.month(), DEC);
    Serial.print('/');
    Serial.print(now.day(), DEC); 
    Serial.print(',');   
    Serial.print(now.hour(), DEC);
    Serial.print(':');
    Serial.print(now.minute(), DEC);
    Serial.print(':');
    Serial.print(now.second(), DEC);
    Serial.print(',');
          Serial.print (Pm25/10);
          Serial.print(',');
          Serial.print (Pm10/10);   
          Serial.println();
delay (1000);
        }
  1. Take out the delay(1000). I know the refresh rate of the sensor is 1 Hz, but I don't think having a delay is a good idea. In fact, take out ALL of your delay() calls. I don't think any of them are needed. If you really only want it to do the serial prints once a second, then use millis() that allow ONLY the prints to execute once a second. This way, you can update the inputs during that second that would otherwise be downtime. It won't affect your code too much, but it is a good practice (especially if you want to expand the project).

  2. You are using only one serial port. This port is both accepting data from the sensor and is sending RTC data to the sensor (does the sensor need to know the date and time?). I assume what you meant to do is read in serial data from the sensor, incorporate RTC data, and then send everything to a computer for a user to read. Is this correct? If so, you need to have separate serial ports for this. If you are using an Uno, then you will need to have the sensor connected to a "softserial" port using the softserial.h library. Many people that use Arduinos with XBee transceivers have to do this, so there is support documentation out there for you.

In short: When using the serial monitor, you CANNOT use the default serial port to read in data from your sensor. You must use a different serial port for the sensor.

  1. Never use Serial.flush(). You should NEVER have to use this method.

  2. Saved the best for last:

Take a look at this from the datasheet:

PM2.5 value: PM2.5 (μg /m3) = ((PM2.5 High byte *256) + PM2.5
low byte)/10

PM10 value: PM10 (μg /m3) = ((PM10 high byte*256) + PM10 low
byte)/10

So, instead of just taking the high and low bytes of each and combining them into a 16 bit int, you actually only have to do some math with them!! This is most likely your main error in the code.

As well as the good advice about using 2 serial ports - use SoftwareSerial to create another port to read your sensor and leave the HardwareSerial port for communication with your PC - you need more reliable code for receiving data that does not rely on timing. Have a look at the examples in Serial Input Basics - simple reliable ways to receive data.

...R

Hi Power_Broker,

  1. Thanks for the suggestion, I've taken out all the delays and replaced it with millis.

  2. Yes, that is correct. Actually I need my project to work like this: Arduino RX is connected to sensor to receive the raw measurement, then Arduino TX will send it to PC via radio telemetry. The problem is I just realized that they operate at a different baud rate (9600 vs 57600) so now I'm working at SoftwareSerial....

  3. Okay... do I need to replace it with something, or I can just delete it?

  4. I'm afraid so. Would this be correct?

Pm25 = mPkt[2] + (mPkt[3]*256);
Pm10 = mPkt[4] + (mPkt[5]*256);

Thank you for the detailed answer!

Hi Robin2,

Oh, this is great. So instead of relying on timing, I can use program to Receive Binary Data, and then parse it to get the High and Low Byte? I'll work on this, too. Thank you!

leida:
Hi Robin2,

Oh, this is great. So instead of relying on timing, I can use program to Receive Binary Data, and then parse it to get the High and Low Byte? I'll work on this, too. Thank you!

The code you are using is parsing the data to get the high and low bytes already. Did you write this sketch yourself or did you get it from online?

leida:
Hi Power_Broker,

  1. Thanks for the suggestion, I've taken out all the delays and replaced it with millis.

No, no, no. Take out all of the delays, but only use millis() to time the serial prints ONLY.

leida:
2. Yes, that is correct. Actually I need my project to work like this: Arduino RX is connected to sensor to receive the raw measurement, then Arduino TX will send it to PC via radio telemetry. The problem is I just realized that they operate at a different baud rate (9600 vs 57600) so now I'm working at SoftwareSerial....

Let me know if you need help with this. By the way, what Arduino are you using? Uno or Mega?

leida:
3. Okay... do I need to replace it with something, or I can just delete it?

You can just delete it.

leida:
4. I'm afraid so. Would this be correct?

Pm25 = mPkt[2] + (mPkt[3]*256);

Pm10 = mPkt[4] + (mPkt[5]*256);

Almost. Here is what it should be:

Pm25 = (mPkt[2] + (mPkt[3]*256))/10;
Pm10 = (mPkt[4] + (mPkt[5]*256))/10;

Remember to divide by 10.

Also, you mentioned you edited your code. Could you post your new code with all the edits?

Take out all of the delays, but only use millis() to time the serial prints ONLY.

Yes that’s what I mean, sorry.

Let me know if you need help with this. By the way, what Arduino are you using? Uno or Mega?

Uno. I’ve figured this part out, the data is sent to PC every second. HardwareSerial for 9600 and SoftwareSerial for 57600 (or is it better the other way around?)

You can just delete it.

Done!

The code you are using is parsing the data to get the high and low bytes already. Did you write this sketch yourself or did you get it from online?

I got it from here. Looked fine… until I realized that the output data is kinda weird. Now mine is good to go, but Robin2’s suggestion seems like a neat alternative to do the parsing.

Remember to divide by 10.

Okay. I guess now I can go back and re-do the comparation test again. Hope it’ll work better! This is the revised code:

#include <Wire.h>
#include <RTClib.h>
RTC_DS1307 rtc;
#include <SoftwareSerial.h>

SoftwareSerial mySerial (8, 9); //RX,TX

float Pm25 = 0;//used for result pm2.5
float Pm10 = 0;//used for result pm10
unsigned char Pm25IsNew = 0;//show if pm25 is refreshed
unsigned long previousMillis = 0;
long interval = 1000;

  
void ProcessSerialData()
{
  uint8_t mData = 0;
  uint8_t i = 0;
  uint8_t mPkt[10] = {0};
  uint8_t mCheck = 0;
while (Serial.available() > 0) 
  { 
    mData = Serial.read();
    if(mData == 0xAA)
     {
        mPkt[0] =  mData;
        mData = Serial.read();
        if(mData == 0xc0)
        {
          mPkt[1] =  mData;
          mCheck = 0;
          for(i=0;i < 6;i++)
          {
             mPkt[i+2] = Serial.read();
             mCheck += mPkt[i+2];
           }
           mPkt[8] = Serial.read();
           mPkt[9] = Serial.read();
           if(mCheck == mPkt[8])
           {

            Pm25 = (mPkt[2] + (mPkt[3]*256))/10;
            Pm10 = (mPkt[4] + (mPkt[5]*256))/10;
            if(Pm25 > 9999)
             Pm25 = 9999;
            if(Pm10 > 9999)
             Pm10 = 9999;            
            //get one good packet
            Pm25IsNew = 1;
             return;
          }
        }      
     }
   } 
}

void setup()  
   {
    Serial.begin(9600);
    mySerial.begin(57600);
    Wire.begin();
    rtc.begin();  
    }

void loop() {

   ProcessSerialData();

DateTime now = rtc.now();

unsigned long currentMillis = millis();

  if(currentMillis - previousMillis > interval) 
  {
    previousMillis = currentMillis;  
    mySerial.print(now.year(), DEC);
    mySerial.print('/');
    mySerial.print(now.month(), DEC);
    mySerial.print('/');
    mySerial.print(now.day(), DEC); 
    mySerial.print(',');   
    mySerial.print(now.hour(), DEC);
    mySerial.print(':');
    mySerial.print(now.minute(), DEC);
    mySerial.print(':');
    mySerial.print(now.second(), DEC);
    mySerial.print(',');
          mySerial.print (Pm25/10);
          mySerial.print(',');
          mySerial.print (Pm10/10);   
          mySerial.println();
  }
}

Thanks for helping!

Looks good!

Let us know if it works or not.

@leida, your use of while (Serial.available() > 0) is not robust because you hope to read more than 1 byte even though you have no guarantee there is more than 1 avaialble. The Arduino works very much faster than serial data arrives. The point of the examples in Serial Input Basics is that they allow for that and only present the data for further processing when it has all arrived.

This aspect of the examples is more important than the way I have done the parsing.

…R

Hi Robin2,

I've read your tutorial on Program to receive binary data and Receiving and parsing several pieces of data. So in my case, I should use the AA and C0 as byte startMarker, and AB as byte endMarker, right?

This is what I've managed so far:

const byte numBytes = 10;
byte receivedBytes[numBytes];
byte numReceived = 0;
byte tempBytes[numBytes]; //temp array 4 use when parsing

//variables 2 hold the parsed data
//???
byte byte25LFromPC[numBytes]={0}; 
byte byte25HFromPC = 0;
byte byte10LFromPC = 0;
byte byte10hFromPC = 0;

boolean newData = false;


void setup()  
{
  lcd.begin();
  lcd.backlight();
  Serial.begin(9600);
  mySerial.begin(57600);
}


void recvBytesWithStartEndMarkers() {
    static boolean recvInProgress = false;
    static byte ndx = 0;
    byte startMarker = 0xAA 0xC0; //???
    byte endMarker = 0xAB;
    byte rb;
   
    while (Serial.available() > 0 && newData == false) {
        rb = Serial.read();

        if (recvInProgress == true) {
            if (rb != endMarker) {
                receivedBytes[ndx] = rb;
                ndx++;
                if (ndx >= numBytes) {
                    ndx = numBytes - 1;
                }
            }
            else {
                receivedBytes[ndx] = '\0'; // terminate the string
                recvInProgress = false;
                numReceived = ndx;  // save the number for use when printing
                ndx = 0;
                newData = true;
            }
        }

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


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

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

    strtokIndx = strtok(tempBytes," ");      // get the first part - the string
    strcpy(byte25LFromPC, strtokIndx); // copy it to byte25LFromPC
 
    strtokIndx = strtok(NULL, " "); // this continues where the previous call left off
    byte25HFromPC = (strtokIndx);    

    strtokIndx = strtok(NULL, " ");
    byte10LFromPC = (strtokIndx); 
        
     strtokIndx = strtok(NULL, " ");
     byte10HFromPC = (strtokIndx);     
}



void showParsedData(){
    mySerial.print(now.year(), DEC);
    mySerial.print('/');
    mySerial.print(now.month(), DEC);
    mySerial.print('/');
    mySerial.print(now.day(), DEC); 
    mySerial.print(',');   
    mySerial.print(now.hour(), DEC);
    mySerial.print(':');
    mySerial.print(now.minute(), DEC);
    mySerial.print(':');
    mySerial.print(now.second(), DEC);
    mySerial.print(',');
    mySerial.print (Pm25);
    mySerial.print(',');
    mySerial.print (Pm10);   
    mySerial.println();

}

void loop() {
     recvBytesWithStartEndMarkers();
     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();
          unsigned long currentMillis = millis();
          if(currentMillis - previousMillis > interval) 
             {
             previousMillis = currentMillis;  
             showParsedData();
             }
      newData = false;
      }
}

As you can see, I still get problems (especially about the Parse Data); would you mind to give me some clue? Any help is very appreciated, thank you!

Maybe take a look at this.

More specifically, this example of strtok():

/* strtok example */
#include <stdio.h>
#include <string.h>

int main ()
{
  char str[] ="- This, a sample string.";
  char * pch;
  printf ("Splitting string \"%s\" into tokens:\n",str);
  pch = strtok (str," ,.-");
  while (pch != NULL)
  {
    printf ("%s\n",pch);
    pch = strtok (NULL, " ,.-");
  }
  return 0;
}

and its output:

Splitting string "- This, a sample string." into tokens:
This
a
sample
string

Honestly, though, the way you were parsing the data before was perfect. Why did you alter it?

leida:
I've read your tutorial on Program to receive binary data

I am a little bit lost now.

What exactly is being sent to the Arduno - please post an example of a complete message.

Are you able to control the format of what is being sent?
OR
Are you required to use some format that is built into some other program that you are using?

Apologies if this is bringing you back over old ground.

...R