esp32-env-monitor/esp32-env-monitor.ino

397 lines
9.6 KiB
Arduino
Raw Normal View History

2019-05-02 15:40:41 +02:00
#include <Arduino.h>
2019-05-02 16:27:48 +02:00
#include <Wire.h>
#include <Preferences.h>
2019-07-31 21:14:49 +02:00
#include <Ticker.h>
2019-05-02 15:40:41 +02:00
2019-05-02 15:06:21 +02:00
#include <WiFi.h>
#include "wifi_login.h"
2019-07-31 15:44:31 +02:00
#include <WebServer.h>
2019-05-02 15:06:21 +02:00
2019-05-07 16:09:15 +02:00
#include <U8g2lib.h>
2019-05-02 15:40:41 +02:00
#ifdef U8X8_HAVE_HW_SPI
#include <SPI.h>
#endif
2019-05-02 15:59:42 +02:00
#include <BME280I2C.h>
2019-05-02 16:27:48 +02:00
#include <Adafruit_SGP30.h>
2019-05-02 15:59:42 +02:00
2019-07-31 15:44:31 +02:00
WebServer server(80);
Preferences preferences;
2019-07-31 21:14:49 +02:00
Ticker metricsReader;
2020-05-11 17:11:07 +02:00
Ticker metricsPrinter;
2019-07-31 21:14:49 +02:00
Ticker baselineReader;
2020-05-11 17:19:25 +02:00
U8G2_SSD1327_WS_128X128_F_4W_HW_SPI u8g2(U8G2_R0, /* cs=*/ 17, /* dc=*/ 3, /* reset=*/ 16);
2019-05-02 15:40:41 +02:00
2019-05-07 20:48:13 +02:00
/* Based on Bosch BME280I2C environmental sensor data sheet.
Weather Monitoring :
forced mode, 1 sample/minute
pressure ×1, temperature ×1, humidity ×1, filter off
Current Consumption = 0.16 μA
RMS Noise = 3.3 Pa/30 cm, 0.07 %RH
Data Output Rate 1/60 Hz */
BME280I2C::Settings settings(
BME280::OSR_X1,
BME280::OSR_X1,
BME280::OSR_X1,
BME280::Mode_Forced,
BME280::StandbyTime_1000ms,
BME280::Filter_Off,
BME280::SpiEnable_False,
BME280I2C::I2CAddr_0x76 // I2C address. I2C specific.
);
BME280I2C bme(settings);
2019-05-02 15:59:42 +02:00
2019-05-02 16:27:48 +02:00
Adafruit_SGP30 sgp;
2019-05-07 16:09:15 +02:00
// setup the terminal (U8G2LOG) and connect to u8g2 for automatic refresh of the display
// The size (width * height) depends on the display
// u8g2_font_t0_15_mf: BBX Width 8, Height 15
// 128/8 = 16
2019-05-02 15:40:41 +02:00
#define U8LOG_WIDTH 16
2019-05-07 16:09:15 +02:00
// 128/15 = 8
#define U8LOG_HEIGHT 8
2019-05-02 15:40:41 +02:00
uint8_t u8log_buffer[U8LOG_WIDTH*U8LOG_HEIGHT];
2019-05-07 16:09:15 +02:00
U8G2LOG u8g2log;
2019-05-02 15:40:41 +02:00
2019-05-02 16:27:48 +02:00
/* return absolute humidity [mg/m^3] with approximation formula
* @param temperature [°C]
* @param humidity [%RH]
*/
uint32_t getAbsoluteHumidity(float temperature, float humidity) {
2019-05-07 20:53:13 +02:00
// approximation formula from Sensirion SGP30 Driver Integration chapter 3.15
const float absoluteHumidity = 216.7f * ((humidity / 100.0f) * 6.112f * exp((17.62f * temperature) / (243.12f + temperature)) / (273.15f + temperature)); // [g/m^3]
const uint32_t absoluteHumidityScaled = static_cast<uint32_t>(1000.0f * absoluteHumidity); // [mg/m^3]
return absoluteHumidityScaled;
2019-05-02 16:27:48 +02:00
}
2019-07-31 17:43:07 +02:00
BME280::TempUnit tempUnit(BME280::TempUnit_Celsius);
BME280::PresUnit presUnit(BME280::PresUnit_hPa);
float temp(NAN), hum(NAN), pres(NAN);
uint16_t TVOC_base, eCO2_base;
bool baseline_set = false;
2020-05-11 17:11:07 +02:00
uint8_t last_printed = 0;
2019-07-31 21:14:49 +02:00
2020-05-11 17:11:07 +02:00
void readMetrics() {
2019-07-31 21:14:49 +02:00
//Read Temperature, humidity and pressure
bme.read(pres, temp, hum, tempUnit, presUnit);
//Read Air Quality
sgp.setHumidity(getAbsoluteHumidity(temp, hum));
if (! sgp.IAQmeasure()) {
u8g2log.print("Air Quality Measurement failed\n");
return;
}
2020-05-11 17:11:07 +02:00
}
void printMetrics() {
2020-05-11 18:08:01 +02:00
//Clear buffer
u8g2.clearBuffer();
2020-05-11 17:11:07 +02:00
//Print a different metrics each time
switch (last_printed) {
case 0:
2020-05-11 18:08:01 +02:00
u8g2.setCursor(0, 15);
u8g2.setFont(u8g2_font_fur14_tf);
u8g2.print("Temperature");
u8g2.setCursor(0, 70);
u8g2.setFont(u8g2_font_fur30_tf);
u8g2.print(temp,1);
u8g2.setFont(u8g2_font_fur11_tf);
u8g2.print(" °C");
2020-05-11 17:11:07 +02:00
last_printed++;
break;
case 1:
2020-05-11 18:08:01 +02:00
u8g2.setCursor(0, 15);
u8g2.setFont(u8g2_font_fur14_tf);
u8g2.print("Humidity");
u8g2.setCursor(0, 70);
u8g2.setFont(u8g2_font_fur30_tf);
u8g2.print(hum,1);
u8g2.setFont(u8g2_font_fur11_tf);
u8g2.print(" % RH");
2020-05-11 17:11:07 +02:00
last_printed++;
break;
case 2:
2020-05-11 18:08:01 +02:00
u8g2.setCursor(0, 15);
u8g2.setFont(u8g2_font_fur14_tf);
u8g2.print("Pressure");
u8g2.setCursor(0, 70);
u8g2.setFont(u8g2_font_fur30_tf);
u8g2.print(pres,0);
u8g2.setFont(u8g2_font_fur11_tf);
u8g2.print(" hPa");
2020-05-11 17:11:07 +02:00
last_printed++;
break;
case 3:
2020-05-11 18:08:01 +02:00
u8g2.setCursor(0, 15);
u8g2.setFont(u8g2_font_fur14_tf);
u8g2.print("Total VOC");
u8g2.setCursor(0, 70);
u8g2.setFont(u8g2_font_fur30_tf);
u8g2.print(sgp.TVOC);
u8g2.setFont(u8g2_font_fur11_tf);
u8g2.print(" ppb");
2020-05-11 17:11:07 +02:00
last_printed++;
break;
case 4:
2020-05-11 18:08:01 +02:00
u8g2.setCursor(0, 15);
u8g2.setFont(u8g2_font_fur14_tf);
u8g2.print("Equiv CO2");
u8g2.setCursor(0, 70);
u8g2.setFont(u8g2_font_fur30_tf);
u8g2.print(sgp.eCO2);
u8g2.setFont(u8g2_font_fur11_tf);
u8g2.print(" ppm");
2020-05-11 17:11:07 +02:00
last_printed = 0;
break;
default:
2020-05-11 18:08:01 +02:00
u8g2.setFont(u8g2_font_fur11_tf);
u8g2.print("invalid last_printed value");
2020-05-11 17:11:07 +02:00
last_printed = 0;
break;
}
2020-06-25 14:33:05 +02:00
u8g2.setCursor(0, 120);
2020-07-09 14:28:36 +02:00
u8g2.setFont(u8g2_font_fur17_tf);
2020-06-25 14:33:05 +02:00
u8g2.print(temp,1);
u8g2.print(" °C");
2020-05-11 18:08:01 +02:00
u8g2.sendBuffer();
2019-07-31 21:14:49 +02:00
}
void readBaseline() {
if (! sgp.getIAQBaseline(&eCO2_base, &TVOC_base)) {
u8g2log.print("Failed to get baseline readings\n");
return;
}
//Init Non Volatile Storage
// Namespace name is limited to 15 chars.
// RW-mode (second parameter has to be false).
preferences.begin("env-monitor", false);
preferences.putBool("baseline_set",true);
preferences.putUShort("TVOC_base", TVOC_base);
preferences.putUShort("eCO2_base", eCO2_base);
preferences.end();
//Next baseline reading in 1h
baselineReader.once(3600, readBaseline);
}
2019-07-31 17:43:07 +02:00
void sendMetrics() {
String message = "# ESP32 Env monitor Prometheus metrics\n\n";
2019-07-31 21:37:31 +02:00
float temperature = temp;
float humidity = hum;
float pressure = pres;
if (temperature < 100) {
message += "# HELP env_temperature The temperature in " + String(tempUnit == BME280::TempUnit_Celsius ? 'C' :'F') + "\n";
message += "# TYPE env_temperature gauge\n";
message += "env_temperature " + String(temperature) + "\n\n";
}
2019-07-31 17:43:07 +02:00
2019-07-31 21:37:31 +02:00
if (pressure < 2000) {
message += "# HELP env_pressure The pressure in " + String(presUnit) + "\n";
message += "# TYPE env_pressure gauge\n";
message += "env_pressure " + String(pressure) + "\n\n";
}
2019-07-31 17:43:07 +02:00
2019-07-31 21:37:31 +02:00
if (humidity < 100){
message += "# HELP env_humidity The humidity in %\n";
message += "# TYPE env_humidity gauge\n";
message += "env_humidity " + String(humidity) + "\n\n";
}
2019-07-31 17:43:07 +02:00
message += "# HELP env_tvoc The TVOC in ppb\n";
message += "# TYPE env_tvoc gauge\n";
message += "env_tvoc " + String(sgp.TVOC) + "\n\n";
message += "# HELP env_eco2 The eCO2 in ppb\n";
message += "# TYPE env_eco2 gauge\n";
message += "env_eco2 " + String(sgp.eCO2) + "\n\n";
server.send(200, "text/plain; version=0.0.4", message);
}
2019-05-02 15:06:21 +02:00
void setup() {
2019-05-07 16:09:15 +02:00
//Init Display
u8g2.begin();
u8g2.setFont(u8g2_font_t0_15_mf);
u8g2.enableUTF8Print();
2019-05-07 20:10:38 +02:00
u8g2log.begin(u8g2, U8LOG_WIDTH, U8LOG_HEIGHT, u8log_buffer);
2019-05-07 16:09:15 +02:00
u8g2log.setLineHeightOffset(0); // set extra space between lines in pixel, this can be negative
u8g2log.setRedrawMode(0); // 0: Update screen with newline, 1: Update screen for every char
2019-05-02 15:40:41 +02:00
2019-05-02 15:06:21 +02:00
//Init wifi connection
WiFi.begin(ssid, password);
2019-05-07 20:49:58 +02:00
u8g2log.print("Connecting to:\n");
2019-05-07 16:09:15 +02:00
u8g2log.print(ssid);
u8g2log.print("\n");
u8g2log.print("\n");
2019-05-02 15:40:41 +02:00
2019-05-02 15:06:21 +02:00
while (WiFi.status() != WL_CONNECTED) {
delay(500);
2019-05-07 16:09:15 +02:00
u8g2log.print(".");
2019-05-02 15:06:21 +02:00
}
2019-05-07 16:09:15 +02:00
u8g2log.print("\n");
u8g2log.print("WiFi connected\n");
u8g2log.print("IP address:\n");
u8g2log.println(WiFi.localIP());
u8g2log.print("\n");
u8g2log.print("\n");
2019-05-02 15:06:21 +02:00
2019-07-31 15:44:31 +02:00
//Init Server
2019-07-31 17:43:07 +02:00
server.on("/",
[]() {
server.send(200, "text/plain", "Welcome to the Env Monitor!\n");
}
);
server.on("/metrics", sendMetrics);
2019-07-31 15:44:31 +02:00
server.onNotFound(
[]() {
server.send(404, "text/plain", "Content not found\n");
}
);
server.begin();
2019-05-02 15:59:42 +02:00
//Init BME280 sensor
Wire.begin();
2019-05-07 20:53:13 +02:00
while(!bme.begin()) {
2019-05-07 16:09:15 +02:00
u8g2log.print("Could not find BME280 sensor!\n");
2019-05-02 15:59:42 +02:00
delay(1000);
}
2019-05-07 20:53:13 +02:00
switch(bme.chipModel()) {
case BME280::ChipModel_BME280:
u8g2log.print("Found BME280 sensor! Success.\n");
break;
case BME280::ChipModel_BMP280:
u8g2log.print("Found BMP280 sensor! No Humidity available.\n");
break;
default:
u8g2log.print("Found UNKNOWN sensor! Error!\n");
2019-05-02 15:59:42 +02:00
}
2019-05-02 16:27:48 +02:00
//Init SGP30 sensor
2019-05-07 20:53:13 +02:00
while(!sgp.begin()) {
2019-05-07 16:09:15 +02:00
u8g2log.print("Could not find SGP30 sensor!\n");
2019-05-02 16:27:48 +02:00
delay(1000);
}
2019-05-07 16:09:15 +02:00
u8g2log.print("Found SGP30 serial #");
u8g2log.print(sgp.serialnumber[0], HEX);
u8g2log.print(sgp.serialnumber[1], HEX);
u8g2log.print(sgp.serialnumber[2], HEX);
u8g2log.print("\n");
2019-05-02 16:27:48 +02:00
2019-05-07 20:50:45 +02:00
if (! sgp.IAQinit()){
u8g2log.print("Failed to init IAQ algorithm\n");
}
2019-07-31 12:33:01 +02:00
//Init Non Volatile Storage
// Namespace name is limited to 15 chars.
// RW-mode (second parameter has to be false).
preferences.begin("env-monitor", false);
baseline_set = preferences.getBool("baseline_set",false);
if (baseline_set){
TVOC_base = preferences.getUShort("TVOC_base",0);
eCO2_base = preferences.getUShort("eCO2_base",0);
if ( TVOC_base != 0 && eCO2_base != 0){
// If you have a baseline measurement from before you can assign it to start, to 'self-calibrate'
u8g2log.print("Found previous baseline\n");
sgp.setIAQBaseline(eCO2_base, TVOC_base);
u8g2log.print("Baseline values: eCO2: 0x");
u8g2log.print(eCO2_base, HEX);
u8g2log.print(" & TVOC: 0x");
u8g2log.print(TVOC_base, HEX);
u8g2log.print("\n");
}
}
2019-05-02 16:27:48 +02:00
2019-07-31 12:33:01 +02:00
preferences.end();
2019-05-07 20:11:07 +02:00
//delay to allow setup output reading
delay(10000);
2019-05-02 16:27:48 +02:00
2019-07-31 21:14:49 +02:00
//read Metrics once
readMetrics();
2019-05-02 16:27:48 +02:00
2019-07-31 21:14:49 +02:00
//and then read Metrics every minutes
metricsReader.attach(60, readMetrics);
2020-05-25 17:25:31 +02:00
//print metrics every 5 seconds
metricsPrinter.attach(5, printMetrics);
2020-05-11 17:11:07 +02:00
2019-07-31 21:14:49 +02:00
//First baseline reading after 12 hours
baselineReader.once(3600 * 12, readBaseline);
2019-07-31 21:14:49 +02:00
}
2019-07-31 21:14:49 +02:00
void loop() {
2019-05-02 16:27:48 +02:00
//detect connection lost and reconnect
if (WiFi.status() != WL_CONNECTED) {
u8g2log.print("WiFi lost connection.\n");
delay(500);
WiFi.begin(ssid, password);
u8g2log.print("Reconnecting to:\n");
u8g2log.print(ssid);
u8g2log.print("\n");
u8g2log.print("\n");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
u8g2log.print(".");
}
};
2019-07-31 21:14:49 +02:00
server.handleClient();
2019-05-02 15:40:41 +02:00
2019-05-02 15:06:21 +02:00
}
2019-05-07 20:53:13 +02:00
// vim: set ts=2 sw=2 et: