Created
December 11, 2020 01:06
-
-
Save Tech-TX/6bddab4aa8a6cbebdc02788afc58c2a4 to your computer and use it in GitHub Desktop.
Development code for PMS5003, BME280 and TFT LCD for a dirt cheap particle counter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Objective of the design: use inexpensive components to produce a cleanroom-style particle counter that can be run semi-continuously, monitored from outside the cleanroom. | |
The 攀藤 (PlanTower) PMS5003 (fifth generation) or PMS7003 (seventh generation) dust sensor costs $15 to $22 on eBay from Shenzhen suppliers. It samples 0.1 liter and calculates dust particles in > 0.3um, 0.5um, 1um, 2.5um, 5um and 10um bins. In order to get a useful reading in a cleanroom, we need to add the results from 100 samples (10 liters of air) or the counts are too small to be reliable. ISO 14644-1:1999 specifies counts in 1 cubic metre of air, which would require 10,000 samples from this small sensor. 100 samples takes roughly 300 seconds to process, so sampling 1 m3 would take 500 minutes (8.3 hours). As this device is not intended to be a NIST-traceable particle counter, sampling at 1% should give a reasonable estimate of the cleanroom quality. Note that the two smallest particle bins have up to 50% uncertainty; the larger particle bins track within a few percent across multiple PlanTower sensors, but there is variance on the two smallest particle bins. | |
The fan in the PlanTower sensor needs 5V to run, but the logic pins are all 3.3V signals. With a 5V microprocessor, you would need level shifters so that you don't violate the input specification. The micro I've chosen for this is the Espressif ESP8266, a 3.3V logic family, so no level shifters are needed. The ESP-12 DevKit and D1 Mini boards made with this chip have on-board 3.3V regulators so you can power the unit with USB or 5V cellphone charger. The maximum current required for the project is ~280mA when sampling and 40mA between sampling cycles when the board is idle. For real-world use, the sampling period should be no less than 10 minutes as the sampling time is long. You can change the code to sample once per hour or any other desired rate. Lower sampling rates will reduce power and extend the life of the fan and laser in the sensor, which is specified at 8000 hours MTTF, about 5 1/2 years @ 30 minute sample rate. Since the complete WiFi particle counter unit costs under $35USD without an LCD display, extending the life of the sensor may be unimportant. | |
Adding temperature and humidity measurement to the device is done using the Bosch BME280, a 3.3V logic part that has good repeatability. High humidity can effect particle count measurements and encourages mold, and low humidity raises ESD levels, so it's wise to monitor the humidity. The Bosch BME280 is available for < $5USD, although you must be careful as many vendors in China are shipping the cheaper BMP280 (no humidity sensing) instead of the BME280. DHT11/DHT22 sensors are another option for temperature and humidity that I've added support for, though not recommended due to erratic operation. | |
Since the ESP8266 chip has WiFi built-in, connecting to a free web data logger makes it possible to monitor the cleanroom from any location with internet access, including smart phones. I've used code to connect to Thingspeak.com for data logging, and it displays 1 day of data with 30 minute sample rates. With 1 hour sample rate you can display 2 days. | |
There are unused port pins on the ESP8266, so adding an LCD display and a MODE switch may be a useful feature if you are trying to locate where the dirty area of a room is. The MODE switch would stop updates to the web server and continuously sample the sensor, displaying directly to the LCD. Either SPI or I2C displays can be accomodated, although the SPI interface would leave only one port pin free unless you used SPI-overlap with the flash port on the DevKit module, which is troublesome to implement. | |
As the components are small, the device would fit in a 7cm cube, not counting the adapter. | |
The module below was my inspiration for design. Without the LCD, mine cost half that. | |
Cleanroom Classifications | |
Cleanrooms are classified by how clean the air is. In US Federal Standard 209E, the number of particles equal to and greater than 0.5mm are measured in one cubic foot of air, and this count is used to classify the cleanroom. The newer standard is from ISO TC 209. Both standards classify a cleanroom by the number of particles found in the cleanroom air. The standards FS 209E and ISO 14644-1 require specific particle count measurements and calculations to classify the cleanliness level of a cleanroom or clean area. | |
Large numbers like "class 100" or "class 1000" refer to FED STD-209E, and denote the number of particles of size 0.5 mm or larger permitted per cubic foot of air. The standard allows interpolation, so it is possible to describe e.g. "class 2000." Small numbers refer to ISO 14644-1 standard, which specify the decimal logarithm of the number of particles 0.1 µm or larger permitted per cubic metre of air. So, for example, an ISO class 5 cleanroom has at most 105 = 100,000 particles per m³. | |
Both FS 209E and ISO 14644-1 assume log-log relationships between particle size and particle concentration. For that reason, there is no such thing as zero particle concentration. Ordinary room air is approximately class 1,000,000 or ISO 9. | |
Due to the limitations of the PlanTower sensor in minimum particle size detection and small sample volume, the range that can be effectively detected is shown below. The ISO sampling volume is 1m3, yet the PlanTower sensor only reads 0.1 liter per measurement, so we add up the data from 100 samples to get a 10 liter sample size, 1/100th the size of the ISO volume. Divide the counts below by 100 and you'll see why the ISO 1 and 2 are impossible to verify. Class 3 will only be 1 to 10 particles > 0.3um in 100 samples, so it's right at the limit of detection. You might see Class 2 (zero particles), but you can't guarantee it due to the 30-50% error rate in 0.3um & 0.5um particle bins. The larger particle bins are more accurate, but they aren't useful for ISO classes below Class 4. ISO Class 3 (Fed 209E Class 1) is the lowest measurable particle concentration | |
. | |
Recommended microprocessors for this design: D1 Mini (below) or NodeMCU DevKit (bottom) | |
Either of these two boards will work, as will an ESP-32 if Bluetooth is needed. I tested these two. | |
The ESP-12F DevKit uses a 1 amp 3.3V regulator, and it can provide a lot of 3.3V power for other devices. Some clones use a cheaper 500mA regulator, but it's enough for this project. The D1 Mini boards may have issues. WeMos boards have 300mA or 500mA regulators, but clones may have only 150mA or 300mA regulators. 150mA is sufficient to run the project without the LCD, but the combined parts draw around 175mA during sampling, which may cause problems with cheap regulators. Most D1 Mini boards use a clone SOT23-5 regulator, which can be replaced directly by a Holtek HT7833 to provide 500mA 3.3V to the project. | |
The LCD controller draws 12mA of 3V3 and the backlight draws 55mA, 7uA total when sleeping | |
The BME280 draws 0.4mA of 3V3 when active, 0.1uA when sleeping | |
The PlanTower PMS5003 draws ~90mA of 5V during active sampling, 7mA when sleeping | |
Current peak is 280mA of 5V, 40mA when sleeping with DevKit, 200mA active and 38mA sleep with D1 Mini | |
The pinout information below is using the port numbers in the body of the previous pictures (D0-D8, A0/AD0, TX, RX, RST). | |
PlanTower PMS5003 dust sensor (PMS7003 has the same signals, but a different connector) | |
PlanTower pin Micro pin | |
1 VCC 5V | |
2 GND GND | |
3 SET D4 (could also use D0 or D3) | |
4 RX not used | |
5 TX RxD (see below) | |
6 RST (connect to RST) | |
The PlanTower TX pin is connected to the UART RxD pin on the module. This pin also connects to the USB/Serial chip to receive data from the USB port. There is a 300 to 470 ohm resistor between the USB chip and the pin, so we overdrive the level right at the ESP8266 pin. The only problem this causes is that the PlanTower TX pin must be disconnected to reflash the board from the USB port. The ESP board also supports OTA reflash, and I've included support for 'basic OTA' which works without disconnecting the PlanTower TX output pin. Moving the TX pin to another GPIO pin would also work, but would require the software serial library, which might break the serial read as it appears to be fairly sensitive to timing. | |
In active mode, the sensor takes a reading every 2.3 seconds, but it reports readings every second, frequently duplicating the previous reading. The only way to filter out the duplicates is to ignore 2 of every 3 reported samples, which increases the read time to ~300 seconds. The PMS7003 acts the same. | |
I tried Deep-sleep instead of delay() between cycles, but it only saved 10-15mA of total power and required D0 to wake the micro. Deep-sleep also required a FET buffer on the PlanTower SET line (above) as the GPIO pins only have a few microamps of sink/source current during Deep-sleep, and the strong pull-up resistor in the PlanTower sensor SET was pulling the pin up, turning itself on. | |
The pinout information below is using the port numbers in the body of the previous pictures (D0-D8, A0/AD0, TX, RX, RST). | |
Bosch BME280 Temperature, Humidity & Pressure sensor | |
GY-BME280 pin Micro pin | |
1 VCC 3V3 (this board is NOT 5V tolerant!) | |
2 GND GND | |
3 SDL D1 (default I2C pin) (could use any free GPIO) | |
4 SDA D2 (default I2C pin) (could use any free GPIO) | |
5 CSB (left unconnected, pulled up, can connect to 3V3) | |
6 SDO (left unconnected, can connect to GND or 3V3 to set I2C address) | |
The Bosch BME280 sensor is a 3.3V part. Some boards have an on-board regulator and level shifters so that you can use them in 3.3V or 5V applications. The GY-BME280 board above is very simple, and only works with 3.3V. Since the micro is 3.3V, this is ideal and wastes less power. | |
I've configured the Bosch sensor for I2C by leaving the CSB pin floating (pulled up on the board). The chip also supports SPI, although it needs 2 more I/O pins. | |
There is not much use for air pressure sensing, unless you can reference it to the air outside the cleanroom to measure the over-pressure in the cleanroom. The air pressure is reported over the USB serial monitor, but not used elsewhere. | |
To match the reading from my IR temperature sensor, I had to subtract 3'F/2C from the Bosch temp reading in the program. I only have one BME280, and I'm not certain if they all read high on temperature. If you let the Bosch sensor run in 'normal' mode, it heats itself up taking rapid measurements, making the temperature reading higher. I programmed the sensor for 'forced' mode, which only takes one measurement each sample cycle. This minimizes the self-heating effect. | |
Bosch has a more expensive sensor (BME680) that also detects VOCs (Volatile Organic Compounds) like the alcohol and acetone we use in our cleanroom, but the air in our cleanroom is exchanged so frequently that it's impossible to get a hazardous concentration of fumes. The BME680 has a warm-up time on the VOC sensor as it runs a hot plate for 100ms at 300'C. I'm not sure what the long-term accuracy or reliability is of something running that hot. | |
In the program definitions, please set Metric = true for use outside the USA. | |
The pinout information below is using the port numbers in the body of the previous pictures (D0-D8, A0/AD0, TX, RX, RST). | |
Aosong DHT11 & DHT22 Temperature & Humidity sensors | |
Aosong says the sensor will run from 3.3V to 5.5V, although a number of web sources say it's 3.5V minimum. I have the lower-resolution DHT11 (1'C resolution vs. 0.1'C for the DHT22) and it runs with 3.3V. If you use this sensor, connect the DATA pin to D2 (GPIO4) and change the define at the top of the program. You can float the NC pin or connect it to VCC. | |
Note: the only library I've found that won't pollute your project with GPL licensing is the Adafruit library, and it continues to have issues with the ESP8266, see | |
https://github.com/adafruit/DHT-sensor-library/issues/116 | |
Therefore, I can't recommend using this family of sensors; use the BME280 instead. | |
In the program definitions, please set Metric = true for use outside the USA. | |
The pinout information below is using the port numbers in the body of the previous pictures (D0-D8, A0/AD0, TX, RX, RST). | |
Optional LCD display (2.2 inch 240x320 TFT display with ILI9341 controller). The ILI9341 is compiled in the code, but the program runs without it connected - there is no check for it, only blind writes. The SPI MISO pin is currently unused. You can change the logo to anything you wish. :-) | |
Pins D5...D8 are the hardware SPI port. Using SPI-OVERLAP you can also use the flash memory SPI port on the ESP-12F DevKit module. Depending on the LCD used, you might need to use D0 for the Data/Command pin on some LCDs. With an I2C display you can use any 2 of the available pins (D0-D2, D5-D8), although I2C displays will have a slower drawing rate. | |
I tried connecting the backlight control pin directly to PMS_SET; it pulled the line down to 2V with a 1K pull-up, and I heard the fan in the dust sensor run slower with that voltage. I added a 33K resistor from the PMS_SET pin to the LCD BACKLIGHT pin, and that worked OK for both PlanTower sensor and LCD. | |
http://www.barth-dev.de/online/rgb565-color-picker/ << to convert colors to RGB565 mode | |
http://www.rinkydinkelectronics.com/t_imageconverter565.php << handy image converter for the logo | |
(the 320x75 logo uses ~ 4% of the available code space) | |
The onboard SD Card socket is used to log data and to store the WiFi configuration. This is safer than letting the ESP8266 write to flash with each WiFi CONNECT or DISCONNECT as people have reported dead flash with numerous writes. | |
For switch input(s), use AD0 (analog input) & multiple resistors. The pin on the ESP8266 only allows 1V maximum input, but most boards have a 2.2:1 resistor divider (220K/100K) so you can read 0-3.3V levels. If you are not sure, put 0.9VDC on the AD0 pin and do an analog read. With 10 bits resolution, if you see a reading ~920 there is NO divider and you must be careful to not exceed 1V signals. If it's ~260 then there is a divider on the board and you can use 0-3.3V signals. Currently the OTA switch will bring up a web configuration page for various parameters, as well as allowing OTA reflash. | |
Software design: | |
The project is written for the Arduino environment v1.8.8 or hither with the ESP8266 board library v2.5.2 and Python 2.7.1x (needed for OTA; check the box during install to let it add itself to your path) | |
https://www.arduino.cc/en/Main/Software | |
https://www.python.org/downloads/ | |
The header lists all of the github repositories I've borrowed code from, including licenses. | |
I started with Viktor Lytsus' demcusensor code and it worked with a little modification. As I looked deeper I found many errors. Finally I replaced his routines interfacing to the PlanTower sensor and only kept the routines that connect WiFi and upload the data. Viktor's code was reading all zeroes for the raw particle counts every 3 to 10 samples, and that was unusable in something meant to read very low particle counts. | |
The serial routine to read the PlanTower sensor from Martin Falatic was solid, once I converted it to work with Arduino & ESP8266. I've looked at many hundreds of samples and never seen anything that was bad, except for the duplicated readings from the PlanTower sensor. | |
I did IFDEF to select one of the two temperature / humidity sensors because they were both using the same data pin, and I didn't want the I2C routines crashing into the DHT11 software serial read. I recommend BME280. | |
You can add some security or comment out the OTA (Over The Air re-flash) statements if security is a concern, but you will have to disconnect the TX pin from the PlanTower sensor to flash the board over USB. You may need to install the Bonjour Browser and have it running while the Arduino IDE is up to see the OTA network port for upload. I didn't need Bonjour, but other people do. https://hobbyistsoftware.com/bonjourbrowser | |
Once the port appears in the Bonjour browser, it shows in the Arduino IDE and you can upload. This is a bug in Arduino IDE versions after 1.8.5, older versions showed the network port consistently. | |
https://github.com/arduino/Arduino/issues/8408 | |
Here's a breakdown of the program execution: | |
01) initialize all of the data types and constants | |
02) decide whether we compiled with the Bosch or DHT22 sensor and initialize the routine | |
03) start the hardware serial port, and then read temperature and humidity to initialize the sensor | |
04) initialize the WiFi & SNTP routines (NTP is not strictly required for this design) | |
main loop: | |
05) turn on PlanTower sensor and WiFi + OTA (it takes 30 seconds for the dust sensor to stabilize) | |
06) connect to WPA-secured router | |
07) check if we need to do an OTA reflash | |
08) read temperature and humidity | |
09) wait <30 seconds (stabilize) & read 100 valid samples from the PlanTower sensor, cumulative | |
10) establish a web connection to thingspeak.com | |
11) upload the environmental data in one long string | |
12) wait for the web connection to close (required by Thingspeak) | |
13) power down the PlanTower sensor, LCD and Wifi, then sleep for 30 minutes | |
14) loop to step 05 | |
The code runs with the LCD disconnected, but hangs without the BME280 sensor. | |
I'm not a programmer, but I can modify other people's code. :-) | |
Other observations and enhancements: | |
Based on studies of the PlanTower sensor compared to reference particle detectors, the 5um and 10um particle counts are estimated solely on the theoretical distribution curve from the smaller particle sizes, and not from actual counts. The detector apparently does NOT measure the larger particle sizes at all, and will report low particle counts if the dust contains only larger particles. For a cleanroom that's fine, as the larger particle sizes are only used to calculate ISO Class 6 (Fed Class 1000) and higher classes with this sensor. | |
Here is one article where they prove the large bin particle count errors: https://www.researchgate.net/publication/320555036_Particle_Distribution_Dependent_Inaccuracy_of_the_Plantower_PMS5003_low-cost_PM-sensor | |
The HEPA filters in the cleanroom should keep the large particle counts down to nearly unmeasurable levels. Both the study above and one done at AQICN.org show that the PlanTower sensor compares fairly well with more expensive particle counters. https://aqicn.org/sensor/pms5003-7003/ | |
I haven't implemented it yet, but it's simple add the code to send an email when the particle counts exceed some specified limit, for instance if the particle counts suddenly exceed ISO Class 6 / Fed Class 1,000. There are several examples on the Internet of a ESP8266 sending SMTP (Simple Mail Transport Protocol) messages when an undesired condition exists. To keep from flooding your e-mail with the same message every half hour, I'd recommend checking the time and only send one ALERT message every 12 hours, maybe once per day. You might consider very low or excessive humidity in the cleanroom another reason to send an alert, or possibly for temperature out of range (failure of the room air conditioner or thermostat). | |
If you want to continuously monitor several locations within a cleanroom I might recommend using the MQTT messaging protol, and only allow the MQTT broker to send messages or composite results outside the cleanroom. Doing that would reduce the traffic on your network, instead of having 5 to 10 sensors each asynchronously messaging every half hour. The MQTT broker could let you know that one of the sensors has gone off-line and has not reported in a while. The MQTT broker might be the local time server for the other modules. | |
I've googled for days and not found an LDAP implementation that will run on the ESP8266. That means it will not connect through the work network unless you use a GUEST WiFi. :-( With the MQTT suggestion above, the broker would have to be a Raspberry Pi, as that has the full Linux suite and can do LDAP. | |
At one sample every 30 minutes Thingspeak will show 1 day worth of data. Another option is to upload the readings to a Googledrive Excel spreadsheet, although the size of the data becomes a problem. There are several examples of Google Sheets on the Internet. However, without LDAP this seems futile. | |
Saving the data locally could be SD card or I2C flash chip. 2 weeks of samples in CSV form is ~47Kbytes. For data compression using a small I2C flash chip, I'd recommend: | |
unix timestamp (4 bytes works until 2038) | |
temperature (1 byte, range scaled for 1/2 degree accuracy) | |
humidity (1 byte, 0-100% 1/2 digit accuracy) | |
ISO class (1 byte, only needs a nibble) | |
raw 0.3um (3 bytes) | |
raw 0.5um (3 bytes) | |
raw 1.0um (3 bytes) | |
raw 5.0um (3 bytes) (since this is estimated, you could safely omit this reading, 16 instead of 19 bytes total) | |
A 256Kbyte flash chip could store 340 days of data with 30 minute sample intervals. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/******************************* | |
PMS5003 & BME280 Particle Counter, temp, pressure & humidity application for cleanroom sanity-check | |
This reads raw particle counts per .1 1iter sample (100 samples) plus environment and sends to cloud | |
application using WIFI connection | |
Libraries used: | |
https://github.com/vlytsus/demcusensor (originally the main code, but has many bugs) (no license) | |
https://github.com/MartyMacGyver/PMS7003-on-Particle (better serial handling) (Apache license) | |
https://github.com/sparkfun/SparkFun_BME280_Arduino_Library (MIT license) | |
https://github.com/beegee-tokyo/DHTesp (DHT22 alternate to BME280) (GPL license) :-( | |
https://github.com/Bodmer/TFT_eSPI (supports SPI_OVERLAP) (MIT, BSD & freeBSD licenses) | |
*******************************/ | |
#include <Arduino.h> | |
#include <ESP8266WiFi.h> // the main WiFi routines | |
#include <ArduinoOTA.h> // the main OTA routines | |
#include <ESP8266mDNS.h> // needed for OTA from the IDE | |
#include <WiFiUdp.h> // more OTA stuff, also used for sNTP? | |
#include <time.h> // time() ctime() | |
#include <sys/time.h> // struct timeval | |
#include <coredecls.h> // settimeofday_cb() if you need to set a RTC chip | |
#include <Wire.h> // I2C library for BME280 | |
#include <SparkFunBME280.h> // library for BME280 | |
#include "DHTesp.h" // library for DHT11/DHT22, not recommended | |
#include <TFT_eSPI.h> // Graphics and font library for LCDs / TFTs | |
#include <SPI.h> // SPI library needed for LCD | |
//#include <BlockDriver.h> // the next 7 includes are all for the embedded SDfat library, TODO | |
//#include <FreeStack.h> | |
//#include <MinimumSerial.h> | |
//#include <SdFat.h> | |
//#include <SdFatConfig.h> | |
//#include <sdios.h> | |
//#include <SysCall.h> | |
#include "logoPrime.h" // this logo uses 4% of the code space, plenty left over | |
#define DEBUG // prints additional information to the USB port | |
#ifdef DEBUG | |
#define DEBUG_PRINTLN(x) Serial.println(x) | |
#define DEBUG_PRINTHEX(x) printHex(x) | |
#define DEBUG_PRINT(x) Serial.print(x) | |
#else | |
#define DEBUG_PRINTLN(x) | |
#define DEBUG_PRINTHEX(x) | |
#define DEBUG_PRINT(x) | |
#endif | |
const char* CurrVersion = "09-19-2019b"; // displayed during TFT INITIALIZING and in Thingspeak STATUS update after boot | |
#define BMEsensor // define either DHTsensor or BMEsensor depending on temp/humidity sensor used | |
bool Metric = false; // set TRUE outside the USA for centigrade & pascals/millibars | |
const char* host = "api.thingspeak.com"; | |
const char* CLOUD_APPLICATION_ENDPOINT = "update?api_key=xxxxxxxxxxxxxxxx"; //replace with your ThingSpeak Write API Key | |
const char* ssid = "your router's SSID"; | |
const char* password = "your router's password"; | |
const int SLEEP_TIME = 30 * 60; // sleep between connections, minutes * seconds, subtract ~5 minutes 15 seconds for the sample loop | |
#define HTTP_TIMEOUT 20000UL // maximum http response wait period, sensor disconnects if no response | |
#define PMSSET D4 // PMS 'SET' pin on D4, GPIO2 (enable HIGH turns on the dust sensor and LCD backlight) | |
#define MIN_WARM_TIME 30000UL // 30s warm-up period required for sensor to enable fan and prepare air chamber | |
unsigned long timeout = 0; // has to be unsigned long to cover the wrap case with millis() | |
uint8_t incomingByte = 0; // for incoming serial data | |
const int MAX_FRAME_LEN = 64; | |
char frameBuf[MAX_FRAME_LEN]; | |
int detectOff = 0; | |
uint8_t frameLen = MAX_FRAME_LEN; | |
bool inFrame = false; | |
uint16_t sample = 0; //good sample (of totl samples) | |
uint8_t dupe = 0; //used to throw away 2 of every 3 samples :-( | |
uint16_t totl = 0; //total number of samples, including errcode and checksum mismatch | |
unsigned long pmRAW03 = 0; // number of particles > 0.3um in 10 liters of air | |
unsigned long pmRAW05 = 0; // number of particles > 0.5um in 10 liters of air | |
unsigned long pmRAW10 = 0; // number of particles > 1um in 10 liters of air | |
unsigned long pmRAW25 = 0; // number of particles > 2.5um in 10 liters of air | |
unsigned long pmRAW50 = 0; // number of particles > 5um in 10 liters of air | |
unsigned long pmRAW100 = 0; // number of particles > 10um in 10 liters of air (currently unused) | |
uint8_t ISOclass = 9; // from the 1999 ISO 14644-1 table | |
uint16_t calcChecksum = 0; | |
struct PMS7003_framestruct { | |
uint8_t frameHeader[2]; | |
uint16_t frameLen = MAX_FRAME_LEN; | |
uint16_t concPM1_0_CF1; // the CF1 samples are only for factory cal | |
uint16_t concPM2_5_CF1; | |
uint16_t concPM10_0_CF1; | |
uint16_t concPM1_0_amb; // the amb samples are concentration (unit) ug/m3 | |
uint16_t concPM2_5_amb; | |
uint16_t concPM10_0_amb; | |
uint16_t rawGt0_3um; // raw particle counts, 0.3um, 0.5um, 1um, 2.5um, 5um, 10um per 0.1 liter | |
uint16_t rawGt0_5um; | |
uint16_t rawGt1_0um; | |
uint16_t rawGt2_5um; | |
uint16_t rawGt5_0um; | |
uint16_t rawGt10_0um; | |
uint8_t vers; | |
uint8_t errorCode; | |
uint16_t checksum; | |
} thisFrame; | |
bool firstLoop; // first time thru after any reset | |
timeval cbtime; // when time set callback was called | |
bool cbtime_set = false; | |
timeval tv; | |
struct timezone tz; | |
timespec tp; | |
time_t tnow; | |
// callback function that is called whenever time is set, could be used to update external RTC chip | |
void time_is_set (void) // currently unused, warning: no delay(), yield() or heavy code in here | |
{ | |
gettimeofday(&cbtime, NULL); | |
cbtime_set = true; | |
} | |
#define UTFT_FORMAT | |
#define PIXEL_BUFFER_SIZE 512 // Must be an integer multiple of 4, must be more than 54 | |
#define TFT_NEONPINK 0xFB39 // color for the status line | |
char StatusUpdate[25]; | |
uint16_t oldX = 0; // cursor position before the bottom line status update | |
uint16_t oldY = 0; | |
TFT_eSPI tft = TFT_eSPI(); // Invoke TFT library | |
float humidity = 0; | |
float temperature = 0; | |
#ifdef BMEsensor | |
BME280 mySensor; | |
#endif | |
#ifdef DHTsensor | |
DHTesp dht; | |
#endif | |
void setup() { | |
firstLoop = true; // first line hit after WDT reset, globals are NOT set back to default! | |
Serial.begin(9600); // use serial0 for debug info output port & dust sensor TX pin (input) | |
Serial.setTimeout(1500); // set the timeout to 1500ms, longer than the data transmission time of the dust sensor | |
pinMode(PMSSET, OUTPUT_OPEN_DRAIN); | |
digitalWrite(PMSSET, HIGH); // turn on dust sensor at PMS5003 'SET' pin, as well as the LCD backlight | |
tft.begin(); | |
tft.init(); | |
tft.setRotation(3); // 1 & 3 are landscape, 2 & 4 are portrait | |
tft.fillScreen(TFT_BLACK); | |
// Draw the logo | |
tft.setSwapBytes(true); // Swap the color byte order when rendering | |
// tft.pushImage(82, 30, 154, 56, logoAlt); // (xpos, ypos, width, height, file) | |
tft.pushImage(0, 0, 320, 75, logoPrime); // (xpos, ypos, width, height, file) | |
tft.setCursor(15, 140, 4); // set the cursor just below center screen with font 4 | |
tft.setTextColor(TFT_CYAN, TFT_BLACK); | |
tft.println(" Initializing, please wait..."); | |
tft.setTextColor(TFT_NEONPINK, TFT_BLACK); | |
tft.setCursor(0, 215, 4); // set the cursor to bottom line with font 4 | |
tft.print("f/w vers. "); | |
tft.println(CurrVersion); | |
#ifdef BMEsensor | |
Wire.begin(); // use D1 for SCL, D2 for SDA | |
Wire.setClock(100000); // standard I2C speed, fast speed = 400000 | |
#endif | |
delay(100); | |
Serial.println(""); // clear the junk from boot status, then output some possibly useful info | |
DEBUG_PRINTLN(F("Init started: DEBUG MODE")); | |
DEBUG_PRINT(F("Chip ID: esp8266-")); | |
DEBUG_PRINTHEX(ESP.getChipId()); | |
DEBUG_PRINTLN(); | |
DEBUG_PRINT(F("CPU freq: ")); | |
DEBUG_PRINT(ESP.getCpuFreqMHz()); | |
DEBUG_PRINTLN(F("MHz")); | |
DEBUG_PRINT(F("Flash Chip ID: ")); | |
DEBUG_PRINTHEX(ESP.getFlashChipId()); | |
DEBUG_PRINTLN(); | |
DEBUG_PRINT(F("Flash Chip Real Size: ")); | |
DEBUG_PRINTLN(ESP.getFlashChipRealSize()); | |
DEBUG_PRINT(F("Flash Chip IDE Size: ")); | |
DEBUG_PRINTLN(ESP.getFlashChipSize()); | |
DEBUG_PRINT(F("Flash Chip Speed: ")); | |
DEBUG_PRINTLN(ESP.getFlashChipSpeed()); | |
DEBUG_PRINT(F("Flash Chip Mode: ")); | |
DEBUG_PRINTLN(ESP.getFlashChipMode()); | |
DEBUG_PRINT(F("Core version: ")); | |
DEBUG_PRINTLN(ESP.getCoreVersion()); | |
DEBUG_PRINT(F("SDK version: ")); | |
DEBUG_PRINTLN(ESP.getSdkVersion()); | |
DEBUG_PRINT("Boot version: "); | |
DEBUG_PRINTLN(ESP.getBootVersion()); | |
DEBUG_PRINT(F("Boot mode: ")); | |
DEBUG_PRINTLN(ESP.getBootMode()); | |
DEBUG_PRINT(F("Reset reason: ")); | |
DEBUG_PRINTLN(ESP.getResetReason()); | |
DEBUG_PRINT(F("Sketch size/left: ")); | |
DEBUG_PRINT(ESP.getSketchSize()); | |
DEBUG_PRINT(F(" / ")); | |
DEBUG_PRINTLN(ESP.getFreeSketchSpace()); | |
DEBUG_PRINT(F("Sketch usage: ")); | |
DEBUG_PRINT((ESP.getFreeSketchSpace() + ESP.getSketchSize()) / ESP.getSketchSize()); | |
DEBUG_PRINTLN("%"); | |
DEBUG_PRINT(F("initial free Heap: ")); | |
DEBUG_PRINTLN(ESP.getFreeHeap()); | |
DEBUG_PRINTLN(); | |
DEBUG_PRINT(F("max free Heap block: ")); | |
DEBUG_PRINTLN(ESP.getMaxFreeBlockSize()); | |
DEBUG_PRINT(F("Heap fragmentation: ")); | |
DEBUG_PRINTLN(ESP.getHeapFragmentation()); | |
#ifdef BMEsensor | |
mySensor.settings.I2CAddress = 0x76; // default I2C address for GY-BME280, alt address is 0x77 with SDO = 1 | |
mySensor.setTempOverSample(2); // Set the temperature oversample mode | |
mySensor.setPressureOverSample(2); // Set the pressure oversample mode | |
mySensor.setHumidityOverSample(2); // Set the humidity oversample mode | |
mySensor.beginI2C(); | |
mySensor.setMode(MODE_SLEEP); // Sleep the BME280 for now, we have the data | |
DEBUG_PRINTLN(F("Bosch sensor initialized")); | |
if (Metric) { | |
temperature = mySensor.readTempC() - 2; | |
} else { | |
temperature = mySensor.readTempF() - 3; | |
} | |
humidity = mySensor.readFloatHumidity(); | |
#endif | |
#ifdef DHTsensor | |
dht.setup(4, DHTesp::DHT11); // Connect DHT sensor to GPIO4, D2 | |
delay(dht.getMinimumSamplingPeriod()); | |
DEBUG_PRINTLN(F("DHT11 sensor initialized")); | |
humidity = dht.getHumidity(); | |
temperature = dht.getTemperature(); | |
DEBUG_PRINT(dht.getStatusString()); | |
DEBUG_PRINT("\t"); | |
DEBUG_PRINT(humidity, 1); | |
DEBUG_PRINT("\t\t"); | |
DEBUG_PRINT(temperature, 1); | |
DEBUG_PRINT("\t\t"); | |
DEBUG_PRINT(dht.toFahrenheit(temperature), 1); | |
DEBUG_PRINT("\t\t"); | |
if (!Metric) temperature = dht.toFahrenheit(temperature); | |
#endif | |
/* Explicitly set the ESP8266 to be a WiFi-client, otherwise by default it | |
would try to act as both a client and an access-point and could cause | |
network-issues with other WiFi devices on your network. */ | |
WiFi.persistent(false); | |
WiFi.mode(WIFI_STA); | |
WiFi.begin(ssid, password); | |
DEBUG_PRINT(F("connecting to WIFI ")); | |
DEBUG_PRINTLN(ssid); | |
DEBUG_PRINT(F("my MAC: ")); | |
DEBUG_PRINTLN(WiFi.macAddress()); | |
while (WiFi.status() != WL_CONNECTED) { | |
delay(500); | |
} | |
DEBUG_PRINTLN(F("WiFi connected")); | |
while (!WiFi.localIP()) { | |
delay(100); | |
} | |
DEBUG_PRINT(F("WiFi Gateway IP: ")); | |
DEBUG_PRINTLN(WiFi.gatewayIP()); | |
DEBUG_PRINT(F("my IP address: ")); | |
DEBUG_PRINTLN(WiFi.localIP()); | |
if (millis() - timeout >= MIN_WARM_TIME) { // if we can't connect to WIFI for a long time | |
timeout = millis(); //reset timer | |
} | |
// set function to call when time is set | |
// is called by NTP code when NTP is used | |
// settimeofday_cb(time_is_set); | |
time_t rtc_time_t = 1541267183; // fake RTC time for now | |
timezone tz = { 0, 0}; | |
timeval tv = { rtc_time_t, 0}; | |
settimeofday(&tv, &tz); | |
// https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html | |
setenv("TZ", "CST+6CDT,M3.2.0/2,M11.1.0/2", 1); // This is Chicago/Dallas, for China/Taiwan use "CST-8" | |
tzset(); // save the TZ variable | |
configTime(0, 0, "pool.ntp.org"); | |
// don't wait, observe time changing when ntp timestamp is received | |
delay(1000); | |
DEBUG_PRINTLN(F("initial Network time acquired")); | |
// bug: note that the network time won't be correct until the next NTP update; this first one is wrong | |
} | |
template<typename T> | |
void printHex(T value) { | |
// pretty-print hex numbers with leading zeros, thanks to lloyddean at forum.arduino.cc for this | |
const size_t NUM_DIGITS = (2 * sizeof(value)); | |
char sz[NUM_DIGITS + 1] = { '0' }; | |
sz[NUM_DIGITS] = 0; | |
for ( size_t i = NUM_DIGITS; i--; value /= 16) { | |
sz[i] = "0123456789ABCDEF"[(value % 16)]; | |
} | |
Serial.print(sz); | |
} | |
bool sendDataToCloud() { | |
// Use WiFiClient class to create TCP connections | |
bool uploadSuccess = false; | |
WiFiClient client; | |
client = WiFiClient(); // add this line for 2.4.1, https://github.com/esp8266/Arduino/issues/4497 | |
if (!client.connect("api.thingspeak.com", 80)) { | |
Serial.println(F("remote host connection failed")); | |
return false; | |
} | |
// create URI for request - could multiply particles by 100 for particles per cubic meter | |
String url = CLOUD_APPLICATION_ENDPOINT + | |
String("&field1=") + String(temperature, 1) + | |
"&field2=" + String(humidity, 1) + | |
"&field3=" + String(ISOclass) + | |
"&field4=" + String(pmRAW03) + | |
"&field5=" + String(pmRAW05) + | |
"&field6=" + String(pmRAW10) + | |
"&field7=" + String(pmRAW25) + | |
"&field8=" + String(pmRAW50); | |
if (firstLoop) // is this is the first loop since boot? | |
{ | |
url += "&status=boot vers. " + String(CurrVersion) + " reset reason: " + String(ESP.getResetReason()); | |
} else { | |
url += "&status=free heap = " + String(ESP.getFreeHeap()); // put whatever else you want to monitor each loop here | |
} | |
DEBUG_PRINTLN("Requesting GET: " + url); | |
// This will send the request to the server | |
client.print(String("GET /") + url + " HTTP/1.1\r\n" + | |
"Host: " + host + "\r\n" + | |
"Accept: */*\r\n" + | |
"User-Agent: Mozilla/4.0 (compatible; esp8266 Lua; Windows NT 5.1)\r\n" + | |
"Connection: close\r\n" + | |
"\r\n"); | |
client.flush(); | |
delay(10); | |
DEBUG_PRINTLN(F("wait for response")); | |
timeout = millis(); | |
while (client.available() == 0) { | |
delay(200); | |
if (millis() - timeout >= HTTP_TIMEOUT) { | |
DEBUG_PRINTLN(F(">>> Client Timeout !")); | |
client.stop(); | |
DEBUG_PRINTLN(F("closing connection by timeout")); | |
return false; | |
} | |
} | |
// Read all the lines of the reply from the server and print them to Serial | |
while (client.available()) { | |
yield(); | |
String line = client.readStringUntil('\r'); | |
if (line == "HTTP/1.1 200 OK") uploadSuccess = true; | |
DEBUG_PRINT(line); | |
} | |
client.stop(); | |
DEBUG_PRINTLN(); | |
DEBUG_PRINTLN(F("closing connection")); | |
return uploadSuccess; | |
} | |
void tftstatus() { | |
// print a status update on the bottom line | |
tft.fillRect(0, 215, 319, 20, TFT_BLACK); //blank the bottom line | |
oldX = tft.getCursorX(); // save the current position of the cursor | |
oldY = tft.getCursorY(); | |
tft.setCursor(0, 215, 4); // set the cursor to bottom line with font 4 | |
tft.setTextColor(TFT_NEONPINK, TFT_BLACK); | |
tft.println(StatusUpdate); | |
tft.setCursor(oldX, oldY, 4); // set the cursor back where we were | |
} | |
bool pms7003_read() { | |
// send data only when you receive data from the dust sensor: | |
bool packetReceived = false; | |
while (!packetReceived) { // up to 1 second blocking function | |
yield(); | |
if (Serial.available() > 32) { | |
int drain = Serial.available(); | |
for (int i = drain; i > 0; i--) { | |
Serial.read(); | |
} | |
} | |
if (Serial.available() > 0) { | |
incomingByte = Serial.read(); | |
if (!inFrame) { | |
if (incomingByte == 0x42 && detectOff == 0) { | |
frameBuf[detectOff] = incomingByte; | |
thisFrame.frameHeader[0] = incomingByte; | |
calcChecksum = incomingByte; // Checksum init! | |
detectOff++; | |
} | |
else if (incomingByte == 0x4D && detectOff == 1) { | |
frameBuf[detectOff] = incomingByte; | |
thisFrame.frameHeader[1] = incomingByte; | |
calcChecksum += incomingByte; | |
inFrame = true; | |
detectOff++; | |
} | |
else { | |
DEBUG_PRINT(F("-- Frame syncing... ")); | |
DEBUG_PRINTHEX(incomingByte); | |
DEBUG_PRINTLN(); | |
} | |
} | |
else { | |
frameBuf[detectOff] = incomingByte; | |
calcChecksum += incomingByte; | |
detectOff++; | |
unsigned int val = frameBuf[detectOff - 1] + (frameBuf[detectOff - 2] << 8); | |
switch (detectOff) { | |
case 4: | |
thisFrame.frameLen = val; | |
frameLen = val + detectOff; | |
break; | |
case 6: | |
thisFrame.concPM1_0_CF1 = val; | |
break; | |
case 8: | |
thisFrame.concPM2_5_CF1 = val; | |
break; | |
case 10: | |
thisFrame.concPM10_0_CF1 = val; | |
break; | |
case 12: | |
thisFrame.concPM1_0_amb = val; | |
break; | |
case 14: | |
thisFrame.concPM2_5_amb = val; | |
break; | |
case 16: | |
thisFrame.concPM10_0_amb = val; | |
break; | |
case 18: | |
thisFrame.rawGt0_3um = val; | |
break; | |
case 20: | |
thisFrame.rawGt0_5um = val; | |
break; | |
case 22: | |
thisFrame.rawGt1_0um = val; | |
break; | |
case 24: | |
thisFrame.rawGt2_5um = val; | |
break; | |
case 26: | |
thisFrame.rawGt5_0um = val; | |
break; | |
case 28: | |
thisFrame.rawGt10_0um = val; | |
break; | |
case 29: | |
val = frameBuf[detectOff - 1]; | |
thisFrame.vers = val; | |
break; | |
case 30: | |
val = frameBuf[detectOff - 1]; | |
thisFrame.errorCode = val; | |
break; | |
case 32: | |
thisFrame.checksum = val; | |
calcChecksum -= ((val >> 8) + (val & 0xFF)); | |
break; | |
default: | |
break; | |
} | |
if (detectOff >= frameLen) { | |
/* DEBUG_PRINT(F("PMS5003 [")); | |
DEBUG_PRINTHEX(thisFrame.frameHeader[0]); | |
DEBUG_PRINT(" "); | |
DEBUG_PRINTHEX(thisFrame.frameHeader[1]); | |
DEBUG_PRINT(F("] (")); | |
DEBUG_PRINTHEX(thisFrame.frameLen); | |
DEBUG_PRINT(F(") CF1=[")); | |
DEBUG_PRINTHEX(thisFrame.concPM1_0_CF1); | |
DEBUG_PRINT(" "); | |
DEBUG_PRINTHEX(thisFrame.concPM2_5_CF1); | |
DEBUG_PRINT(" "); | |
DEBUG_PRINTHEX(thisFrame.concPM10_0_CF1); | |
DEBUG_PRINT(F("] amb=[")); | |
DEBUG_PRINTHEX(thisFrame.concPM1_0_amb); | |
DEBUG_PRINT(" "); | |
DEBUG_PRINTHEX(thisFrame.concPM2_5_amb); | |
DEBUG_PRINT(" "); | |
DEBUG_PRINTHEX(thisFrame.concPM10_0_amb); | |
DEBUG_PRINT(F("] raw=[")); | |
DEBUG_PRINTHEX(thisFrame.rawGt0_3um); | |
DEBUG_PRINT(" "); | |
DEBUG_PRINTHEX(thisFrame.rawGt0_5um); | |
DEBUG_PRINT(" "); | |
DEBUG_PRINTHEX(thisFrame.rawGt1_0um); | |
DEBUG_PRINT(" "); | |
DEBUG_PRINTHEX(thisFrame.rawGt2_5um); | |
DEBUG_PRINT(" "); | |
DEBUG_PRINTHEX(thisFrame.rawGt5_0um); | |
DEBUG_PRINT(" "); | |
DEBUG_PRINTHEX(thisFrame.rawGt10_0um); | |
DEBUG_PRINT("] ver="); | |
DEBUG_PRINTHEX(thisFrame.vers); | |
DEBUG_PRINT(F(" err=")); | |
DEBUG_PRINTHEX(thisFrame.errorCode); | |
DEBUG_PRINT(F(" csum=")); | |
DEBUG_PRINTHEX(thisFrame.checksum); | |
DEBUG_PRINT(calcChecksum == thisFrame.checksum ? " == " : " != "); | |
DEBUG_PRINT(F("xsum=")); | |
DEBUG_PRINTHEX(calcChecksum); | |
DEBUG_PRINTLN(); */ | |
packetReceived = true; | |
detectOff = 0; | |
inFrame = false; | |
} | |
} | |
} | |
} | |
return (calcChecksum == thisFrame.checksum); | |
} | |
void loop() { | |
DEBUG_PRINTLN(F("power on sensors & LCD, then connect WiFi")); | |
timeout = millis(); | |
unsigned long SampleStart = millis(); | |
digitalWrite(PMSSET, HIGH); // turn on dust sensor & LCD at PMS5003 'SET' pin | |
// let the dust sensor fan blow air over the temp/humid sensor while WiFi connects so we get a valid reading and not stale air | |
tft.writecommand(ILI9341_DISPON); // wake up TFT if we slept it | |
tft.writecommand(ILI9341_SLPOUT); | |
delay(5); | |
tft.setCursor(0, 0, 4); // set the cursor to 0,0 with font 4 | |
tft.fillScreen(TFT_BLACK); | |
tft.setTextColor(TFT_CYAN, TFT_BLACK); | |
tft.println("Cleanroom Particle Counter"); | |
strcpy(StatusUpdate, "connecting WiFi"); | |
tftstatus(); | |
DEBUG_PRINTLN(F("connecting WiFi")); | |
WiFi.forceSleepWake(); | |
delay(1); // it doesn't wake up until control briefly returns to the WiFi section, delay or yield needed | |
WiFi.persistent(false); // don't write the connection data to Flash each time | |
WiFi.mode( WIFI_STA ); | |
WiFi.begin(ssid, password); | |
while (WiFi.status() != WL_CONNECTED) { | |
delay(500); | |
} | |
Serial.print(F("connected to WIFI ")); | |
Serial.println(ssid); | |
Serial.print(F("my MAC: ")); | |
Serial.println(WiFi.macAddress()); | |
while (!WiFi.localIP()) { | |
delay(100); | |
} | |
Serial.print(F("my IP address: ")); | |
Serial.println(WiFi.localIP()); | |
/* The next section is the OTA setup | |
Port defaults to 8266, ArduinoOTA.setPort(8266); | |
Hostname defaults to esp8266-[ChipID], or use ArduinoOTA.setHostname("myesp8266"); | |
No authentication by default, ArduinoOTA.setPassword((const char *)"123"); */ | |
ArduinoOTA.onStart([]() { | |
String type; | |
if (ArduinoOTA.getCommand() == U_FLASH) { | |
type = "sketch"; | |
} else { // U_SPIFFS | |
type = "filesystem"; | |
} | |
// NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() | |
Serial.println("Start updating " + type); | |
}); | |
ArduinoOTA.onEnd([]() { | |
Serial.println("\nEnd"); | |
}); | |
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { | |
Serial.printf("Progress: %u%%\r", (progress / (total / 100))); | |
}); | |
ArduinoOTA.onError([](ota_error_t error) { | |
Serial.printf("Error[%u]: ", error); | |
if (error == OTA_AUTH_ERROR) Serial.println(F("Auth Failed")); | |
else if (error == OTA_BEGIN_ERROR) Serial.println(F("Begin Failed")); | |
else if (error == OTA_CONNECT_ERROR) Serial.println(F("Connect Failed")); | |
else if (error == OTA_RECEIVE_ERROR) Serial.println(F("Receive Failed")); | |
else if (error == OTA_END_ERROR) Serial.println(F("End Failed")); | |
}); | |
ArduinoOTA.begin(); // start the OTA function | |
yield(); | |
ArduinoOTA.handle(); // see if we need to reflash, end of OTA setup | |
yield(); | |
Serial.print(F("Samples begun ")); // bug: not checking to insure we have NTP sync | |
tnow = time(nullptr); | |
char* myctime; | |
myctime = ctime(&tnow); | |
int len_of_new_line = strlen(myctime) - 1; | |
myctime[len_of_new_line] = '\0'; | |
Serial.println(myctime); | |
strcpy(StatusUpdate, myctime); | |
tftstatus(); | |
DEBUG_PRINT(F("Current Unix time ")); | |
DEBUG_PRINTLN(tnow); | |
delay(4000); | |
#ifdef BMEsensor | |
DEBUG_PRINTLN(F("Reading Bosch sensor")); | |
mySensor.setMode(MODE_FORCED); // wake up Bosch sensor and take reading | |
while (mySensor.isMeasuring() == false) ; // wait for sensor to start measurment | |
while (mySensor.isMeasuring() == true) ; // hang out while sensor completes the reading | |
// sensor is now back asleep but we have the data | |
if (Metric) { | |
temperature = mySensor.readTempC() - 2; // correction for sensor error | |
} else { | |
temperature = mySensor.readTempF() - 3; // correction for sensor error | |
} | |
humidity = mySensor.readFloatHumidity(); | |
DEBUG_PRINT(F("Corrected Temp: ")); | |
DEBUG_PRINT(temperature); | |
if (Metric) { | |
DEBUG_PRINTLN("'C"); | |
} else { | |
DEBUG_PRINTLN("'F"); | |
} | |
DEBUG_PRINT(F("Humidity: ")); | |
DEBUG_PRINT(mySensor.readFloatHumidity()); | |
DEBUG_PRINTLN(F("% RH")); | |
DEBUG_PRINT(F("Corrected Pressure: ")); | |
if (Metric) { | |
DEBUG_PRINT((mySensor.readFloatPressure() / 100) + 14.2); | |
DEBUG_PRINTLN(F(" millibars")); | |
} else { | |
DEBUG_PRINT((mySensor.readFloatPressure() / 3386.4) + .42); | |
DEBUG_PRINTLN(F("in Hg")); | |
} | |
#endif | |
yield(); | |
#ifdef DHTsensor | |
temperature = dht.getTemperature(); | |
if (!Metric) temperature = dht.toFahrenheit(temperature) - 2; // correction for sensor error | |
humidity = dht.getHumidity(); | |
#endif | |
yield(); | |
DEBUG_PRINTLN(); | |
temperature += .05; // only save the first decimal point | |
humidity += .05; | |
String prTemp = String(temperature, 1); | |
String prHumid = String(humidity, 1); | |
tft.setTextColor(TFT_WHITE, TFT_BLACK); | |
tft.print(" Temp = "); | |
tft.print(prTemp); | |
if (Metric) { | |
tft.print("'C RH = "); | |
} else { | |
tft.print("'F RH = "); | |
} | |
tft.print(prHumid); | |
tft.println("%"); | |
timeout = MIN_WARM_TIME - (millis() - timeout); | |
if (timeout < MIN_WARM_TIME) { | |
strcpy(StatusUpdate, "stabilizing dust sensor"); | |
tftstatus(); | |
DEBUG_PRINT(F("dust sensor warm-up: ")); | |
DEBUG_PRINT(timeout / 1000); | |
DEBUG_PRINTLN(F(" seconds until stable reading")); | |
delay(timeout); | |
} | |
if (!pms7003_read()) { | |
Serial.println(F("DUST SENSOR ERROR!")); | |
delay(800); | |
} | |
pmRAW03 = 0; | |
pmRAW05 = 0; | |
pmRAW10 = 0; | |
pmRAW25 = 0; | |
pmRAW50 = 0; | |
pmRAW100 = 0; | |
sample = 0; // sample n of 100 samples | |
totl = 0; // total number of samples including frames with errors | |
yield(); | |
tft.fillRect(0, 215, 319, 20, TFT_BLACK); | |
tft.setTextColor(TFT_NEONPINK, TFT_BLACK); | |
tft.setCursor(0, 215, 4); // set the cursor to bottom line with font 4 | |
tft.print("Samples remaining = "); | |
while ( sample < 100 ) { | |
pms7003_read(); // update rate is 1s, but actual data update frequency is up to 2.3s, so lots of duplicate data | |
pms7003_read(); | |
pms7003_read(); // only keep the last sample, as 1 out of 3 must be unique data | |
ArduinoOTA.handle(); // see if we need to reflash | |
yield(); | |
if (calcChecksum == thisFrame.checksum && thisFrame.errorCode == 0) { | |
pmRAW03 += thisFrame.rawGt0_3um; | |
pmRAW05 += thisFrame.rawGt0_5um; | |
pmRAW10 += thisFrame.rawGt1_0um; | |
pmRAW25 += thisFrame.rawGt2_5um; | |
pmRAW50 += thisFrame.rawGt5_0um; | |
pmRAW100 += thisFrame.rawGt10_0um; | |
sample++; | |
tft.setCursor(238, 215, 4); // set the cursor to bottom line with font 4 | |
tft.print(100 - sample); | |
tft.print(" "); | |
} | |
totl++; | |
} | |
yield(); | |
Serial.println(); | |
Serial.print(F("composite 0.3um particles in 10L air = ")); | |
Serial.println(pmRAW03); | |
Serial.print(F("composite 0.5um particles in 10L air = ")); | |
Serial.println(pmRAW05); | |
Serial.print(F("composite 1.0um particles in 10L air = ")); | |
Serial.println(pmRAW10); | |
Serial.print(F("composite 2.5um particles in 10L air = ")); | |
Serial.println(pmRAW25); | |
Serial.print(F("composite 5.0um particles in 10L air = ")); | |
Serial.println(pmRAW50); | |
Serial.print(F("composite 10um particles in 10L air = ")); | |
Serial.println(pmRAW100); | |
oldY += 4; | |
tft.setCursor(oldX, oldY, 4); // set the cursor to the third line down | |
tft.setTextColor(TFT_YELLOW, TFT_BLACK); | |
tft.print("particles > 0.3um = "); | |
tft.println(pmRAW03); | |
tft.print("particles > 0.5um = "); | |
tft.println(pmRAW05); | |
tft.print("particles > 1.0um = "); | |
tft.println(pmRAW10); | |
tft.print("particles > 5.0um = "); | |
tft.println(pmRAW50); | |
tft.print("particles > 10um = "); | |
tft.println(pmRAW100); | |
sample = 0; | |
totl = 0; | |
ISOclass = 9; | |
String FEDclass = "(Room Air)"; //class 1,000,000 | |
if (pmRAW05 < 352000 && pmRAW10 < 83200 && pmRAW50 < 2930) { | |
ISOclass = 8; | |
FEDclass = "(Class 100K)"; | |
} | |
if (pmRAW05 < 35200 && pmRAW10 < 8320 && pmRAW50 < 293) { | |
ISOclass = 7; | |
FEDclass = "(Class 10K)"; | |
} | |
if (pmRAW05 < 3520 && pmRAW10 < 832 && pmRAW50 < 29) { | |
ISOclass = 6; | |
FEDclass = "(Class 1000)"; | |
} | |
if (pmRAW03 < 1020 && pmRAW05 < 352 && pmRAW10 < 83) { | |
ISOclass = 5; | |
FEDclass = "(Class 100)"; | |
} | |
if (pmRAW03 < 102 && pmRAW05 < 35 && pmRAW10 < 8) { | |
ISOclass = 4; | |
FEDclass = "(Class 10)"; | |
} | |
if (pmRAW03 < 10 && pmRAW05 < 3 && pmRAW10 < 1) { | |
ISOclass = 3; | |
FEDclass = "(Class 1)"; | |
} | |
if (pmRAW03 < 1 && pmRAW05 < 1 && pmRAW10 < 1) { | |
ISOclass = 2; | |
FEDclass = "(< Class 1)"; | |
} | |
Serial.print(F("Currently ISO ")); | |
Serial.print(ISOclass); | |
Serial.print(F(", FED ")); | |
Serial.println(FEDclass); | |
Serial.println(); | |
tft.print("ISO Class = "); | |
tft.print(ISOclass); | |
tft.print(" "); | |
tft.print(FEDclass); | |
strcpy(StatusUpdate, "Sending data to cloud"); | |
DEBUG_PRINTLN(StatusUpdate); | |
tftstatus(); | |
// sendDataToCloud(); | |
yield(); | |
if (sendDataToCloud()) { | |
strcpy(StatusUpdate, "upload successful!"); | |
tftstatus(); | |
DEBUG_PRINTLN(F("Remote upload successful!")); | |
} else { | |
strcpy(StatusUpdate, "upload failed!"); | |
DEBUG_PRINTLN(F("Remote upload failed!")); | |
tftstatus(); | |
} | |
// delay(200); | |
// WiFi.disconnect(); //causes a WDT in the delay after WiFi.forceSleepBegin(); | |
delay(1000); | |
DEBUG_PRINT(F("free Heap after send to cloud = ")); | |
DEBUG_PRINTLN(ESP.getFreeHeap()); | |
DEBUG_PRINT(F("max free Heap block after send to cloud = ")); | |
DEBUG_PRINTLN(ESP.getMaxFreeBlockSize()); | |
DEBUG_PRINT(F("Heap fragmentation after send to cloud = ")); | |
DEBUG_PRINTLN(ESP.getHeapFragmentation()); | |
// turn off WiFi & LCD, Dust Sensor already turned off in main loop | |
strcpy(StatusUpdate, "going to sleep zzz..."); | |
tftstatus(); | |
delay(2000); | |
WiFi.mode(WIFI_OFF); | |
delay(1000); | |
WiFi.forceSleepBegin(); | |
yield(); | |
delay(3000); // give them several seconds to view the results | |
tft.writecommand(ILI9341_DISPOFF); // sleep the TFT, saves 8-10mA, comment out if you want to use a switch to turn the backlight on | |
tft.writecommand(ILI9341_SLPIN); | |
digitalWrite(PMSSET, LOW); // turn off the dust sensor & LCD, we're done with them | |
firstLoop = false; | |
unsigned long SampleEnd = millis(); | |
unsigned long SleepSecs = SLEEP_TIME - ((SampleEnd - SampleStart) / 1000); | |
if (SleepSecs > SLEEP_TIME) { // probably not needed since the switch from NTP to purely millis() for the loop timing | |
DEBUG_PRINTLN(F("NTP Clock error! Using default sleep time.")); | |
SleepSecs = SLEEP_TIME - 315; | |
} | |
DEBUG_PRINT(F("snoozing for ")); | |
DEBUG_PRINT(SleepSecs / 60); | |
DEBUG_PRINTLN(F(" minutes")); | |
DEBUG_PRINTLN(); | |
delay(SleepSecs * 1000); // sleep time in milliseconds, adjusted for loop time; chip idles at low current in delay() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment