diff --git a/.gitignore b/.gitignore index 0686dc8..0545a9e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .pio +.DS_Store CMakeLists.txt CMakeListsPrivate.txt cmake-build-*/ diff --git a/README.md b/README.md index 7311c96..9fb5f0e 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,36 @@ # CorO₂Sens +Disclaimer and credits: this repository is a fork from https://github.com/kmetz/coro2sens. + +Several changes have been made in this fork. Please also consult the README file of the original version. + +The following sections have been updated for this repository: + + + Build a simple device that warns if CO₂ concentration in a room becomes a risk for COVID-19 aerosol infections. - Measures CO₂ concentration in room air. -- Controls an RGB LED (green, yellow, red, like a traffic light). -- A buzzer can be connected that alarms if levels are critical. -- Also opens a WiFi portal which shows current readings and a graph (not connected to the internet). +- Controls two LEDs (green + orange/red) for indication of warning/critical CO2 level. +- Controls a NeoPixel ring for indication of CO2 level and its criticality (optional). +- A buzzer can be connected that alarms if levels are critical (optional). +- Also opens a WiFi portal which shows current readings and a graph (not connected to the internet; optional). - Can be built for ~ $60 / 50€ (parts cost). This project was heavily inspired by [ideas from Umwelt-Campus Birkenfeld](https://www.umwelt-campus.de/forschung/projekte/iot-werkstatt/ideen-zur-corona-krise). You can also find a good overview of the topic by Rainer Winkler here: [Recommendations for use of CO2 sensors to control room air quality during the COVID-19 pandemic](https://medium.com/@rainer.winkler.poaceae/recommendations-for-use-of-co2-sensors-to-control-room-air-quality-during-the-covid-19-pandemic-c04cac6644d0). -![coro2sens overview](coro2sens.jpeg) + ## Sensors -- The sensor used here is the Sensirion SCD30 (around $50 / 40€) which is optionally augmented by a BME280 pressure sensor to improve accuracy. -- [Look here](https://github.com/RainerWinkler/CO2-Measurement-simple) if you want to use MH-Z19B sensors. - +The sensor used here is the Sensirion SCD30 (around 70 USD / 60 €). + +The pressure compensation by an additional sensor BME280 is currently no longer supported in this forked repo. + + + ## Threshold values | LED color |CO₂ concentration | @@ -29,73 +41,48 @@ You can also find a good overview of the topic by Rainer Winkler here: [Recommen Based on a [Recommendation from the REHVA](https://www.rehva.eu/fileadmin/user_upload/REHVA_COVID-19_guidance_document_V3_03082020.pdf) (Federation of European Heating, Ventilation and Air Conditioning associations, [rehva.eu](https://www.rehva.eu/)) -for preventing COVID-19 aerosol spread, especially in schools. +for preventing COVID-19 aerosol spread, especially in schools. -## Web server -You can read current levels and a simple graph for the last hour by connecting to the WiFi `coro2sens` that is created. -Most devices will open a captive portal, immediately showing the data. You can also open `http://10.0.0.1/` in a browser. -## You need -1. Any ESP32 or ESP8266 board (like a [WEMOS D32](https://docs.wemos.cc/en/latest/d32/d32.html) (about $18 / 15€) or [WEMOS LOLIN D1 Mini](https://docs.wemos.cc/en/latest/d1/d1_mini.html) (about $7 / 6€)). -ESP32 has bluetooth, for future expansion. -1. [Sensirion SCD30](https://www.sensirion.com/en/environmental-sensors/carbon-dioxide-sensors/carbon-dioxide-sensors-co2/) I2C carbon dioxide sensor module ([mouser](https://mouser.com/ProductDetail/Sensirion/SCD30?qs=rrS6PyfT74fdywu4FxpYjQ==), [digikey](https://www.digikey.com/product-detail/en/sensirion-ag/SCD30/1649-1098-ND/8445334)) (around $50 / 40€). -1. 1 [NeoPixel](https://www.adafruit.com/category/168) compatible RGB LED (WS2812B, like the V2 Flora RGB Smart NeoPixel LED, you can also remove one from a larger strip which might be cheaper). -1. A 3V piezo buzzer or a small speaker. -1. Optional: [Bosch BME280](https://www.bosch-sensortec.com/products/environmental-sensors/humidity-sensors-bme280/) I2C sensor module (like the GY-BME280 board), for air pressure compensation, improves accuracy (less than $5 / 4€). -1. A nice case :) Make shure the sensor has enough air flow. +## Web server +You can read current levels and a simple graph for the last hour by connecting to the WiFi `coro2sens_` that is created. +Most devices will open a captive portal, immediately showing the data. You can also open `http://10.0.0.1/` in a browser. +You can also control most of the settings via the web interface. Visit `http://10.0.0.1/config` in a browser. -### Wiring -| ESP8266 pin | ESP32 pin | goes to | -|:-------------|:--------------|:-------------------------------------------| -| 3V3 | 3V3 | SCD30 VIN, BME280 VIN | -| 5V | 5V | LED +5V | -| GND | GND | SCD30 GND, BME280 GND, LED GND, Buzzer (-) | -| SCL / D1 | SCL / GPIO 22 | SCD30 SCL, BME280 SCL | -| SDA / D2 | SDA / GPIO 21 | SCD30 SDA, BME280 SDA | -| GPIO 0 / D3 | GPIO 16 | LED DIN | -| GPIO 14 / D5 | GPIO 19 | Buzzer (+) | - - -### Flashing the ESP using [PlatfomIO](https://platformio.org/) -- Simply open the project, select your env (`esp12e` for ESP8266 / `esp32dev` for ESP32) and run / upload. -- Or via command line: - - `pio run -t -e esp12e upload` for ESP8266. - - `pio run -t -e esp32dev upload` for ESP32. -- Libraries will be installed automatically. - -### Flash using the Arduino IDE -- Install [the latest Arduino IDE](https://www.arduino.cc/en/main/software). -- [Download the latest code](https://github.com/kmetz/coro2sens/archive/master.zip) and unzip it somewhere. -- Open `coro2sense.ino` in the `coro2sens` sub folder in your Arduino IDE. -- Install (or update) your board platform: - (*Tools –> Board –> Board Manager...*) - - Install `esp8266` or `esp32`. -- Install (or update) the following libraries using the built-in library manager: - (*Tools –> Library Manager...*) - - For ESP8266: - - `SparkFun BME280` - - `FastLED` - - For ESP32: - - `SparkFun SCD30 Arduino Library` - - `SparkFun BME280` - - `FastLED` -- Install the following external libraries: - (download .zip file, then import it via *Sketch –> Include Library –> Add .ZIP Library...*) - - For ESP8266: +## You need +- A NodeMCU board (any other ESP8266 based board might also work). +- [Sensirion SCD30](https://www.sensirion.com/en/environmental-sensors/carbon-dioxide-sensors/carbon-dioxide-sensors-co2/) I2C carbon dioxide sensor module ([mouser](https://mouser.com/ProductDetail/Sensirion/SCD30?qs=rrS6PyfT74fdywu4FxpYjQ==), [digikey](https://www.digikey.com/product-detail/en/sensirion-ag/SCD30/1649-1098-ND/8445334)). +- *(optional)* NeoPixel ring +- *(optional)* 2 LEDs (green + yellow/red); the green one indicates "all good" (< 800 ppm) the other one that this limit has been exceeded +- *(optional)* A 3V piezo buzzer or a small speaker. +- *(optional)* You may want to work with Guido Burger's IoT Octopus PCB. This helps fixing the sensor and the NodeMCU, as well as the LEDs. +- *(optional)* A nice case :) Make sure the sensor has enough air flow. +- An Arduino IDE (select the board "NodeMCU 1.0 (ESP-12E Module)", v2.7.1 and v2.7.4 have been tested and should work; you may need to install the board support for the "esp8266") +- Arduino libraries: + - FastLED (via the library manager) + - zip file libraries + (download .zip file, then import it via *Sketch –> Include Library –> Add .ZIP Library...*) - [paulvha/scd30](https://github.com/paulvha/scd30) ([.zip](https://github.com/paulvha/scd30/archive/master.zip)) - [me-no-dev/ESPAsyncTCP](https://github.com/me-no-dev/ESPAsyncTCP) ([.zip](https://github.com/me-no-dev/ESPAsyncTCP/archive/master.zip)) - [me-no-dev/ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) ([.zip](https://github.com/me-no-dev/ESPAsyncWebServer/archive/master.zip)) - - For ESP32: - - [lbernstone/Tone32](https://github.com/lbernstone/Tone32) ([.zip](https://github.com/lbernstone/Tone32/archive/master.zip)) - - [me-no-dev/AsyncTCP](https://github.com/me-no-dev/AsyncTCP) ([.zip](https://github.com/me-no-dev/AsyncTCP/archive/master.zip)) - - [me-no-dev/ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) ([.zip](https://github.com/me-no-dev/ESPAsyncWebServer/archive/master.zip)) -- Run & upload :) -Please let me know of any issues you might encounter ([open a GitHub issue](https://github.com/kmetz/coro2sens/issues/new/choose) or write me on [twitter.com/kmetz](https://twitter.com/kmetz) or k@kjpm.de). -Also, I'd be for hire for customizations. + +### Wiring + +| ESP8266 pin | goes to | +| :---------- | :----------------------------------------- | +| 3V3 | SCD30 VIN | +| GND | SCD30 GND, Buzzer (-) | +| SCL / D1 | SCD30 SCL | +| SDA / D2 | SCD30 SDA | +| D3 | LED DIN | +| D5 | Green LED | +| D6 | Buzzer (+) | +| D7 | Warning LED (yellow or red is a good idea) | +| D8 | NeoPixel ring | diff --git a/coro2sens.jpeg b/coro2sens.jpeg deleted file mode 100644 index 158c6d0..0000000 Binary files a/coro2sens.jpeg and /dev/null differ diff --git a/coro2sens/config.h b/coro2sens/config.h new file mode 100644 index 0000000..4ab0d22 --- /dev/null +++ b/coro2sens/config.h @@ -0,0 +1,126 @@ +// Define if the Octopus PCB layout is used or not +// ---------------------------------------------------------------------------- +#define OCTOPUS 1 + +// Logging to serial console +// ---------------------------------------------------------------------------- +#define SERIAL_BAUDRATE 115200 +#define USE_SERIAL_CONSOLE 0 +//#undef USE_SERIAL_CONSOLE // Uncomment this line to disable logging to serial console (e.g. "deactivates" bright blue LED on NodeMCU) +#if defined(USE_SERIAL_CONSOLE) +#define serial_begin Serial.begin +#define serial_printf Serial.printf +#define serial_println Serial.println +#else +#define serial_begin +#define serial_printf +#define serial_println +#endif + +// LED and NeoPixel ring +// ---------------------------------------------------------------------------- +// Activity indicator LED (use the built-in LED if your board has one). +// Which pin on the Arduino is connected to the NeoPixels? + +#if defined(OCTOPUS) +#define LED_GREEN_PIN D5 +//#define LED_YELLOW_PIN D7 +//#define LED_RED_PIN D6 +#define LED_WARN_PIN D7 /* combined warning LED to replace separate yellow+red LEDs */ +#define NEOPIXEL_PIN D8 +#else +#define LED_GREEN_PIN D8 +#define LED_YELLOW_PIN D7 +#define LED_RED_PIN D6 +#define NEOPIXEL_PIN D4 +#endif + +//#undef NEOPIXEL_PIN + +// How many NeoPixels are attached to the Arduino? +#if defined(NEOPIXEL_PIN) + #define LED_INTENSITY_MIN 5 + #define LED_INTENSITY_MAX 100 + #define LED_INTENSITY 50 + #define NUMPIXELS 16 + #define COLOR_BLACK 0, 0, 0 + #define COLOR_GREEN 0, 3*settings.neopixel_brightness/10, 0 + #define COLOR_YELLOW 2*settings.neopixel_brightness/10, settings.neopixel_brightness/10, 0 + #define COLOR_RED 3*settings.neopixel_brightness/10, 0, 0 +#endif /* defined(NEOPIXEL_PIN) */ + +// Buzzer, activated continuously when CO2 level is critical. +// ---------------------------------------------------------------------------- +#if defined(OCTOPUS) +#define BUZZER_PIN D6 +#endif +//#undef BUZZER_PIN // Uncomment if buzzer shall not be used + +#ifdef BUZZER_PIN + #define BEEP_DURATION_MS 200 /* Beep duration in milliseconds */ + #define BEEP_DURATION_MS_MIN 20 + #define BEEP_DURATION_MS_MAX 3000 + #define BEEP_TONE_FREQ 1047 /* Note C6 (see also: pitches.h on toneMelody example) */ + #define BEEP_TONE_FREQ_MIN 261 + #define BEEP_TONE_FREQ_MAX 2093 + + #define BUZZER_MAX_BEEPS 7 /* Maximum number of beeps for an active alarm */ + #define BUZZER_MAX_BEEPS_MIN 0 + #define BUZZER_MAX_BEEPS_MAX 99 +#endif + + +// CO2 thresholds for warning and critical warning +// ---------------------------------------------------------------------------- +// CO2 Thresholds (ppm). +// +// Recommendation from REHVA (Federation of European Heating, Ventilation and Air Conditioning associations, rehva.eu) +// for preventing COVID-19 aerosol spread especially in schools: +// - warn: 800, critical: 1000 +// (https://www.rehva.eu/fileadmin/user_upload/REHVA_COVID-19_guidance_document_V3_03082020.pdf) +// +// General air quality recommendation by the German Federal Environmental Agency (2008): +// - warn: 1000, critical: 2000 +// (https://www.umweltbundesamt.de/sites/default/files/medien/pdfs/kohlendioxid_2008.pdf) +// +#define CO2_WARN_PPM 800 +#define CO2_WARN_PPM_MIN 500 +#define CO2_WARN_PPM_MAX 2000 +#define CO2_CRITICAL_PPM 1000 +#define CO2_CRITICAL_PPM_MIN 600 +#define CO2_CRITICAL_PPM_MAX 3000 +#define CO2_PLASABILIT_PPM_MIN 300 + +// CO2 measurement +// ---------------------------------------------------------------------------- +// Update CO2 level every MEASURE_INTERVAL_S seconds. +// Should be kept at 2 unless you want to save power. +#define MEASURE_INTERVAL_S 2 + +// WiFi hotspot configuration (including IP + DNS) +// ---------------------------------------------------------------------------- +// Set to 0 to disable altogether. +#define WIFI_ENABLED 1 +#define WIFI_HOTSPOT_PREFIX "coro2sens" +#define WIFI_HOTSPOT_SIZE 64 +#define WIFI_AP_IP 10, 0, 0, 1 +#define WIFI_AP_NETMASK 255, 255, 255, 0 +#define WIFI_DNS_PORTNO 53 + +// How long the graph/log in the WiFi portal should go back, in minutes. +#define LOG_MINUTES 120 +// Label describing the time axis. +#define TIME_LABEL "2 Stunden" + +#define GRAPH_W 600 +#define GRAPH_H 260 +#define LOG_SIZE GRAPH_W + +// Button +// ---------------------------------------------------------------------------- +#define BUTTON_PIN D3 // "GPIO0/FLASH" button on NodeMCU + +// Storage for settings (non-volatile memory) +// ---------------------------------------------------------------------------- +#define SETTINGS_STORAGE +#define SETTINGS_EEPROM_MAGIC 0x6D616568 diff --git a/coro2sens/coro2sens.ino b/coro2sens/coro2sens.ino index a0e2512..3e55893 100644 --- a/coro2sens/coro2sens.ino +++ b/coro2sens/coro2sens.ino @@ -1,82 +1,11 @@ // SETUP ======================================================================= -// CO2 Thresholds (ppm). -// -// Recommendation from REHVA (Federation of European Heating, Ventilation and Air Conditioning associations, rehva.eu) -// for preventing COVID-19 aerosol spread especially in schools: -// - warn: 800, critical: 1000 -// (https://www.rehva.eu/fileadmin/user_upload/REHVA_COVID-19_guidance_document_V3_03082020.pdf) -// -// General air quality recommendation by the German Federal Environmental Agency (2008): -// - warn: 1000, critical: 2000 -// (https://www.umweltbundesamt.de/sites/default/files/medien/pdfs/kohlendioxid_2008.pdf) -// -#define CO2_WARN_PPM 800 -#define CO2_CRITICAL_PPM 1000 - -// LED warning light (always on, green / yellow / red). -#if defined(ESP32) - #define LED_PIN 16 -#elif defined(ESP8266) - #define LED_PIN D3 -#endif -#define LED_CHIPSET WS2812B -#define LED_COLOR_ORDER GRB -#define LED_BRIGHTNESS 42 // 0-255 -#define NUM_LEDS 1 - -// Buzzer, activated continuously when CO2 level is critical. -#if defined(ESP32) - #define BUZZER_PIN 19 -#elif defined(ESP8266) - #define BUZZER_PIN D5 -#endif -#define BEEP_DURATION_MS 100 // Beep milliseconds -#define BEEP_TONE 1047 // C6 - -// BME280 pressure sensor (optional). -// Address should be 0x76 or 0x77. -#define BME280_I2C_ADDRESS 0x76 - -// Update CO2 level every MEASURE_INTERVAL_S seconds. -// Should be kept at 2 unless you want to save power. -#define MEASURE_INTERVAL_S 2 - -// WiFi. -// Set to 0 to disable altogether. -#define WIFI_ENABLED 1 - -// 1 = captive portal hotspot without internet connection, showing data when you connect with it. -// 0 = WiFi client, showing data when accessed via IP address. -#define WIFI_HOTSPOT_MODE 1 - -// AP name when WIFI_HOTSPOT_MODE is 1 -#define WIFI_HOTSPOT_NAME "coro2sens" - -// Credentials when WIFI_HOTSPOT_MODE is 0 -#define WIFI_CLIENT_SSID "your WiFi name" -#define WIFI_CLIENT_PASSWORD "*****" - -// How long the graph/log in the WiFi portal should go back, in minutes. -#define LOG_MINUTES 60 -// Label describing the time axis. -#define TIME_LABEL "1 hour" - -// Activity indicator LED (use the built-in LED if your board has one). -//#define ACTIVITY_LED_PIN 5 - -// ============================================================================= - - -#define GRAPH_W 600 -#define GRAPH_H 260 -#define LOG_SIZE GRAPH_W - +/* Platform and feature specific configuration */ +#include "config.h" #include #include -#include -#include +#include #if defined(ESP32) #include @@ -86,7 +15,6 @@ #include #include #endif - #elif defined(ESP8266) #include #if WIFI_ENABLED @@ -100,203 +28,436 @@ #include #endif +#ifdef SETTINGS_STORAGE + #include +#endif + +struct settings_t { + uint32_t magic; + uint16_t co2_ppm_warn; + uint16_t co2_ppm_alarm; + uint16_t co2_ppm_critical; + uint8_t neopixel_brightness; + uint16_t buzzer_tone_freq_hz; + uint16_t buzzer_tone_duration_ms; + uint16_t buzzer_max_num_beeps; + char hotspot_name[WIFI_HOTSPOT_SIZE]; +} settings; SCD30 scd30; uint16_t co2 = 0; unsigned long lastMeasureTime = 0; bool alarmHasTriggered = false; +uint16_t alarmActiveCount = 0; uint16_t co2log[LOG_SIZE] = {0}; // Ring buffer. uint32_t co2logPos = 0; // Current buffer start position. -uint16_t co2logDownsample = max(1, ((((LOG_MINUTES) * 60) / MEASURE_INTERVAL_S) / LOG_SIZE)); +uint16_t co2logDownsample = std::max(1, ((((LOG_MINUTES) * 60) / MEASURE_INTERVAL_S) / LOG_SIZE)); uint16_t co2avg, co2avgSamples = 0; // Used for downsampling. +byte mac[6] = {0xDE, 0xAD, 0xBE, 0xEF, 0xC0, 0xFE}; // MAC address Wifi shield -BME280 bme280; -bool bme280isConnected = false; -uint16_t pressure = 0; - -CRGB leds[NUM_LEDS]; +#ifdef NEOPIXEL_PIN + Adafruit_NeoPixel pixels(NUMPIXELS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800); + // Note: could handle dynamic strip length; setter pixels.updateLength() + stored setting +#endif #if WIFI_ENABLED -AsyncWebServer server(80); -IPAddress apIP(10, 0, 0, 1); -IPAddress netMsk(255, 255, 255, 0); -DNSServer dnsServer; -void handleCaptivePortal(AsyncWebServerRequest *request); + AsyncWebServer server(80); + IPAddress apIP(WIFI_AP_IP); + IPAddress netMsk(WIFI_AP_NETMASK); + DNSServer dnsServer; + void handleCaptivePortal(AsyncWebServerRequest *request); + void handleConfig(AsyncWebServerRequest *request); #endif +/* List function prototypes */ +void indicate_calib(void); +void set_green_led(int value); +void set_yellow_led(int value); +void set_red_led(int value); +void set_warn_led(int value); +void init_leds(void); +void alarmSound(); /** - * Triggered once when the CO2 level goes critical. - */ -void alarmOnce() { + Triggered continuously when the CO2 level is critical. +*/ +void alarmSound() +{ +#if defined(BUZZER_PIN) + serial_printf("alarmActiveCount: %d\n", alarmActiveCount); + // beep only up to a specific amount of times after alarm threshold was hit + if( alarmActiveCount <= settings.buzzer_max_num_beeps ) + { +#if defined(ESP32) + // Use Tone32. + tone(BUZZER_PIN, settings.buzzer_tone_freq_hz, settings.buzzer_tone_duration_ms, 0); +#else + // Use Arduino tone(). + tone(BUZZER_PIN, settings.buzzer_tone_freq_hz, settings.buzzer_tone_duration_ms); +#endif + } +#endif /* defined(BUZZER_PIN) */ } +void store_current_settings() +{ +#ifdef SETTINGS_STORAGE + EEPROM.put(0, settings); + EEPROM.commit(); +#endif +} -/** - * Triggered continuously when the CO2 level is critical. - */ -void alarmContinuous() { -#if defined(ESP32) - // Use Tone32. - tone(BUZZER_PIN, BEEP_TONE, BEEP_DURATION_MS, 0); -#else - // Use Arduino tone(). - tone(BUZZER_PIN, BEEP_TONE, BEEP_DURATION_MS); +void load_settings() +{ +#ifdef SETTINGS_STORAGE + EEPROM.begin(sizeof(settings_t)); + serial_printf("EEPROM length: %d\n", EEPROM.length()); + EEPROM.get(0, settings); + + serial_printf("settings.magic: 0x%08X\n", settings.magic); + + // EEPROM has never been written before, store default values; + // also set the following line to `if(false)` when restructuring the data structure (data sizes or order); expanding shouldn't be a problem + //if(false) + if(SETTINGS_EEPROM_MAGIC == settings.magic) + { + serial_printf("Settings have been loaded from EEPROM"); + } + else + { +#endif + // Do some "maehgic" + settings.magic = SETTINGS_EEPROM_MAGIC; + // Use default settings + settings.co2_ppm_warn = CO2_WARN_PPM; + settings.co2_ppm_alarm = CO2_CRITICAL_PPM; + settings.co2_ppm_critical = CO2_CRITICAL_PPM; + settings.neopixel_brightness = LED_INTENSITY; + settings.buzzer_tone_freq_hz = BEEP_TONE_FREQ; + settings.buzzer_tone_duration_ms = BEEP_DURATION_MS; + settings.buzzer_max_num_beeps = BUZZER_MAX_BEEPS; + snprintf(settings.hotspot_name, WIFI_HOTSPOT_SIZE, "%s_%02X%02X%02X%02X%02X%02X", WIFI_HOTSPOT_PREFIX, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + + serial_printf("Settings have been loaded from defaults"); +#ifdef SETTINGS_STORAGE + // Store default settings in EEPROM + store_current_settings(); + serial_printf(" and been stored in EEPROM"); + } #endif + serial_printf(".\n"); } +void setup() +{ +#if USE_SERIAL_CONSOLE + serial_begin(SERIAL_BAUDRATE); + serial_println(); // start with line break to get rid of gibberish output after flashing +#endif + +#if WIFI_ENABLED + // Overwrite dummy MAC address with real data + WiFi.macAddress(mac); + serial_printf("WiFi is enabled (MAC: %02X%02X%02X%02X%02X%02X)\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); +#endif -void setup() { - Serial.begin(115200); + load_settings(); - // Initialize pins. - pinMode(BUZZER_PIN, OUTPUT); -#if defined(ACTIVITY_LED_PIN) - pinMode(ACTIVITY_LED_PIN, OUTPUT); - digitalWrite(ACTIVITY_LED_PIN, HIGH); + serial_println("settings:"); + serial_printf(" .magic: 0x%08X\n", settings.magic); + serial_printf(" .co2_ppm_warn: %d\n", settings.co2_ppm_warn); + serial_printf(" .co2_ppm_alarm: %d\n", settings.co2_ppm_alarm); + serial_printf(" .co2_ppm_critical: %d\n", settings.co2_ppm_critical); + serial_printf(" .buzzer_tone_freq_hz: %d\n", settings.buzzer_tone_freq_hz); + serial_printf(" .buzzer_tone_duration_ms: %d\n", settings.buzzer_tone_duration_ms); + serial_printf(" .buzzer_max_num_beeps: %d\n", settings.buzzer_max_num_beeps); + serial_printf(" .hotspot_name: %s\n", settings.hotspot_name); + +#ifdef NEOPIXEL_PIN + serial_printf(" .neopixel_brightness: %d\n", settings.neopixel_brightness); + + // Initialize NeoPixel strip's LED(s). + pixels.begin(); // INITIALIZE NeoPixel strip object (REQUIRED) + pixels.clear(); // Set all pixel colors to 'off' #endif - // Initialize LED(s). - FastLED.addLeds(leds, NUM_LEDS); - FastLED.setBrightness(LED_BRIGHTNESS); - FastLED.showColor(CRGB(255, 255, 255), 10); + // Initialize GPIOs for discrete LEDs. + init_leds(); + + // initialize digital pin for the button as input. + pinMode(BUTTON_PIN, INPUT); // Initialize buzzer. +#ifdef BUZZER_PIN pinMode(BUZZER_PIN, OUTPUT); +#endif + + delay(2000); // Initialize SCD30 sensor. Wire.begin(); - if (scd30.begin(Wire)) { - Serial.println("SCD30 CO2 sensor detected."); + if(scd30.begin(Wire)) + { + serial_println("SCD30 CO2 sensor detected."); } - else { - Serial.println("SCD30 CO2 sensor not detected. Please check wiring. Freezing."); + else + { + serial_println("SCD30 CO2 sensor not detected. Please check wiring. Freezing."); + // Light up all discrete LEDs to indicate HW error. + set_green_led(HIGH); + set_yellow_led(HIGH); + set_red_led(HIGH); + set_warn_led(HIGH); + indicate_calib(); // Also use the NeoPixel strip. delay(UINT32_MAX); } scd30.setMeasurementInterval(MEASURE_INTERVAL_S); - // Initialize BME280 sensor. - bme280.setI2CAddress(BME280_I2C_ADDRESS); - if (bme280.beginI2C(Wire)) { - Serial.println("BMP280 pressure sensor detected."); - bme280isConnected = true; - // Settings. - bme280.setFilter(4); - bme280.setStandbyTime(0); - bme280.setTempOverSample(1); - bme280.setPressureOverSample(16); - bme280.setHumidityOverSample(1); - bme280.setMode(MODE_FORCED); - } - else { - Serial.println("BMP280 pressure sensor not detected. Please check wiring. Continuing without ambient pressure compensation."); - } - #if WIFI_ENABLED // Initialize WiFi, DNS and web server. -#if WIFI_HOTSPOT_MODE - Serial.println("Starting WiFi hotspot ..."); + serial_println("Starting WiFi hotspot ..."); + //serial_println( WiFi.macAddress() ); WiFi.mode(WIFI_AP); WiFi.softAPConfig(apIP, apIP, netMsk); - WiFi.softAP(WIFI_HOTSPOT_NAME); + WiFi.softAP(settings.hotspot_name); dnsServer.setErrorReplyCode(DNSReplyCode::NoError); - dnsServer.start(53, "*", apIP); - Serial.printf("WiFi hotspot started (\"%s\")\r\n", WIFI_HOTSPOT_NAME); -#else - Serial.println("Connecting WiFi ..."); - WiFi.begin(WIFI_CLIENT_SSID, WIFI_CLIENT_PASSWORD); - uint timeout = 30; - while (timeout > 0 && WiFi.status() != WL_CONNECTED) { - delay(1000); - timeout--; - } - if (WiFi.status() == WL_CONNECTED) { - Serial.printf("WiFi connected (%s).\r\n", WiFi.localIP().toString().c_str()); - } - else { - Serial.println("WiFi connection failed."); - }; -#endif + dnsServer.start(WIFI_DNS_PORTNO, "*", apIP); + serial_printf("WiFi hotspot started (\"%s\")\n", settings.hotspot_name); + server.on("/", HTTP_GET, handleCaptivePortal); +#ifdef SETTINGS_STORAGE + server.on("/config", HTTP_GET, handleConfig); + server.on("/config", HTTP_POST, handleConfig); +#endif server.onNotFound(handleCaptivePortal); server.begin(); #endif } +void set_pixel_by_co2(uint16_t co2_ppm) +{ +#ifdef NEOPIXEL_PIN + static int num_leds_old = 0; + static int num_leds = 0; + static uint32_t colorval = pixels.Color(COLOR_BLACK); + num_leds = co2_ppm / 100; /* 1600 max., 16 pixels --> 100 ppm/pixel; FIXME: static HW dependency! */ + num_leds = (num_leds > NUMPIXELS) ? NUMPIXELS : num_leds; + + //serial_printf("num_leds: %d.\n", num_leds); // only for debugging + + /* avoid flickering, so switch off pixels only if number of LEDs switched on has decreased */ + if (num_leds_old > num_leds) + { + pixels.clear(); // Set all pixel colors to 'off' + } + num_leds_old = num_leds; + + /* Select color for all pixels */ + if (co2_ppm < settings.co2_ppm_warn) + { + colorval = pixels.Color(COLOR_GREEN); + } + else if (co2_ppm >= settings.co2_ppm_warn && co2_ppm <= settings.co2_ppm_critical) + { + colorval = pixels.Color(COLOR_YELLOW); + } + else + { + colorval = pixels.Color(COLOR_RED); + } + + for (int i = 0; i < num_leds; i++) + { + pixels.setPixelColor(i, colorval); + } + pixels.show(); // Send the updated pixel colors to the hardware (after the loop). +#endif + + /* if at least one LED is used */ +#if defined(LED_GREEN_PIN) || defined(LED_YELLOW_PIN) || defined(LED_RED_PIN) + if (co2_ppm < settings.co2_ppm_warn) + { + set_green_led(HIGH); + set_yellow_led(LOW); + set_red_led(LOW); + set_warn_led(LOW); + } + else if (co2_ppm >= settings.co2_ppm_warn && co2_ppm <= settings.co2_ppm_critical) + { + set_green_led(LOW); + set_yellow_led(HIGH); + set_red_led(LOW); + set_warn_led(HIGH); + } + else + { + set_green_led(LOW); + set_yellow_led(LOW); + set_red_led(HIGH); + set_warn_led(HIGH); + } +#endif +} + +void init_leds(void) +{ +#ifdef LED_GREEN_PIN + pinMode(LED_GREEN_PIN, OUTPUT); + set_green_led(HIGH); +#endif +#ifdef LED_RED_PIN + pinMode(LED_RED_PIN, OUTPUT); + set_red_led(HIGH); +#endif +#ifdef LED_YELLOW_PIN + pinMode(LED_YELLOW_PIN, OUTPUT); + set_yellow_led(HIGH); +#endif +#ifdef LED_WARN_PIN + pinMode(LED_WARN_PIN, OUTPUT); + set_warn_led(HIGH); +#endif +} + +void set_green_led(int value) +{ +#ifdef LED_GREEN_PIN + digitalWrite(LED_GREEN_PIN, value); +#endif +} + +void set_yellow_led(int value) +{ +#ifdef LED_YELLOW_PIN + digitalWrite(LED_YELLOW_PIN, value); +#endif +} + +void set_red_led(int value) +{ +#ifdef LED_RED_PIN + digitalWrite(LED_RED_PIN, value); +#endif +} + +void set_warn_led(int value) +{ +#ifdef LED_WARN_PIN + digitalWrite(LED_WARN_PIN, value); +#endif +} + +void indicate_calib(void) +{ +#ifdef NEOPIXEL_PIN + pixels.clear(); // Set all pixel colors to 'off' + for (int i = 0; i < NUMPIXELS; i++) + { + if( i % 3 == 0 ) + { + pixels.setPixelColor(i, pixels.Color(COLOR_GREEN)); + } + else if( i % 3 == 1 ) + { + pixels.setPixelColor(i, pixels.Color(COLOR_YELLOW)); + } + else + { + pixels.setPixelColor(i, pixels.Color(COLOR_RED)); + } + } + pixels.show(); // Send the updated pixel colors to the hardware. +#endif +} + +void loop() +{ + static uint8_t nLoopCnt = 0; -void loop() { // Tasks that need to run continuously. -#if WIFI_ENABLED && WIFI_HOTSPOT_MODE +#if WIFI_ENABLED dnsServer.processNextRequest(); #endif // Early exit. - if ((millis() - lastMeasureTime) < (MEASURE_INTERVAL_S * 1000)) { + if ((millis() - lastMeasureTime) < (MEASURE_INTERVAL_S * 1000)) + { return; } -#if defined(ACTIVITY_LED_PIN) - digitalWrite(ACTIVITY_LED_PIN, LOW); +#if WIFI_ENABLED + if( ++nLoopCnt == 20 ) + { + serial_printf("WiFi hotspot available (\"%s\")\n", settings.hotspot_name); + nLoopCnt = 0; + } #endif - // Read sensors. - if (bme280isConnected) { - pressure = (uint16_t) (bme280.readFloatPressure() / 100); - scd30.setAmbientPressure(pressure); + if( LOW == digitalRead(BUTTON_PIN) ) + { + Serial.print("Start SCD 30 calibration, please wait 30 s ..."); + indicate_calib(); + delay(30000); + scd30.setAutoSelfCalibration(false); // deactivate self-calibration; setting is stored in non-volatile memory + delay(1000); + scd30.setForceRecalibration(410); // set to fresh air, estimate 410 ppm as a reference (400 ppm is the minimum for the function parameter) + delay(1000); + pixels.clear(); } - if (scd30.dataAvailable()) { + + if( scd30.dataAvailable() ) + { co2 = scd30.getCO2(); + + if( co2 < CO2_PLASABILIT_PPM_MIN ) // do not use implausible values, concentration is unlikely to be < CO2_PLASABILIT_PPM_MIN + { + serial_printf("[WARN] Implausible CO2 value: %d\n", co2); + co2 = CO2_PLASABILIT_PPM_MIN; + } } // Average (downsample) and log CO2 values for the graph. co2avg = ((co2avgSamples * co2avg) + co2) / (co2avgSamples + 1); co2avgSamples++; - if (co2avgSamples >= co2logDownsample) { + if (co2avgSamples >= co2logDownsample) + { co2log[co2logPos] = co2avg; co2logPos++; co2logPos %= LOG_SIZE; co2avg = co2avgSamples = 0; } - // Print all sensor values. - Serial.printf( + // Print all sensor values to the serial console. + serial_printf( "[SCD30] temp: %.2f°C, humid: %.2f%%, CO2: %dppm\r\n", scd30.getTemperature(), scd30.getHumidity(), co2 ); - if (bme280isConnected) { - Serial.printf( - "[BME280] temp: %.2f°C, humid: %.2f%%, press: %dhPa\r\n", - bme280.readTempC(), bme280.readFloatHumidity(), pressure - ); - } - Serial.println("-----------------------------------------------------"); + serial_println("-----------------------------------------------------"); // Update LED(s). - if (co2 < CO2_WARN_PPM) { - FastLED.showColor(CRGB(0, 255, 0)); // Green. - } - else if (co2 < CO2_CRITICAL_PPM) { - FastLED.showColor(CRGB(255, 127, 0)); // Yellow. - } - else { - FastLED.showColor(CRGB(255, 0, 0)); // Red. - } + set_pixel_by_co2(co2); - // Trigger alarms. - if (co2 >= CO2_CRITICAL_PPM) { - alarmContinuous(); - if (!alarmHasTriggered) { - alarmOnce(); + // Handle alarms (trigger, reset) + if (co2 >= settings.co2_ppm_alarm) { + /* rising edge detection for trigger */ + if (!alarmHasTriggered) + { alarmHasTriggered = true; } + alarmActiveCount++; + alarmSound(); } - if (co2 < CO2_CRITICAL_PPM && alarmHasTriggered) { - alarmHasTriggered = false; - } + else + { + /* falling edge detection for trigger */ + if( alarmHasTriggered) + { + alarmHasTriggered = false; + } -#if defined(ACTIVITY_LED_PIN) - digitalWrite(ACTIVITY_LED_PIN, HIGH); -#endif + /* reset active timer value */ + alarmActiveCount = 0; + } lastMeasureTime = millis(); } @@ -304,28 +465,34 @@ void loop() { #if WIFI_ENABLED /** - * Handle requests for the captive portal. - * @param request - */ -void handleCaptivePortal(AsyncWebServerRequest *request) { - Serial.println("handleCaptivePortal"); + Handle requests for the captive portal. + @param request +*/ +void handleCaptivePortal(AsyncWebServerRequest *request) +{ + serial_println("handleCaptivePortal"); AsyncResponseStream *res = request->beginResponseStream("text/html"); - res->print(""); - res->print("coro2sens"); + res->print("\n"); + res->print("coro2sens\n"); res->print(R"()"); - res->printf(R"()", max(MEASURE_INTERVAL_S, 10)); + res->printf(R"()", std::max(MEASURE_INTERVAL_S, 10)); res->print(R"()"); - res->print(""); + res->print("\n"); + + // Print request URL for debugging + //res->printf("

