Adafruit_PM25AQI (Air Particulate Sensor) Library killing me

Sorry to make another post so soon, but I am having a hell of a time with this sensor: https://learn.adafruit.com/pmsa003i which in the linked tutorial has a library here: Adafruit_PM25AQI.h Library.

I have tried and tried pulling the library across to my build, but it keeps failing in endless different parts.

The library also calls for a second library (Adafruit_I2CDevice.h) which appears to reside in Adafruit_BusIO. I think is the root cause of the issue, as it is referring to Adafruit i2C interface rather than the Particle Argon i2c?..

I have tried replacing all of the <Arduino.h> with “Particle.h” and moving the .h and .cpp files into separate src folders. I’m dying here…

Any help would be amazing after I spent a good month getting these sensors to speak to InfluxDB, I now really need this data to send. All my other sensors seem to be relatively ok, but this one is killing me.

I would personally hack the Adafruit_PM25AQI.h like this, the Wire library is included with Particle so you don’t need to include the Adafruit_I2CDevice which is to handle a range of different devices. The .cpp is very simple; begin() just needs Wire.begin(); and the reading of data needs to start with Wire.requestFrom(address, quantity); then Wire.read() into the receive buffer until no more available.

#include "Particle.h"

class Adafruit_PM25AQI {
public:
  Adafruit_PM25AQI();
  bool begin();
  bool read(PM25_AQI_Data *data);

private:
  uint8_t _readbuffer[32];
};
2 Likes

Thanks so much for this, I have edited the .h and it looks pretty clean. but not entirely clear what you mean with the .cpp file.

I have made a bit of a dogs breakfast of it tbh.

Because I removed the reference to ‘Adafruit_I2CDevice *i2c_dev = NULL;’ in the .h it now cant define what the i2c_dev is. But, am I reading it right that as I know I am using i2c in this instance, I can just get rid of this definition and not have the ‘if’ statements?

I have gotten in a little out of my depth here, but it looks like I need to strip the references to Adafruit_I2CDevice. But then I end up with a mostly empty file that doesn’t appear to do anything?

Thanks again, really appreciate it!

Ok, so firstly, thanks again, I actually didnt understand how the .h and .cpp linked before this, so thank’s for forcing me to do some more digging. Here is where I am at the moment:

Adafruit_PM25AQI.h

#include "Particle.h"

// the i2c address
#define PMSA003I_I2CADDR_DEFAULT 0x12 ///< PMSA003I has only one I2C address

// /**! Structure holding Plantower's standard packet **/
typedef struct PMSAQIdata {
  uint16_t framelen;       ///< How long this data chunk is
  uint16_t pm10_standard,  ///< Standard PM1.0
      pm25_standard,       ///< Standard PM2.5
      pm100_standard;      ///< Standard PM10.0
  uint16_t pm10_env,       ///< Environmental PM1.0
      pm25_env,            ///< Environmental PM2.5
      pm100_env;           ///< Environmental PM10.0
  uint16_t particles_03um, ///< 0.3um Particle Count
      particles_05um,      ///< 0.5um Particle Count
      particles_10um,      ///< 1.0um Particle Count
      particles_25um,      ///< 2.5um Particle Count
      particles_50um,      ///< 5.0um Particle Count
      particles_100um;     ///< 10.0um Particle Count
  uint16_t unused;         ///< Unused
  uint16_t checksum;       ///< Packet checksum
} PM25_AQI_Data;

class Adafruit_PM25AQI
{
public:
  Adafruit_PM25AQI();
  bool begin();   // <-- Do i need anything in here to return data from the .cpp?
  bool read(PM25_AQI_Data *data);

private:
  uint8_t _readbuffer[32];
};

Adafruit_PM25AQI.cpp

#include "Adafruit_PM25AQI.h"

/*!
 *  @brief  Instantiates a new PM25AQI class
 */
Adafruit_PM25AQI::Adafruit_PM25AQI() {}

