diff --git a/.gitignore b/.gitignore index 49f18f2..0350c43 100644 --- a/.gitignore +++ b/.gitignore @@ -301,3 +301,8 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk +*.xml +*.filters +*.vcxproj +examples/Basic/__vm/.Basic.vsarduino.h +examples/Advanced/__vm/.Advanced.vsarduino.h diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..85a3f68 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Mariusz Kacki + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..12520c8 --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +# PMS Library +Arduino library for Plantower PMS sensors. +Supports PMS x003 sensors (1003, 3003, 5003, 6003, 7003). +## Installation +Just use Arduino Library Manager and search "PMS Library" in Sensors category. +## Main assumptions +- easy as possible, +- minimal memory consumption, +- non-blocking functions, +- supporting a wide range of PMS sensors from Plantower, +- supporting additional modes e.g.: sleeping, passive, active (depends on the sensor model). + +As a data source you can use **any object** that implements the **Stream class**, such as Wire, Serial, EthernetClient, e.t.c. +## Basic example +Read in active mode. +> Default mode is active after power up. In this mode sensor would send serial data to the host automatically. The active mode is divided into two sub-modes: stable mode and fast mode. If the concentration change is small the sensor would run at stable mode with the real interval of 2.3s. And if the change is big the sensor would be changed to fast mode automatically with the interval of 200~800ms, the higher of the concentration, the shorter of the interval. +```cpp +#include "PMS.h" + +PMS pms(Serial); +PMS::DATA data; + +void setup() +{ + Serial.begin(9600); // GPIO1, GPIO3 (TX/RX pin on ESP-12E Development Board) + Serial1.begin(9600); // GPIO2 (D4 pin on ESP-12E Development Board) +} + +void loop() +{ + // Non-blocking function + if (pms.read(data)) + { + Serial1.println("Data:"); + + Serial1.print("PM 1.0 (ug/m3): "); + Serial1.println(data.PM_AE_UG_1_0); + + Serial1.print("PM 2.5 (ug/m3): "); + Serial1.println(data.PM_AE_UG_2_5); + + Serial1.print("PM 10.0 (ug/m3): "); + Serial1.println(data.PM_AE_UG_10_0); + + Serial1.println(); + } +} +``` +## Output +``` +Data: +PM 1.0 (ug/m3): 13 +PM 2.5 (ug/m3): 18 +PM 10.0 (ug/m3): 23 + +Data: +PM 1.0 (ug/m3): 13 +PM 2.5 (ug/m3): 18 +PM 10.0 (ug/m3): 23 + +Data: +PM 1.0 (ug/m3): 12 +PM 2.5 (ug/m3): 19 +PM 10.0 (ug/m3): 24 +``` +## Advanced example +Read in passive mode but not the best way (see additional remarks). +```cpp +#include "PMS.h" + +PMS pms(Serial); +PMS::DATA data; + +void setup() +{ + Serial.begin(9600); // GPIO1, GPIO3 (TX/RX pin on ESP-12E Development Board) + Serial1.begin(9600); // GPIO2 (D4 pin on ESP-12E Development Board) + pms.passiveMode(); // Switch to passive mode +} + +void loop() +{ + Serial1.println("Wake up and wait 30 seconds for stable readings..."); + pms.wakeUp(); + delay(30000); + + Serial1.println("Send request read..."); + pms.requestRead(); + + Serial1.println("Wait max. 10 seconds for read (blocking function)..."); + if (pms.read(data, 10000)) + { + Serial1.println("Data:"); + + Serial1.print("PM 1.0 (ug/m3): "); + Serial1.println(data.PM_AE_UG_1_0); + + Serial1.print("PM 2.5 (ug/m3): "); + Serial1.println(data.PM_AE_UG_2_5); + + Serial1.print("PM 10.0 (ug/m3): "); + Serial1.println(data.PM_AE_UG_10_0); + + Serial1.println(); + } + else + { + Serial1.println("No data."); + } + + Serial1.println("Going to sleep for 60 seconds."); + pms.sleep(); + delay(60000); +} +``` +## Output +``` +Wake up, wait 30 seconds for stable readings... +Send request read... +Wait max. 10 seconds for read... +Data: +PM 1.0 (ug/m3): 18 +PM 2.5 (ug/m3): 24 +PM 10.0 (ug/m3): 24 +Going to sleep for 60 seconds. +Wake up, wait 30 seconds for stable readings... +Send request read... +Wait max. 10 seconds for read... +Data: +PM 1.0 (ug/m3): 20 +PM 2.5 (ug/m3): 30 +PM 10.0 (ug/m3): 40 +Going to sleep for 60 seconds. +``` +## Additional remarks +Tested with PMS 7003 and ESP-12E Development Board. +All Plantower PMS sensors uses the same protocol (let me know if you have any problems). + +Please consider, that delay() function in examples is a blocking function. +Try to avoid such a solution if your project requires it. + +For more accurate measurements, you can read several samples (in passive or active mode) and calculate the average. +> Stable data should be got at least 30 seconds after the sensor wakeup from the sleep mode because of the fan's performance. +> I got nice resuylt in active mode. + +Personally, I get more repeatable measurements in active mode (Basic example). \ No newline at end of file diff --git a/examples/Advanced/Advanced.ino b/examples/Advanced/Advanced.ino new file mode 100644 index 0000000..c32ede9 --- /dev/null +++ b/examples/Advanced/Advanced.ino @@ -0,0 +1,44 @@ +#include "PMS.h" + +PMS pms(Serial); +PMS::DATA data; + +void setup() +{ + Serial.begin(9600); // GPIO1, GPIO3 (TX/RX pin on ESP-12E Development Board) + Serial1.begin(9600); // GPIO2 (D4 pin on ESP-12E Development Board) + pms.passiveMode(); // Switch to passive mode +} + +void loop() +{ + Serial1.println("Wake up, wait 30 seconds for stable readings..."); + pms.wakeUp(); + delay(30000); + + Serial1.println("Send request read..."); + pms.requestRead(); + + Serial1.println("Wait max. 10 seconds for read..."); + if (pms.read(data, 10000)) + { + Serial1.println("Data:"); + + Serial1.print("PM 1.0 (ug/m3): "); + Serial1.println(data.PM_AE_UG_1_0); + + Serial1.print("PM 2.5 (ug/m3): "); + Serial1.println(data.PM_AE_UG_2_5); + + Serial1.print("PM 10.0 (ug/m3): "); + Serial1.println(data.PM_AE_UG_10_0); + } + else + { + Serial1.println("No data."); + } + + Serial1.println("Going to sleep for 60 seconds."); + pms.sleep(); + delay(60000); +} diff --git a/examples/Basic/Basic.ino b/examples/Basic/Basic.ino new file mode 100644 index 0000000..8585c86 --- /dev/null +++ b/examples/Basic/Basic.ino @@ -0,0 +1,29 @@ +#include "PMS.h" + +PMS pms(Serial); +PMS::DATA data; + +void setup() +{ + Serial.begin(9600); // GPIO1, GPIO3 (TX/RX pin on ESP-12E Development Board) + Serial1.begin(9600); // GPIO2 (D4 pin on ESP-12E Development Board) +} + +void loop() +{ + if (pms.read(data)) + { + Serial1.println("Data:"); + + Serial1.print("PM 1.0 (ug/m3): "); + Serial1.println(data.PM_AE_UG_1_0); + + Serial1.print("PM 2.5 (ug/m3): "); + Serial1.println(data.PM_AE_UG_2_5); + + Serial1.print("PM 10.0 (ug/m3): "); + Serial1.println(data.PM_AE_UG_10_0); + + Serial1.println(); + } +} diff --git a/extras/ESP-12E.png b/extras/ESP-12E.png new file mode 100644 index 0000000..be60c4e Binary files /dev/null and b/extras/ESP-12E.png differ diff --git a/extras/Schematic.png b/extras/Schematic.png new file mode 100644 index 0000000..88b8972 Binary files /dev/null and b/extras/Schematic.png differ diff --git a/keywords.txt b/keywords.txt new file mode 100644 index 0000000..80f1811 --- /dev/null +++ b/keywords.txt @@ -0,0 +1,34 @@ +####################################### +# Syntax Coloring Map For PMS Library +####################################### + +####################################### +# Datatypes (KEYWORD1) +####################################### + +PMS KEYWORD1 + +####################################### +# Methods and Functions (KEYWORD2) +####################################### + +sleep KEYWORD2 +wakeUp KEYWORD2 +activeMode KEYWORD2 +passiveMode KEYWORD2 +requestRead KEYWORD2 +read KEYWORD2 +loop KEYWORD2 + +####################################### +# Instances (KEYWORD2) +####################################### + +####################################### +# Constants (LITERAL1) +####################################### + +STATUS_WAITING LITERAL1 +STATUS_OK LITERAL1 +MODE_ACTIVE LITERAL1 +MODE_PASSIVE LITERAL1 diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..ea405d5 --- /dev/null +++ b/library.properties @@ -0,0 +1,9 @@ +name=PMS Library +version=1.0.0 +author=Mariusz Kacki +maintainer=Mariusz Kacki +sentence=Arduino library for Plantower PMS sensors. +paragraph=Supports PMS x003 sensors (1003, 3003, 5003, 6003, 7003). +category=Sensors +url=https://github.com/fu-hsi/pms +architectures=* diff --git a/src/PMS.cpp b/src/PMS.cpp new file mode 100644 index 0000000..f056e92 --- /dev/null +++ b/src/PMS.cpp @@ -0,0 +1,157 @@ +#include "Arduino.h" +#include "PMS.h" + +PMS::PMS(Stream& stream) +{ + this->_stream = &stream; +} + +// Standby mode. For low power consumption and prolong the life of the sensor. +void PMS::sleep() +{ + uint8_t command[] = { 0x42, 0x4D, 0xE4, 0x00, 0x00, 0x01, 0x73 }; + _stream->write(command, sizeof(command)); +} + +// Operating mode. Stable data should be got at least 30 seconds after the sensor wakeup from the sleep mode because of the fan's performance. +void PMS::wakeUp() +{ + uint8_t command[] = { 0x42, 0x4D, 0xE4, 0x00, 0x01, 0x01, 0x74 }; + _stream->write(command, sizeof(command)); +} + +// Active mode. Default mode after power up. In this mode sensor would send serial data to the host automatically. +void PMS::activeMode() +{ + uint8_t command[] = { 0x42, 0x4D, 0xE1, 0x00, 0x01, 0x01, 0x71 }; + _stream->write(command, sizeof(command)); + _mode = MODE_ACTIVE; +} + +// Passive mode. In this mode, sensor would send serial data to the host only for request. +void PMS::passiveMode() +{ + uint8_t command[] = { 0x42, 0x4D, 0xE1, 0x00, 0x00, 0x01, 0x70 }; + _stream->write(command, sizeof(command)); + _mode = MODE_PASSIVE; +} + +// Request read in Active Mode. +void PMS::requestRead() +{ + if (_mode == MODE_PASSIVE) + { + uint8_t command[] = { 0x42, 0x4D, 0xE2, 0x00, 0x00, 0x01, 0x71 }; + _stream->write(command, sizeof(command)); + } +} + +// Non-blocking function for parse response. +// If you want to wait for the full response (blocking), specify timeout parameter (1000ms is sufficient). This makes it easier to work in passive mode. +bool PMS::read(DATA& data, uint16_t timeout) +{ + _data = &data; + if (timeout > 0) + { + uint32_t start = millis(); + do + { + loop(); + if (_status == STATUS_OK) break; + } while (millis() - start < timeout); + } + else + { + loop(); + } + + return _status == STATUS_OK; +} + +void PMS::loop() +{ + _status = STATUS_WAITING; + if (_stream->available()) + { + uint8_t ch = _stream->read(); + + switch (_index) + { + case 0: + if (ch != 0x42) + { + return; + } + _calculatedChecksum = ch; + break; + + case 1: + if (ch != 0x4D) + { + _index = 0; + return; + } + _calculatedChecksum += ch; + break; + + case 2: + _calculatedChecksum += ch; + _frameLen = ch << 8; + break; + + case 3: + _frameLen |= ch; + // Unsupported sensor, different frame length, transmission error e.t.c. + if (_frameLen != 2 * 9 + 2 && _frameLen != 2 * 13 + 2) + { + _index = 0; + return; + } + _calculatedChecksum += ch; + break; + + default: + if (_index == _frameLen + 2) + { + _checksum = ch << 8; + } + else if (_index == _frameLen + 2 + 1) + { + _checksum |= ch; + + if (_calculatedChecksum == _checksum) + { + _status = STATUS_OK; + + // Factory environment. + _data->PM_FE_UG_1_0 = makeWord(_payload[0], _payload[1]); + _data->PM_FE_UG_2_5 = makeWord(_payload[2], _payload[3]); + _data->PM_FE_UG_10_0 = makeWord(_payload[4], _payload[5]); + + // Atmospheric environment. + _data->PM_AE_UG_1_0 = makeWord(_payload[6], _payload[7]); + _data->PM_AE_UG_2_5 = makeWord(_payload[8], _payload[9]); + _data->PM_AE_UG_10_0 = makeWord(_payload[10], _payload[11]); + } + + _index = 0; + return; + } + else + { + _calculatedChecksum += ch; + uint8_t payloadIndex = _index - 4; + + // Payload is common to all sensors (first 2x6 bytes). + if (payloadIndex < sizeof(_payload)) + { + _payload[payloadIndex] = ch; + } + } + + break; + } + + _index++; + } +} diff --git a/src/PMS.h b/src/PMS.h new file mode 100644 index 0000000..069d5bc --- /dev/null +++ b/src/PMS.h @@ -0,0 +1,48 @@ +#ifndef PMS_H +#define PMS_H + +#include "Stream.h" + +class PMS +{ +public: + struct DATA { + // Factory environment + uint16_t PM_FE_UG_1_0; + uint16_t PM_FE_UG_2_5; + uint16_t PM_FE_UG_10_0; + + // Atmospheric environment + uint16_t PM_AE_UG_1_0; + uint16_t PM_AE_UG_2_5; + uint16_t PM_AE_UG_10_0; + }; + + enum STATUS { STATUS_WAITING, STATUS_OK }; + enum MODE { MODE_ACTIVE, MODE_PASSIVE }; + + PMS(Stream&); + void sleep(); + void wakeUp(); + void activeMode(); + void passiveMode(); + + void requestRead(); + bool read(DATA& data, uint16_t timeout = 0); + +private: + uint8_t _payload[12]; + Stream* _stream; + DATA* _data; + STATUS _status; + MODE _mode = MODE::MODE_ACTIVE; + + uint8_t _index = 0; + uint16_t _frameLen; + uint16_t _checksum; + uint16_t _calculatedChecksum; + + void loop(); +}; + +#endif \ No newline at end of file