first commit

This commit is contained in:
Pan Kotskiy 2024-04-05 18:49:10 +03:00
commit 6a390b6197
13 changed files with 735 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.pio
.clang_complete
.ccls
/logs/

11
README.org Normal file
View File

@ -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

39
include/README Normal file
View File

@ -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

11
include/http_static.h Normal file
View File

@ -0,0 +1,11 @@
// http_static.h
#include <Arduino.h>
#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

101
logplotter.py Normal file
View File

@ -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)

21
platformio.ini Normal file
View File

@ -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

41
populate_static.py Executable file
View File

@ -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()

26
populate_static.sh Executable file
View File

@ -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

10
src/http_static.cpp Normal file
View File

@ -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";
}

258
src/main.cpp Normal file
View File

@ -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 <WiFi.h>
#include <Wire.h>
#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 <time.h>
//#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 "<span color='red' title='" + String(ccs811.errstat_str(errstat)) + "'>CCS811 sensor error</span>";
}
}
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);
}

97
src/static/index.html Normal file
View File

@ -0,0 +1,97 @@
<!DOCTYPE HTML>
<html>
<head>
<title>ESP32 WxServer</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
<style>
html {
font-family: Arial;
display: inline-block;
margin: 0px auto;
text-align: center;
}
h2 { font-size: 3.0rem; }
p { font-size: 3.0rem; }
.units { font-size: 1.2rem; }
.sensor-lable{
font-size: 1.5rem;
vertical-align:middle;
padding-bottom: 15px;
}
</style>
</head>
<body>
<h2>ESP32 CCS881 & HDC1080 Server</h2>
<h3>Loaded: %TIMESTAMP%</h3>
<p>
<i class="fas fa-thermometer-half" style="color:#059e8a;"></i>
<span class="sensor-lable">Temperature</span>
<span id="temperature">%TEMPERATURE%</span>
<sup class="units">&deg;C</sup>
</p>
<p>
<i class="fas fa-tint" style="color:#00add6;"></i>
<span class="sensor-lable">Humidity</span>
<span id="humidity">%HUMIDITY%</span>
<sup class="units">&percnt;</sup>
</p>
<p>
<i class="fas fa-gas-pump" style="color:#6959cd;"></i>
<span class="sensor-lable">CO2 concentration</span>
<span id="eco2">%ECO2%</span>
<sup class="units">ppm</sup>
</p>
<p>
<i class="fas fa-smog" style="color:#8b008b;"></i>
<span class="sensor-lable">TVOC concentration:</span>
<span id="tvoc">%TVOC%</span>
<sup class="units">ppb</sup>
</p>
</body>
<script>
setInterval(function ( ) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("temperature").innerHTML = this.responseText;
}
};
xhttp.open("GET", "/temperature", true);
xhttp.send();
}, 10000 ) ;
setInterval(function ( ) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("tvoc").innerHTML = this.responseText;
}
};
xhttp.open("GET", "/tvoc", true);
xhttp.send();
}, 10000 ) ;
setInterval(function ( ) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("eco2").innerHTML = this.responseText;
}
};
xhttp.open("GET", "/eco2", true);
xhttp.send();
}, 10000 ) ;
setInterval(function ( ) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("humidity").innerHTML = this.responseText;
}
};
xhttp.open("GET", "/humidity", true);
xhttp.send();
}, 10000 ) ;
</script>
</html>

View File

@ -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%"
}
]
}
]
}

11
test/README Normal file
View File

@ -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