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

Merge branch 'co2_refactor' into develop

parents b28542b7 6781d9d6
...@@ -64,7 +64,7 @@ void setup() { ...@@ -64,7 +64,7 @@ void setup() {
Serial.begin(BAUDS); Serial.begin(BAUDS);
pinMode(0, INPUT); // Flash button (used for forced calibration) pinMode(0, INPUT); // Flash button (used for forced calibration)
LedEffects::setupRing(); LedEffects::setupRing();
...@@ -111,73 +111,12 @@ void loop() { ...@@ -111,73 +111,12 @@ void loop() {
//TODO: Restart every day or week, in order to not let t0 overflow? //TODO: Restart every day or week, in order to not let t0 overflow?
uint32_t t0 = millis(); uint32_t t0 = millis();
/**
* USER INTERACTION
*/
keepServicesAlive(); keepServicesAlive();
// Short press for night mode, Long press for calibration. // Short press for night mode, Long press for calibration.
checkFlashButton(); checkFlashButton();
/** sensor::processData();
* GET DATA
*/
bool freshData = sensor::scd30.dataAvailable(); // Alternative : close to time-step AND dataAvailable, to avoid asking the sensor too often.
if (freshData) {
//TODO: Move to co2_sensor.cpp
//TODO: Save count of stable measurements
//TODO: Compare time to previous measurements, check that it's not too far away from config::measurement_interval
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; uint32_t duration = millis() - t0;
if (duration > max_loop_duration) { if (duration > max_loop_duration) {
......
...@@ -2,17 +2,17 @@ ...@@ -2,17 +2,17 @@
namespace config { namespace config {
// Values should be defined in config.h // Values should be defined in config.h
uint16_t measurement_timestep = MEASUREMENT_TIMESTEP; // [s] Value between 2 and 1800 (range for SCD30 sensor) 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] const uint16_t altitude_above_sea_level = ALTITUDE_ABOVE_SEA_LEVEL; // [m]
uint16_t co2_calibration_level = ATMOSPHERIC_CO2_CONCENTRATION; // [ppm] uint16_t co2_calibration_level = ATMOSPHERIC_CO2_CONCENTRATION; // [ppm]
#ifdef TEMPERATURE_OFFSET #ifdef TEMPERATURE_OFFSET
// Residual heat from CO2 sensor seems to be high enough to change the temperature reading. How much should it be 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. // NOTE: Sign isn't relevant. The returned temperature will always be shifted down.
const float temperature_offset = TEMPERATURE_OFFSET; // [K] const float temperature_offset = TEMPERATURE_OFFSET; // [K]
#else #else
const float temperature_offset = -3.0; // [K] Temperature measured by sensor is usually at least 3K too high. const float temperature_offset = -3.0; // [K] Temperature measured by sensor is usually at least 3K too high.
#endif #endif
const bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false] const bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false]
} }
namespace sensor { namespace sensor {
...@@ -21,13 +21,16 @@ namespace sensor { ...@@ -21,13 +21,16 @@ namespace sensor {
float temperature = 0; float temperature = 0;
float humidity = 0; float humidity = 0;
String timestamp = ""; String timestamp = "";
int16_t stable_measurements = 0;
uint32_t waiting_color = color::blue;
bool should_calibrate = false;
void initialize() { void initialize() {
#if defined(ESP8266) #if defined(ESP8266)
Wire.begin(12, 14); // ESP8266 - D6, D5; Wire.begin(12, 14); // ESP8266 - D6, D5;
#endif #endif
#if defined(ESP32) #if defined(ESP32)
Wire.begin(21, 22); // ESP32 Wire.begin(21, 22); // ESP32
/** /**
* SCD30 ESP32 * SCD30 ESP32
* VCC --- 3V3 * VCC --- 3V3
...@@ -46,11 +49,12 @@ namespace sensor { ...@@ -46,11 +49,12 @@ namespace sensor {
} }
// SCD30 has its own timer. // SCD30 has its own timer.
//NOTE: The timer seems to be inaccurate, though, possibly depending on voltage. Should it be offset?
Serial.println(); Serial.println();
Serial.print(F("Setting SCD30 timestep to ")); Serial.print(F("Setting SCD30 timestep to "));
Serial.print(config::measurement_timestep); Serial.print(config::measurement_timestep);
Serial.println(" s."); Serial.println(" s.");
scd30.setMeasurementInterval(config::measurement_timestep); // [s] scd30.setMeasurementInterval(config::measurement_timestep); // [s]
Serial.print(F("Setting temperature offset to -")); Serial.print(F("Setting temperature offset to -"));
Serial.print(abs(config::temperature_offset)); Serial.print(abs(config::temperature_offset));
...@@ -66,8 +70,43 @@ namespace sensor { ...@@ -66,8 +70,43 @@ namespace sensor {
Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF."); Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF.");
} }
void waitUntilMeasurementsAreStable() { //NOTE: should timer deviation be used to adjust measurement_timestep?
//TODO: Refactor completely, in order to avoid very long loop? void checkTimerDeviation() {
static int32_t previous_measurement_at = 0;
int32_t now = millis();
Serial.print("Measurement time offset : ");
Serial.print(now - previous_measurement_at - config::measurement_timestep * 1000);
Serial.println(" ms.");
previous_measurement_at = now;
}
void countStableMeasurements() {
static int16_t previous_co2 = 0;
if (co2 > (previous_co2 - 30) && co2 < (previous_co2 + 30)) {
stable_measurements++;
Serial.print(F("Number of stable measurements : "));
Serial.println(stable_measurements);
waiting_color = color::green;
} else {
stable_measurements = 0;
waiting_color = color::red;
}
previous_co2 = co2;
}
bool updateDataIfAvailable() {
if (scd30.dataAvailable()) {
// checkTimerDeviation();
timestamp = ntp::getLocalTime();
co2 = scd30.getCO2();
temperature = scd30.getTemperature();
humidity = scd30.getHumidity();
return true;
}
return false;
}
void startCalibrationProcess() {
/** From the sensor documentation: /** From the sensor documentation:
* For best results, the sensor has to be run in a stable environment in continuous mode at * 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. * a measurement rate of 2s for at least two minutes before applying the FRC command and sending the reference value.
...@@ -76,41 +115,81 @@ namespace sensor { ...@@ -76,41 +115,81 @@ namespace sensor {
scd30.setMeasurementInterval(2); // [s] The change will only take effect after next measurement. scd30.setMeasurementInterval(2); // [s] The change will only take effect after next measurement.
Serial.println(F("Waiting until the measurements are stable for at least 2 minutes.")); Serial.println(F("Waiting until the measurements are stable for at least 2 minutes."));
Serial.println(F("It could take a very long time.")); Serial.println(F("It could take a very long time."));
should_calibrate = true;
//########################################################################################################
// (c) Mario Lukas
// https://github.com/mariolukas/Watterott-CO2-Ampel-Plus-Firmware/blob/main/CO2-Ampel_Plus/Sensor.cpp#L57
uint32_t last_color = color::blue;
int stable_measurements = 0, last_co2 = 0;
for (stable_measurements = 0; stable_measurements < 60;) {
if (scd30.dataAvailable()) {
co2 = scd30.getCO2();
//No more than +/-30ppm variation compared to previous measurement.
if ((co2 > (last_co2 - 30)) && (co2 < (last_co2 + 30))) {
last_color = color::green;
stable_measurements++;
} else {
last_color = color::red;
stable_measurements = 0;
}
last_co2 = co2;
}
LedEffects::showKITTWheel(last_color, 1);
}
//########################################################################################################
} }
void startCalibrationProcess() { void calibrateAndRestart() {
waitUntilMeasurementsAreStable(); Serial.print(F("Calibrating SCD30 now..."));
Serial.print("Starting SCD30 calibration...");
scd30.setAltitudeCompensation(config::altitude_above_sea_level); scd30.setAltitudeCompensation(config::altitude_above_sea_level);
scd30.setForcedRecalibrationFactor(config::co2_calibration_level); scd30.setForcedRecalibrationFactor(config::co2_calibration_level);
Serial.println(" Done!"); Serial.println(F(" Done!"));
Serial.println("Sensor calibrated."); Serial.println(F("Sensor calibrated."));
Serial.println("Sensor will now restart."); resetAmpel();
LedEffects::showKITTWheel(color::green, 5); }
//TODO: Add LEDs off and move to util::reset()
FS_LIB.end(); void logToSerial() {
ESP.restart(); Serial.println(timestamp);
Serial.print(F("co2(ppm): "));
Serial.print(co2);
Serial.print(F(" temp(C): "));
Serial.print(temperature);
Serial.print(F(" humidity(%): "));
Serial.println(humidity);
}
void displayCO2OnLedRing() {
if (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 (co2 < 2000) {
LedEffects::displayCO2color(co2);
LedEffects::breathe(co2);
} else {
// >= 2000: entire ring blinks red
LedEffects::redAlert();
}
}
void processData() {
bool freshData = updateDataIfAvailable();
//NOTE: Data is available, but it's sometimes erroneous: the sensor outputs zero ppm but non-zero temperature and non-zero humidity.
if (co2 <= 0) {
// No measurement yet. Waiting.
LedEffects::showWaitingLED(color::blue);
return;
}
/**
* Fresh data. Log it and send it if needed.
*/
if (freshData) {
if (should_calibrate) {
countStableMeasurements();
}
logToSerial();
csv_writer::logIfTimeHasCome(timestamp, co2, temperature, humidity);
#ifdef MQTT
mqtt::publishIfTimeHasCome(timestamp, co2, temperature, humidity);
#endif
}
if (should_calibrate) {
if (stable_measurements == 60) {
calibrateAndRestart();
}
LedEffects::showWaitingLED(waiting_color);
return;
}
displayCO2OnLedRing();
} }
} }
...@@ -6,13 +6,18 @@ ...@@ -6,13 +6,18 @@
#include "src/lib/SparkFun_SCD30_Arduino_Library/src/SparkFun_SCD30_Arduino_Library.h" // From: http://librarymanager/All#SparkFun_SCD30 #include "src/lib/SparkFun_SCD30_Arduino_Library/src/SparkFun_SCD30_Arduino_Library.h" // From: http://librarymanager/All#SparkFun_SCD30
#include "config.h" #include "config.h"
#include "led_effects.h" #include "led_effects.h"
#include "util.h"
#include "csv_writer.h" // To close filesystem before restart. #include "csv_writer.h" // To close filesystem before restart.
#include <Wire.h> #include <Wire.h>
#ifdef MQTT
# include "mqtt.h"
#endif
namespace config { namespace config {
extern uint16_t measurement_timestep; // [s] Value between 2 and 1800 (range for SCD30 sensor) extern uint16_t measurement_timestep; // [s] Value between 2 and 1800 (range for SCD30 sensor)
extern const bool auto_calibrate_sensor; // [true / false] extern const bool auto_calibrate_sensor; // [true / false]
extern uint16_t co2_calibration_level; // [ppm] extern uint16_t co2_calibration_level; // [ppm]
extern const float temperature_offset; // [K] Sign isn't relevant. extern const float temperature_offset; // [K] Sign isn't relevant.
} }
...@@ -24,6 +29,7 @@ namespace sensor { ...@@ -24,6 +29,7 @@ namespace sensor {
extern String timestamp; extern String timestamp;
void initialize(); void initialize();
void processData();
void startCalibrationProcess(); void startCalibrationProcess();
} }
#endif #endif
...@@ -11,7 +11,7 @@ namespace config { ...@@ -11,7 +11,7 @@ namespace config {
/***************************************************************** /*****************************************************************
* Configuration (calculated from above values) * * Configuration (calculated from above values) *
*****************************************************************/ *****************************************************************/
namespace config //TODO: Use a class instead. NightMode could then be another state. namespace config //TODO: Use a class instead. NightMode could then be another state.
{ {
const float average_brightness = 0.5 * (config::max_brightness + config::min_brightness); const float average_brightness = 0.5 * (config::max_brightness + config::min_brightness);
const float brightness_amplitude = 0.5 * (config::max_brightness - config::min_brightness); const float brightness_amplitude = 0.5 * (config::max_brightness - config::min_brightness);
...@@ -34,12 +34,6 @@ const uint16_t CO2_TICKS[NUMPIXELS + 1] = { 0, 500, 600, 700, 800, 900, 1000, 12 ...@@ -34,12 +34,6 @@ const uint16_t CO2_TICKS[NUMPIXELS + 1] = { 0, 500, 600, 700, 800, 900, 1000, 12
const uint16_t LED_HUES[NUMPIXELS] = { 21845, 19114, 16383, 13653, 10922, 8191, 5461, 2730, 0, 0, 0, 0 }; // [hue angle] const uint16_t LED_HUES[NUMPIXELS] = { 21845, 19114, 16383, 13653, 10922, 8191, 5461, 2730, 0, 0, 0, 0 }; // [hue angle]
Adafruit_NeoPixel pixels(NUMPIXELS, NEOPIXELS_PIN, NEO_GRB + NEO_KHZ800); Adafruit_NeoPixel pixels(NUMPIXELS, NEOPIXELS_PIN, NEO_GRB + NEO_KHZ800);
namespace counter {
uint16_t wheel_offset = 0;
uint16_t kitt_offset = 0;
uint16_t breathing_offset = 0;
} // namespace counter
namespace LedEffects { namespace LedEffects {
//On-board LED on D4, aka GPIO02 //On-board LED on D4, aka GPIO02
const int ONBOARD_LED_PIN = 2; const int ONBOARD_LED_PIN = 2;
...@@ -56,18 +50,23 @@ namespace LedEffects { ...@@ -56,18 +50,23 @@ namespace LedEffects {
digitalWrite(ONBOARD_LED_PIN, LOW); digitalWrite(ONBOARD_LED_PIN, LOW);
} }
void LEDsOff() {
pixels.clear();
pixels.show();
onBoardLEDOff();
}
void setupRing() { void setupRing() {
pixels.begin(); pixels.begin();
pixels.setBrightness(config::max_brightness); pixels.setBrightness(config::max_brightness);
pixels.clear(); LEDsOff();
} }
void toggleNightMode() { void toggleNightMode() {
config::night_mode = !config::night_mode; config::night_mode = !config::night_mode;
if (config::night_mode) { if (config::night_mode) {
Serial.println(F("NIGHT MODE!")); Serial.println(F("NIGHT MODE!"));
pixels.clear(); LEDsOff();
pixels.show();
} else { } else {
Serial.println(F("DAY MODE!")); Serial.println(F("DAY MODE!"));
} }
...@@ -79,13 +78,14 @@ namespace LedEffects { ...@@ -79,13 +78,14 @@ namespace LedEffects {
if (config::night_mode) { if (config::night_mode) {
return; return;
} }
static uint16_t kitt_offset = 0;
pixels.clear(); pixels.clear();
for (int j = config::kitt_tail; j >= 0; j--) { for (int j = config::kitt_tail; j >= 0; j--) {
int ledNumber = abs((counter::kitt_offset - j + NUMPIXELS) % (2 * NUMPIXELS) - NUMPIXELS) % NUMPIXELS; // Triangular function int ledNumber = abs((kitt_offset - j + NUMPIXELS) % (2 * NUMPIXELS) - NUMPIXELS) % NUMPIXELS; // Triangular function
pixels.setPixelColor(ledNumber, color * pixels.gamma8(255 - j * 76) / 255); pixels.setPixelColor(ledNumber, color * pixels.gamma8(255 - j * 76) / 255);
} }
pixels.show(); pixels.show();
counter::kitt_offset += 1; kitt_offset++;
} }
// Start K.I.T.T. led effect. Red color as default. // Start K.I.T.T. led effect. Red color as default.
...@@ -135,12 +135,13 @@ namespace LedEffects { ...@@ -135,12 +135,13 @@ namespace LedEffects {
if (config::night_mode) { if (config::night_mode) {
return; return;
} }
static uint16_t wheel_offset = 0;
unsigned long t0 = seconds(); unsigned long t0 = seconds();
pixels.setBrightness(config::max_brightness); pixels.setBrightness(config::max_brightness);
while (seconds() < t0 + duration_s) { while (seconds() < t0 + duration_s) {
for (int i = 0; i < NUMPIXELS; i++) { for (int i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / NUMPIXELS + counter::wheel_offset)); pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / NUMPIXELS + wheel_offset));
counter::wheel_offset += hue_increment; wheel_offset += hue_increment;
} }
pixels.show(); pixels.show();
delay(10); delay(10);
...@@ -165,14 +166,14 @@ namespace LedEffects { ...@@ -165,14 +166,14 @@ namespace LedEffects {
void breathe(int16_t co2) { void breathe(int16_t co2) {
if (!config::night_mode) { if (!config::night_mode) {
static uint16_t breathing_offset = 0;
//TODO: use integer sine //TODO: use integer sine
pixels.setBrightness( pixels.setBrightness(
static_cast<int>(config::average_brightness static_cast<int>(config::average_brightness + cos(breathing_offset * 0.1) * config::brightness_amplitude));
+ cos(counter::breathing_offset * 0.1) * config::brightness_amplitude));
pixels.show(); pixels.show();
counter::breathing_offset += 1; breathing_offset++;
} }
delay(co2 > 1600 ? 50 : 100); // faster breathing for higher CO2 values delay(co2 > 1600 ? 50 : 100); // faster breathing for higher CO2 values
} }
/** /**
...@@ -181,7 +182,7 @@ namespace LedEffects { ...@@ -181,7 +182,7 @@ namespace LedEffects {
*/ */
int countdownToZero() { int countdownToZero() {
if (config::night_mode) { if (config::night_mode) {
Serial.println("Night mode. Not doing anything."); Serial.println(F("Night mode. Not doing anything."));
delay(1000); // Wait for a while, to avoid coming back to this function too many times when button is pressed. delay(1000); // Wait for a while, to avoid coming back to this function too many times when button is pressed.
return 1; return 1;
} }
......
...@@ -22,6 +22,7 @@ namespace LedEffects { ...@@ -22,6 +22,7 @@ namespace LedEffects {
void onBoardLEDOff(); void onBoardLEDOff();
void onBoardLEDOn(); void onBoardLEDOn();
void toggleNightMode(); void toggleNightMode();
void LEDsOff();
void setupRing(); void setupRing();
void redAlert(); void redAlert();
......
...@@ -162,8 +162,7 @@ namespace mqtt { ...@@ -162,8 +162,7 @@ namespace mqtt {
} else if (messageString == "local_ip") { } else if (messageString == "local_ip") {
sendInfoAboutLocalNetwork(); sendInfoAboutLocalNetwork();
} else if (messageString == "reset") { } else if (messageString == "reset") {
FS_LIB.end(); resetAmpel();
ESP.restart();
} else { } else {
LedEffects::showKITTWheel(color::red, 1); LedEffects::showKITTWheel(color::red, 1);
Serial.println(F("Message not supported. Doing nothing.")); Serial.println(F("Message not supported. Doing nothing."));
......
...@@ -38,6 +38,14 @@ namespace ntp { ...@@ -38,6 +38,14 @@ namespace ntp {
} }
} }
void resetAmpel() {
Serial.print("Resetting");
FS_LIB.end();
LedEffects::LEDsOff();
delay(1000);
ESP.restart();
}
uint32_t max_loop_duration = 0; uint32_t max_loop_duration = 0;
//FIXME: Remove every instance of Strings, to avoid heap fragmentation problems. (Start: "Free heap space : 17104 bytes") //FIXME: Remove every instance of Strings, to avoid heap fragmentation problems. (Start: "Free heap space : 17104 bytes")
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
#include <Arduino.h> #include <Arduino.h>
#include "config.h" #include "config.h"
#include "wifi_util.h" // To get MAC #include "wifi_util.h" // To get MAC
#include "csv_writer.h" // To close filesystem before reset
#include <WiFiUdp.h> //required for NTP #include <WiFiUdp.h> //required for NTP
#include "src/lib/NTPClient-master/NTPClient.h" // NTP #include "src/lib/NTPClient-master/NTPClient.h" // NTP
...@@ -27,4 +28,6 @@ namespace ntp { ...@@ -27,4 +28,6 @@ namespace ntp {
extern uint32_t max_loop_duration; extern uint32_t max_loop_duration;
const extern String SENSOR_ID; const extern String SENSOR_ID;
void resetAmpel();
#endif #endif
...@@ -88,7 +88,7 @@ namespace web_server { ...@@ -88,7 +88,7 @@ namespace web_server {
"<tr><td>Last MQTT publish</td><td>%s</td></tr>\n" "<tr><td>Last MQTT publish</td><td>%s</td></tr>\n"
"<tr><td>MQTT publish timestep</td><td>%5d s</td></tr>\n" "<tr><td>MQTT publish timestep</td><td>%5d s</td></tr>\n"
#endif #endif
"<tr><td>Temperature offset</td><td>%.1fK</td></tr>\n" "<tr><td>Temperature offset</td><td>%.1fK</td></tr>\n" //TODO: Read it from sensor?
"<tr><td>Local address</td><td><a href='http://%s.local/'>%s.local</a></td></tr>\n" "<tr><td>Local address</td><td><a href='http://%s.local/'>%s.local</a></td></tr>\n"
"<tr><td>Local IP</td><td><a href='http://%s'>%s</a></td></tr>\n" "<tr><td>Local IP</td><td><a href='http://%s'>%s</a></td></tr>\n"
"<tr><td>Free heap space</td><td>%6d bytes</td></tr>\n" "<tr><td>Free heap space</td><td>%6d bytes</td></tr>\n"
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment