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() {
Serial.begin(BAUDS);
pinMode(0, INPUT); // Flash button (used for forced calibration)
pinMode(0, INPUT); // Flash button (used for forced calibration)
LedEffects::setupRing();
......@@ -111,73 +111,12 @@ void loop() {
//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) {
//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();
}
sensor::processData();
uint32_t duration = millis() - t0;
if (duration > max_loop_duration) {
......
......@@ -2,17 +2,17 @@
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]
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]
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]
const bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false]
}
namespace sensor {
......@@ -21,13 +21,16 @@ namespace sensor {
float temperature = 0;
float humidity = 0;
String timestamp = "";
int16_t stable_measurements = 0;
uint32_t waiting_color = color::blue;
bool should_calibrate = false;
void initialize() {
#if defined(ESP8266)
Wire.begin(12, 14); // ESP8266 - D6, D5;
#endif
#if defined(ESP32)
Wire.begin(21, 22); // ESP32
Wire.begin(21, 22); // ESP32
/**
* SCD30 ESP32
* VCC --- 3V3
......@@ -46,11 +49,12 @@ namespace sensor {
}
// SCD30 has its own timer.
//NOTE: The timer seems to be inaccurate, though, possibly depending on voltage. Should it be offset?
Serial.println();
Serial.print(F("Setting SCD30 timestep to "));
Serial.print(config::measurement_timestep);
Serial.println(" s.");
scd30.setMeasurementInterval(config::measurement_timestep); // [s]
scd30.setMeasurementInterval(config::measurement_timestep); // [s]
Serial.print(F("Setting temperature offset to -"));
Serial.print(abs(config::temperature_offset));
......@@ -66,8 +70,43 @@ namespace sensor {
Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF.");
}
void waitUntilMeasurementsAreStable() {
//TODO: Refactor completely, in order to avoid very long loop?
//NOTE: should timer deviation be used to adjust measurement_timestep?
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:
* 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.
......@@ -76,41 +115,81 @@ namespace sensor {
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("It could take a very long time."));
//########################################################################################################
// (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);
}
//########################################################################################################
should_calibrate = true;
}
void startCalibrationProcess() {
waitUntilMeasurementsAreStable();
Serial.print("Starting SCD30 calibration...");
void calibrateAndRestart() {
Serial.print(F("Calibrating SCD30 now..."));
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);
//TODO: Add LEDs off and move to util::reset()
FS_LIB.end();
ESP.restart();
Serial.println(F(" Done!"));
Serial.println(F("Sensor calibrated."));
resetAmpel();
}
void logToSerial() {
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 @@
#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 "util.h"
#include "csv_writer.h" // To close filesystem before restart.
#include <Wire.h>
#ifdef MQTT
# include "mqtt.h"
#endif
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 uint16_t co2_calibration_level; // [ppm]
extern uint16_t co2_calibration_level; // [ppm]
extern const float temperature_offset; // [K] Sign isn't relevant.
}
......@@ -24,6 +29,7 @@ namespace sensor {
extern String timestamp;
void initialize();
void processData();
void startCalibrationProcess();
}
#endif
......@@ -11,7 +11,7 @@ namespace config {
/*****************************************************************
* 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 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
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);
namespace counter {
uint16_t wheel_offset = 0;
uint16_t kitt_offset = 0;
uint16_t breathing_offset = 0;
} // namespace counter
namespace LedEffects {
//On-board LED on D4, aka GPIO02
const int ONBOARD_LED_PIN = 2;
......@@ -56,18 +50,23 @@ namespace LedEffects {
digitalWrite(ONBOARD_LED_PIN, LOW);
}
void LEDsOff() {
pixels.clear();
pixels.show();
onBoardLEDOff();
}
void setupRing() {
pixels.begin();
pixels.setBrightness(config::max_brightness);
pixels.clear();
LEDsOff();
}
void toggleNightMode() {
config::night_mode = !config::night_mode;
if (config::night_mode) {
Serial.println(F("NIGHT MODE!"));
pixels.clear();
pixels.show();
LEDsOff();
} else {
Serial.println(F("DAY MODE!"));
}
......@@ -79,13 +78,14 @@ namespace LedEffects {
if (config::night_mode) {
return;
}
static uint16_t kitt_offset = 0;
pixels.clear();
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.show();
counter::kitt_offset += 1;
kitt_offset++;
}
// Start K.I.T.T. led effect. Red color as default.
......@@ -135,12 +135,13 @@ namespace LedEffects {
if (config::night_mode) {
return;
}
static uint16_t wheel_offset = 0;
unsigned long t0 = seconds();
pixels.setBrightness(config::max_brightness);
while (seconds() < t0 + duration_s) {
for (int i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / NUMPIXELS + counter::wheel_offset));
counter::wheel_offset += hue_increment;
pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / NUMPIXELS + wheel_offset));
wheel_offset += hue_increment;
}
pixels.show();
delay(10);
......@@ -165,14 +166,14 @@ namespace LedEffects {
void breathe(int16_t co2) {
if (!config::night_mode) {
static uint16_t breathing_offset = 0;
//TODO: use integer sine
pixels.setBrightness(
static_cast<int>(config::average_brightness
+ cos(counter::breathing_offset * 0.1) * config::brightness_amplitude));
static_cast<int>(config::average_brightness + cos(breathing_offset * 0.1) * config::brightness_amplitude));
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 {
*/
int countdownToZero() {
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.
return 1;
}
......
......@@ -22,6 +22,7 @@ namespace LedEffects {
void onBoardLEDOff();
void onBoardLEDOn();
void toggleNightMode();
void LEDsOff();
void setupRing();
void redAlert();
......
......@@ -162,8 +162,7 @@ namespace mqtt {
} else if (messageString == "local_ip") {
sendInfoAboutLocalNetwork();
} else if (messageString == "reset") {
FS_LIB.end();
ESP.restart();
resetAmpel();
} else {
LedEffects::showKITTWheel(color::red, 1);
Serial.println(F("Message not supported. Doing nothing."));
......
......@@ -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;
//FIXME: Remove every instance of Strings, to avoid heap fragmentation problems. (Start: "Free heap space : 17104 bytes")
......
......@@ -3,6 +3,7 @@
#include <Arduino.h>
#include "config.h"
#include "wifi_util.h" // To get MAC
#include "csv_writer.h" // To close filesystem before reset
#include <WiFiUdp.h> //required for NTP
#include "src/lib/NTPClient-master/NTPClient.h" // NTP
......@@ -27,4 +28,6 @@ namespace ntp {
extern uint32_t max_loop_duration;
const extern String SENSOR_ID;
void resetAmpel();
#endif
......@@ -88,7 +88,7 @@ namespace web_server {
"<tr><td>Last MQTT publish</td><td>%s</td></tr>\n"
"<tr><td>MQTT publish timestep</td><td>%5d s</td></tr>\n"
#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 IP</td><td><a href='http://%s'>%s</a></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