// SDS011 Air Quality Monitor // -------------------------- // Optionally, used in conjunction with PC Server/plotter application at (c) vwlowen.co.uk // // Based on SDS011 Sensor libray by R. Zschiegner (rz@madavi.de). #include // https://platformio.org/lib/show/1563/SDS011%20sensor%20Library #include // https://github.com/adafruit/Adafruit-GFX-Library #include "Adafruit_ILI9341.h" // https://github.com/adafruit/Adafruit_ILI9341 #include #include #include #include #include const char* ssid = "abcdefg"; // Your WiFi SSID. const char* password = "*****"; // Your WiFi Password. String serverIP = "192.168.1.3:8802"; // The server IP address and Port number set up in the PC Server/plotter Application. String deviceId = "air_quality"; // The device ID that the PC server Application will recognize. //#define TFT_RST // (Not connected. Pull TFT RST HIGH with 10k resistor) //#define TFT_SCLK D5 // SCLK is explicit and must be connected to D5 (GPIO14) //#define TFT_MOSI D7 // MOSI is explicit and must be connected to D7 (GPIO13) #define TFT_CS D3 // GPIO0 #define TFT_DC D2 // GPIO4 #define SAVE_COUNTER 12 // Data is saved to EEPROM every SAVE_COUNTER * SAMPLE_INTERVAL minutes. (1 hour). #define SAMPLE_INTERVAL 5 // Take air sample every SAMPLE_INTERVAL minutes #define SAMPLE_SECS 30 // Run fan for SAMPLE_SECONDS, then take air sample #define SDS_TX D1 // SDS011 Tx Pin GPIO 5 #define SDS_RX D6 // SDS011 Rx Pin GPIO 12 (Unused IO - **Do Not Use**) #define SDS011_PWR D8 // Power control to SDS011 Sensor. #define SAVE_DATA D4 // int loopCount = 0; Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC); // Adafruit TFT library. Create an instance. SDS011 sdsSensor; // Sensor library - create an instance of the sensor. String quality; // Define PM2.5 value as LOW, MEDIUM etc (UK Defra scale). int colour; // Define PM2.5 value as colour (UK Defra scale) short pm25Array[320]; // Array to hold sensor values for the histogram. float p10, p25; // Variabled for PM10 and MP2.5 data from sensor. int error; // Confirms valid data from sensor. 0 = error. short arrayPointer = 15; // Array element currently being written to (16-bit integer) int yPos; // Vertical marker for bar chart. int sleepSeconds; float volts = 0.0; const float Vmax = 5.75; // Max voltage can be set here. Depends on resistor tolerances. void saveData() { EEPROM.put(0, arrayPointer); for (int i= 15; i<=319; i++ ) { EEPROM.put(i*2, pm25Array[i]); } EEPROM.commit(); tft.setCursor(245, 10); tft.setTextColor(ILI9341_BLUE, ILI9341_BLACK); tft.print("SAVED"); while (digitalRead(SAVE_DATA) == LOW); delay(250); } void plotHistogram() { // Function to re-draw the histogram. tft.fillRect(15,120, 319, 90, ILI9341_BLACK); // Clear plotting area byte line; for (int i = 15; i <= 319; i++){ getTextData25(pm25Array[i] / 10); // Get colour corrsponding to each air quality level. Value is stored // multiplied by 10 so divide by 10 here to get true value. line = constrain(sqrt(pm25Array[i]*40), 0, 105); // Calculate length of line to plot. sqrt compresses higher values. tft.drawFastVLine(i, 225 - line, line, colour); // Draw vertical line in chosen colour. } } #define LIGHT_GREEN 0x9FF3 // Define colours used by UK Defra to specify pollutant bands. #define MID_GREEN 0x37E0 #define DARK_GREEN 0x3660 #define LIGHT_YELLOW 0xFFE0 #define MID_YELLOW 0xFE60 #define ORANGE 0xFCC0 #define LIGHT_RED 0xFB2C #define MID_RED 0xF800 #define DARK_RED 0x9800 #define PURPLE 0xC99F // UK air pollution bands for PM2.5 and PM10 Particles. // https://uk-air.defra.gov.uk/air-pollution/daqi?view=more-info&pollutant=pm25#pollutant int getTextData25(int value) { // Function sets three global variables: 'Ypos' switch (value) { // (vertical cursor position), 'colour' & case 0 ... 11 : yPos = 100; colour = LIGHT_GREEN; quality = "1 LOW"; break; // 'quality' and returns half the length of the case 12 ... 23 : yPos = 90; colour = MID_GREEN; quality = "2 LOW"; break; // text string 'quality' whose value is used to case 24 ... 35 : yPos = 80; colour = DARK_GREEN; quality = "3 LOW"; break; // centre justify the text on the display. case 36 ... 41 : yPos = 70; colour = LIGHT_YELLOW; quality = "4 MODERATE"; break; case 42 ... 47 : yPos = 60; colour = MID_YELLOW; quality = "5 MODERATE"; break; case 48 ... 53 : yPos = 50; colour = ORANGE; quality = "6 MODERATE"; break; case 54 ... 58 : yPos = 40; colour = LIGHT_RED; quality = "7 HIGH"; break; case 59 ... 64 : yPos = 30; colour = MID_RED; quality = "8 HIGH"; break; case 65 ... 70 : yPos = 20; colour = DARK_RED; quality = "9 HIGH"; break; case 71 ... 9999: yPos = 10; colour = PURPLE; quality = "10 VERY HIGH"; break; default: yPos = 10; colour = ILI9341_MAGENTA; quality = "HAZARDOUS"; break; } return (quality.length() / 2) * 6; } int getTextDataPM10(int value) { switch (value) { case 0 ... 16 : colour = LIGHT_GREEN; quality = "1 LOW"; break; case 17 ... 33 : colour = MID_GREEN; quality = "2 LOW"; break; case 34 ... 50 : colour = DARK_GREEN; quality = "3 LOW"; break; case 51 ... 58 : colour = LIGHT_YELLOW; quality = "4 MODERATE"; break; case 59 ... 66 : colour = MID_YELLOW; quality = "5 MODERATE"; break; case 67 ... 75 : colour = ORANGE; quality = "6 MODERATE"; break; case 76 ... 83 : colour = LIGHT_RED; quality = "7 HIGH"; break; case 84 ... 91 : colour = MID_RED; quality = "8 HIGH"; break; case 92 ... 100 : colour = DARK_RED; quality = "9 HIGH"; break; case 101 ... 9999: colour = PURPLE; quality = "10 VERY HIGH"; break; default: colour = ILI9341_MAGENTA; quality = "HAZARDOUS"; break; } return (quality.length() / 2) * 6; } void setup() { pinMode(SDS011_PWR, OUTPUT); pinMode(SAVE_DATA, INPUT_PULLUP); pinMode(A0, INPUT); EEPROM.begin(1000); //-- The following block of code tests if the EEPROM has been 'prepared' ---- //-- with all zeroes and clears it if necessary.------------------------------ bool eraseFlag = false; for (int i = 2; i< 30; i++ ) { if (EEPROM.read(i) != 0) { eraseFlag = true; break; } } if (eraseFlag) { for (int i=0; i< 640; i++){ EEPROM.write(i, (byte) 0); // Reset EEPROM addresses to zero } EEPROM.put(0, (short) 15); // Reset array Pointer to start address (15) EEPROM.commit(); } //------------------------------------------------------------------------------- EEPROM.get(0, arrayPointer); for (int i=15; i<319; i++) { EEPROM.get(i*2, pm25Array[i]); } tft.begin(); tft.setRotation(1); tft.setTextWrap(true); tft.fillScreen(ILI9341_BLACK); tft.setTextSize(1); sleepSeconds = (SAMPLE_INTERVAL * 60) - SAMPLE_SECS; // Calculate sleep time in seconds. WiFi.begin(ssid, password); // Connect WiFi to server running on PC to // plot PM10 and PM2.5 values. int wifi_timeout = 0; tft.setCursor(0, 10); tft.print("Connecting to WiFi..."); while (WiFi.status() != WL_CONNECTED) { delay(10); tft.setCursor(0, 20); tft.fillRect(0, 20, 100, 10, ILI9341_BLACK); tft.print(wifi_timeout); wifi_timeout++; if(wifi_timeout > 1000) { tft.setCursor(0, 35); tft.println("Failed to connect to Wifi"); break; } } if (WiFi.status() == WL_CONNECTED) { tft.setCursor(0, 20); tft.print("Connected to "); tft.println(WiFi.SSID()); tft.print("IP address: "); tft.println(WiFi.localIP()); delay(5000); } tft.fillScreen(ILI9341_BLACK); tft.setCursor(15, tft.height() -20); tft.setTextSize(1); // Print static labels and headers on display. tft.setCursor(150, 10); tft.println("PM 2.5"); tft.setCursor(150, 17); tft.print("ug/m3"); tft.setCursor(5, 10); tft.print("PM 10"); tft.setCursor(5, 17); tft.print("ug/m3"); tft.fillRect(312, 10, 6, 10, PURPLE); // Print a colour key for the Defra pollutant bands. tft.fillRect(312, 20, 6, 10, DARK_RED); tft.fillRect(312, 30, 6, 10, MID_RED); tft.fillRect(312, 40, 6, 10, LIGHT_RED); tft.fillRect(312, 50, 6, 10, ORANGE); tft.fillRect(312, 60, 6, 10, MID_YELLOW); tft.fillRect(312, 70, 6, 10, LIGHT_YELLOW); tft.fillRect(312, 80, 6, 10, DARK_GREEN); tft.fillRect(312, 90, 6, 10, MID_GREEN); tft.fillRect(312, 100, 6, 10, LIGHT_GREEN); tft.drawFastVLine(13, 120, 108, ILI9341_BLUE); // Draw histogram vertical axis tft.setTextColor(ILI9341_BLUE); tft.setCursor(0, 120); tft.print(" ^"); tft.setCursor(0, 130); tft.print("50"); tft.setCursor(0, 170); tft.print("10"); tft.setCursor(0, 200); tft.print(" 1"); tft.drawFastHLine(12, tft.height() - 14, tft.width()-1, ILI9341_BLUE); // Draw histogram horizontal axis for (int x = 319; x > 15; x-=12) { tft.drawFastVLine(x, 227, 3, ILI9341_BLUE); // Draw 1-hour ticks on horizontal axis. } tft.setTextColor(ILI9341_BLUE); tft.setCursor(105, 232); tft.print("Air Quality Monitor"); int rssi = WiFi.RSSI(); tft.setCursor(0, 232); tft.fillRect(0, 232, 50, 8, ILI9341_BLACK); tft.setTextSize(1); if (WiFi.status() == WL_CONNECTED) { tft.setTextColor(ILI9341_BLUE); tft.print("RSSI " + String(rssi) + " dB"); } else { tft.setTextColor(ILI9341_RED); tft.print("No WiFi"); } tft.setTextColor(ILI9341_GREEN); tft.setTextSize(3); Serial.begin(9600); sdsSensor.begin(SDS_TX, SDS_RX); // Begin sensor and define Tx and Rx pins. plotHistogram(); } void loop() { digitalWrite(SDS011_PWR, HIGH); // Turn on SDS011 Sensor power tft.setTextSize(1); // tft.setCursor(248, 232); tft.setTextColor(ILI9341_GREEN, ILI9341_BLACK); tft.print("SAMPLING "); for (int i = SAMPLE_SECS; i>=0; i--) { // Run fan for 30 seconds to ensure new air tft.setCursor(300, 232); if (i < 10) { tft.print("0"); } tft.print(i); if (digitalRead(SAVE_DATA) == LOW) { saveData(); } delay(1000); } int raw = 0; // Get supply voltage. Useful when battery operated. for (byte i=0;i<10;i++) { raw += analogRead(A0); delay(10); } raw = raw / 10; float volts = (raw / 1023.0) * Vmax; tft.fillRect(248, 232, 70, 10, ILI9341_BLACK); tft.fillRect(85, 105, 65, 8, ILI9341_BLACK); error = sdsSensor.read(&p25,&p10); // Read PM2.5 and PM10 values from sensor. if (! error) { Serial.print("P2.5: "); Serial.println(p25); Serial.print("P10: "); Serial.println(p10); int x = getTextData25(p25); // Function retuns (width of text)/2 so we can // centre-justify it on the display. It also sets tft.setTextColor(colour); // the text colour appropriate to the PM2.5 value as // defined by the UK Defra documentation. tft.fillRect(305, 10, 5, 105, ILI9341_BLACK); // Clear old triangle tft.fillTriangle(305, yPos, 308, yPos+5, 305, yPos+10, colour); // Plot new position of triangle on colour scale tft.fillRect(100, 40, 110, 8, ILI9341_BLACK); // Clear display areas where new text will // be drawn. (Graphical fonts don't overwrite tft.fillRect(0, 36, 285, 77, ILI9341_BLACK); // previous text. tft.setCursor(165 - x, 40); // Set cursor to centre of display area. tft.print(quality); tft.setTextSize(2); tft.setFont(&FreeSansBold18pt7b); // Change to new font. String sp25 = String(p25); // Convert PM2.5 value to text because the // 'getTextBounds' function needs text. int16_t x1, y1; uint16_t w, h; tft.getTextBounds(sp25, 0,0, &x1, &y1, &w, &h); // We mainly want the width of the text that // we're about to print so we can centre-justify it. tft.setCursor(183-(w/2), 110); tft.print(p25, 1); tft.setFont(); // Revert to standard font. tft.setTextSize(1); tft.setTextColor(colour, ILI9341_BLACK); // PM10 data is less-used so just print it tft.fillRect(0, 30, 100, 10, ILI9341_BLACK); // in the top left corner of the display. tft.setCursor(0, 40); tft.print(quality); tft.setTextSize(2); tft.fillRect(0, 55, 60, 20, ILI9341_BLACK); tft.setCursor(2, 55); tft.print(p10, 1); tft.setTextSize(1); tft.setTextColor(ILI9341_GREEN); tft.fillRect(80, 10, 50, 10, ILI9341_BLACK); tft.setCursor(80, 10); tft.print(volts); tft.print("v"); tft.fillRect(245, 10, 30, 10, ILI9341_BLACK); // ====== plot histogram (bar graph) ============== if (arrayPointer >= 319) { // If array has been filled, move all values down one. for (int i = 15; i <= 319; i++) { pm25Array[i] = pm25Array[i+1]; } } pm25Array[arrayPointer] = (short) (p25 * 10); // Multiply float value by 10 to make short integer. plotHistogram(); if (arrayPointer < 319) arrayPointer++; // Increment the pointer to store the next value. delay(100); //======= end plot ==================== int rssi = WiFi.RSSI(); // Get the WiFi signal strength and print on the display. tft.setCursor(0, 232); tft.fillRect(0, 232, 50, 8, ILI9341_BLACK); tft.setTextSize(1); if (WiFi.status() == WL_CONNECTED) { tft.setTextColor(ILI9341_BLUE); tft.print(" RSSI " + String(rssi) + " dB"); } else { tft.setTextColor(ILI9341_RED); tft.print(" No WiFi"); } if (WiFi.status() == WL_CONNECTED) { HTTPClient http; // Specify request destination, including your GET variables String http_request = ""; http_request = "http://" + serverIP + "/apage?"; // Build the text string for the HTTP GET request to the PC server. http_request += "id=" + deviceId; http_request += "&leftaxis=" + sp25; http_request += "&rightaxis=" + String(p10); http_request += "&rssi=" + String(rssi); http_request += "&volts=" + String(volts); Serial.println("Making HTTP request..."); Serial.println(http_request); http.begin(http_request); // Send the request int httpCode = http.GET(); // Check the returning HTTP code if (httpCode > 0) { // Get a response back from the server String payload = http.getString(); // Print the response Serial.println("HTTP Response: "); Serial.println(payload); } // Close the HTTP connection http.end(); } } digitalWrite(SDS011_PWR, LOW); // Turn off SDS011 Power. delay(1000); tft.setTextSize(1); tft.setCursor(248, 232); tft.setTextColor(ILI9341_BLUE, ILI9341_BLACK); tft.print("SLEEP "); int secs; int mins; for (int i = sleepSeconds; i>0; i--) { // Sleep for the sample interval (less the 30 seconds warmup time) tft.setCursor(285, 232); // secs = i; tft.print(secs / 60); // Print minutes remaining. tft.print(":"); if (secs % 60 < 10) { tft.print("0"); } tft.print(secs % 60); // Print seconds remaining. tft.print(" "); if (digitalRead(SAVE_DATA) == LOW) { // Manually save histogram data to EEPROM - don't wait for the saveData(); // auto-save after one hour to expire. } delay(1000); } loopCount++; if (loopCount >= SAVE_COUNTER) { // Each loop takes 5 minutes. 12 loops = 5 * 12 = 60 minutes loopCount = 0; saveData(); } Serial.print("Heap size at end of loop "); Serial.println(system_get_free_heap_size() ); // Free memory check! }