Using Adafruit PM2.5 sensor with particle board

I’ve been trying to get the Adafruit PM 2.5 sensor to work with my Particle Photon. I used the example code off of Adafruit’s website. The problem is it was written for Arduino IDE. Because I couldn’t use SoftwareSerial, I added the ParticleSoftSerial and SparkIntervalTimer library to the code. However, I kept getting the same error on the line where the buffer for the sensor data is created as an unsigned integer. Apparently Arduino lets you create a buffer as both a char and an unsigned integer but particle does not. I tried making the buffer a char and the code compiled, but when I looked at the data it was blank. Any ideas for how I can make this work on Particle Web IDE?

Here’s the website I got the code from.
And here’s the original code.

// On Leonardo/Micro or others with hardware serial, use those!
// uncomment this line:
// #define pmsSerial Serial1

// For UNO and others without hardware serial, we must use software serial...
// pin #2 is IN from sensor (TX pin on sensor), leave pin #3 disconnected
// comment these two lines if using hardware serial
#include <SoftwareSerial.h>
SoftwareSerial pmsSerial(2, 3);

void setup() {
  // our debugging output
  Serial.begin(115200);

  // sensor baud rate is 9600
  pmsSerial.begin(9600);
}

struct pms5003data {
  uint16_t framelen;
  uint16_t pm10_standard, pm25_standard, pm100_standard;
  uint16_t pm10_env, pm25_env, pm100_env;
  uint16_t particles_03um, particles_05um, particles_10um, particles_25um, particles_50um, particles_100um;
  uint16_t unused;
  uint16_t checksum;
};

struct pms5003data data;
    
void loop() {
  if (readPMSdata(&pmsSerial)) {
    // reading data was successful!
    Serial.println();
    Serial.println("---------------------------------------");
    Serial.println("Concentration Units (standard)");
    Serial.print("PM 1.0: "); Serial.print(data.pm10_standard);
    Serial.print("\t\tPM 2.5: "); Serial.print(data.pm25_standard);
    Serial.print("\t\tPM 10: "); Serial.println(data.pm100_standard);
    Serial.println("---------------------------------------");
    Serial.println("Concentration Units (environmental)");
    Serial.print("PM 1.0: "); Serial.print(data.pm10_env);
    Serial.print("\t\tPM 2.5: "); Serial.print(data.pm25_env);
    Serial.print("\t\tPM 10: "); Serial.println(data.pm100_env);
    Serial.println("---------------------------------------");
    Serial.print("Particles > 0.3um / 0.1L air:"); Serial.println(data.particles_03um);
    Serial.print("Particles > 0.5um / 0.1L air:"); Serial.println(data.particles_05um);
    Serial.print("Particles > 1.0um / 0.1L air:"); Serial.println(data.particles_10um);
    Serial.print("Particles > 2.5um / 0.1L air:"); Serial.println(data.particles_25um);
    Serial.print("Particles > 5.0um / 0.1L air:"); Serial.println(data.particles_50um);
    Serial.print("Particles > 10.0 um / 0.1L air:"); Serial.println(data.particles_100um);
    Serial.println("---------------------------------------");
  }
}

boolean readPMSdata(Stream *s) {
  if (! s->available()) {
    return false;
  }
  
  // Read a byte at a time until we get to the special '0x42' start-byte
  if (s->peek() != 0x42) {
    s->read();
    return false;
  }

  // Now read all 32 bytes
  if (s->available() < 32) {
    return false;
  }
    
  uint8_t buffer[32];    
  uint16_t sum = 0;
  s->readBytes(buffer, 32);

  // get checksum ready
  for (uint8_t i=0; i<30; i++) {
    sum += buffer[i];
  }

  /* debugging
  for (uint8_t i=2; i<32; i++) {
    Serial.print("0x"); Serial.print(buffer[i], HEX); Serial.print(", ");
  }
  Serial.println();
  */
  
  // The data comes in endian'd, this solves it so it works on all platforms
  uint16_t buffer_u16[15];
  for (uint8_t i=0; i<15; i++) {
    buffer_u16[i] = buffer[2 + i*2 + 1];
    buffer_u16[i] += (buffer[2 + i*2] << 8);
  }

  // put it into a nice struct :)
  memcpy((void *)&data, (void *)buffer_u16, 30);

  if (sum != data.checksum) {
    Serial.println("Checksum failure");
    return false;
  }
  // success!
  return true;
}

You can use typecasts

No need to add SparkIntervalTimer explicitly since ParticleSoftSerial has that dependency set internally.

BTW, when you get error messages it's always good to quote them verbatim (with contextual messages like note messages) to allow for explicit advice how to resolve the issue.

Hi thanks for responding. When it first compiled I got this error.

no matching function for call to 'USARTSerial::readBytes(uint16_t [32], int)'

So I casted the buffer as a char. The code compiled, but when I look at the data there is none.

 uint16_t buffer[32];    
  uint16_t sum = 0;
  pmsSerial.readBytes((char*) buffer, 32);

You want uint8_t or similar. uint16_t would be two byte per array field.

However, you wouldn’t need to use ParticleSoftSerial since you can just use Serial1. The Photon sports a hardware USART on the RX/TX pins.

1 Like

So like this?

uint8_t buffer[32];    
  uint16_t sum = 0;
  Serial1.readBytes((char*)buffer, 32);

That would be a way but whether or not this works as expected depends on the sending device too.
And of course you’d need to setup the interface via Serial1.begin(9600) and wire the correct pins.

Should the sum be a single byte as well?

Nope, since that wouldn’t hold a sum of 30 values potentially also exhausting the range.

And should the variables in the struct be single byte instead of uint16_t?

struct pms5003data {
 
  uint16_t framelen;
  uint16_t pm10_standard, pm25_standard, pm100_standard;
  uint16_t pm10_env, pm25_env, pm100_env;
  uint16_t particles_03um, particles_05um, particles_10um, particles_25um, particles_50um, particles_100um;
  uint16_t unused;
  uint16_t checksum;
};

That depends on the value range for each field, how you populate the fields and if you expect memory issues by donating extra space.

The data is still blank is there another way to do this?

Yes, you need to track back till you get some raw data, see where that contradicts your expectation, find out why and how to work around that and then move forward again from there.

Well, I know the buffer does receive data. What is the process for using the start byte to distribute that data into separate variables? Maybe a way without memcpy?

One possible take on that could look like that (not tested, just off the top of my head :wink: )

bool readPMSdata(Stream *s) {
  int      i, sum = 0;
  uint8_t  buf[sizeof(pms5003data)];
  uint8_t* d = &data:
 
  while ((i = s->read()) != 0x42)       // wait for start byte (flushing any intermediate byte in the process)
    if (i < 0) return false;            // on end-of RX buffer bail out

  if (s->readBytes(buf, sizeof(buf)) < sizeof(buf)) 
    return false;                      // when read timed out -> bail out

  for (i = 0; i < sizeof(buf); i += 2) {
    // swap bytes to adjust endianness (and add up in the process)
    sum += (d[i + 1] = buf[i + 0]) 
        +  (d[i + 0] = buf[i + 1]); 
  }  
                                      // for convenience we also added up the last two bytes 
  sum -= (buf[--i] + buf[--i]);       // so we have to subtract checksum bytes again here

  if (sum != data.checksum) {
    Serial.println("Checksum failure");
    return false;
  }
  return true;
}

Thanks for putting together that code for me! There were some compiling errors so I type casted a few functions. However, the data is still blank.

bool readPMSdata(Stream *s) {
  int      i, sum = 0;
  uint8_t  buf[sizeof(pms5003data)];
  uint8_t* d = (unsigned char*) &data;
 
  while ((i = s->read()) != 0x42)       // wait for start byte (flushing any intermediate byte in the process)
    if (i < 0) return false;            // on end-of RX buffer bail out

  if (s->readBytes((char*)buf, sizeof(buf)) < sizeof(buf)) {
    return false;                      // when read timed out -> bail out
}
  for (i = 0; i < sizeof(buf); i += 2) {
    // swap bytes to adjust endianness (and add up in the process)
    sum += (d[i + 1] = buf[i + 0]) 
        +  (d[i + 0] = buf[i + 1]); 
  }  
                                      // for convenience we also added up the last two bytes 
  sum -= (buf[--i] + buf[--i]);       // so we have to subtract checksum bytes again here

  if (sum != data.checksum) {
    Serial.println("Checksum failure");
    return false;
  }
  return true;
}

I was also wondering why you had an if statement that waits for sizeof buf to be less than size of buf

 if (s->readBytes((char*)buf, sizeof(buf)) < sizeof(buf)) {

Actually that statement doesn't wait to be less than the expected number. The s->readBytes() call itself waits for up to 32 bytes or a timeout condition. The if() statement merely checks what the result of that waiting was.

readBytes() will read up to size(buf) bytes but if doesn't get all the bytes within the timeout period (see StreamClass::setTimeout()) the function tells you how many bytes you actually got. If that number doesn't fit the expected value, you know your read attempt timed out and there will be little use in carrying on with the rest of the function, which you would do without checking the return value.

Then you may want to print out the raw values you actually get and also some intermediate status/debug messages to know where the actual results start to differ from your expectations.

@TyTodd Was there any updates on this? I can generate the raw values but can’t seem to have them compare with the readBytes()