diff --git a/assets/1416589372_dashboard_medium_512.png b/assets/1416589372_dashboard_medium_512.png new file mode 100644 index 0000000000000000000000000000000000000000..b2c713a1678744fa7468a73cd84eddc66dfe20e7 Binary files /dev/null and b/assets/1416589372_dashboard_medium_512.png differ diff --git a/data/browserconfig.xml b/data/browserconfig.xml new file mode 100644 index 0000000000000000000000000000000000000000..ba9981335cd01a5cddc7600a3d6bf0b75d37b28d --- /dev/null +++ b/data/browserconfig.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<browserconfig> + <msapplication> + <tile> + <square150x150logo src="img/mstile-150x150.png"/> + <TileColor>#da532c</TileColor> + </tile> + </msapplication> +</browserconfig> diff --git a/data/favicon.ico b/data/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f240d44fe35ec981ead1b0f3f95c17f065a29b46 Binary files /dev/null and b/data/favicon.ico differ diff --git a/data/img/android-chrome-192x192.png b/data/img/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..5ac8b925d9e92d7d34ade2c395c672eda34a16e8 Binary files /dev/null and b/data/img/android-chrome-192x192.png differ diff --git a/data/img/android-chrome-512x512.png b/data/img/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..1a3734fe2a2e26123f33691befe9c1eae3b52014 Binary files /dev/null and b/data/img/android-chrome-512x512.png differ diff --git a/data/img/apple-touch-icon.png b/data/img/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0feb785f8e5d6316185515e2ec64588b833cc090 Binary files /dev/null and b/data/img/apple-touch-icon.png differ diff --git a/data/img/favicon-16x16.png b/data/img/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..79a8a4814560f97c89f1eff5d6f926ad5ea19e7b Binary files /dev/null and b/data/img/favicon-16x16.png differ diff --git a/data/img/favicon-32x32.png b/data/img/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..619638bacaf10c4a16d88730bd8d1c6bfe32a319 Binary files /dev/null and b/data/img/favicon-32x32.png differ diff --git a/data/img/mstile-150x150.png b/data/img/mstile-150x150.png new file mode 100644 index 0000000000000000000000000000000000000000..94ea937706c85bdf9da6976f9df0f5b657780206 Binary files /dev/null and b/data/img/mstile-150x150.png differ diff --git a/data/index.html b/data/index.html new file mode 100644 index 0000000000000000000000000000000000000000..bec52089e8bca0bf9afeb033cb700fbfd4d31767 --- /dev/null +++ b/data/index.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <link rel="apple-touch-icon" sizes="180x180" href="img/apple-touch-icon.png"> + <link rel="icon" type="image/png" sizes="32x32" href="img/favicon-32x32.png"> + <link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png"> + <link rel="manifest" href="/site.webmanifest"> + <meta name="msapplication-TileColor" content="#da532c"> + <meta name="theme-color" content="#ffffff"> + <title>Air Quality Monitor</title> +</head> +<body> +dis gon b gud. +</body> +</html> + diff --git a/data/site.webmanifest b/data/site.webmanifest new file mode 100644 index 0000000000000000000000000000000000000000..462ff017caaccc75c092914119a05036ecae0c1d --- /dev/null +++ b/data/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "img/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "img/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/platformio.ini b/platformio.ini index d379479d4938370a44a4042470feab6ab46a789c..4a6cf5fd163c43526d5b7e736a97a839cc442072 100644 --- a/platformio.ini +++ b/platformio.ini @@ -8,19 +8,26 @@ ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html -[env:esp32dev] -platform = espressif32 -board = esp32dev +[env:d1_mini] +platform = espressif8266 +board = d1_mini framework = arduino +upload_port = COM6 monitor_speed = 115200 -monitor_port = COM4 +monitor_port = COM6 +board_build.filesystem = littlefs +board_build.ldscript = eagle.flash.4m1m.ld lib_deps = - avaldebe/PMSerial @ ^1.1.1 - ESP Async WebServer + ESP8266WiFi + ESP8266WebServer + ESP8266mDNS ArduinoOTA + plerup/EspSoftwareSerial @ ^6.15.1 + avaldebe/PMSerial @ ^1.1.1 + boschsensortec/BSEC Software Library @ ^1.6.1480 knolleary/PubSubClient @ ^2.8 -[env:esp32dev_ota] -extends = env:esp32dev +[env:d1_mini_ota] +extends = env:d1_mini upload_port = airbox.local upload_protocol = espota diff --git a/src/main.cpp b/src/main.cpp index 735e8183a044c2c861cfd00beb6c3b8fa022a512..71889a8e4d3ddcee6424ada7aaf7b974e2d1594e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,30 +1,32 @@ #include "config.h" #include <Arduino.h> -#include <PMserial.h> -#include <WiFi.h> -#include <Wire.h> -#include <ESPmDNS.h> -#include <WiFiUdp.h> +#include <SoftwareSerial.h> +#include <ESP8266WiFi.h> +#include <FS.h> +#include <LittleFS.h> #include <ArduinoOTA.h> -#include <ESPAsyncWebServer.h> +#include <ESP8266mDNS.h> +#include <ESP8266WebServer.h> +#include <Wire.h> +#include <PMserial.h> +#include <bsec.h> #include <PubSubClient.h> -// -// Configuration -// +#define PMS_TX D3 +#define PMS_RX D4 -SerialPM pms(PMS5003, Serial2); -IPAddress mqttServer; +SoftwareSerial swSerial; WiFiClient wifiClient; +MDNSResponder mdns; +ESP8266WebServer server(80); +SerialPM pms(PMS5003, PMS_RX, PMS_TX); +Bsec iaqSensor; PubSubClient mqttClient(wifiClient); -AsyncWebServer server(80); +IPAddress mqttServer; long checkMillis = 0; long checkInterval = 5000; // 5s -#define PMS_RX 16 -#define PMS_TX 17 - void logLine(String line, bool newline = true) { Serial.print(line); @@ -37,6 +39,7 @@ void logLine(String line, bool newline = true) void setupWifi() { logLine("Connecting to '" + String(WIFI_SSID) + "' ", false); + WiFi.hostname(HOSTNAME); WiFi.begin(WIFI_SSID, WIFI_PSK); int i = 50; @@ -64,28 +67,53 @@ void setupWifi() } } -void setupWebserver() -{ - server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) - { request->send(200, "text/plain", "ok"); }); - server.begin(); +String getContentType(String filename) +{ // convert the file extension to the MIME type + if (filename.endsWith(".html")) + return "text/html"; + else if (filename.endsWith(".css")) + return "text/css"; + else if (filename.endsWith(".js")) + return "application/javascript"; + else if (filename.endsWith(".ico")) + return "image/x-icon"; + else if (filename.endsWith(".gz")) + return "application/x-gzip"; + return "text/plain"; } -void setupMDNS() -{ - if (MDNS.begin(HOSTNAME)) - { - logLine("MDNS responder started as " + String(HOSTNAME)); - MDNS.addService("http", "tcp", 80); - } - else - { - logLine("MDNS.begin failed"); +bool handleFileRead(String path) +{ // send the right file to the client (if it exists) + Serial.println("handleFileRead: " + path); + if (path.endsWith("/")) + path += "index.html"; // If a folder is requested, send the index file + String contentType = getContentType(path); // Get the MIME type + String pathWithGz = path + ".gz"; + if (LittleFS.exists(pathWithGz) || LittleFS.exists(path)) + { // If the file exists, either as a compressed archive, or normal + if (LittleFS.exists(pathWithGz)) // If there's a compressed version available + path += ".gz"; // Use the compressed version + File file = LittleFS.open(path, "r"); // Open the file + size_t sent = server.streamFile(file, contentType); // Send it to the client + file.close(); // Close the file again + Serial.printf("Sent file: %s (%d)\r\n", path.c_str(), sent); + return true; } + Serial.println(String("\tFile Not Found: ") + path); + return false; // If the file doesn't exist, return false } -void messageReceived(String &topic, String &payload) +void setupWebserver() { + LittleFS.begin(); + server.onNotFound([]() { // If the client requests any URI + if (!handleFileRead(server.uri())) // send it if it exists + server.send(404, "text/plain", "404: Not Found"); // otherwise, respond with a 404 (Not Found) error + }); + server.begin(); +} + +void messageReceived(String &topic, String &payload) { Serial.println("incoming: " + topic + " - " + payload); } @@ -122,75 +150,113 @@ void setupMQTT() void setupOTA() { ArduinoOTA.setHostname(HOSTNAME); - 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); - }) - .onEnd([]() - { Serial.println("\nEnd"); }) - .onProgress([](unsigned int progress, unsigned int total) - { Serial.printf("Progress: %u%%\r", (progress / (total / 100))); }) - .onError([](ota_error_t error) - { - Serial.printf("Error[%u]: ", error); - if (error == OTA_AUTH_ERROR) - Serial.println("Auth Failed"); - else if (error == OTA_BEGIN_ERROR) - Serial.println("Begin Failed"); - else if (error == OTA_CONNECT_ERROR) - Serial.println("Connect Failed"); - else if (error == OTA_RECEIVE_ERROR) - Serial.println("Receive Failed"); - else if (error == OTA_END_ERROR) - Serial.println("End Failed"); - }); - + ArduinoOTA.onStart([]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) + { + type = "sketch"; + } + else + { // U_SPIFFS + type = "filesystem"; + LittleFS.end(); + } + Serial.println("Start updating " + type); + }); + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + }); + ArduinoOTA.onEnd([]() { + Serial.println("\nEnd"); + }); + ArduinoOTA.onError([](ota_error_t error) { + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) + Serial.println("Auth Failed"); + else if (error == OTA_BEGIN_ERROR) + Serial.println("Begin Failed"); + else if (error == OTA_CONNECT_ERROR) + Serial.println("Connect Failed"); + else if (error == OTA_RECEIVE_ERROR) + Serial.println("Receive Failed"); + else if (error == OTA_END_ERROR) + Serial.println("End Failed"); + }); ArduinoOTA.begin(); } -void publishMetric(uint16_t data[]) -{ - String stringPmData = "\"pm1\": " + String(data[0]) + ", \"pm2.5\": " + String(data[1]) + ", \"pm10\": " + String(data[2]); - String stringNcData = "\"nc0.3\": " + String(data[3]) + ", \"nc0.5\": " + String(data[4]) + ", \"nc1\": " + String(data[5]) + ", \"nc2.5\": " + String(data[6]) + ", \"nc5\": " + String(data[7]) + ", \"nc10\": " + String(data[8]); - String payloadString = "{\"name\": \"air\", \"node\": \"" + String(HOSTNAME) + "\", " + stringPmData + ", " + stringNcData + "}"; - //logLine(payloadString); - int len_payload = payloadString.length() + 1; - char payload[len_payload]; - payloadString.toCharArray(payload, len_payload); - mqttClient.publish(MQTT_TOPIC, payload); -} - void setupPms() { Serial.println("Initializing PMS, wait 30 seconds for stable readings..."); - Serial2.begin(9600, SERIAL_8N1, PMS_RX, PMS_TX); + swSerial.begin(9600, SWSERIAL_8N1, PMS_RX, PMS_TX); pms.init(); } +void checkIaqSensorStatus(void) +{ + if (iaqSensor.status != BSEC_OK) + { + if (iaqSensor.status < BSEC_OK) + { + logLine("BSEC error code : " + String(iaqSensor.status)); + } + else + { + logLine("BSEC warning code : " + String(iaqSensor.status)); + } + } + + if (iaqSensor.bme680Status != BME680_OK) + { + if (iaqSensor.bme680Status < BME680_OK) + { + logLine("BME680 error code : " + String(iaqSensor.bme680Status)); + } + else + { + logLine("BME680 warning code : " + String(iaqSensor.bme680Status)); + } + } +} + +void setupBme() +{ + iaqSensor.begin(BME680_I2C_ADDR_PRIMARY, Wire); + logLine("\nBSEC library version " + String(iaqSensor.version.major) + "." + String(iaqSensor.version.minor) + "." + String(iaqSensor.version.major_bugfix) + "." + String(iaqSensor.version.minor_bugfix)); + checkIaqSensorStatus(); + bsec_virtual_sensor_t sensorList[10] = { + BSEC_OUTPUT_RAW_TEMPERATURE, + BSEC_OUTPUT_RAW_PRESSURE, + BSEC_OUTPUT_RAW_HUMIDITY, + BSEC_OUTPUT_RAW_GAS, + BSEC_OUTPUT_IAQ, + BSEC_OUTPUT_STATIC_IAQ, + BSEC_OUTPUT_CO2_EQUIVALENT, + BSEC_OUTPUT_BREATH_VOC_EQUIVALENT, + BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE, + BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY, + }; + + iaqSensor.updateSubscription(sensorList, 10, BSEC_SAMPLE_RATE_LP); + checkIaqSensorStatus(); +} + void setup() { + Wire.begin(); Serial.begin(115200); delay(10); Serial.println(); setupPms(); + setupBme(); setupWifi(); setupOTA(); setupWebserver(); - setupMDNS(); setupMQTT(); } -void publishPms() +String readPms() { - pms.read(); if (pms) { // successfull read @@ -203,7 +269,7 @@ void publishPms() Serial.printf("NC >=2.5µm: %5d #/100cm³\r\n", pms.n2p5); Serial.printf("NC >= 5µm: %5d #/100cm³\r\n", pms.n5p0); Serial.printf("NC >= 10µm: %5d #/100cm³\r\n", pms.n10p0); - publishMetric(pms.data); + logLine("-----"); } else { @@ -237,12 +303,68 @@ void publishPms() break; } } - Serial.println("..............................................."); + String pmData = "pm1=" + String(pms.data[0]) + ",pm2.5=" + String(pms.data[1]) + ",pm10=" + String(pms.data[2]); + String ncData = "nc0.3=" + String(pms.data[3]) + ",nc0.5=" + String(pms.data[4]) + ",nc1=" + String(pms.data[5]) + ",nc2.5=" + String(pms.data[6]) + ",nc5=" + String(pms.data[7]) + ",nc10=" + String(pms.data[8]); + return pmData + "," + ncData; +} + +String readBme() +{ + if (iaqSensor.run()) + { // If new data is available + Serial.printf("Temperature : %f °C\r\n", iaqSensor.temperature); + Serial.printf("Humidity : %f %%\r\n", iaqSensor.humidity); + Serial.printf("Pressure : %f hPa\r\n", iaqSensor.pressure); + Serial.printf("IAQ : %f\r\n", iaqSensor.iaq); + Serial.printf("IAQ Accuracy : %d\r\n", iaqSensor.iaqAccuracy); + Serial.printf("Static IAQ : %f\r\n", iaqSensor.staticIaq); + Serial.printf("Stat IAQ Acc : %d\r\n", iaqSensor.staticIaqAccuracy); + Serial.printf("CO2 Equiv : %f\r\n", iaqSensor.co2Equivalent); + Serial.printf("CO2 Accuracy : %d\r\n", iaqSensor.co2Accuracy); + Serial.printf("bVOC Equiv : %f\r\n", iaqSensor.breathVocEquivalent); + Serial.printf("bVOC Accuracy: %d\r\n", iaqSensor.breathVocAccuracy); + Serial.printf("Gas Percent : %f %%\r\n", iaqSensor.gasPercentage); + Serial.printf("Gas Per Accur: %d\r\n", iaqSensor.gasPercentageAcccuracy); + Serial.printf("Raw Temp : %f °C\r\n", iaqSensor.rawTemperature); + Serial.printf("Raw Rel Humid: %f %%\r\n", iaqSensor.rawHumidity); + Serial.printf("Gas Resist : %f Ohm\r\n", iaqSensor.gasResistance); + } + else + { + checkIaqSensorStatus(); + } + return "temperature=" + String(iaqSensor.temperature) + ",humidity=" + String(iaqSensor.humidity) + ",pressure=" + String(iaqSensor.pressure) + + ",iaq=" + String(iaqSensor.iaq) + ",iaq_acc=" + String(iaqSensor.iaqAccuracy) + + ",s_iaq=" + String(iaqSensor.staticIaq) + ",s_iaq_acc=" + String(iaqSensor.staticIaqAccuracy) + + ",eco2=" + String(iaqSensor.co2Equivalent) + ",eco2_acc=" + String(iaqSensor.co2Accuracy) + + ",bvoc=" + String(iaqSensor.breathVocEquivalent) + ",bvoc_acc=" + String(iaqSensor.breathVocAccuracy); +} + +void publishMetric(String metricValues) +{ + String payloadString = "air,node=" + String(HOSTNAME) + " " + metricValues; + //logLine(payloadString); + int len_payload = payloadString.length() + 1; + char payload[len_payload]; + payloadString.toCharArray(payload, len_payload); + mqttClient.publish(MQTT_TOPIC, payload); +} + +void getSensorData() +{ + + String pmsData = readPms(); + String bmeData = readBme(); + String metricValues = pmsData + "," + bmeData; + publishMetric(metricValues); + logLine("#####"); } void loop() { ArduinoOTA.handle(); + server.handleClient(); + mdns.update(); unsigned long currentMillis = millis(); if (currentMillis - checkMillis > checkInterval) @@ -264,7 +386,6 @@ void loop() if (millis() - updateTimer >= 5000) { updateTimer = millis(); - publishPms(); + getSensorData(); } - }