From 90fde567623bc30141d85d35738713c765ef30f4 Mon Sep 17 00:00:00 2001 From: Myk UT3UMS Date: Fri, 5 Apr 2024 18:51:21 +0300 Subject: [PATCH] first commit --- .gitignore | 4 + README.org | 11 ++ include/README | 39 +++++ include/http_static.h | 11 ++ logplotter.py | 101 +++++++++++ platformio.ini | 21 +++ populate_static.py | 41 +++++ populate_static.sh | 26 +++ src/http_static.cpp | 10 ++ src/main.cpp | 258 +++++++++++++++++++++++++++++ src/static/index.html | 97 +++++++++++ src/static/sensor_things_tmpl.json | 105 ++++++++++++ test/README | 11 ++ 13 files changed, 735 insertions(+) create mode 100644 .gitignore create mode 100644 README.org create mode 100644 include/README create mode 100644 include/http_static.h create mode 100644 logplotter.py create mode 100644 platformio.ini create mode 100755 populate_static.py create mode 100755 populate_static.sh create mode 100644 src/http_static.cpp create mode 100644 src/main.cpp create mode 100644 src/static/index.html create mode 100644 src/static/sensor_things_tmpl.json create mode 100644 test/README diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88cabfd --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.pio +.clang_complete +.ccls +/logs/ diff --git a/README.org b/README.org new file mode 100644 index 0000000..1028a21 --- /dev/null +++ b/README.org @@ -0,0 +1,11 @@ +* Balcony WX station de UT3UMS +** Inspiration ++ [[https://how2electronics.com/monitor-ccs811-co2-tvoc-on-esp8266-esp32-webserver/][Monitor CCS811 CO2 & TVOC on ESP8266/ESP32 Webserver]] + It has only equivalent carbon dioxide (eCO2) with metal oxide (MOX) levels ++ https://github.com/bfaliszek/CJMCU-8118_InfluxDB + + +** Levels +#+BEGIN_EXAMPLE +In the event of TVOC issues, inspections should be carried out to find the root cause of the problem and address it as exposure to the VOCs may harm health. Indoor VOC levels up to 350 ppb are acceptable. However they should not exceed 500ppb. – CO2 ppm range of 450 to 2000 ppm (parts per million). +#+END_EXAMPLE diff --git a/include/README b/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/include/http_static.h b/include/http_static.h new file mode 100644 index 0000000..b90b61a --- /dev/null +++ b/include/http_static.h @@ -0,0 +1,11 @@ +// http_static.h +#include +#ifndef HTTP_STATIC_H +#define HTTP_STATIC_H + +namespace http_static { + extern const char index_html[] PROGMEM; + extern const char sensor_things_resp[] PROGMEM; +} + +#endif diff --git a/logplotter.py b/logplotter.py new file mode 100644 index 0000000..4c0230d --- /dev/null +++ b/logplotter.py @@ -0,0 +1,101 @@ +import re +import matplotlib.pyplot as plt +from datetime import datetime, timedelta + +def parse_sensor_log(filename): + timestamps = [] + temperatures = [] + humidities = [] + eco2_concentrations = [] + tvoc_concentrations = [] + + # Extract start time from the filename + # start_time_str = re.search(r'(\d{6})', filename).group(1) + start_time_str = re.search(r'-(\d{6})\.', filename).group(1) + print(start_time_str) + start_time = datetime.strptime(start_time_str, '%H%M%S') + + with open(filename, 'r') as file: + lines = file.readlines() + + for line in lines: + if "Waiting" in line: + # Increment the current time by one minute + start_time += timedelta(minutes=1) + continue + + match = re.match(r'^temperature: (.+) C$', line) + if match: + temperatures.append(float(match.group(1))) + timestamps.append(start_time) + continue + + match = re.match(r'^humidity: (.+) %$', line) + if match: + humidities.append(float(match.group(1))) + continue + + match = re.match(r'^eCO2 concentration: (.+) ppm$', line) + if match: + eco2_concentrations.append(float(match.group(1))) + continue + + match = re.match(r'^TVOC concentration: (.+) ppb$', line) + if match: + tvoc_concentrations.append(float(match.group(1))) + continue + + return timestamps, temperatures, humidities, eco2_concentrations, tvoc_concentrations + +# Multiline +def plot_sensor_data(timestamps, temperatures, humidities, eco2_concentrations, tvoc_concentrations): + plt.figure(figsize=(12, 8)) + + plt.subplot(2, 2, 1) + plt.plot(timestamps, temperatures, 'r.-') + plt.xlabel('Time') + plt.ylabel('Temperature (C)') + plt.title('Temperature over Time') + + plt.subplot(2, 2, 2) + plt.plot(timestamps, humidities, 'g.-') + plt.xlabel('Time') + plt.ylabel('Humidity (%)') + plt.title('Humidity over Time') + + plt.subplot(2, 2, 3) + plt.plot(timestamps, eco2_concentrations, 'b.-') + plt.xlabel('Time') + plt.ylabel('eCO2 Concentration (ppm)') + plt.title('eCO2 Concentration over Time') + + plt.subplot(2, 2, 4) + plt.plot(timestamps, tvoc_concentrations, 'm.-') + plt.xlabel('Time') + plt.ylabel('TVOC Concentration (ppb)') + plt.title('TVOC Concentration over Time') + + plt.tight_layout() + plt.show() + +# def plot_sensor_data(timestamps, temperatures, humidities, eco2_concentrations, tvoc_concentrations): +# plt.figure(figsize=(12, 8)) + +# plt.plot(timestamps, temperatures, 'r.-', label='Temperature (C)') +# plt.plot(timestamps, humidities, 'g.-', label='Humidity (%)') +# plt.plot(timestamps, eco2_concentrations, 'b.-', label='eCO2 Concentration (ppm)') +# plt.plot(timestamps, tvoc_concentrations, 'm.-', label='TVOC Concentration (ppb)') + +# plt.xlabel('Time') +# plt.ylabel('Sensor Readings') +# plt.title('Sensor Readings over Time') +# plt.legend() +# plt.grid(True) + +# plt.tight_layout() +# plt.show() + +if __name__ == "__main__": + filename = './logs/device-monitor-240403-030529.log' + timestamps, temperatures, humidities, eco2_concentrations, tvoc_concentrations = parse_sensor_log(filename) + plot_sensor_data(timestamps, temperatures, humidities, eco2_concentrations, tvoc_concentrations) diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..7a99824 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,21 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:upesy_wroom] +platform = espressif32 +board = upesy_wroom +framework = arduino +lib_deps = + maarten-pennings/CCS811@^12.0.0 + closedcube/ClosedCube HDC1080@^1.3.2 + https://github.com/me-no-dev/ESPAsyncWebServer.git#master +monitor_speed = 115200 +monitor_filters = default, log2file +extra_scripts = populate_static.py diff --git a/populate_static.py b/populate_static.py new file mode 100755 index 0000000..ee00483 --- /dev/null +++ b/populate_static.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +import os, re + +# Define the file paths +tmpl_file = "./src/http_static.cpp.tmpl" +out_file = "./src/http_static.cpp" +static_dir = "./src/static/" + +def main(): + # Print current working directory and username + print("Current working directory:", os.getcwd()) + print("Current user:", os.getlogin()) + + # Read template file content + with open(tmpl_file, 'r') as f: + body = f.read() + + # Iterate over placeholder expressions in the template + for tmpl_import in find_placeholder_expressions(body): + print("Found expression:", tmpl_import) + tmpl_filename = tmpl_import[1:-1] + template_path = os.path.join(static_dir, tmpl_filename) + + # Read content of the file specified by the placeholder + with open(template_path, 'r') as f: + template = f.read() + + # Replace placeholder with content of the file + body = body.replace(tmpl_import, template) + + # Write modified content to the output file + with open(out_file, 'w') as f: + f.write(body) + +def find_placeholder_expressions(body): + # Find and return placeholder expressions in the body + return [expr.group() for expr in re.finditer(r"%[^%]*%", body)] + +if __name__ == "__main__": + main() diff --git a/populate_static.sh b/populate_static.sh new file mode 100755 index 0000000..8946d32 --- /dev/null +++ b/populate_static.sh @@ -0,0 +1,26 @@ +#!/bin/bash +LANG=C +# Define the file path +tmpl_file="./src/http_static.cpp.tmpl"; +out_file="./src/http_static.cpp"; +static_dir="./src/static/"; +echo pwd; +echo whoami; + +# Define the body variable with multiline content +body=$(cat "$tmpl_file") + +# Iterate over the array +while IFS= read -r tmpl_import; do + echo "Found expression: $tmpl_import" + + tmpl_filename="${tmpl_import:1:-1}" + template=$(cat "${static_dir}${tmpl_filename}") + + body="${body//"$tmpl_import"/"$template"}" + + # Print or save the output +done < <(grep -o "%[^%]*%" <<< "$body") + +echo "$body" > "$out_file" +b diff --git a/src/http_static.cpp b/src/http_static.cpp new file mode 100644 index 0000000..f3349c9 --- /dev/null +++ b/src/http_static.cpp @@ -0,0 +1,10 @@ +#include "http_static.h" + +namespace http_static { + const char index_html[] PROGMEM = R"rawliteral( +#include "./static/index.html" + )rawliteral"; + const char sensor_things_resp[] PROGMEM = R"rawliteral( +#include "./static/sensor_things_tmpl.json"%" + )rawliteral"; +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..266f2c6 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,258 @@ +/*************************************************** + + Wemos D1 mini or NodeMCU 1.0 + VCC - 3.3V + GND - G + SCL - D1 -- GPIO 5 + SDA - D2 -- GPIO 4 + WAK - D3 -- GPIO 0 + + ESP32 + VCC - 3.3V + GND - G + SCL - 19 + SDA - 18 + WAK - 23 + + ****************************************************/ + +#include "ESPAsyncWebServer.h" +#include +#include +#include "ClosedCube_HDC1080.h" // HDC1080 library - https://github.com/closedcube/ClosedCube_HDC1080_Arduino // 14.04.2019 +#include "ccs811.h" // CCS811 library - https://github.com/maarten-pennings/CCS811 // 13.03.2020 +#include +//#include "src/ESPinfluxdb.h" // https://github.com/hwwong/ESP_influxdb // 14.04.2019 +#include "http_static.h" // HTTP pages and JSON request templates +// ********************** Config ********************** + +// DeepSleep time – send data every 60 seconds +const int sleepTimeS = 60; + +//Global sensor objects +#define CCS811_WAK 23 + +CCS811 ccs811(CCS811_WAK); +ClosedCube_HDC1080 hdc1080; + +// WiFi Config +#define WiFi_SSID "Ischtar" +#define WiFi_Password "highfive" + +// NTP conf +const char* ntpServer = "pool.ntp.org"; +const long gmtOffset_sec = 0; +const int daylightOffset_sec = 3600; + +// Create AsyncWebServer object on port 80 +AsyncWebServer server(80); + +// Globals for CCS811 +uint16_t eco2, etvoc, errstat, raw; +// Globals for timestamp +struct tm timeinfo; + +// loop cycle +#define workCycle 60 //seconds +// ******************** Config End ******************** + +String readHDC1080Temperature() { + // Sensor readings may also be up to 2 seconds 'old' (its a very slow sensor) + float t = hdc1080.readTemperature(); + if (isnan(t)) { + Serial.println("Failed to read from HDC1080 sensor!"); + return "--"; + } + else { + return String(t); + } +} + +String readHDC1080Humidity() { + // Sensor readings may also be up to 2 seconds 'old' (its a very slow sensor) + float h = hdc1080.readHumidity(); + if (isnan(h)) { + Serial.println("Failed to read from HDC1080 sensor!"); + return "--"; + } + else { + return String(h); + } +} + +String processCCS811Error(err_t errstat) { + if ( errstat == CCS811_ERRSTAT_OK_NODATA ) { + Serial.println("CCS811: waiting for (new) data"); + return "loading"; + } else if ( errstat & CCS811_ERRSTAT_I2CFAIL ) { + Serial.println("CCS811: I2C error"); + return "i2c error"; + } else { + Serial.print("CCS811: errstat="); Serial.print(errstat, HEX); + Serial.print("="); Serial.println( ccs811.errstat_str(errstat) ); + return "CCS811 sensor error"; + } +} + + + +String readCCS811TVOC() { + return String(etvoc); +} +String readCCS811ECO2() { + return String(eco2); +} + +String formatISO8601() { + char timestamp[20]; // Buffer for timestamp + + // Format the time into ISO 8601 format + strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", &timeinfo); + + // Convert the formatted timestamp to a String object + return String(timestamp); +} + + +// Replaces placeholder in HTML template with real values +// SSR if you will +String processor(const String& var){ + if(var == "TEMPERATURE"){ + return readHDC1080Temperature(); + } + else if(var == "HUMIDITY"){ + return readHDC1080Humidity(); + } + else if(var == "TVOC"){ + return readCCS811TVOC(); + } + else if(var == "ECO2"){ + return readCCS811ECO2(); + } else if(var == "TIMESTAMP"){ + return formatISO8601(); + } + return String(); +} + +void connectToWiFi() { + WiFi.mode(WIFI_STA); + WiFi.begin(WiFi_SSID, WiFi_Password); + + Serial.println(); + Serial.print("Connecting to WiFi: "); + Serial.print(WiFi_SSID); + + int attempts = 0; + while (WiFi.status() != WL_CONNECTED && attempts < 10) { + delay(500); + Serial.print("."); + attempts++; + } + + if (WiFi.status() == WL_CONNECTED) { + Serial.println("\nWiFi connected"); + Serial.print("IP address: http://"); + Serial.println(WiFi.localIP()); + } else { + Serial.println("\nFailed to connect to WiFi"); + // Handle connection failure, e.g., retry or reset the ESP32 + } +} + +void setup() +{ + Serial.begin(115200); + delay(10); + Serial.println(""); + + connectToWiFi(); + delay(10); + Serial.println(""); + // Config NTP + configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); + + // hdc1080 info + hdc1080.begin(0x40); + Serial.print("Manufacturer ID=0x"); + Serial.println(hdc1080.readManufacturerId(), HEX); // 0x5449 ID of Texas Instruments + Serial.print("Device ID=0x"); + Serial.println(hdc1080.readDeviceId(), HEX); // 0x1050 ID of the device + + // i2c + Wire.begin(); + + Serial.println("CCS811 test"); + // Enable CCS811 + bool ok = ccs811.begin(); + if ( !ok ) Serial.println("setup: CCS811 begin FAILED"); + + // Print CCS811 versions + Serial.print("setup: hardware version: "); Serial.println(ccs811.hardware_version(), HEX); + Serial.print("setup: bootloader version: "); Serial.println(ccs811.bootloader_version(), HEX); + Serial.print("setup: application version: "); Serial.println(ccs811.application_version(), HEX); + + // Start measuring + ok = ccs811.start(CCS811_MODE_1SEC); + if ( !ok ) Serial.println("init: CCS811 start FAILED"); + + // Pages and JSONs + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send_P(200, "text/html", http_static::index_html, processor); + }); + server.on("/api/sensors.json", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send_P(200, "text/html", http_static::sensor_things_resp, processor); + }); + + // lightweight named endpoints + server.on("/temperature", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send_P(200, "text/plain", readHDC1080Temperature().c_str()); + }); + server.on("/humidity", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send_P(200, "text/plain", readHDC1080Humidity().c_str()); + }); + server.on("/tvoc", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send_P(200, "text/plain", readCCS811TVOC().c_str()); + }); + server.on("/eco2", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send_P(200, "text/plain", readCCS811ECO2().c_str()); + }); + + // Start server + server.begin(); +} + +void loop() +{ + if (WiFi.status() != WL_CONNECTED) { + Serial.println("WiFi connection lost. Reconnecting..."); + connectToWiFi(); + } + + if(!getLocalTime(&timeinfo)){ + Serial.println("Failed to obtain time"); + return; + } + Serial.print(&timeinfo, "[%M-%d-%Y--%H:%M:%S]: "); + + Serial.print("H="); Serial.print(readHDC1080Temperature()); Serial.print(" °C "); + Serial.print("T="); Serial.print(readHDC1080Temperature()); Serial.print(" % "); + // Read CCS811 + ccs811.read(&eco2,&etvoc,&errstat,&raw); + + // Process CCS811 + if( errstat==CCS811_ERRSTAT_OK ) { + Serial.print("eco2="); Serial.print(eco2); Serial.print(" ppm "); + Serial.print("etvoc="); Serial.print(etvoc); Serial.print(" ppb "); + } else if( errstat==CCS811_ERRSTAT_OK_NODATA ) { + Serial.print("waiting for (new) data"); + } else if( errstat & CCS811_ERRSTAT_I2CFAIL ) { + Serial.print("I2C error"); + } else { + Serial.print( "error: " ); + Serial.print( ccs811.errstat_str(errstat) ); + } + Serial.println(); + + // Wait + delay(workCycle*1000); +} diff --git a/src/static/index.html b/src/static/index.html new file mode 100644 index 0000000..06eab58 --- /dev/null +++ b/src/static/index.html @@ -0,0 +1,97 @@ + + + + ESP32 WxServer + + + + + +

ESP32 CCS881 & HDC1080 Server

+

Loaded: %TIMESTAMP%

+

+ + Temperature + %TEMPERATURE% + °C +

+

+ + Humidity + %HUMIDITY% + % +

+

+ + CO2 concentration + %ECO2% + ppm +

+

+ + TVOC concentration: + %TVOC% + ppb +

+ + + diff --git a/src/static/sensor_things_tmpl.json b/src/static/sensor_things_tmpl.json new file mode 100644 index 0000000..b5f3295 --- /dev/null +++ b/src/static/sensor_things_tmpl.json @@ -0,0 +1,105 @@ +{ + "id": "urn:myweatherstation:sensor1", + "name": "WeatherStation001", + "description": "A weather station providing temperature, humidity, TVOC, and eCO2 readings", + "encodingType": "application/json", + "metadata": "https://myweatherstation.com/metadata", + "Datastreams": [ + { + "id": "urn:myweatherstation:datastream:temperature", + "name": "Temperature", + "description": "Temperature readings", + "unitOfMeasurement": { + "name": "Degree Celsius", + "symbol": "°C", + "definition": "http://www.qudt.org/qudt/owl/1.0.0/unit/Instances.html#DegreeCelsius" + }, + "ObservedProperty": { + "name": "Temperature", + "definition": "http://www.qudt.org/qudt/owl/1.0.0/quantity/Instances.html#Temperature" + }, + "Sensor": { + "name": "Temperature Sensor", + "description": "Sensor for measuring temperature" + }, + "Observations": [ + { + "phenomenonTime": "%TIMESTAMP%", + "result": "%TEMPERATURE%" + } + ] + }, + { + "id": "urn:myweatherstation:datastream:humidity", + "name": "Humidity", + "description": "Humidity readings", + "unitOfMeasurement": { + "name": "Percent", + "symbol": "%%", + "definition": "http://www.qudt.org/qudt/owl/1.0.0/unit/Instances.html#Percent" + }, + "ObservedProperty": { + "name": "Relative Humidity", + "definition": "http://www.qudt.org/qudt/owl/1.0.0/quantity/Instances.html#RelativeHumidity" + }, + "Sensor": { + "name": "Humidity Sensor", + "description": "Sensor for measuring humidity" + }, + "Observations": [ + { + "phenomenonTime": "%TIMESTAMP%", + "result": "%HUMIDITY%" + } + ] + }, + { + "id": "urn:myweatherstation:datastream:TVOC", + "name": "TVOC", + "description": "Total Volatile Organic Compounds readings", + "unitOfMeasurement": { + "name": "Parts Per Billion", + "symbol": "ppb", + "definition": "http://www.qudt.org/qudt/owl/1.0.0/unit/Instances.html#PartsPerBillion" + }, + "ObservedProperty": { + "name": "Total Volatile Organic Compounds", + "definition": "http://www.qudt.org/qudt/owl/1.0.0/quantity/Instances.html#TotalVolatileOrganicCompounds" + }, + "Sensor": { + "name": "TVOC Sensor", + "description": "Sensor for measuring Total Volatile Organic Compounds" + }, + "Observations": [ + { + "phenomenonTime": "%TIMESTAMP%", + "result": "%TVOC%" + } + ] + }, + { + "id": "urn:myweatherstation:datastream:eCO2", + "name": "eCO2", + "description": "Equivalent Carbon Dioxide readings", + "unitOfMeasurement": { + "name": "Parts Per Million", + "symbol": "ppm", + "definition": "http://www.qudt.org/qudt/owl/1.0.0/unit/Instances.html#PartsPerMillion" + }, + "ObservedProperty": { + "name": "Equivalent Carbon Dioxide", + "definition": "http://www.qudt.org/qudt/owl/1.0.0/quantity/Instances.html#CarbonDioxideConcentration" + }, + "Sensor": { + "name": "eCO2 Sensor", + "description": "Sensor for measuring Equivalent Carbon Dioxide" + }, + "Observations": [ + { + "phenomenonTime": "%TIMESTAMP%", + "result": "%ECO2%" + } + ] + } + ] +} diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html