├── .gitignore ├── LICENSE ├── README.md ├── examples ├── customWorkingPeriod │ └── customWorkingPeriod.ino ├── hardwareSerial │ └── hardwareSerial.ino ├── queryReportingMode │ └── queryReportingMode.ino └── quickstart │ └── quickstart.ino ├── library.json ├── library.properties └── src ├── SdsDustSensor.cpp ├── SdsDustSensor.h ├── SdsDustSensorCommands.h ├── SdsDustSensorResults.cpp ├── SdsDustSensorResults.h └── Serials.h /.gitignore: -------------------------------------------------------------------------------- 1 | src/main.cpp 2 | src/src.ino 3 | platformio.ini 4 | .pioenvs 5 | .piolibdeps 6 | .clang_complete 7 | .gcc-flags.json 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Paweł Kołodziejczyk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nova Fitness SDS dust sensors arduino library 2 | Supports Nova Fitness SDS011, SDS021 however should work for other Nova Fitness SDS sensors as well. 3 | This library attempts to provide easy-to-use abstraction over [Laser Dust Sensor Control Protocol V1.3](https://cdn.sparkfun.com/assets/parts/1/2/2/7/5/Laser_Dust_Sensor_Control_Protocol_V1.3.pdf). 4 | Each response coming from sensor is validated whether it has correct head, command id, checksum and tail. 5 | Library also handles automatic retries in case of not available / failed response from sensor. 6 | 7 | ## Quickstart 8 | ```arduino 9 | #include "SdsDustSensor.h" 10 | 11 | int rxPin = D1; 12 | int txPin = D2; 13 | SdsDustSensor sds(rxPin, txPin); 14 | // SdsDustSensor sds(Serial1); // if you are on a SAMD based board 15 | 16 | void setup() { 17 | Serial.begin(9600); 18 | sds.begin(); 19 | 20 | Serial.println(sds.queryFirmwareVersion().toString()); // prints firmware version 21 | Serial.println(sds.setActiveReportingMode().toString()); // ensures sensor is in 'active' reporting mode 22 | Serial.println(sds.setContinuousWorkingPeriod().toString()); // ensures sensor has continuous working period - default but not recommended 23 | } 24 | 25 | void loop() { 26 | PmResult pm = sds.readPm(); 27 | if (pm.isOk()) { 28 | Serial.print("PM2.5 = "); 29 | Serial.print(pm.pm25); 30 | Serial.print(", PM10 = "); 31 | Serial.println(pm.pm10); 32 | 33 | // if you want to just print the measured values, you can use toString() method as well 34 | Serial.println(pm.toString()); 35 | } else { 36 | Serial.print("Could not read values from sensor, reason: "); 37 | Serial.println(pm.statusToString()); 38 | } 39 | 40 | delay(500); 41 | } 42 | ``` 43 | For more examples see [examples](examples/) folder. 44 | 45 | ## Initialization 46 | Communication with sensor can be handled by SoftwareSerial or HardwareSerial. You can pass SoftwareSerial or HardwareSerial directly to the constructor or provide rx & tx pins (library will use SoftwareSerial then). 47 | 48 | ### Using tx and rx pins 49 | Communication will be implicitly handled by SoftwareSerial. 50 | ```arduino 51 | int rxPin = D1; 52 | int txPin = D2; 53 | SdsDustSensor sds(rxPin, txPin); // you can tune retry mechanism with additional parameters: retryDelayMs and maxRetriesNotAvailable 54 | sds.begin(); // you can pass custom baud rate as parameter (9600 by default) 55 | ``` 56 | 57 | > This constructor is not available on SAMD based boards. 58 | > SAMD boards should provide more than enough HardwareSerial ports / SERCOM ports. 59 | 60 | ### Explicit SoftwareSerial 61 | ```arduino 62 | int rxPin = D1; 63 | int txPin = D2; 64 | SoftwareSerial softwareSerial(rxPin, txPin); 65 | SdsDustSensor sds(softwareSerial); // you can tune retry mechanism with additional parameters: retryDelayMs and maxRetriesNotAvailable 66 | sds.begin(); // you can pass custom baud rate as parameter (9600 by default) 67 | ``` 68 | 69 | > This constructor is not available on SAMD based boards. 70 | > SAMD boards should provide more than enough HardwareSerial ports / SERCOM ports. 71 | 72 | ### Explicit HardwareSerial 73 | ```arduino 74 | SdsDustSensor sds(Serial1); // passing HardwareSerial as parameter, you can tune retry mechanism with additional parameters: retryDelayMs and maxRetriesNotAvailable 75 | sds.begin(); // you can pass custom baud rate as parameter (9600 by default) 76 | ``` 77 | 78 | ## Supported operations 79 | All operations listed in [Laser Dust Sensor Control Protocol V1.3](https://cdn.sparkfun.com/assets/parts/1/2/2/7/5/Laser_Dust_Sensor_Control_Protocol_V1.3.pdf) are fully supported. They are listed below: 80 | * read PM2.5, PM10 values (reads data from software serial, does not write anything to the serial, all other operations write date to the sensor and expect response - sensor should be set to "active" reporting mode), 81 | * query PM2.5, PM10 values (in opposite to above, this one sends command to sensor and it responds with PM2.5 and PM10 values - sensor should be set to "query" reporting mode), 82 | * query reporting mode, 83 | * set reporting mode to "active", 84 | * set reporting mode to "query", 85 | * set new device id, 86 | * query working state, 87 | * set working state to "sleeping", 88 | * set working state to "working", 89 | * query working period, 90 | * set "continuous" working period (factory default - sensor sends pm values every 1 second), 91 | * set "custom" working period (recommended setting - 1-30min cycles), 92 | * check firmware version (year, month, day). 93 | 94 | Additionally you can read device id from every sensor response. 95 | 96 | ### Reading PM2.5 and PM10 values 97 | The following function (readPm()) checks whether there is available data sent from sensor, it does not send any request to sensor so it has to be in 'active' reporting mode. 98 | ```arduino 99 | PmResult result = sds.readPm(); 100 | result.pm25; // float 101 | result.pm10; // float 102 | ``` 103 | 104 | ### Querying PM2.5 and PM10 values 105 | In opposite to above function, this one sends request to sensor and expects the response. Sensor should be in 'query' reporting mode. 106 | ```arduino 107 | PmResult result = sds.queryPm(); 108 | result.pm25; // float 109 | result.pm10; // float 110 | ``` 111 | 112 | ### Setting custom working period - recommended over continuous 113 | In order to set custom working period you need to specify single argument - duration (minutes) of the cycle. One cycle means working 30 sec, doing measurement and sleeping for ```duration-30 [sec]```. This setting is recommended when using 'active' reporting mode. 114 | ```arduino 115 | int cycleMinutes = 4; 116 | WorkingPeriodResult result = sds.setCustomWorkingPeriod(cycleMinutes); 117 | result.period; // 4 118 | result.isContinuous(); // false 119 | result.toString(); 120 | ``` 121 | 122 | ### Setting reporting mode to 'query' 123 | When 'query' mode is active the sensor will not send data automatically, you need to send `sds.queryPm()` command on order to measure PM values. 124 | ```arduino 125 | ReportingModeResult result = sds.setQueryReportingMode(); 126 | result.mode; // MODE::QUERY 127 | ``` 128 | 129 | ### Setting sensor state to 'sleeping' 130 | ```arduino 131 | WorkingStateResult result = sds.sleep(); 132 | result.isWorking(); // false 133 | ``` 134 | 135 | ### Waking up 136 | Safe wakeup tries to perform wakeup twice to assure proper response from sensor. When waking up after sleep sensor seems to respond with random bytes or not to respond at all. Despite incorrect response it seems to wake up correctly (fan starts working). Second wakeup forces sensor to send proper response. 137 | Because of the fact that sensor seems to work correctly (despite invalid response), you can use unsafe method if you don't care about the response. 138 | 139 | #### Safe wakeup 140 | ```arduino 141 | WorkingStateResult result = sds.wakeup(); 142 | result.isWorking(); // true 143 | ``` 144 | #### Unsafe wakeup 145 | ```arduino 146 | WorkingStateResult result = sds.wakeupUnsafe(); 147 | result.isWorking(); // true 148 | ``` 149 | 150 | ### Other functions 151 | Responses format of other functions can be found in [src](src/) folder. 152 | 153 | ## Helpful methods 154 | Additionally with every sensor result you can: 155 | * access result status ```result.status```, which can be one of {Ok, NotAvailable, InvalidChecksum, InvalidResponseId, InvalidHead, InvalidTail}, 156 | * easily check whether response is correct with ```result.isOk()``` method, 157 | * convert status to string with ```result.statusToString()``` method. 158 | 159 | You can also access ```result.deviceId()``` (pointer to the 1st (of 2) device id byte) and ```result.rawBytes``` (pointer to raw sensor response - byte array). 160 | 161 | ## Additional notes and observations 162 | ### Power consumption and modes 163 | "Query" reporting mode probably consumes less power when sleeping than "Active" because the sensor doesn't have to know when it should wake up (doesn't have schedule any internal tasks). 164 | 165 | ## References 166 | * [Laser Dust Sensor Control Protocol V1.3](https://cdn.sparkfun.com/assets/parts/1/2/2/7/5/Laser_Dust_Sensor_Control_Protocol_V1.3.pdf) 167 | * http://www.inovafitness.com/en/a/chanpinzhongxin/95.html 168 | -------------------------------------------------------------------------------- /examples/customWorkingPeriod/customWorkingPeriod.ino: -------------------------------------------------------------------------------- 1 | #include "SdsDustSensor.h" 2 | 3 | int rxPin = D1; 4 | int txPin = D2; 5 | SdsDustSensor sds(rxPin, txPin); 6 | 7 | void setup() { 8 | Serial.begin(9600); 9 | sds.begin(); 10 | 11 | Serial.println(sds.queryFirmwareVersion().toString()); // prints firmware version 12 | Serial.println(sds.setActiveReportingMode().toString()); // ensures sensor is in 'active' reporting mode 13 | Serial.println(sds.setCustomWorkingPeriod(3).toString()); // sensor sends data every 3 minutes 14 | } 15 | 16 | void loop() { 17 | PmResult pm = sds.readPm(); 18 | if (pm.isOk()) { 19 | Serial.print("PM2.5 = "); 20 | Serial.print(pm.pm25); 21 | Serial.print(", PM10 = "); 22 | Serial.println(pm.pm10); 23 | 24 | // if you want to just print the measured values, you can use toString() method as well 25 | Serial.println(pm.toString()); 26 | } else { 27 | // notice that loop delay is set to 5s (sensor sends data every 3 minutes) and some reads are not available 28 | Serial.print("Could not read values from sensor, reason: "); 29 | Serial.println(pm.statusToString()); 30 | } 31 | 32 | delay(5000); 33 | } 34 | -------------------------------------------------------------------------------- /examples/hardwareSerial/hardwareSerial.ino: -------------------------------------------------------------------------------- 1 | #include "SdsDustSensor.h" 2 | 3 | // tested on Arduino Leonardo with Serial1 4 | SdsDustSensor sds(Serial1); // passing HardwareSerial& as parameter 5 | 6 | void setup() { 7 | Serial.begin(9600); 8 | sds.begin(); // this line will begin Serial1 with given baud rate (9600 by default) 9 | 10 | Serial.println(sds.queryFirmwareVersion().toString()); // prints firmware version 11 | Serial.println(sds.setQueryReportingMode().toString()); // ensures sensor is in 'query' reporting mode 12 | } 13 | 14 | void loop() { 15 | sds.wakeup(); 16 | delay(30000); // working 30 seconds 17 | 18 | PmResult pm = sds.queryPm(); 19 | if (pm.isOk()) { 20 | Serial.print("PM2.5 = "); 21 | Serial.print(pm.pm25); 22 | Serial.print(", PM10 = "); 23 | Serial.println(pm.pm10); 24 | 25 | // if you want to just print the measured values, you can use toString() method as well 26 | Serial.println(pm.toString()); 27 | } else { 28 | Serial.print("Could not read values from sensor, reason: "); 29 | Serial.println(pm.statusToString()); 30 | } 31 | 32 | WorkingStateResult state = sds.sleep(); 33 | if (state.isWorking()) { 34 | Serial.println("Problem with sleeping the sensor."); 35 | } else { 36 | Serial.println("Sensor is sleeping"); 37 | delay(60000); // wait 1 minute 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/queryReportingMode/queryReportingMode.ino: -------------------------------------------------------------------------------- 1 | #include "SdsDustSensor.h" 2 | 3 | int rxPin = D1; 4 | int txPin = D2; 5 | SdsDustSensor sds(rxPin, txPin); 6 | 7 | void setup() { 8 | Serial.begin(9600); 9 | sds.begin(); 10 | 11 | Serial.println(sds.queryFirmwareVersion().toString()); // prints firmware version 12 | Serial.println(sds.setQueryReportingMode().toString()); // ensures sensor is in 'query' reporting mode 13 | } 14 | 15 | void loop() { 16 | sds.wakeup(); 17 | delay(30000); // working 30 seconds 18 | 19 | PmResult pm = sds.queryPm(); 20 | if (pm.isOk()) { 21 | Serial.print("PM2.5 = "); 22 | Serial.print(pm.pm25); 23 | Serial.print(", PM10 = "); 24 | Serial.println(pm.pm10); 25 | 26 | // if you want to just print the measured values, you can use toString() method as well 27 | Serial.println(pm.toString()); 28 | } else { 29 | Serial.print("Could not read values from sensor, reason: "); 30 | Serial.println(pm.statusToString()); 31 | } 32 | 33 | WorkingStateResult state = sds.sleep(); 34 | if (state.isWorking()) { 35 | Serial.println("Problem with sleeping the sensor."); 36 | } else { 37 | Serial.println("Sensor is sleeping"); 38 | delay(60000); // wait 1 minute 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/quickstart/quickstart.ino: -------------------------------------------------------------------------------- 1 | #include "SdsDustSensor.h" 2 | 3 | int rxPin = D1; 4 | int txPin = D2; 5 | SdsDustSensor sds(rxPin, txPin); 6 | 7 | void setup() { 8 | Serial.begin(9600); 9 | sds.begin(); 10 | 11 | Serial.println(sds.queryFirmwareVersion().toString()); // prints firmware version 12 | Serial.println(sds.setActiveReportingMode().toString()); // ensures sensor is in 'active' reporting mode 13 | Serial.println(sds.setContinuousWorkingPeriod().toString()); // ensures sensor has continuous working period - default but not recommended 14 | } 15 | 16 | void loop() { 17 | PmResult pm = sds.readPm(); 18 | if (pm.isOk()) { 19 | Serial.print("PM2.5 = "); 20 | Serial.print(pm.pm25); 21 | Serial.print(", PM10 = "); 22 | Serial.println(pm.pm10); 23 | 24 | // if you want to just print the measured values, you can use toString() method as well 25 | Serial.println(pm.toString()); 26 | } else { 27 | // notice that loop delay is set to 0.5s and some reads are not available 28 | Serial.print("Could not read values from sensor, reason: "); 29 | Serial.println(pm.statusToString()); 30 | } 31 | 32 | delay(500); 33 | } 34 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nova Fitness Sds dust sensors library", 3 | "version": "1.5.1", 4 | "keywords": "sds011, sds, sds021, dust, sensor, pm10, pm25, arduino, esp8266, esp32", 5 | "description": "A high-level abstaction over Sds sensors family", 6 | "homepage": "https://github.com/lewapek/sds-dust-sensors-arduino-library", 7 | "license": "MIT", 8 | "authors": { 9 | "name": "Paweł Kołodziejczyk", 10 | "url": "https://github.com/lewapek", 11 | "maintainer": true 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/lewapek/sds-dust-sensors-arduino-library", 16 | "branch": "master" 17 | }, 18 | "frameworks": "arduino", 19 | "platforms": [ 20 | "atmelavr", 21 | "espressif8266", 22 | "espressif32", 23 | "atmelsam" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=Nova Fitness Sds dust sensors library 2 | version=1.5.1 3 | author=Paweł Kołodziejczyk 4 | maintainer=Pawel Kołodziejczyk 5 | sentence=A high-level abstaction over Sds sensors family 6 | paragraph=Supports Sds011, implements whole Laser Dust Sensor Control Protocol V1.3, should also work with other Sds sensors. 7 | category=Sensors 8 | url=https://github.com/lewapek/sds-dust-sensors-arduino-library 9 | architectures=avr,esp8266,esp32,sam,samd 10 | includes=SdsDustSensor.h 11 | -------------------------------------------------------------------------------- /src/SdsDustSensor.cpp: -------------------------------------------------------------------------------- 1 | #include "SdsDustSensor.h" 2 | 3 | void SdsDustSensor::write(const Command &command) { 4 | for (int i = 0; i < Command::length; ++i) { 5 | sdsStream->write(command.bytes[i]); 6 | #ifdef __DEBUG_SDS_DUST_SENSOR__ 7 | Serial.print("|"); 8 | Serial.print(command.bytes[i], HEX); 9 | #endif 10 | } 11 | #ifdef __DEBUG_SDS_DUST_SENSOR__ 12 | Serial.println("| <- written bytes"); 13 | #endif 14 | delay(500); 15 | } 16 | 17 | Status SdsDustSensor::readIntoBytes(byte responseId) { 18 | int checksum = 0; 19 | int readBytesQuantity = 0; 20 | 21 | while (sdsStream->available() >= Result::lenght - readBytesQuantity) { 22 | byte readByte = sdsStream->read(); 23 | response[readBytesQuantity] = readByte; 24 | 25 | #ifdef __DEBUG_SDS_DUST_SENSOR__ 26 | Serial.print("|"); 27 | Serial.print(readByte, HEX); 28 | #endif 29 | 30 | ++readBytesQuantity; 31 | switch (readBytesQuantity) { 32 | case 1: 33 | if (readByte != 0xAA) { 34 | #ifdef __DEBUG_SDS_DUST_SENSOR__ 35 | Serial.println("| <- read bytes with invalid head error"); 36 | #endif 37 | return Status::InvalidHead; 38 | } 39 | break; 40 | case 2: 41 | if (readByte != responseId) { 42 | #ifdef __DEBUG_SDS_DUST_SENSOR__ 43 | Serial.println("| <- read bytes with invalid response id"); 44 | #endif 45 | return Status::InvalidResponseId; 46 | } 47 | break; 48 | case 3 ... 8: 49 | checksum += readByte; 50 | break; 51 | case 9: 52 | if (readByte != checksum % 256) { 53 | #ifdef __DEBUG_SDS_DUST_SENSOR__ 54 | Serial.println("| <- read bytes with invalid checksum"); 55 | #endif 56 | return Status::InvalidChecksum; 57 | } 58 | break; 59 | case 10: 60 | if (readByte != 0xAB) { 61 | #ifdef __DEBUG_SDS_DUST_SENSOR__ 62 | Serial.println("| <- read bytes with invalid tail"); 63 | #endif 64 | return Status::InvalidTail; 65 | } 66 | break; 67 | } 68 | if (readBytesQuantity == Result::lenght) { 69 | #ifdef __DEBUG_SDS_DUST_SENSOR__ 70 | Serial.println("| <- read bytes with success"); 71 | #endif 72 | return Status::Ok; 73 | } 74 | yield(); 75 | } 76 | 77 | #ifdef __DEBUG_SDS_DUST_SENSOR__ 78 | Serial.println("| <- read bytes with no more available"); 79 | #endif 80 | 81 | return Status::NotAvailable; 82 | } 83 | 84 | void SdsDustSensor::flushStream() { 85 | while (sdsStream->available() > 0) { 86 | #ifdef __DEBUG_SDS_DUST_SENSOR__ 87 | Serial.print("|"); 88 | Serial.print(sdsStream->read(), HEX); 89 | Serial.println("| <- omitted byte"); 90 | #else 91 | sdsStream->read(); 92 | #endif 93 | } 94 | } 95 | 96 | Status SdsDustSensor::retryRead(byte responseId) { 97 | Status status = readIntoBytes(responseId); 98 | for (int i = 0; status == Status::NotAvailable && i < maxRetriesNotAvailable; ++i) { 99 | #ifdef __DEBUG_SDS_DUST_SENSOR__ 100 | Serial.print("Retry #"); 101 | Serial.print(i); 102 | Serial.println(" due to not available response"); 103 | #endif 104 | delay(retryDelayMs); 105 | status = readIntoBytes(responseId); 106 | } 107 | 108 | for (int i = 2; status == Status::InvalidHead && i < Result::lenght; ++i) { 109 | #ifdef __DEBUG_SDS_DUST_SENSOR__ 110 | Serial.print("Retry #"); 111 | Serial.print(i - 2); 112 | Serial.println(" due to invalid response head"); 113 | #endif 114 | status = readIntoBytes(responseId); 115 | } 116 | 117 | return status; 118 | } 119 | -------------------------------------------------------------------------------- /src/SdsDustSensor.h: -------------------------------------------------------------------------------- 1 | // Nova Fitness Sds dust sensors library 2 | // A high-level abstaction over Sds sensors family 3 | // 4 | // MIT License 5 | // 6 | // Copyright (c) 2018 Paweł Kołodziejczyk 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | 26 | #ifndef __SDS_DUST_SENSOR_H__ 27 | #define __SDS_DUST_SENSOR_H__ 28 | 29 | #include "SdsDustSensorCommands.h" 30 | #include "SdsDustSensorResults.h" 31 | #include "Serials.h" 32 | 33 | #define RETRY_DELAY_MS_DEFAULT 5 34 | #define MAX_RETRIES_NOT_AVAILABLE_DEFAULT 100 35 | 36 | class SdsDustSensor { 37 | public: 38 | 39 | #if !defined(ARDUINO_SAMD_VARIANT_COMPLIANCE) && !defined(ESP32) 40 | SdsDustSensor(int pinRx, 41 | int pinTx, 42 | int retryDelayMs = RETRY_DELAY_MS_DEFAULT, 43 | int maxRetriesNotAvailable = MAX_RETRIES_NOT_AVAILABLE_DEFAULT): 44 | abstractSerial(new Serials::InternalSoftware(pinRx, pinTx)), 45 | retryDelayMs(retryDelayMs), 46 | maxRetriesNotAvailable(maxRetriesNotAvailable) { 47 | sdsStream = abstractSerial->getStream(); 48 | } 49 | 50 | SdsDustSensor(SoftwareSerial &softwareSerial, 51 | int retryDelayMs = RETRY_DELAY_MS_DEFAULT, 52 | int maxRetriesNotAvailable = MAX_RETRIES_NOT_AVAILABLE_DEFAULT): 53 | abstractSerial(new Serials::Software(softwareSerial)), 54 | retryDelayMs(retryDelayMs), 55 | maxRetriesNotAvailable(maxRetriesNotAvailable) { 56 | sdsStream = abstractSerial->getStream(); 57 | } 58 | #endif 59 | 60 | SdsDustSensor(HardwareSerial &hardwareSerial, 61 | int retryDelayMs = RETRY_DELAY_MS_DEFAULT, 62 | int maxRetriesNotAvailable = MAX_RETRIES_NOT_AVAILABLE_DEFAULT): 63 | abstractSerial(new Serials::Hardware(hardwareSerial)), 64 | retryDelayMs(retryDelayMs), 65 | maxRetriesNotAvailable(maxRetriesNotAvailable) { 66 | sdsStream = abstractSerial->getStream(); 67 | } 68 | 69 | ~SdsDustSensor() { 70 | if (abstractSerial != NULL) { 71 | delete abstractSerial; 72 | } 73 | } 74 | 75 | void begin(int baudRate = 9600) { 76 | abstractSerial->begin(baudRate); 77 | } 78 | 79 | byte *getLastResponse() { 80 | return response; 81 | } 82 | 83 | ReportingModeResult queryReportingMode() { 84 | Status status = execute(Commands::queryReportingMode); 85 | return ReportingModeResult(status, response); 86 | } 87 | 88 | ReportingModeResult setQueryReportingMode() { 89 | Status status = execute(Commands::setQueryReportingMode); 90 | return ReportingModeResult(status, response); 91 | } 92 | 93 | ReportingModeResult setActiveReportingMode() { 94 | Status status = execute(Commands::setActiveReportingMode); 95 | return ReportingModeResult(status, response); 96 | } 97 | 98 | PmResult queryPm() { 99 | Status status = execute(Commands::queryPm); 100 | return PmResult(status, response); 101 | } 102 | 103 | // warning: this method doesn't write anything to the sensor, it just reads incoming bytes 104 | PmResult readPm() { 105 | Status status = retryRead(Commands::queryPm.responseId); 106 | return PmResult(status, response); 107 | } 108 | 109 | Result setDeviceId(byte newDeviceIdByte1, byte newDeviceIdByte2) { 110 | (Commands::setDeviceId).setDeviceId(newDeviceIdByte1, newDeviceIdByte2); 111 | Status status = execute(Commands::setDeviceId); 112 | return Result(status, response); 113 | } 114 | 115 | WorkingStateResult queryWorkingState() { 116 | Status status = execute(Commands::queryWorkingState); 117 | return WorkingStateResult(status, response); 118 | } 119 | 120 | WorkingStateResult sleep() { 121 | Status status = execute(Commands::sleep); 122 | return WorkingStateResult(status, response); 123 | } 124 | 125 | // warning: according to 'Laser Dust Sensor Control Protocol V1.3' this method should work 126 | // however sensor responds with random bytes or doesn't response at all 127 | // despite the above issue it seems that sensor wakes up properly (fan starts working) 128 | WorkingStateResult wakeupUnsafe() { 129 | Status status = execute(Commands::wakeup); 130 | return WorkingStateResult(status, response); 131 | } 132 | 133 | // warning: double wakeup in order to assure proper sensor response 134 | // if you don't care about sensor response you can use 'wakeupUnsafe' 135 | WorkingStateResult wakeup() { 136 | Status status = execute(Commands::wakeup); 137 | if (status != Status::Ok) { 138 | status = execute(Commands::wakeup); 139 | } 140 | return WorkingStateResult(status, response); 141 | } 142 | 143 | WorkingPeriodResult queryWorkingPeriod() { 144 | Status status = execute(Commands::queryWorkingPeriod); 145 | return WorkingPeriodResult(status, response); 146 | } 147 | 148 | WorkingPeriodResult setContinuousWorkingPeriod() { 149 | (Commands::setWorkingPeriod).setContinuousWorkingPeriod(); 150 | Status status = execute(Commands::setWorkingPeriod); 151 | return WorkingPeriodResult(status, response); 152 | } 153 | 154 | WorkingPeriodResult setCustomWorkingPeriod(byte workingPeriod) { 155 | (Commands::setWorkingPeriod).setCustomWorkingPeriod(workingPeriod); 156 | Status status = execute(Commands::setWorkingPeriod); 157 | return WorkingPeriodResult(status, response); 158 | } 159 | 160 | FirmwareVersionResult queryFirmwareVersion() { 161 | Status status = execute(Commands::queryFirmwareVersion); 162 | return FirmwareVersionResult(status, response); 163 | } 164 | 165 | Status execute(const Command &command) { 166 | flushStream(); 167 | write(command); 168 | return retryRead(command.responseId); 169 | } 170 | 171 | void write(const Command &command); 172 | Status readIntoBytes(byte responseId); 173 | 174 | private: 175 | Serials::AbstractSerial *abstractSerial; 176 | Stream *sdsStream = NULL; 177 | byte response[Result::lenght]; 178 | int retryDelayMs; 179 | int maxRetriesNotAvailable; 180 | 181 | void flushStream(); 182 | Status retryRead(byte responseId); 183 | }; 184 | 185 | #endif // __SDS_DUST_SENSOR_H__ 186 | -------------------------------------------------------------------------------- /src/SdsDustSensorCommands.h: -------------------------------------------------------------------------------- 1 | #ifndef __SDS_DUST_SENSOR_COMMAND_H__ 2 | #define __SDS_DUST_SENSOR_COMMAND_H__ 3 | 4 | #include 5 | 6 | struct Command { 7 | static const int length = 19; 8 | static const int headIndex = 0; 9 | static const int commandIdIndex = 1; 10 | static const int dataStartIndex = 2, dataEndIndex = 14; 11 | static const int devideIdByte1Index = 15; 12 | static const int devideIdByte2Index = 16; 13 | static const int checksumIndex = 17; 14 | static const int tailIndex = 18; 15 | 16 | static const byte head = 0xAA; 17 | static const byte tail = 0xAB; 18 | 19 | byte *bytes = NULL; 20 | byte responseId; 21 | 22 | Command(byte commandId, 23 | const byte *commandData, 24 | int commandDataLength, 25 | byte responseId, 26 | byte deviceIdByte1 = 0xFF, 27 | byte deviceIdByte2 = 0xFF): responseId(responseId) { 28 | bytes = new byte[length]; 29 | 30 | bytes[headIndex] = head; 31 | bytes[commandIdIndex] = commandId; 32 | for (int i = 0; i < commandDataLength; ++i) { 33 | bytes[dataStartIndex + i] = commandData[i]; // data bytes 34 | } 35 | for (int i = dataStartIndex + commandDataLength; i <= dataEndIndex; ++i) { 36 | bytes[i] = 0x00; // other data bytes 37 | } 38 | bytes[devideIdByte1Index] = deviceIdByte1; 39 | bytes[devideIdByte2Index] = deviceIdByte2; 40 | bytes[checksumIndex] = calculateChecksum(); 41 | bytes[tailIndex] = tail; 42 | } 43 | 44 | ~Command() { 45 | if (bytes != NULL) { 46 | delete bytes; 47 | } 48 | } 49 | 50 | byte getChecksum() { 51 | return bytes[checksumIndex]; 52 | } 53 | 54 | int calculateChecksum() { 55 | int sum = 0; 56 | for (int i = dataStartIndex; i <= dataEndIndex; ++i) { 57 | sum += bytes[i]; 58 | } 59 | sum += bytes[devideIdByte1Index] + bytes[devideIdByte2Index]; 60 | return (sum % 256); 61 | } 62 | 63 | void setDeviceId(byte byte1, byte byte2) { 64 | bytes[devideIdByte1Index] = byte1; 65 | bytes[devideIdByte2Index] = byte2; 66 | bytes[checksumIndex] = calculateChecksum(); 67 | } 68 | }; 69 | 70 | struct SetWorkingPeriodCommand: public Command { 71 | static const int workingPeriodIndex = 4; 72 | 73 | using Command::Command; 74 | 75 | void setCustomWorkingPeriod(byte period) { 76 | bytes[workingPeriodIndex] = period; 77 | bytes[checksumIndex] = calculateChecksum(); 78 | } 79 | 80 | void setContinuousWorkingPeriod() { 81 | bytes[workingPeriodIndex] = 0x00; 82 | bytes[checksumIndex] = calculateChecksum(); 83 | } 84 | }; 85 | 86 | struct SetDeviceIdCommand: public Command { 87 | static const int newDeviceIdIndex1 = 13; 88 | static const int newDeviceIdIndex2 = 14; 89 | 90 | using Command::Command; 91 | 92 | void setNewDeviceId(byte byte1, byte byte2) { 93 | bytes[newDeviceIdIndex1] = byte1; 94 | bytes[newDeviceIdIndex2] = byte2; 95 | bytes[checksumIndex] = calculateChecksum(); 96 | } 97 | }; 98 | 99 | namespace Commands { 100 | // reporting mode 101 | static const byte queryReportingModeData[1] = {0x02}; 102 | static const Command queryReportingMode(0xB4, queryReportingModeData, sizeof(queryReportingModeData), 0xC5); 103 | 104 | static const byte setActiveReportingModeData[2] = {0x02, 0x01}; 105 | static const Command setActiveReportingMode(0xB4, setActiveReportingModeData, sizeof(setActiveReportingModeData), 0xC5); 106 | 107 | static const byte setQueryReportingModeData[3] = {0x02, 0x01, 0x01}; 108 | static const Command setQueryReportingMode(0xB4, setQueryReportingModeData, sizeof(setQueryReportingModeData), 0xC5); 109 | 110 | // query data 111 | static const byte queryPmData[1] = {0x04}; 112 | static const Command queryPm(0xB4, queryPmData, sizeof(queryPmData), 0xC0); 113 | 114 | // set device id 115 | static const byte setDeviceIdData[1] = {0x05}; 116 | static SetDeviceIdCommand setDeviceId(0xB4, setDeviceIdData, sizeof(setDeviceIdData), 0xC5); 117 | 118 | // sleep and work 119 | static const byte queryWorkingStateData[1] = {0x06}; 120 | static const Command queryWorkingState(0xB4, queryWorkingStateData, sizeof(queryWorkingStateData), 0xC5); 121 | 122 | static const byte sleepData[2] = {0x06, 0x01}; 123 | static const Command sleep(0xB4, sleepData, sizeof(sleepData), 0xC5); 124 | 125 | static const byte wakeupData[3] = {0x06, 0x01, 0x01}; 126 | static const Command wakeup(0xB4, wakeupData, sizeof(wakeupData), 0xC5); 127 | 128 | // working period 129 | static const byte queryWorkingPeriodData[1] = {0x08}; 130 | static const Command queryWorkingPeriod(0xB4, queryWorkingPeriodData, sizeof(queryWorkingPeriodData), 0xC5); 131 | 132 | static const byte setWorkingPeriodData[2] = {0x08, 0x01}; 133 | static SetWorkingPeriodCommand setWorkingPeriod(0xB4, setWorkingPeriodData, sizeof(setWorkingPeriodData), 0xC5); 134 | 135 | // firmware version 136 | static const byte queryFirmwareVersionData[1] = {0x07}; 137 | static const Command queryFirmwareVersion(0xB4, queryFirmwareVersionData, sizeof(queryFirmwareVersionData), 0xC5); 138 | }; 139 | 140 | #endif // __SDS_DUST_SENSOR_COMMAND_H__ 141 | -------------------------------------------------------------------------------- /src/SdsDustSensorResults.cpp: -------------------------------------------------------------------------------- 1 | #include "SdsDustSensorResults.h" 2 | 3 | String Result::statusToString() { 4 | switch (status) { 5 | case Status::Ok: return "Ok"; 6 | case Status::NotAvailable: return "Not available"; 7 | case Status::InvalidChecksum: return "Invalid checksum"; 8 | case Status::InvalidResponseId: return "Invalid response id"; 9 | case Status::InvalidHead: return "Invalid head"; 10 | case Status::InvalidTail: return "Invalid tail"; 11 | default: return "undefined status"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/SdsDustSensorResults.h: -------------------------------------------------------------------------------- 1 | #ifndef __SDS_DUST_SENSOR_RESULTS_H__ 2 | #define __SDS_DUST_SENSOR_RESULTS_H__ 3 | 4 | #include "SdsDustSensorCommands.h" 5 | #if !defined(ARDUINO_SAMD_VARIANT_COMPLIANCE) && !defined(ESP32) 6 | #include 7 | #endif 8 | 9 | enum class Status { 10 | Ok, NotAvailable, InvalidChecksum, InvalidResponseId, InvalidHead, InvalidTail 11 | }; 12 | 13 | struct Result { 14 | static const int lenght = 10; 15 | static const int deviceIdStartIndex = 6; 16 | Status status; 17 | byte rawBytes[lenght]; 18 | 19 | Result(const Status &status, byte *bytes): status(status) { 20 | if (isOk()) { 21 | for (int i = 0; i < lenght; ++i) { 22 | rawBytes[i] = bytes[i]; 23 | } 24 | } else { 25 | for (int i = 0; i < lenght; ++i) { 26 | rawBytes[i] |= 0xFF; 27 | } 28 | } 29 | }; 30 | 31 | bool isOk() { 32 | return status == Status::Ok; 33 | } 34 | 35 | byte *deviceId() { 36 | // warning: device id has 2 bytes, to access 2nd byte: deviceId()[1] 37 | return rawBytes + deviceIdStartIndex; 38 | } 39 | 40 | String statusToString(); 41 | }; 42 | 43 | struct ReportingModeResult: public Result { 44 | enum class Mode { Query, Active, Undefined }; 45 | 46 | static const int modeIndex = 4; 47 | Mode mode; 48 | 49 | ReportingModeResult(const Status &status, byte *bytes): Result(status, bytes) { 50 | switch (rawBytes[modeIndex]) { 51 | case 0: mode = Mode::Active; break; 52 | case 1: mode = Mode::Query; break; 53 | default: mode = Mode::Undefined; break; 54 | } 55 | } 56 | 57 | bool isActive() { 58 | return mode == Mode::Active; 59 | } 60 | 61 | String toString() { 62 | switch (mode) { 63 | case Mode::Query: return "Mode: query"; 64 | case Mode::Active: return "Mode: active"; 65 | default: return "Mode: undefined"; 66 | } 67 | } 68 | }; 69 | 70 | struct PmResult: public Result { 71 | float pm25 = -1.0; 72 | float pm10 = -1.0; 73 | 74 | PmResult(const Status &status, byte *bytes): Result(status, bytes) { 75 | if (isOk()) { 76 | pm25 = (rawBytes[2] | (rawBytes[3] << 8)) / 10.0; 77 | pm10 = (rawBytes[4] | (rawBytes[5] << 8)) / 10.0; 78 | } 79 | } 80 | 81 | String toString() { 82 | return "pm25: " + String(pm25) + ", pm10: " + String(pm10); 83 | } 84 | }; 85 | 86 | struct WorkingStateResult: public Result { 87 | enum class State { Sleeping, Working, Undefined }; 88 | 89 | static const int stateIndex = 4; 90 | State state; 91 | 92 | WorkingStateResult(const Status &status, byte *bytes): Result(status, bytes) { 93 | switch (rawBytes[stateIndex]) { 94 | case 0: state = State::Sleeping; break; 95 | case 1: state = State::Working; break; 96 | default: state = State::Undefined; break; 97 | } 98 | } 99 | 100 | bool isWorking() { 101 | return state == State::Working; 102 | } 103 | 104 | String toString() { 105 | switch (state) { 106 | case State::Sleeping: return "State: sleeping"; 107 | case State::Working: return "State: working"; 108 | default: return "State: undefined"; 109 | } 110 | } 111 | }; 112 | 113 | struct WorkingPeriodResult: public Result { 114 | static const int periodIndex = 4; 115 | byte period; 116 | 117 | WorkingPeriodResult(const Status &status, byte *bytes): Result(status, bytes) { 118 | period = rawBytes[periodIndex]; 119 | } 120 | 121 | bool isContinuous() { 122 | return period == 0; 123 | } 124 | 125 | String toString() { 126 | switch (period) { 127 | case 0: return "Working period: continuous"; 128 | case 1 ... 30: return "Working period: " + String(period) + " min cycles: work 30 seconds, measure and sleep"; 129 | default: return "Working period: undefined"; 130 | } 131 | } 132 | }; 133 | 134 | struct FirmwareVersionResult: public Result { 135 | static const int periodIndex = 4; 136 | int year = -1, month = -1, day = -1; 137 | 138 | FirmwareVersionResult(const Status &status, byte *bytes): Result(status, bytes) { 139 | if (isOk()) { 140 | year = rawBytes[3]; 141 | month = rawBytes[4]; 142 | day = rawBytes[5]; 143 | } 144 | } 145 | 146 | String toString() { 147 | return "Firmware version [year.month.day]: " + String(year) + "." + String(month) + "." + String(day); 148 | } 149 | }; 150 | 151 | 152 | #endif // __SDS_DUST_SENSOR_RESULTS_H__ 153 | -------------------------------------------------------------------------------- /src/Serials.h: -------------------------------------------------------------------------------- 1 | #ifndef __SDS_ABSTRACT_SERIAL_H__ 2 | #define __SDS_ABSTRACT_SERIAL_H__ 3 | 4 | #if !defined(ARDUINO_SAMD_VARIANT_COMPLIANCE) && !defined(ESP32) 5 | #include 6 | #endif 7 | #include 8 | 9 | namespace Serials { 10 | 11 | // cpp is really weird... 12 | // there was compilation warning about missing virtual destructor for class AbstractSerial 13 | // I added virtual destructor with empty body 14 | // remaining virtual methods didn't have body (they were 'pure virtual') 15 | // after that the following error appeared: undefined reference to `vtable for Serials::AbstractSerial' 16 | // what a nonsense ... 17 | // just to satisfy linker in gcc I needed to add empty parentheses to other virtual methods... 18 | class AbstractSerial { 19 | public: 20 | virtual void begin(int baudRate) = 0; 21 | virtual Stream *getStream() = 0; 22 | virtual ~AbstractSerial() {}; 23 | }; 24 | 25 | struct Hardware: public AbstractSerial { 26 | Hardware(HardwareSerial &serial): serial(serial) {} 27 | 28 | void begin(int baudRate) { 29 | serial.begin(baudRate); 30 | } 31 | 32 | Stream *getStream() { 33 | return &serial; 34 | } 35 | 36 | HardwareSerial &serial; 37 | }; 38 | 39 | #if !defined(ARDUINO_SAMD_VARIANT_COMPLIANCE) && !defined(ESP32) 40 | struct Software: public AbstractSerial { 41 | Software(SoftwareSerial &serial): serial(serial) {} 42 | 43 | void begin(int baudRate) { 44 | serial.begin(baudRate); 45 | } 46 | 47 | Stream *getStream() { 48 | return &serial; 49 | } 50 | 51 | SoftwareSerial &serial; 52 | }; 53 | 54 | struct InternalSoftware: public AbstractSerial { 55 | InternalSoftware(const int &pinRx, const int &pinTx): 56 | serial(new SoftwareSerial(pinRx, pinTx)) {} 57 | 58 | ~InternalSoftware() { 59 | if (serial != NULL) { 60 | delete serial; 61 | } 62 | } 63 | 64 | void begin(int baudRate) { 65 | serial->begin(baudRate); 66 | } 67 | 68 | Stream *getStream() { 69 | return serial; 70 | } 71 | 72 | SoftwareSerial *serial; 73 | }; 74 | #endif // ARDUINO_SAMD_VARIANT_COMPLIANCE 75 | 76 | } 77 | 78 | #endif // __SDS_ABSTRACT_SERIAL_H__ 79 | --------------------------------------------------------------------------------