Commit d436497e authored by Eric Duminil's avatar Eric Duminil
Browse files

Current version from old git. Release 0.0.9

parents
.pioenvs
.pio/libdeps
.pio/build
.vscode
*.ino.cpp
config.h
config.hftstuttgart.h
.project
.cproject
.settings
#ifndef AMPEL_H_INCLUDED
#define AMPEL_H_INCLUDED
/*****************************************************************
* Libraries *
*****************************************************************/
#include "config.h"
#ifndef MEASUREMENT_TIMESTEP
# error Missing config.h file. Please copy config.example.h to config.h.
#endif
#ifdef MQTT
# include "mqtt.h"
#endif
#include "util.h"
#include "wifi_util.h"
#include "co2_sensor.h"
#ifdef HTTP
# include "web_server.h"
#endif
#include "led_effects.h"
#include "csv_writer.h"
#if defined(ESP8266)
//allows sensor to be seen as SENSOR_ID.local, from the local network. For example : espd05cc9.local
# include <ESP8266mDNS.h>
#elif defined(ESP32)
# include <ESPmDNS.h>
#endif
void keepServicesAlive();
void checkFlashButton();
#endif
/***
* ____ ___ ____ _ _
* / ___/ _ \___ \ / \ _ __ ___ _ __ ___| |
* | | | | | |__) | / _ \ | '_ ` _ \| '_ \ / _ \ |
* | |__| |_| / __/ / ___ \| | | | | | |_) | __/ |
* \____\___/_____| /_/__ \_\_| |_| |_| .__/ \___|_| _
* | | | |/ _|_ _| / ___|| |_ _ _| |_| |_ __ _ __ _ _ __| |_
* | |_| | |_ | | \___ \| __| | | | __| __/ _` |/ _` | '__| __|
* | _ | _| | | ___) | |_| |_| | |_| || (_| | (_| | | | |_
* |_| |_|_| |_| |____/ \__|\__,_|\__|\__\__, |\__,_|_| \__|
* |___/
*/
#include "Ampel.h"
/*****************************************************************
* GPL License *
*****************************************************************/
/*
* This file is part of the "CO2 Ampel" project (https://gitlab.rz.hft-stuttgart.de/otto/hft-stuttgart_co2_ampel)
* Copyright (c) 2020 HfT Stuttgart.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*****************************************************************
* Authors *
*****************************************************************/
/*
* Eric Duminil
* Robert Otto
* Myriam Guedey
* Tobias Gabriel Erhart
* Jonas Stave
*/
/*****************************************************************
* Configuration *
*****************************************************************/
/*
* Please define settings in 'config.h'.
* There's an example config file called 'config.example.h'.
* You can copy 'config.public.h' (stored in Git) to 'config.h' (not stored in Git),
* and define your credentials and parameters in 'config.h'.
*/
/*****************************************************************
* Setup *
*****************************************************************/
void setup() {
LedEffects::setupOnBoardLED();
LedEffects::onBoardLEDOff();
Serial.begin(BAUDS);
pinMode(0, INPUT); // Flash button (used for forced calibration)
LedEffects::setupRing();
sensor::initialize();
Serial.print(F("Sensor ID: "));
Serial.println(SENSOR_ID);
Serial.print(F("Board : "));
Serial.println(BOARD);
// Try to connect to Wi-Fi
WiFiConnect(SENSOR_ID);
Serial.print(F("WiFi STATUS: "));
Serial.println(WiFi.status());
if (WiFi.status() == WL_CONNECTED) {
#ifdef HTTP
web_server::initialize();
#endif
ntp::initialize();
if (MDNS.begin(SENSOR_ID.c_str())) { // Start the mDNS responder for SENSOR_ID.local
MDNS.addService("http", "tcp", 80);
Serial.println(F("mDNS responder started"));
} else {
Serial.println(F("Error setting up MDNS responder!"));
}
#ifdef MQTT
mqtt::initialize("CO2sensors/" + SENSOR_ID);
#endif
}
csv_writer::initialize();
}
/*****************************************************************
* Main loop *
*****************************************************************/
void loop() {
//NOTE: Loop should never take more than 1000ms. Split in smaller methods and logic if needed.
//TODO: Restart every day or week, in order to not let t0 overflow?
uint32_t t0 = millis();
/**
* USER INTERACTION
*/
keepServicesAlive();
// Short press for night mode, Long press for calibration.
checkFlashButton();
/**
* GET DATA
*/
bool freshData = sensor::scd30.dataAvailable(); // Alternative : close to time-step AND dataAvailable, to avoid asking the sensor too often.
if (freshData) {
sensor::co2 = sensor::scd30.getCO2();
sensor::temperature = sensor::scd30.getTemperature();
sensor::humidity = sensor::scd30.getHumidity();
}
//NOTE: Data is available, but it's sometimes erroneous: the sensor outputs zero ppm but non-zero temperature and non-zero humidity.
if (sensor::co2 <= 0) {
// No measurement yet. Waiting.
LedEffects::showWaitingLED(color::blue);
return;
}
/**
* Fresh data. Show it and send it if needed.
*/
if (freshData) {
sensor::timestamp = ntp::getLocalTime();
Serial.println(sensor::timestamp);
Serial.print(F("co2(ppm): "));
Serial.print(sensor::co2);
Serial.print(F(" temp(C): "));
Serial.print(sensor::temperature);
Serial.print(F(" humidity(%): "));
Serial.println(sensor::humidity);
csv_writer::logIfTimeHasCome(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity);
#ifdef MQTT
mqtt::publishIfTimeHasCome(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity);
#endif
}
if (sensor::co2 < 250) {
// Sensor should be calibrated.
LedEffects::showWaitingLED(color::magenta);
return;
}
/**
* Display data, even if it's "old" (with breathing).
* Those effects include a short delay.
*/
if (sensor::co2 < 2000) {
LedEffects::displayCO2color(sensor::co2);
LedEffects::breathe(sensor::co2);
} else { // >= 2000: entire ring blinks red
LedEffects::redAlert();
}
uint32_t duration = millis() - t0;
if (duration > max_loop_duration) {
max_loop_duration = duration;
Serial.print("Max loop duration : ");
Serial.print(max_loop_duration);
Serial.println(" ms.");
}
}
/**
* Checks if flash button has been pressed:
* If not, do nothing.
* If short press, toggle LED display.
* If long press, start calibration process.
*/
void checkFlashButton() {
if (!digitalRead(0)) { // Button has been pressed
LedEffects::onBoardLEDOn();
delay(300);
if (digitalRead(0)) {
Serial.println(F("Flash has been pressed for a short time. Should toggle night mode."));
LedEffects::toggleNightMode();
} else {
Serial.println(F("Flash has been pressed for a long time. Keep it pressed for calibration."));
if (LedEffects::countdownToZero() < 0) {
sensor::startCalibrationProcess();
}
}
LedEffects::onBoardLEDOff();
}
}
void keepServicesAlive() {
if (WiFi.status() == WL_CONNECTED) {
#if defined(ESP8266)
//NOTE: Sadly, there seems to be a bug in the current MDNS implementation.
// It stops working after 2 minutes. And forcing a restart leads to a memory leak.
MDNS.update();
#endif
ntp::update(); // NTP client has its own timer. It will connect to NTP server every 60s.
#ifdef HTTP
web_server::update();
#endif
#ifdef MQTT
mqtt::keepConnection(); // MQTT client has its own timer. It will keep alive every 15s.
#endif
}
}
This diff is collapsed.
all:
pio -f -c vim run
upload:
pio -f -c vim run --target upload -e $(board)
clean:
pio -f -c vim run --target clean
program:
pio -f -c vim run --target program
uploadfs:
pio -f -c vim run --target uploadfs
update:
pio -f -c vim update
monitor:
pio device monitor --filter colorize
# CO<sub>2</sub> Ampel
*CO<sub>2</sub> Ampel* is an open-source project, written in C++ for ESP8266 or ESP32.
It measures the current CO<sub>2</sub> concentration (in ppm), and displays it on an LED ring.
The room should be ventilated as soon as one LED turns red.
## Hardware Requirements
* [ESP8266](https://en.wikipedia.org/wiki/ESP8266) or [ESP32](https://en.wikipedia.org/wiki/ESP32) microcontroller (this project has been tested with *ESP8266 ESP-12 WIFI* and *TTGO ESP32 SX1276 LoRa*)
* [Sensirion SCD30](https://www.sensirion.com/en/environmental-sensors/carbon-dioxide-sensors/carbon-dioxide-sensors-co2/) "Sensor Module for HVAC and Indoor Air Quality Applications"
* [NeoPixel Ring - 12](https://www.adafruit.com/product/1643)
## Software Requirements
* [PlatformIO](https://platformio.org/)
or
* [Arduino IDE](https://www.arduino.cc/en/software)
## Installation
* If `config.h` does not exist, copy it from `config.public.h`
* Modify `config.h`, e.g. for measurement time-steps, WiFi access, MQTT, NTP and web-server.
### PlatformIO
PlatformIO can be run from [VSCODE](https://platformio.org/install/ide?install=vscode), [Eclipse CDT](https://www.eclipse.org/cdt/) or console:
```bash
make upload board=esp8266 && make monitor # For ESP8266
```
```bash
make upload board=esp32 && make monitor # For ESP32
```
### Arduino IDE
* All the libraries are included in this repository. No need to install anything via *Library Manager*.
* Add your board to the [board manager](https://github.com/esp8266/Arduino#installing-with-boards-manager). Either ESP8266:
http://arduino.esp8266.com/stable/package_esp8266com_index.json
or ESP32:
https://dl.espressif.com/dl/package_esp32_index.json
* Choose the correct board in *Tools > Board > ...*
* Choose the correct *Flash size* (e.g. "Flash Size : 4MB (1MB FS, OTA:~1019kB)" for *ESP8266 ESP-12 WIFI*)
* *Verify*
* *Upload*
* *Tools > Serial Monitor*
## Authors
* Eric Duminil
* Robert Otto
* Myriam Guedey
* Tobias Gabriel Erhart
* Jonas Stave
Hochschule für Technik Stuttgart
## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
## License
Copyright © 2020, [HfT Stuttgart](https://www.hft-stuttgart.de/)
[GPLv3](https://choosealicense.com/licenses/gpl-3.0/)
#include "co2_sensor.h"
namespace config {
// Values should be defined in config.h
uint16_t measurement_timestep = MEASUREMENT_TIMESTEP; // [s] Value between 2 and 1800 (range for SCD30 sensor)
const uint16_t altitude_above_sea_level = ALTITUDE_ABOVE_SEA_LEVEL; // [m]
uint16_t co2_calibration_level = ATMOSPHERIC_CO2_CONCENTRATION; // [ppm]
#ifdef TEMPERATURE_OFFSET
// Residual heat from CO2 sensor seems to be high enough to change the temperature reading. How much should it be offset?
// NOTE: Sign isn't relevant. The returned temperature will always be shifted down.
const float temperature_offset = TEMPERATURE_OFFSET; // [K]
#else
const float temperature_offset = -3.0; // [K] Temperature measured by sensor is usually at least 3K too high.
#endif
const bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false]
}
namespace sensor {
SCD30 scd30;
int16_t co2 = 0;
float temperature = 0;
float humidity = 0;
String timestamp = "";
void initialize() {
#if defined(ESP8266)
Wire.begin(12, 14); // ESP8266 - D6, D5;
#endif
#if defined(ESP32)
Wire.begin(21, 22); // ESP32
/**
* SCD30 ESP32
* VCC --- 3V3
* GND --- GND
* SCL --- SCL (GPIO22) //NOTE: GPIO3 Would be more convenient (right next to GND)
* SDA --- SDA (GPIO21) //NOTE: GPIO1 would be more convenient (right next to GPO3)
*/
#endif
// CO2
if (scd30.begin(config::auto_calibrate_sensor) == false) {
Serial.println("Air sensor not detected. Please check wiring. Freezing...");
while (1) {
LedEffects::showWaitingLED(color::red);
}
}
// SCD30 has its own timer.
Serial.println("\nSetting SCD30 timestep to " + String(config::measurement_timestep) + " s.");
scd30.setMeasurementInterval(config::measurement_timestep); // [s]
Serial.print("Setting temperature offset to -");
Serial.print(abs(config::temperature_offset));
Serial.println(" K.");
scd30.setTemperatureOffset(abs(config::temperature_offset)); // setTemperatureOffset only accepts positive numbers, but shifts the temperature down.
Serial.print("Temperature offset is : -");
Serial.print(scd30.getTemperatureOffset());
Serial.println(" K");
Serial.print("Auto-calibration is ");
Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF.");
}
// Force SCD30 calibration with countdown.
void startCalibrationProcess() {
/** From the sensor documentation:
* For best results, the sensor has to be run in a stable environment in continuous mode at
* a measurement rate of 2s for at least two minutes before applying the FRC command and sending the reference value.
*/
Serial.println("Setting SCD30 timestep to 2s, prior to calibration.");
scd30.setMeasurementInterval(2); // [s] The change will only take effect after next measurement.
LedEffects::showKITTWheel(color::blue, config::measurement_timestep);
Serial.println("Waiting 2 minutes.");
LedEffects::showKITTWheel(color::blue, 120);
Serial.print("Starting SCD30 calibration...");
scd30.setAltitudeCompensation(config::altitude_above_sea_level);
scd30.setForcedRecalibrationFactor(config::co2_calibration_level);
Serial.println(" Done!");
Serial.println("Sensor calibrated.");
Serial.println("Sensor will now restart.");
LedEffects::showKITTWheel(color::green, 5);
FS_LIB.end();
ESP.restart();
}
}
#ifndef CO2_SENSOR_H_
#define CO2_SENSOR_H_
// The SCD30 from Sensirion is a high quality Nondispersive Infrared (NDIR) based CO₂ sensor capable of detecting 400 to 10000ppm with an accuracy of ±(30ppm+3%).
// https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library
#include "src/lib/SparkFun_SCD30_Arduino_Library/src/SparkFun_SCD30_Arduino_Library.h" // From: http://librarymanager/All#SparkFun_SCD30
#include "config.h"
#include "led_effects.h"
#include "csv_writer.h" // To close filesystem before restart.
#include <Wire.h>
namespace config {
extern uint16_t measurement_timestep; // [s] Value between 2 and 1800 (range for SCD30 sensor)
extern const bool auto_calibrate_sensor; // [true / false]
extern uint16_t co2_calibration_level; // [ppm]
extern const float temperature_offset; // [K] Sign isn't relevant.
}
namespace sensor {
extern SCD30 scd30;
extern int16_t co2;
extern float temperature;
extern float humidity;
extern String timestamp;
void initialize();
void startCalibrationProcess();
}
#endif
#ifndef CONFIG_H_INCLUDED
# define CONFIG_H_INCLUDED
// This file is a config template, and can be copied to config.h. Please don't save any important password in this template.
/**
* WIFI
*/
// Setting WIFI_SSID to "NO_WIFI" will disable WiFi completely, and all other dependent services (MQTT, HTTP, NTP, ...)
# define WIFI_SSID "NO_WIFI"
# define WIFI_PASSWORD "P4SSW0RD"
# define WIFI_TIMEOUT 20 // [s]
/**
* Sensor
*/
// How often should measurement be performed, and displayed?
//NOTE: SCD30 timer does not seem to be very precise. Variations may occur.
# define MEASUREMENT_TIMESTEP 60 // [s] Value between 2 and 1800 (range for SCD30 sensor)
// How often measurements should be sent to MQTT server?
// Probably a good idea to use a multiple of MEASUREMENT_TIMESTEP, so that averages can be calculated
// Set to 0 if you want to send values after each measurement
// # define SENDING_INTERVAL MEASUREMENT_TIMESTEP * 5 // [s]
# define SENDING_INTERVAL 300 // [s]
// How often should measurements be appended to CSV ?
// Probably a good idea to use a multiple of MEASUREMENT_TIMESTEP, so that averages can be calculated
// Set to 0 if you want to send values after each measurement
# define CSV_INTERVAL 300 // [s]
// Residual heat from CO2 sensor seems to be high enough to change the temperature reading. How much should it be offset?
// NOTE: Sign isn't relevant. The returned temperature will always be shifted down.
# define TEMPERATURE_OFFSET -3 // [K]
// Altitude above sea level
// Used for CO2 calibration
// here: Stuttgart, Schellingstr. 24. (Source: Google Earth)
# define ALTITUDE_ABOVE_SEA_LEVEL 260 // [m]
// The reference CO2 concentration has to be within the range 400 ppm ≤ cref(CO2) ≤ 2000 ppm.
// Used for CO2 calibration
// here : measured concentration in Stuttgart
# define ATMOSPHERIC_CO2_CONCENTRATION 425 // [ppm]
// Should the sensor try to calibrate itself?
// Sensirion recommends 7 days of continuous readings with at least 1 hour a day of 'fresh air' for self-calibration to complete.
# define AUTO_CALIBRATE_SENSOR true // [true / false]
/**
* LEDs
*/
// LED brightness, which can vary between min and max brightness ("LED breathing")
// max_brightness should be between 0 and 255.
// min_brightness should be between 0 and max_brightness
# define MAX_BRIGHTNESS 255
# define MIN_BRIGHTNESS 60
/**
* WEB SERVER
* available at http://local_ip, with user HTTP_USER and password HTTP_PASSWORD
*/
# define HTTP // Comment or remove this line if you want to disable HTTP webserver
// Define empty strings in order to disable authentication, or remove the constants altogether.
# define HTTP_USER "co2ampel"
# define HTTP_PASSWORD "my_password"
/**
* MQTT SERVER
*/
# define MQTT // Comment or remove this line if you want to disable MQTT
/*
* If MQTT is enabled, co2ampel will publish data every SENDING_INTERVAL seconds.
* An MQTT subscriber can then get the data from the corresponding broker, either encrypted or unencrypted:
*
* ❯ mosquitto_sub -h 'test.mosquitto.org' -p 8883 -t 'CO2sensors/#' --cafile mosquitto.org.crt -v
* CO2sensors/ESPd05cc9 {"time":"2020-12-13 13:14:37+01", "co2":571, "temp":18.9, "rh":50.9}
* CO2sensors/ESPd05cc9 {"time":"2020-12-13 13:14:48+01", "co2":573, "temp":18.9, "rh":50.2}
* ...
*
* ❯ mosquitto_sub -h 'test.mosquitto.org' -t 'CO2sensors/#' -v
* CO2sensors/ESPd05cc9 {"time":"2020-12-13 13:15:09+01", "co2":568, "temp":18.9, "rh":50.1}
* CO2sensors/ESPd05cc9 {"time":"2020-12-13 13:15:20+01", "co2":572, "temp":18.9, "rh":50.3}
* ...
*/
/*
* Allow sensor to be configured over MQTT? Very useful for debugging. For example:
* mosquitto_pub -h 'test.mosquitto.org' -t 'CO2sensors/ESPe08dc9/control' -m 'timer 30'
* mosquitto_pub -h 'test.mosquitto.org' -t 'CO2sensors/ESPe08dc9/control' -m 'calibrate'
* mosquitto_pub -h 'test.mosquitto.org' -t 'CO2sensors/ESPe08dc9/control' -m 'reset'
*/
# define ALLOW_MQTT_COMMANDS false
# define MQTT_SERVER "test.mosquitto.org" // MQTT server URL or IP address
# define MQTT_PORT 8883
# define MQTT_USER ""
# define MQTT_PASSWORD ""
# define MQTT_SERVER_FINGERPRINT "EE BC 4B F8 57 E3 D3 E4 07 54 23 1E F0 C8 A1 56 E0 D3 1A 1C" // SHA1 for test.mosquitto.org
/**
* NTP
*/
# define NTP_SERVER "pool.ntp.org"
# define UTC_OFFSET_IN_SECONDS 3600 // [s] 3600 for UTC+1
/**
* Others
*/
# define BAUDS 115200 // Transmission rate
#endif
#include "csv_writer.h"
namespace config {
// Values should be defined in config.h
uint16_t csv_interval = CSV_INTERVAL; // [s]
}
namespace csv_writer {
unsigned long last_written_at = 0;
String last_successful_write = "";
#if defined(ESP8266)
/**
* SPECIFIC FUNCTIONS FOR LITTLEFS
*/
FSInfo fs_info;
bool mountFS() {
return LittleFS.begin(); // format if needed.
}
void updateFsInfo() {
FS_LIB.info(fs_info);
}
int getTotalSpace() {
return fs_info.totalBytes;