bool Adafruit_PM25AQI::begin(){
  Wire.begin();
  return true;
}

    /*!
 *  @brief  Setups the hardware and detects a valid UART PM2.5
 *  @param  data
 *          Pointer to PM25_AQI_Data that will be filled by read()ing
 *  @return True on successful read, false if timed out or bad data
 */
    bool Adafruit_PM25AQI::read(PM25_AQI_Data *data)
{
  uint8_t buffer[32];
  uint16_t sum = 0;
  Wire.requestFrom(PMSA003I_I2CADDR_DEFAULT, 32);
  Wire.read();

  if (!data) {
    return false;
  }

  
  // Check that start byte is correct!
  if (buffer[0] != 0x42) {
    return false;
  }

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

  // 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) {
    return false;
  }

  // success!
  return true;
}

The problem I am getting is that in my .ino file trying to run this, I am getting a failure when I call:


  if (!aqi.read(&data))
  {
    Particle.publish("Serial","Could not read from AQI",PRIVATE);
    delay(500); // try again in a bit!
    return;
  }

The address I am using is the default address 0x12 in the Wire.read(address, 32)?

In your Adafruit_PM25AQI::read() you are never reading data into your buffer, hence whenever you check that buffer (e.g. here if (buffer[0] != 0x42)) you are working on uninitialized data.

Instead of that single Wire.read(); which only reads one byte but immediately discards it you can try Wire.readBytes((char*)buffer, sizeof(buffer)); which actually tries to read 32 bytes and feeding it into the buffer.
For good practice you should also catch the return value of that call and compare it against your expected number of bytes to detect a timeout condition (e.g. when the I2C client won’t send all data in time).

1 Like

Thanks for that.

I have amended the Adafruit_PM25AQI::read() to now be:

bool Adafruit_PM25AQI::read(PM25_AQI_Data *data)
{
  uint8_t buffer[32];
  uint16_t sum = 0;
  Wire.requestFrom(PMSA003I_I2CADDR_DEFAULT, 32);
  //Wire.read();
  Wire.readBytes((char *)buffer, sizeof(buffer));

  if (!data)
  {
    return false;
  }

  // Check that start byte is correct!
  if (buffer[0] != 0x42)
  {
    return false;
  }

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

  // 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)
  {
    return false;
  }

  // success!
  return true;
}

But it seems to be hanging as soon as my .ino calls the read(). It reports everything up until it hits the loop.

Apologies if the use of Particle.publish as a defacto Serial isnt kosher… It seems easier to have this dumping to a terminal than playing with a serial. :expressionless:

#include "Adafruit_PM25AQI.h"

Adafruit_PM25AQI aqi = Adafruit_PM25AQI();

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

  Particle.publish("Serial", "Adafruit PMSA003I Air Quality Sensor", PRIVATE);

  // Wait one second for sensor to boot up!
  delay(1000);

  // If using serial, initialize it and set baudrate before starting!
  aqi.begin();
  // There are 3 options for connectivity!
  if (!aqi.begin())
  { // connect to the sensor over I2C
    Particle.publish("Serial", "Could not find PM 2.5 sensor!", PRIVATE);
    while (1)
      delay(10);
  }

  Particle.publish("Serial", "PM25 found!", PRIVATE); // <-- THIS IS THE LAST MESSAGE TO COME THROUGH
}

void loop()
{
  PM25_AQI_Data data;

  if (!aqi.read(&data))
  {
    Particle.publish("Serial","Could not read from AQI",PRIVATE);
    delay(500); // try again in a bit!
    return;
  }
  Particle.publish("serial","AQI reading success",PRIVATE);

  aqi.read(&data);
}

Not sure if this is a stupid question, but where does the ‘data’ in the below get populated?

if (!data)
  {
    return false;
  }

You haven't heeded this tho'

You'r loop() is also bound to violate the publishing rate limit.

And why are you calling aqi.read() twice in loop()?

Can you explain that?

I usually don't have issues with Serial.println() or a SerialLogHandler
You should definetly try adding Serial.println() statements into your read() function to see where and why the call fails.
Additionally you could opt for int rather than bool as return value and hand back an error code to tell you where it failed.

This only checks whether the pointer data has got a non-NULL address.
Since you are passing &data into the function that parameter will definetly not be NULL.

