I'm proud to present my Arduino library supporting PMS5003 Air Quality Sensor.
pms5003 library is distributed under Boost Software License 1.0 (BSL-1.0).
Release 2.1 brings a lot of changes and improvements:
Previous release: 1.0 is still available.
There is one interesting fork supporting ESP8266: https://github.com/riverscn/pmsx003
My library supports PMS5003 (checked)
For most Plantover sensors probably it is an easy task to add support.
list of compatible sensors is available as a separate document.
OK
or NO_DATA
or some kinds of errors but your process never waits for the data.pms5003 API description is available as a separate document.
write()
will return PmsStatus
instead of bool
begin()
and end()
for viewsisWorking()
should return tribool
write(CMD_WAKEUP)
should not delay if already awokenwrite()
multiple commands sequentiallypms5003 library is developed using:
pms5003 library was successfully checked using:
Install pms5003 library.
Important: PMS5003 sensor uses 3.3V logic. Use converters if required or make sure your Arduino board uses 3.3V logic too.
Use the code: examples\p01basic\p01basic.ino
#include <pms.h>
PmsAltSerial pmsSerial;
pmsx::Pms pms(&pmsSerial);
////////////////////////////////////////
void setup(void) {
Serial.begin(115200);
while (!Serial) {}
Serial.println(pmsx::pmsxApiVersion);
if (!pms.begin()) {
Serial.println("PMS sensor: communication failed");
return;
}
pms.setPinReset(6);
pms.setPinSleepMode(7);
if (!pms.write(pmsx::PmsCmd::CMD_RESET)) {
pms.write(pmsx::PmsCmd::CMD_SLEEP);
pms.write(pmsx::PmsCmd::CMD_WAKEUP);
}
pms.write(pmsx::PmsCmd::CMD_MODE_PASSIVE);
pms.write(pmsx::PmsCmd::CMD_READ_DATA);
pms.waitForData(pmsx::Pms::TIMEOUT_PASSIVE, pmsx::PmsData::FRAME_SIZE);
pmsx::PmsData data;
auto status = pms.read(data);
if (status != pmsx::PmsStatus::OK) {
Serial.print("PMS sensor: ");
Serial.println(status.getErrorMsg());
}
pms.write(pmsx::PmsCmd::CMD_MODE_ACTIVE);
if (!pms.isWorking()) {
Serial.println("PMS sensor failed");
}
Serial.print("Time of setup(): ");
Serial.println(millis());
}
////////////////////////////////////////
void loop(void) {
static auto lastRead = millis();
pmsx::PmsData data;
auto status = pms.read(data);
switch (status) {
case pmsx::PmsStatus::OK: {
Serial.println("_________________");
const auto newRead = millis();
Serial.print("Wait time ");
Serial.println(newRead - lastRead);
lastRead = newRead;
auto view = data.particles;
for (auto i = 0; i < view.getSize(); ++i) {
Serial.print(view.getValue(i));
Serial.print("\t");
Serial.print(view.getName(i));
Serial.print(" [");
Serial.print(view.getMetric(i));
Serial.print("] ");
// Serial.print(" Level: ");
// Serial.print(view.getLevel(i));
Serial.print(" | diameter: ");
Serial.print(view.getDiameter(i));
Serial.println();
}
break;
}
case pmsx::PmsStatus::NO_DATA:
break;
default:
Serial.print("!!! Pms error: ");
Serial.println(status.getErrorMsg());
}
}
And the result is (something like this):
Port open
pms5003 2.1
Time of setup(): 2589
_________________
Wait time 906
27 PM1.0, CF=1 [micro g/m3] | diameter: 1.00
34 PM2.5, CF=1 [micro g/m3] | diameter: 2.50
35 PM10. CF=1 [micro g/m3] | diameter: 10.00
23 PM1.0 [micro g/m3] | diameter: 1.00
31 PM2.5 [micro g/m3] | diameter: 2.50
35 PM10. [micro g/m3] | diameter: 10.00
8760 Particles > 0.3 micron [/0.1L] | diameter: 0.30
1780 Particles > 0.5 micron [/0.1L] | diameter: 0.50
70 Particles > 1.0 micron [/0.1L] | diameter: 1.00
18 Particles > 2.5 micron [/0.1L] | diameter: 2.50
1 Particles > 5.0 micron [/0.1L] | diameter: 5.00
1 Particles > 10. micron [/0.1L] | diameter: 10.00
37120 Reserved_0 [???] | diameter: 0.00
To use the library:
pms.h
in your code:#include <pms.h>
Create instance of serial driver
PmsAltSerial pmsSerial;
Create instance of pmsx::Pms object:
pmsx
Pms
pms
pmsSerial
pmsx::Pms pms(&pmsSerial);
Initialize serial library. If Arduino can't communicate with PMS5003 - there is no sense to perform the next steps.
pms5003 takes care on protocol details (speed, data length, parity and so on).
if (!pms.begin()) {
Serial.println("PMS sensor: communication failed");
return;
}
The next step is to define Arduino pins connected to pms5003:
This step is optional.
If pins are not connected - just remove appropriate setPinReset
/setPinSleepMode
lines.
pms.setPinReset(6);
pms.setPinSleepMode(7);
The next task is to put sensor in a well known state. There are two aspects of PMS5003 state:
Both can be examined using isModeActive()
/isModeSleep()
. Please note, that result value is a tristate logic tribool
: Yes / No / I don't know.
Please refer to myArduinoLibrary. It is Arduino port of boost.tribool library description
Please note, that it is possible, that Arduino was restarted for any reason, but PMS5003 was set in a strange state and it was not restarted. It is the reason, that initial states of PMS5003 is "I don't know"
Well known state (awoken and active) can be achieved after sensor hardware reset or sleep+wakeup sequence
if (!pms.write(pmsx::PmsCmd::CMD_RESET)) {
pms.write(pmsx::PmsCmd::CMD_SLEEP);
pms.write(pmsx::PmsCmd::CMD_WAKEUP);
}
The next task is to make sure, that Arduino can communicate with PMS5003. To accomplish the task we are:
pms.write(pmsx::PmsCmd::CMD_MODE_PASSIVE);
pms.write(pmsx::PmsCmd::CMD_READ_DATA);
pms.waitForData(pmsx::Pms::TIMEOUT_PASSIVE, pmsx::PmsData::FRAME_SIZE);
pmsx::PmsData data;
auto status = pms.read(data);
if (status != pmsx::PmsStatus::OK) {
Serial.print("PMS sensor: ");
Serial.println(status.getErrorMsg());
}
if (!pms.isWorking()) {
Serial.println("PMS sensor failed");
}
Finally we put back PMS5003 in active mode - it sends data periodically and automatically.
pms.write(pmsx::PmsCmd::CMD_MODE_ACTIVE);
First of all: pms5003 does not block on data read
Try to read the data :
pmsx::PmsData data;
auto status = pms.read(data);
switch (status) {
If there is something interesting: display it:
case pmsx::PmsStatus::OK: {
....
If there are no data: do something else:
case pmsx::PmsStatus::NO_DATA:
break;
In case of error: show the error message:
default:
Serial.print("!!! Pms error: ");
Serial.println(status.getErrorMsg());
}
Lets go back to the situation where there is something interesting:
case pmsx::PmsStatus::OK: {
Data received from PMS5003 (see Appendix I) may be worth attention:
pmsx::pmsData_t
numbers, that is 13 unsigned int
numbers)To get access to them:
auto view = data.raw;
or
auto view = data.concentrationCf;
or
auto view = data.concentration;
or
auto view = data.particles;
Each "view" provides similar interface:
getSize()
to get counter of data in a view:
for (auto i = 0; i < view.getSize(); ++i)
.getValue()
to get particular data from index
Serial.print(view.getValue(i));
.getName()
to get description of particular data; for example "Particles > 1.0 micron"
Serial.print(view.getName(i));
.getMetric()
to get unit of measure for particular data; for example "/0.1L"
Serial.print(view.getMetric(i));
.getDiameter()
to get particle diameter corresponding to particular data; for example 1.0F
Serial.print(view.getDiameter(i));
particles
"view" provides .getLevel()
- ISO classification of air cleanliness
Serial.print(view.getLevel(i));
Such a "views" (data partitions) are implemented with no execution time nor memory overhead.
auto view = data.particles;
for (auto i = 0; i < view.getSize(); ++i) {
Serial.print(view.getValue(i));
Serial.print("\t");
Serial.print(view.getName(i));
Serial.print(" [");
Serial.print(view.getMetric(i));
Serial.print("] ");
// Serial.print(" Level: ");
// Serial.print(view.getLevel(i));
Serial.print(" | diameter: ");
Serial.print(view.getDiameter(i));
Serial.println();
}
break;
}
}
If you prefer C style: constants and arrays instead of method calls - please note examples\p02cStyle\p02cStyle.ino
auto view = data.particles;
for (auto i = 0; i < view.SIZE; ++i) {
Serial.print(view[i]);
Serial.print("\t");
Serial.print(view.names[i]);
Serial.print(" [");
Serial.print(view.metrics[i]);
Serial.print("] ");
// Serial.print(" Level: ");
// Serial.print(view.getLevel(i));
Serial.print(" | diameter: ");
Serial.print(view.diameters[i]);
Serial.println();
}
SIZE
to get counter of data in a view:
for (auto i = 0; i < view.SIZE; ++i)
[]
to get particular data
Serial.print(view[i]);
.names[]
array to get description of particular data; for example "Particles > 1.0 micron"
Serial.print(view.names[i]);
.metrics[]
array to get unit of measure for particular data; for example "/0.1L"
Serial.print(view.metrics[i]);
.diameters[]
to get particle diameter corresponding to particular data; for example 1.0F
Serial.print(view.diameters[i]);
particles
"view" provides .getLevel()
- ISO classification of air cleanliness. It is not implemented as array - it is a function (a method)
Serial.print(view.getLevel(i));
Which one is better? It doesn't matter - code size, memory usage and resulting code is exactly the same using both approaches.
Common pattern in C++ is "initialization in constructor". Unfortunately Arduino breaks that rule.
There is a code from: hardware\arduino\avr\cores\arduino\main.cpp modified for simplicity
// global variables constructors are executed before main()
int main(void) {
init();
initVariant();
setup(); // our setup() procedure
for (;;) {
loop(); // our loop() procedure
}
return 0;
}
Lets imagine:
pms
of type Pms
.pms.begin()
within pms
constructorsetup()
is executed.Our serial connection started in step 3) is destroyed during Arduino initialization in step 4.
There are at least two possible solutions:
By the way: if you are not sure if everything was properly initialized - execute begin()
manually and check the result.
Pms
, do nothing in constructorsetup()
: call pms.begin()
pms.
methodsPmsAltSerial pmsSerial;
pmsx::Pms pms(&pmsSerial);
void setup(void) {
if (!pms.begin()) {
#define PMS_DYNAMIC
*Pms
(reference to Pms
)setup()
create new object of type Pms
, assign created object to the reference from previous step
Pms()
constructor is executed automaticallyPms()
constructor executes begin()
begin()
of the serial port driverpms->
methodsThis approach adds some code size - compiler adds dynamic memory management.
PmsAltSerial pmsSerial;
pmsx::Pms* pms = nullptr;
void setup(void) {
pms = new pmsx::Pms(&pmsSerial);
if (!pms->initialized()) {
C/Arduino way using begin()
is closer to Arduino programming style.
In my opinion: C++ way is closer to modern programming style.
particles
view provides support for ISO 14644-1 classification of air cleanliness levels.
Please refer to p03cppStyle.ino
The code (loop()
function only):
void loop(void) {
static auto lastRead = millis();
pmsx::PmsData data;
auto status = pms->read(data);
switch (status) {
case pmsx::PmsStatus::OK: {
Serial.println("_________________");
const auto newRead = millis();
Serial.print("Wait time ");
Serial.println(newRead - lastRead);
lastRead = newRead;
auto view = data.particles;
for (auto i = decltype(view.SIZE){0}; i < view.getSize(); ++i) {
Serial.print(view.getValue(i));
Serial.print("\t");
Serial.print(view.getName(i));
Serial.print(" [");
Serial.print(view.getMetric(i));
Serial.print("] ");
Serial.print(" Level: ");
Serial.print(view.getLevel(i));
Serial.print(" | diameter: ");
Serial.print(view.getDiameter(i));
Serial.println();
}
break;
}
case pmsx::PmsStatus::NO_DATA:
break;
default:
Serial.print("!!! Pms error: ");
Serial.println(status.getErrorMsg());
}}
and the example of the result:
_________________
Wait time 906
1875 Particles > 0.3 micron [/0.1L] Level: 8.27 | diameter: 0.30
505 Particles > 0.5 micron [/0.1L] Level: 8.16 | diameter: 0.50
62 Particles > 1.0 micron [/0.1L] Level: 7.87 | diameter: 1.00
7 Particles > 2.5 micron [/0.1L] Level: 7.75 | diameter: 2.50
1 Particles > 5.0 micron [/0.1L] Level: 7.53 | diameter: 5.00
0 Particles > 10. micron [/0.1L] Level: 0.00 | diameter: 10.00
pms5003 API description is available as a separate document.
Serial interface is not managed by Pms
. You can suspend data transfer, enter sleep mode, even replace serial port. Just remember to execute pms.begin()
to reinitialize the connection.
pms5003
library is designed to avoid namespace pollution. All classes are located in the namespace pmsx
.
Examples use the fully qualified names like pmsx::Pms pms(&pmsSerial);
To reduce typing it is OK to add using namespace pmsx;
at the beginning and not to type pmsx:: anymore as in examples\p04usingPmsx\p04usingPmsx.ino
It does not change resulting code size.