Webpage at %s

", request->url().c_str()); // Current measurement. res->printf(R"(

%d ppm CO2

)", - co2 > CO2_CRITICAL_PPM ? "red" : co2 > CO2_WARN_PPM ? "yellow" : "green", co2); + (co2 >= settings.co2_ppm_critical) ? "red" : ((co2 >= settings.co2_ppm_warn) ? "yellow" : "green"), co2); // Generate SVG graph. - uint16_t maxVal = CO2_CRITICAL_PPM + (CO2_CRITICAL_PPM - CO2_WARN_PPM); - for (uint16_t val : co2log) { - if (val > maxVal) { + uint16_t maxVal = settings.co2_ppm_critical + (settings.co2_ppm_critical - settings.co2_ppm_warn); + for (uint16_t val : co2log) + { + if (val > maxVal) + { maxVal = val; } } @@ -334,16 +501,16 @@ void handleCaptivePortal(AsyncWebServerRequest *request) { res->printf(R"()", w, h); // Background. res->printf(R"()", - 0, 0, w, (int) map(maxVal - CO2_CRITICAL_PPM, 0, maxVal, 0, h)); + 0, 0, w, (int) map(maxVal - settings.co2_ppm_critical, 0, maxVal, 0, h)); res->printf(R"()", - 0, (int) map(maxVal - CO2_CRITICAL_PPM, 0, maxVal, 0, h), w, (int) map(CO2_WARN_PPM, 0, maxVal, 0, h)); + 0, (int) map(maxVal - settings.co2_ppm_critical, 0, maxVal, 0, h), w, (int) map(settings.co2_ppm_warn, 0, maxVal, 0, h)); res->printf(R"()", - 0, (int) map(maxVal - CO2_WARN_PPM, 0, maxVal, 0, h), w, (int) map(CO2_WARN_PPM, 0, maxVal, 0, h)); + 0, (int) map(maxVal - settings.co2_ppm_warn, 0, maxVal, 0, h), w, (int) map(settings.co2_ppm_warn, 0, maxVal, 0, h)); // Threshold values. - res->printf(R"(> %d ppm)", - 4, (int) map(maxVal - CO2_CRITICAL_PPM, 0, maxVal, 0, h) - 6, CO2_CRITICAL_PPM); - res->printf(R"(< %d ppm)", - 4, (int) map(maxVal - CO2_WARN_PPM, 0, maxVal, 0, h) + 12, CO2_WARN_PPM); + res->printf(R"(≥ %d ppm)", + 4, (int) map(maxVal - settings.co2_ppm_critical, 0, maxVal, 0, h) - 6, settings.co2_ppm_critical); + res->printf(R"(≤ %d ppm)", + 4, (int) map(maxVal - settings.co2_ppm_warn, 0, maxVal, 0, h) + 12, settings.co2_ppm_warn); // Plot line. res->print(R"(print(""); // Labels. - res->printf("

%s

", TIME_LABEL); + res->printf("

%s

\n", TIME_LABEL); res->print(""); request->send(res); } -#endif + +bool getPostUintParam(AsyncWebServerRequest *request, const String& name, uint16_t& value, const uint16_t min, const uint16_t max) +{ + if(request->hasParam(name, true)) + { + serial_printf("has parameter '%s'", name); + uint16_t tmpValue = std::atoi(request->getParam(name, true)->value().c_str()); + if( (tmpValue >= min) && (tmpValue <= max) ) + { + value = tmpValue; + return true; + } + } + return false; +} + +#ifdef SETTINGS_STORAGE +/** + Handle requests for config page. + @param request +*/ +void handleConfig(AsyncWebServerRequest *request) +{ + serial_println("handleConfig"); + AsyncResponseStream *res = request->beginResponseStream("text/html"); + + int params = request->params(); + for (int i = 0; i < params; i++) { + AsyncWebParameter* p = request->getParam(i); + serial_printf("POST[%s]: %s\n", p->name().c_str(), p->value().c_str()); + } + + res->print("\n"); + res->print("coro2sens config"); + res->print(R"(\n\n\n"); + res->print("

Settings

\n"); + + // Check if we got any params, i.e. most likely the form has been submit (but could be from both POST and GET method)! + if(params > 0) + { + bool settingsChanged = false; // is set to true when a change is detected later on + bool printedNotification = false; + + if(request->hasParam("hotspot_name", true)) + { + if( 0 != strncmp(request->getParam("hotspot_name", true)->value().c_str(), settings.hotspot_name, WIFI_HOTSPOT_SIZE ) ) + { + strncpy( settings.hotspot_name, request->getParam("hotspot_name", true)->value().c_str(), WIFI_HOTSPOT_SIZE-1 ); + settings.hotspot_name[WIFI_HOTSPOT_SIZE-1] = '\0'; + settingsChanged = true; + if( !printedNotification ) + { + res->print(R"(
)"); + printedNotification = true; + } + res->print("Hotspot name has changed. Changes are adopted after reboot (re-power the device)!
\n"); + } + } + + uint16_t co2_ppm_warn; + if( getPostUintParam(request, "co2_ppm_warn", co2_ppm_warn, CO2_WARN_PPM_MIN, CO2_WARN_PPM_MAX) ) + { + if( settings.co2_ppm_warn != co2_ppm_warn ) + { + settings.co2_ppm_warn = co2_ppm_warn; + settingsChanged = true; + if( !printedNotification ) + { + res->print(R"(
)"); + printedNotification = true; + } + res->print("CO2 warning level (ppm) setting has changed.
\n"); + } + } + + uint16_t co2_ppm_critical; + if( getPostUintParam(request, "co2_ppm_critical", co2_ppm_critical, CO2_CRITICAL_PPM_MIN, CO2_CRITICAL_PPM_MAX) ) + { + if( settings.co2_ppm_critical != co2_ppm_critical ) + { + settings.co2_ppm_critical = co2_ppm_critical; + settingsChanged = true; + if( !printedNotification ) + { + res->print(R"(
)"); + printedNotification = true; + } + res->print("Critical CO2 level (ppm) setting has changed.
\n"); + } + } + + uint16_t neopixel_brightness; + if( getPostUintParam(request, "neopixel_brightness", neopixel_brightness, LED_INTENSITY_MIN, LED_INTENSITY_MAX) ) + { + if( settings.neopixel_brightness != neopixel_brightness ) + { + settings.neopixel_brightness = neopixel_brightness; + settingsChanged = true; + if( !printedNotification ) + { + res->print(R"(
)"); + printedNotification = true; + } + res->print("NeoPixel LED brightness setting has changed.
\n"); + } + } + + uint16_t co2_ppm_alarm; + if( getPostUintParam(request, "co2_ppm_alarm", co2_ppm_alarm, CO2_CRITICAL_PPM_MIN, CO2_CRITICAL_PPM_MAX) ) + { + if( settings.co2_ppm_alarm != co2_ppm_alarm ) + { + settings.co2_ppm_alarm = co2_ppm_alarm; + settingsChanged = true; + if( !printedNotification ) + { + res->print(R"(
)"); + printedNotification = true; + } + res->print("Buzzer alarm threshold setting has changed.
\n"); + } + } + + uint16_t buzzer_tone_freq_hz; + if( getPostUintParam(request, "buzzer_tone_freq_hz", buzzer_tone_freq_hz, BEEP_TONE_FREQ_MIN, BEEP_TONE_FREQ_MAX) ) + { + if( settings.buzzer_tone_freq_hz != buzzer_tone_freq_hz ) + { + settings.buzzer_tone_freq_hz = buzzer_tone_freq_hz; + settingsChanged = true; + if( !printedNotification ) + { + res->print(R"(
)"); + printedNotification = true; + } + res->print("Buzzer tone frequency setting has changed.
\n"); + } + } + + uint16_t buzzer_tone_duration_ms; + if( getPostUintParam(request, "buzzer_tone_duration_ms", buzzer_tone_duration_ms, BEEP_DURATION_MS_MIN, BEEP_DURATION_MS_MAX) ) + { + if( settings.buzzer_tone_duration_ms != buzzer_tone_duration_ms ) + { + settings.buzzer_tone_duration_ms = buzzer_tone_duration_ms; + settingsChanged = true; + if( !printedNotification ) + { + res->print(R"(
)"); + printedNotification = true; + } + res->print("Buzzer tone duration setting has changed.
\n"); + } + } + + uint16_t buzzer_max_num_beeps; + if( getPostUintParam(request, "buzzer_max_num_beeps", buzzer_max_num_beeps, BUZZER_MAX_BEEPS_MIN, BUZZER_MAX_BEEPS_MAX) ) + { + if( settings.buzzer_max_num_beeps != buzzer_max_num_beeps ) + { + settings.buzzer_max_num_beeps = buzzer_max_num_beeps; + settingsChanged = true; + if( !printedNotification ) + { + res->print(R"(
)"); + printedNotification = true; + } + res->print("Number of buzzer beeps during an alarm setting has changed.
\n"); + } + } + + if( settingsChanged ) + { + if( !printedNotification ) + { + res->print(R"(
)"); + printedNotification = true; + } + res->print("Storing new setting(s) to EEPROM.
\n"); + store_current_settings(); + } + + if( printedNotification ) + { + res->print("
\n"); // end notification div + } + } + + // (Re-)Load settings to be displayed + load_settings(); + + res->print(R"(
)"); + + res->print(R"(
)"); + res->print(R"(
MAC address
)"); + char mac_cstr[20]; + snprintf(mac_cstr, 20, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + res->printf(R"(
%s
)", mac_cstr); + res->print("
\n"); + + res->print(R"(
)"); + res->print(R"(
Hotspot name
)"); + res->printf(R"(
)", settings.hotspot_name, WIFI_HOTSPOT_SIZE-1); + res->print("
\n"); + + res->print(R"(
)"); + res->print(R"(
Warning level
)"); + res->printf(R"(
 ppm
)", + settings.co2_ppm_warn, CO2_WARN_PPM_MIN, CO2_WARN_PPM_MAX); + res->print("
\n"); + + res->print(R"(
)"); + res->print(R"(
Critical level
)"); + res->printf(R"(
 ppm
)", + settings.co2_ppm_critical, CO2_CRITICAL_PPM_MIN, CO2_CRITICAL_PPM_MAX); + res->print("
\n"); + + res->print(R"(
)"); + res->print(R"(
NeoPixel LED brightness
)"); + res->printf(R"(
 %
)", + settings.neopixel_brightness, LED_INTENSITY_MIN, LED_INTENSITY_MAX); + res->print("
\n"); + + res->print(R"(
)"); + res->print(R"(
Buzzer alarm threshold
)"); + res->printf(R"(
 ppm
)", + settings.co2_ppm_alarm, CO2_CRITICAL_PPM_MIN, CO2_CRITICAL_PPM_MAX); + res->print("
\n"); + + res->print(R"(
)"); + res->print(R"(
Buzzer tone frequency
)"); + res->printf(R"(
 Hz
)", + settings.buzzer_tone_freq_hz, BEEP_TONE_FREQ_MIN, BEEP_TONE_FREQ_MAX); + res->print("
\n"); + + res->print(R"(
)"); + res->print(R"(
Buzzer tone duration
)"); + res->printf(R"(
 ms per beep
)", + settings.buzzer_tone_duration_ms, BEEP_DURATION_MS_MIN, BEEP_DURATION_MS_MAX); + res->print("
\n"); + + res->print(R"(
)"); + res->print(R"(
Number of buzzer beeps
)"); + res->printf(R"(
(0=muted)
)", + settings.buzzer_max_num_beeps, BUZZER_MAX_BEEPS_MIN, BUZZER_MAX_BEEPS_MAX); + res->print("
\n"); + + res->print(R"(
)"); + res->print(R"(
)"); + res->print(R"(
)"); + res->print("
\n"); + + res->print("
\n\n\n"); + + request->send(res); +} +#endif /* SETTINGS_STORAGE */ +#endif /* WIFI_ENABLED */ diff --git a/platformio.ini b/platformio.ini deleted file mode 100644 index c88c5e1..0000000 --- a/platformio.ini +++ /dev/null @@ -1,39 +0,0 @@ -; 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 - -[platformio] -src_dir = coro2sens - -[env:esp32dev] -platform = espressif32 -framework = arduino -board = esp32dev -lib_deps = - SparkFun SCD30 Arduino Library - SparkFun BME280 - FastLED - lbernstone/Tone32 - AsyncTCP - ESP Async WebServer -monitor_speed = 115200 -monitor_filters = time, esp32_exception_decoder - -[env:esp12e] -platform = espressif8266 -framework = arduino -board = esp12e -lib_deps = - https://github.com/paulvha/scd30.git - SparkFun BME280 - FastLED - ESPAsyncTCP - ESP Async WebServer -monitor_speed = 115200 -monitor_filters = time