I'd also advise against the use of number literals like here

it would be better to use this

memcpy((void *)data, (void *)buffer_u16, sizeof(PM25_AQI_Data));

This ensures (providing you don't pass an incompatible pointer) that memcpy() will not copy more data than the buffer can hold.

1 Like

Thanks for this, I’ll have a tinker. Really appreciate it.

I thought that was the point of this:

  if (sum != data->checksum)
  {
    return false;
  }

Nope, this will only check whether you can re-calculate the checksum and make it match the respective value that was calculated and transmitted by the sensor.

But this will not tell you whether you got all bytes read as expected but will only fail without giving a reason - or in a highly unlikely case may even check out although the data may actually be wrong :wink:

I can break my statement down:
"catch the return value"

  int bytesRead;
  bytesRead = Wire.readBytes((char *)buffer, sizeof(buffer));

"compare it against your expected number"

  if (bytesRead != sizeof(buffer)) 
    return bytesRead - sizeof(buffer); // return number of bytes that couldn't be read (as negative)

Together with

and some values like this (although I'd use an enum for the error codes)

  if (!data)
    return -100; // indicate no valid data parameter provided
  ...
  if (buffer[0] != 0x42)
    return -101; // indicate bad starting byte
  ...
  if (sum != data->checksum)
    return -102; // indicate bad checksum
  ...
  return 0; // indicate success

You can put a switch statement in your calling function to deal with the respective errors specifically.

2 Likes

Great, this is really helpful, I'll get to integrating it!

Interestingly, I have added a bunch of Serial posts in each step of the code and it grinds to a dead halt as commented below (note, I also get the "Serial", "PM25 found!" from the void setup() before this). The issue appears to be in the Wire.requestFrom():

bool Adafruit_PM25AQI::read(PM25_AQI_Data *data)
{
  uint8_t buffer[32];
  uint16_t sum = 0;

  Particle.publish("Serial","001",PRIVATE); // LAST MESSAGE I SEE IS THIS
  delay(1000);

  Wire.requestFrom(PMSA003I_I2CADDR_DEFAULT, sizeof(buffer)); // PRESUME THIS IS KILLING IT?

  Particle.publish("Serial", "002", PRIVATE); // NEVER SEE THIS MESSAGE...
  delay(1000);

  //Wire.read();
  Wire.readBytes((char *)buffer, sizeof(buffer));

  Particle.publish("Serial", "003", PRIVATE);
  delay(1000);
...

I get the "Serial", "001" message, but the 002 never arrives and my console is telling me the device becomes unreachable.

The device wont even respond to an OTA at this point and I have to manually put it into DFU mode and Local Flash any changes.

If i flash something basic on there it runs fine and can update at will without a hard DFU set.

Is it possible something isn't completing in the

Wire.requestFrom(PMSA003I_I2CADDR_DEFAULT, sizeof(buffer));

That might be causing it to hang, or memory leak, or not complete? Or, is the read address for an i2c device different to it's address (0x12 in this case)?

Because of this, I can't even get to the steps of running the check for a complete read/timeout.

Edit: If i unplug the sensor, the whole chain of "Serial", "001, 002, 003, ..." run through and I get the "Serial", "Could not read from AQI" coming through... It seems to be at the point it is reading from the sensor it never completes?

Can you run an I2C scanner sketch to see whether you can communicate with the device at all and are using the correct address?

You can also use Safe Mode to allow for OTA updates.

Sure, when your device isn't actually returning 32 bytes in response to the request that could cause the issue.

That might also be the case (hence the suggestion to find the actual I2C client address via the scanner sketch).
I haven't read the datasheet for that sensor but some sensors use some internal addressing scheme to request particular registers but these would not be provided via the Wire.requestFrom() call.
Wire.requestFrom() wants to be told the I2C address of the client. The internal "register address" needs to be pre-set via a separate Wire.write() to the I2C client address before requesting the data from that register.

BTW, how have you wired the sensor?

1 Like

Ok, well, at least we have the right address!

I2C Scanner
Scanning...
I2C device found at address 0x12  !
done

3.3v to VIN
GND to GND
SDA to SDA
SCL to SCL

Don't need pull-ups as they are integrated on the board.

Have used this same shield for other 12c devices and has worked fine, so I don't think it's hardware. It takes VIN ranging from 3-5V as it has an onboard regulator.

After a quick look in the Adafruit library I see that sensor would also support UART mode - just in case you can’t solve the I2C issue.

However, given the situation, I’d track back a bit and remove some complexity from the project by ignoring the Adafruit_PM24AQI class and try to test raw communication without interpreting the data but merely using Wire.available() and Wire.read() in a loop to see what’s going on at all.

The I2C scanner obviously shows that there is no issue with using Serial.println(), so you should use that instead of Particle.publish().

You can use the I2C scanner sketch as a basis for your tests.

1 Like

No problemo, will do, leave it with me.

Indeed, I just use Particle.publish() so I can get minimal messages remotely, and it's become a habit rather than firing up putty and connecting to the COM port.

Will do via Serial now and report back.

Thanks again,

1 Like

Righto, so using this:

void setup()
{
  Wire.begin();
  Serial.begin(115200);
  delay(5000); // Wait for me to open the serial monitor.
}

void loop()
{
  Serial.println("\nNext Read:");
  Wire.requestFrom(0x12, 32);

  while (Wire.available())
  {
    char c = Wire.read();
    Serial.print(c);   
  }
  
  Serial.println("");
  Serial.println("Read Complete. Waiting for next loop()");
  delay(1000);
}

I got an export of:

Next Read:
BM
▒fU     ▒▒
Read Complete. Waiting for next loop()

Next Read:
▒fY▒▒
Read Complete. Waiting for next loop()

Next Read:
▒fY▒▒
Read Complete. Waiting for next loop()

Next Read:
w_V▒▒
Read Complete. Waiting for next loop()

Next Read:
BMw_V▒▒
Read Complete. Waiting for next loop()

Next Read:
BM8FL▒{
Read Complete. Waiting for next loop()

Next Read:
5E▒.
Read Complete. Waiting for next loop()

Next Read:
BM
5E▒(
Read Complete. Waiting for next loop()

Next Read:
DJL▒▒
Read Complete. Waiting for next loop()

Next Read:
DJL▒▒
Read Complete. Waiting for next loop()

Next Read:
▒uO▒0
Read Complete. Waiting for next loop()

Next Read:
▒yO▒@
Read Complete. Waiting for next loop()

I've checked the baud rate, and it matches the serial monitor. I also tried it at 9600 to be sure and it's a similar illegibility).

After letting it run for a while it has started to become more consistent with:

Next Read:
BM

{▒u
Read Complete. Waiting for next loop()

Next Read:
BM

{▒u
Read Complete. Waiting for next loop()

Next Read:
BM

9▒C
Read Complete. Waiting for next loop()

Next Read:
BM

9▒C
Read Complete. Waiting for next loop()

Next Read:
BM

  ▒"9▒▒
Read Complete. Waiting for next loop()

Would it be a fair assumption that the "BM" at the beginning means it is putting out a consistent starting byte, which correlates with the:

// Check that start byte is correct!
  if (buffer[0] != 0x42)
  {
    return false;
  }

Edit: Looked it up. 0x42 in ASCII is Uppercase B! Nice!
Edit 2: Been watching it for a while, consistently starting with BM now.

Should I be going into the .cpp and editing the:

Wire.readBytes((char *)buffer, sizeof(buffer));

With something that reads similar to the below?:

  int i = 0;
  while (Wire.available())
  {
    buffer[i] = Wire.read();
    ++i;
  }

Edit 3: nope... still hanging in the same place...

Edit 4: oh... my... God... I forgot to include Wire.begin()...

Now am through to it spitting out "Could not read from AQI" in a 1s loop, so now will work on your data checker and see what the issue is.

1 Like

These “funny” characters you see stem from the fact that you are receiving mostly binary data which will render mainly non-printable characters.
Try Serial.printlnf("0x%02x %3d (%c)", c, c, c); instead. This will give you the HEX and DEC representation of the byte (and the ASCII in parentheses).
With that you can calculate the checksum manually and compare with the expected value.

2 Likes

Bingo!

Next Read:
0x42  66 (B)0x4d  77 (M)0x00   0 (0x1c  28 ()0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0xed 237 (▒)0x00   0 (0x44  68 (D)0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x97 151 (▒)0x00   0 (0x02   2 ()0x73 115 (s)
Read Complete. Waiting for next loop()

Next Read:
0x42  66 (B)0x4d  77 (M)0x00   0 (0x1c  28 ()0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x01   1 ()0x17  23 ()0x00   0 (0x52  82 (R)0x00   0 (0x03   3 ()0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x97 151 (▒)0x00   0 (0x01   1 ()0xaf 175 (▒)
Read Complete. Waiting for next loop()

Next Read:
0x42  66 (B)0x4d  77 (M)0x00   0 (0x1c  28 ()0x00   0 (0x01   1 ()0x00   0 (0x02   2 ()0x00   0 (0x02   2 ()0x00   0 (0x01   1 ()0x00   0 (0x02   2 ()0x00   )0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x97 151 (▒)0x00   0 (0x01   1 ()0xda 218 (▒)
Read Complete. Waiting for next loop()

Next Read:
0x42  66 (B)0x4d  77 (M)0x00   0 (0x1c  28 ()0x00   0 (0x01   1 ()0x00   0 (0x02   2 ()0x00   0 (0x02   2 ()0x00   0 (0x01   1 ()0x00   0 (0x02   2 ()0x00   0 (0x02   2 ()0x01   1 ()0x95 149 (▒)0x00   0 (0x83 131 (▒)0x00   0 (0x10  16 ()0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x00   0 (0x97 151 (▒)0x00   0 (0x02   2 ()0x75 117 (u)
Read Complete. Waiting for next loop()

Next Read:
0x42  66 (B)0x4d  77 (M)0x00   0 (0x1c  28 ()0x00   0 (0x01   1 ()0x00   0 (0x03   3 ()0x00   0 (0x04   4 ()0x00   0 (0x01   1 ()0x00   0 (0x03   3 ()0x00   0 (0x04   4 ()0x01   1 ()0xc8 200 (▒)0x00   0 (0x98 152 (▒)0x00   0 (0x1a  26 ()0x00   0 (0x03   3 ()0x00   0 (0x03   3 ()0x00   0 (0x00   0 (0x97 151 (▒)0x00   0 (0x02   2 ()0xd3 211 (▒)
Read Complete. Waiting for next loop()

That output does not quite look as intended :wink:
It should rather look something like this

Next Read:
0x42  66 (B)
0x4d  77 (M)
0x00   0 ()
0x1c  28 ()
...
Read Complete. Waiting for next loop()

Although for that we’d need to add some special treatment for bytes between 0x00 and 0x1F like this

Serial.printlnf("0x%02x %3d (%c)", c, c, (c >= 0x20) ? c : 0xFF);
1 Like

Ahh whoops, I forgot to use printlnf and used printf instead.

Looks like this now:

Next Read:
0x42  66 (B)
0x4d  77 (M)
0x00   0 (▒)
0x1c  28 (▒)
0x00   0 (▒)
0x00   0 (▒)
0x00   0 (▒)
0x00   0 (▒)
0x00   0 (▒)
0x00   0 (▒)
0x00   0 (▒)
0x00   0 (▒)
0x00   0 (▒)
0x00   0 (▒)
0x00   0 (▒)
0x00   0 (▒)
0x00   0 (▒)
0x9f 159 (▒)
0x00   0 (▒)
0x32  50 (2)
0x00   0 (▒)
0x06   6 (▒)
0x00   0 (▒)
0x03   3 (▒)
0x00   0 (▒)
0x00   0 (▒)
0x00   0 (▒)
0x00   0 (▒)
0x97 151 (▒)
0x00   0 (▒)
0x02   2 (▒)
0x1c  28 (▒)

Read Complete. Waiting for next loop()
1 Like