Adafruit_PM25AQI (Air Particulate Sensor) Library killing me

According to this output the checksum test should succeed.

1 Like

Itā€™s not getting that far, itā€™s failing at the check of the first byte not being 0x42.

When I run the serial read script independently I get as above, but when I move back to the .cpp library file I have it failing at:

  if (buffer[0] != 0x42)
  {
    Serial.println("buffer[0] not 0x42");
    return false;
  }

Considering that when im direct dumping to serial itā€™s showing 0x42 as the first character and I am (in the library) using:

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

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

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

  if (!data)
  {
    Serial.println("!data failed");
    return false;
  }      

   // Check that start byte is correct!
  if (buffer[0] != 0x42) // ITS FAILING THIS TEST???
  {
    Serial.println("buffer[0] not 0x42");
    return false;
  }
  ...

First Iā€™d rearrange the if(!data) check to happen before the Wire.requestFrom() call as there is no point in requesting the data when there is no place to put it in.

Next, you can use the same code as above to check the content of buffer after reading it and I advise again to include the timeout test I offered above.

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

  if (!data) 
    return -100;

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

  for (int i = 0; i < readBytes; i++) // print out received data
    Serial.printlnf("%2d: 0x%02x %3d (%c)", i, buffer[i], buffer[i], (buffer[i] >= 0x20) ? buffer[i] : 0xFF);

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

  if (buffer[0] != 0x42)
    return -101; // indicate bad starting byte
  ...
  return 0;     // indicate success
}
1 Like

So, heres a strange oneā€¦ I added Wire.begin() in to it here:

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

  Wire.begin(); // <-- JUST ADDED THIS TO CHECK IT WAS CARRYING FROM MAIN CODE...

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

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

And it now passes all tests and the bool Adafruit_PM25AQI::read(PM25_AQI_Data *data){} is returning true now?

Why would calling Wire.begin(); a second time be helping it?

Will add the timeout test now. Got ahead of myself.

That should not be required and doesn't make sense.

You should put Wire.begin() into Adafruit_PM25AQI::begin() and call that (respectively aqi.begin()) only once in setup().

BTW, you would (should) not use Adafruit_PM25AQI aqi = Adafruit_PM25AQI(); but just Adafruit_PM25AQI aqi;.

1 Like

Aaaaaand we have LIFTOFF!

AQI reading success
Concentration Units (standard) - PM 1.0: 0  PM 2.5: 0  PM 10: 0
Concentration Units (environmental) - PM 1.0: 0  PM 2.5: 0  PM 10: 0
Particles in air > 0.3um/0.1L: 51  0.5um/0.1L: 17  1.0um/0.1L: 0  2.5um/0.1L: 0  5.0um/0.1L: 0  50um/0.1L: 0
Success!
AQI reading success
Concentration Units (standard) - PM 1.0: 0  PM 2.5: 0  PM 10: 0
Concentration Units (environmental) - PM 1.0: 0  PM 2.5: 0  PM 10: 0
Particles in air > 0.3um/0.1L: 51  0.5um/0.1L: 17  1.0um/0.1L: 0  2.5um/0.1L: 0  5.0um/0.1L: 0  50um/0.1L: 0
Success!
AQI reading success
Concentration Units (standard) - PM 1.0: 0  PM 2.5: 0  PM 10: 0
Concentration Units (environmental) - PM 1.0: 0  PM 2.5: 0  PM 10: 0
Particles in air > 0.3um/0.1L: 51  0.5um/0.1L: 17  1.0um/0.1L: 0  2.5um/0.1L: 0  5.0um/0.1L: 0  50um/0.1L: 0
Success!
AQI reading success
Concentration Units (standard) - PM 1.0: 0  PM 2.5: 0  PM 10: 0
Concentration Units (environmental) - PM 1.0: 0  PM 2.5: 0  PM 10: 0
Particles in air > 0.3um/0.1L: 30  0.5um/0.1L: 10  1.0um/0.1L: 0  2.5um/0.1L: 0  5.0um/0.1L: 0  50um/0.1L: 0
Success!
AQI reading success
Concentration Units (standard) - PM 1.0: 0  PM 2.5: 0  PM 10: 0
Concentration Units (environmental) - PM 1.0: 0  PM 2.5: 0  PM 10: 0
Particles in air > 0.3um/0.1L: 39  0.5um/0.1L: 13  1.0um/0.1L: 0  2.5um/0.1L: 0  5.0um/0.1L: 0  50um/0.1L: 0

For future generations, heres where I ended up:

AQI_Test.ino

#include "Adafruit_PM25AQI.h"

Adafruit_PM25AQI aqi;

void setup()
{
  Serial.begin(115200);
  Wire.begin();
  delay(5000);

  Serial.println("Adafruit PMSA003I Air Quality Sensor");

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

  // There are 3 options for connectivity!
  if (!aqi.begin())
  { // connect to the sensor over I2C
    Serial.println("Could not find PM 2.5 sensor!");
    while (1)
      delay(10);
  }

  delay(1000);

  Serial.println("PM25 found!");
}

void loop()
{
  PM25_AQI_Data data;

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

  String payload;
  payload = "Concentration Units (standard) - PM 1.0: ";
  payload += String(data.pm10_standard);
  payload += "  PM 2.5: ";
  payload += String(data.pm25_standard);
  payload += "  PM 10: ";
  payload += String(data.pm100_standard);

  Serial.println(payload);

  payload = "Concentration Units (environmental) - PM 1.0: ";
  payload += String(data.pm10_env);
  payload += "  PM 2.5: ";
  payload += String(data.pm25_env);
  payload += "  PM 10: ";
  payload += String(data.pm100_env);

  Serial.println(payload);

  payload = "Particles in air > 0.3um/0.1L: ";
  payload += String(data.particles_03um);
  payload += "  0.5um/0.1L: ";
  payload += String(data.particles_05um);
  payload += "  1.0um/0.1L: ";
  payload += String(data.particles_10um);
  payload += "  2.5um/0.1L: ";
  payload += String(data.particles_25um);
  payload += "  5.0um/0.1L: ";
  payload += String(data.particles_50um);
  payload += "  50um/0.1L: ";
  payload += String(data.particles_100um);

  Serial.println(payload);

  delay(1000);
}

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();
  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;
  int bytesRead;

  if (!data)
  {
    Serial.println("!data failed");
    return false;
  }

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

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

  if (bytesRead != sizeof(buffer))
  {
    Serial.print("size mismatch of: ");
    Serial.println(bytesRead - sizeof(buffer));
    return false;
  }

  // Check that start byte is correct!
  if (buffer[0] != 0x42)
  {
    Serial.println("buffer[0] not 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, sizeof(PM25_AQI_Data));

  if (sum != data->checksum)
  {
    Serial.println("bad checksum");
    return false;
  }

  // success!
  Serial.println("Success!");
  return true;
}

I think that included your timeout (without re-structuring the bool into an int), but just noticed there might also still be a redundant Wire.begin() in there. As I have one in setup() and one in the .cpp

For better practice you should use aqi.begin() instead.
Although this isn't the case with this library but there may be other things that need to be done before you can properly use a library - hence you should always see whether used libraries feature a begin(), init() or similar method for that and call it in setup().

1 Like

Thanks again, I really appreciate all the help. I have learned more in this past 3.5 days than in the past few months!

3 Likes

@ScruffR Saves the day again! :grinning: :spark:

I learn the most from situations like this where you have to really dig in to get something working.

Here is another more advanced PM2.5 sensor that took quite awhile to get a working library for:

Here is the library for that SPS30 Dust Sensor if you are ever interested in that one also:

4 Likes

When you refer to using aqi.begin() in place of Wire.begin(), are you just referring to the instance where I call it in the .ino or also the Wire.begin() within the .cpp of the library that gets called when i do the aqi.begin()?

I am now trying to add a second sensor to the same i2c and it is not playing nice. If i re-flash with just a library from the second sensor, it works, fine... If i copy/paste it all over (and update libraries) it doesnt.... I presume it's because the Wire.begin() is hogging it to the dust sensor?

Will play with the Wire.begin() mentions and see how I go.

Edit: specifically, i'm referring to this in the .spp:

bool Adafruit_PM25AQI::begin()
{
  Wire.begin();  /// this gets called when i do api.begin(). I presume thats the right time to use Wire?
  return true;
}

aqi is an instance of the Adafruit_PM25AQI class and the begin() method of that class does call Wire.begin() internally (typically alongside other important startup tasks).
So yes, Wire.begin() should be called inside Adafruit_PM25AQI::begin() but your .ino code should not.

BTW, you can guard the Wire.begin() call with a if (!Wire.isEnabled()) check
e.g. like this

bool Adafruit_PM25AQI::begin()
{
  if (!Wire.isEnabled())   // only when not already done
    Wire.begin();  

  return Wire.isEnabled(); // and tell us the current state after that
}

When you use multiple sensors on the same I2C bus they all need to use a unique I2C addresses but this library only uses the default 0x12.

Providing an alternative address would be one of the tasks the class constructor or begin() method should support.

3 Likes

Thanks for this. They definitely have different addresses (first thing I checked), and oddly, it will work for one cycle and then stop working thereafter (the call for the second sensor happens before the Air Quality Sensor), so I think it is something that is happening further down in the loop that is overruling the original settings?

So, if Wire.begin() is called once and Wire.isEnabled() is still true, then theres no need to call it, even if it's for a new address? I will have a tinker and see how I go if thats the case.

Thanks again, appreciate it.

Exactly.

A possible reason could be that the bus is not released correctly after the last action that appeared to work.
However, when you talk about another sensor it would be good to also state what sensor that is :wink:

1 Like

How would I check/force it to release correctly? Seems counterintuitive to use something like Wire.end() if I am to only call Wire.begin() once? Within the library there doesnt seem to be a function that looks to be terminating it... Youre right though, it seems like it is not releasing the aqi.

It's this badboy Grove SHT31

I have had the library working before in isolation, but it's giving me hell now. Will work for one loop, then it fizzles...

I think you're onto it with it not releasing correctly though.

Nope, you wouldnā€™t call Wire.end() as this would also unassign the I2C pins.
Normally after a successful transmission the master indicates the stop condition with a final low to high transition on the CLK line.
But the CLK line can be kept low to ā€œblockā€ the bus from being taken over by another master (in a multi master setup) or to prevent slaves from talking.

e.g. Wire.endTransmission(false) prevents the bus from being freed after the transmission.
You may want to check the return value of Wire.endTransmission() too.

You could check the I2C lines with a logic analyzer or oscilloscope to see what happens.
Or you try locating the ā€œoffendingā€ actions by adding some Serial.print() statements and checking relevant variables and states.

Here you can read a bit more about I2C communication
https://i2c.info/i2c-bus-specification#:~:text=Start%20and%20Stop%20Condition,to%20high%20transition%20as%20STOP.

BTW, there are also other threads in this forum that deal with potential issue with the SHT31 sensor. Maybe worth a read too.

Cheers, will give this a tinker and see how I go.

Yes, I found them useful but also they trashed the SHT31, which I concur with and have also ordered a BME280 which I have running in another project flawlessly. Doesnt seem to be a problem with the SHT this time though.

Will play with the above and let you know how I go. Cheers.

edit: bonus question... can I use EEPROM.put() and .get() to store an array?

Yes, you can.

1 Like

Excellento!