From ffa9f3f90a8a9524c320355df04da996c7b9dba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ppler?= Date: Sat, 17 Apr 2021 22:46:05 +0200 Subject: [PATCH 1/8] co2_sensor: introduce states * define sensor states * log every change of state to serial There is no new functionality added yet. --- ampel-firmware/co2_sensor.cpp | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/ampel-firmware/co2_sensor.cpp b/ampel-firmware/co2_sensor.cpp index 5dff676..50930c6 100644 --- a/ampel-firmware/co2_sensor.cpp +++ b/ampel-firmware/co2_sensor.cpp @@ -25,6 +25,29 @@ namespace sensor { char timestamp[23]; int16_t stable_measurements = 0; uint32_t waiting_color = color::blue; + + /** + * Define sensor states + * INITIAL -> initial state + * BOOTUP -> state after initializing the sensor, i.e. after scd.begin() + * READY -> sensor does output valid information (> 0 ppm) and no other condition takes place + * (NOTE: This state is currently unused) + * NEEDSCALIBRATION -> sensor measurements are too low (< 250 ppm) + * PREPARECALIBRATION -> forced calibration was initiated, waiting for stable measurements + * CALIBRATION -> the sensor does calibrate itself + */ + enum state {INITIAL, BOOTUP, READY, NEEDSCALIBRATION, PREPARECALIBRATION, CALIBRATION}; + const char *state_names[] = { + "INITIAL", + "BOOTUP", + "READY", + "NEEDSCALIBRATION", + "PREPARECALIBRATION", + "CALIBRATION" + }; + state current_state = INITIAL; + void switchState(state); + bool should_calibrate = false; void initialize() { @@ -49,6 +72,8 @@ namespace sensor { ESP.restart(); } + switchState(BOOTUP); + // SCD30 has its own timer. //NOTE: The timer seems to be inaccurate, though, possibly depending on voltage. Should it be offset? Serial.println(); @@ -116,9 +141,11 @@ namespace sensor { Serial.println(F("Waiting until the measurements are stable for at least 2 minutes.")); Serial.println(F("It could take a very long time.")); should_calibrate = true; + switchState(PREPARECALIBRATION); } void calibrateAndRestart() { + switchState(CALIBRATION); Serial.print(F("Calibrating SCD30 now...")); scd30.setAltitudeCompensation(config::altitude_above_sea_level); scd30.setForcedRecalibrationFactor(config::co2_calibration_level); @@ -137,10 +164,20 @@ namespace sensor { Serial.println(humidity, 1); } + void switchState(state new_state) { + if (new_state == current_state) return; + Serial.print(F("Changing sensor state: ")); + Serial.print(state_names[current_state]); + Serial.print(" -> "); + Serial.println(state_names[new_state]); + current_state = new_state; + } + void displayCO2OnLedRing() { if (co2 < 250) { // Sensor should be calibrated. led_effects::showWaitingLED(color::magenta); + switchState(NEEDSCALIBRATION); return; } /** @@ -174,8 +211,11 @@ namespace sensor { //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. + switchState(BOOTUP); led_effects::showWaitingLED(color::blue); return false; + } else if (current_state != PREPARECALIBRATION) { + switchState(READY); } /** -- GitLab From 7224beff38c8faacb6ad0b8f10be7b7bb08edb3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ppler?= Date: Wed, 21 Apr 2021 17:13:29 +0200 Subject: [PATCH 2/8] No special state handling in displayCO2OnLedRing --- ampel-firmware/co2_sensor.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/ampel-firmware/co2_sensor.cpp b/ampel-firmware/co2_sensor.cpp index 50930c6..e2805bb 100644 --- a/ampel-firmware/co2_sensor.cpp +++ b/ampel-firmware/co2_sensor.cpp @@ -174,12 +174,6 @@ namespace sensor { } void displayCO2OnLedRing() { - if (co2 < 250) { - // Sensor should be calibrated. - led_effects::showWaitingLED(color::magenta); - switchState(NEEDSCALIBRATION); - return; - } /** * Display data, even if it's "old" (with breathing). * A short delay is required in order to let background tasks run on the ESP8266. @@ -236,6 +230,13 @@ namespace sensor { return false; } + if (co2 < 250) { + // Sensor should be calibrated. + led_effects::showWaitingLED(color::magenta); + switchState(NEEDSCALIBRATION); + return false; + } + displayCO2OnLedRing(); return freshData; } -- GitLab From 295800aa26c14d2fb72d46d4487201a62d82312e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ppler?= Date: Thu, 22 Apr 2021 22:29:44 +0200 Subject: [PATCH 3/8] co2_sensor: cleanup handling for special states Previously, the control flow was determined by a mixture of `if-else`-constructes and premature `return` statements, which made it hard to understand which case occurs when. --- ampel-firmware/co2_sensor.cpp | 65 +++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/ampel-firmware/co2_sensor.cpp b/ampel-firmware/co2_sensor.cpp index e2805bb..1fe9297 100644 --- a/ampel-firmware/co2_sensor.cpp +++ b/ampel-firmware/co2_sensor.cpp @@ -188,6 +188,21 @@ namespace sensor { } } + void showState() { + switch(current_state) { + case BOOTUP: led_effects::showWaitingLED(color::blue); break; + // No special signaling, we want to show the CO2 value + case READY: break; + case NEEDSCALIBRATION: led_effects::showWaitingLED(color::magenta); break; + case PREPARECALIBRATION: led_effects::showWaitingLED(waiting_color); break; + // No special signaling here, too. + case CALIBRATION: break; + // This should not happen. + default: + Serial.println(F("Encountered unknown sensor state")); + } + } + /** Gets fresh data if available, checks calibration status, displays CO2 levels. * Returns true if fresh data is available, for further processing (e.g. MQTT, CSV or LoRa) */ @@ -200,44 +215,34 @@ namespace sensor { co2 = scd30.getCO2(); temperature = scd30.getTemperature(); humidity = 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 (co2 <= 0) { - // No measurement yet. Waiting. - switchState(BOOTUP); - led_effects::showWaitingLED(color::blue); - return false; - } else if (current_state != PREPARECALIBRATION) { - switchState(READY); - } - - /** - * Fresh data. Log it and send it if needed. - */ - if (freshData) { - if (should_calibrate) { + //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. + switchState(BOOTUP); + } else if (co2 < 250) { + // Sensor should be calibrated. + switchState(NEEDSCALIBRATION); + } else if (should_calibrate) { countStableMeasurements(); + if (stable_measurements == config::enough_stable_measurements) { + calibrateAndRestart(); + } + } else { + switchState(READY); } - logToSerial(); - } - if (should_calibrate) { - if (stable_measurements == config::enough_stable_measurements) { - calibrateAndRestart(); - } - led_effects::showWaitingLED(waiting_color); - return false; + // Log every time fresh data is available. + logToSerial(); } - if (co2 < 250) { - // Sensor should be calibrated. - led_effects::showWaitingLED(color::magenta); - switchState(NEEDSCALIBRATION); - return false; + // We need to show LED effects for "old" data, too, as long as we get new data. + if (current_state == READY) { + displayCO2OnLedRing(); + } else { + showState(); } - displayCO2OnLedRing(); return freshData; } -- GitLab From 1440e361ab4940906754966ca67bb6a881eb773e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ppler?= Date: Thu, 22 Apr 2021 11:53:08 +0200 Subject: [PATCH 4/8] co2_sensor: Start calibration process by state This removes the superfluous bool `should_calibrate`. --- ampel-firmware/co2_sensor.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ampel-firmware/co2_sensor.cpp b/ampel-firmware/co2_sensor.cpp index 1fe9297..cd4058d 100644 --- a/ampel-firmware/co2_sensor.cpp +++ b/ampel-firmware/co2_sensor.cpp @@ -48,8 +48,6 @@ namespace sensor { state current_state = INITIAL; void switchState(state); - bool should_calibrate = false; - void initialize() { #if defined(ESP8266) Wire.begin(12, 14); // ESP8266 - D6, D5; @@ -140,7 +138,6 @@ 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.")); - should_calibrate = true; switchState(PREPARECALIBRATION); } @@ -223,7 +220,7 @@ namespace sensor { } else if (co2 < 250) { // Sensor should be calibrated. switchState(NEEDSCALIBRATION); - } else if (should_calibrate) { + } else if (current_state == PREPARECALIBRATION) { countStableMeasurements(); if (stable_measurements == config::enough_stable_measurements) { calibrateAndRestart(); -- GitLab From 0878a4134ae4c6a487b95612aa4e2e69e7ec5ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ppler?= Date: Thu, 22 Apr 2021 12:56:31 +0200 Subject: [PATCH 5/8] co2_sensor: Move comment and wrap overlong line --- ampel-firmware/co2_sensor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ampel-firmware/co2_sensor.cpp b/ampel-firmware/co2_sensor.cpp index cd4058d..8a031b1 100644 --- a/ampel-firmware/co2_sensor.cpp +++ b/ampel-firmware/co2_sensor.cpp @@ -213,9 +213,9 @@ namespace sensor { temperature = scd30.getTemperature(); humidity = 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 (co2 <= 0) { - // No measurement yet. Waiting. + // NOTE: Data is available, but it's sometimes erroneous: the sensor outputs + // zero ppm but non-zero temperature and non-zero humidity. switchState(BOOTUP); } else if (co2 < 250) { // Sensor should be calibrated. -- GitLab From 0604a79c4e09b44d767ef6409b38f137f675f306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ppler?= Date: Thu, 22 Apr 2021 12:58:38 +0200 Subject: [PATCH 6/8] co2_sensor: Report invalid CO2 measurements --- ampel-firmware/co2_sensor.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/ampel-firmware/co2_sensor.cpp b/ampel-firmware/co2_sensor.cpp index 8a031b1..a41162a 100644 --- a/ampel-firmware/co2_sensor.cpp +++ b/ampel-firmware/co2_sensor.cpp @@ -216,6 +216,7 @@ namespace sensor { if (co2 <= 0) { // NOTE: Data is available, but it's sometimes erroneous: the sensor outputs // zero ppm but non-zero temperature and non-zero humidity. + Serial.println(F("Invalid sensor data - CO2 concentration <= 0 ppm")); switchState(BOOTUP); } else if (co2 < 250) { // Sensor should be calibrated. -- GitLab From a5e847e9d9868ffb9d04717942edfbe0d519a8f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ppler?= Date: Thu, 22 Apr 2021 22:33:31 +0200 Subject: [PATCH 7/8] co2_sensor: Handle pre-calibration by states * split state PREPARECALIBRATION into PREPARECALIBRATION_INSTABLE (co2 measurements are changing too much to start calibration) and PREPARECALIBRATION_STABLE (co2 measurements are stable, now waiting for 60 stable measurements) * Handle all possible LED signal states in showState() * drop now unused `waiting_color` --- ampel-firmware/co2_sensor.cpp | 39 +++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/ampel-firmware/co2_sensor.cpp b/ampel-firmware/co2_sensor.cpp index a41162a..946e09b 100644 --- a/ampel-firmware/co2_sensor.cpp +++ b/ampel-firmware/co2_sensor.cpp @@ -24,7 +24,6 @@ namespace sensor { float humidity = 0; char timestamp[23]; int16_t stable_measurements = 0; - uint32_t waiting_color = color::blue; /** * Define sensor states @@ -36,13 +35,21 @@ namespace sensor { * PREPARECALIBRATION -> forced calibration was initiated, waiting for stable measurements * CALIBRATION -> the sensor does calibrate itself */ - enum state {INITIAL, BOOTUP, READY, NEEDSCALIBRATION, PREPARECALIBRATION, CALIBRATION}; + enum state { + INITIAL, + BOOTUP, + READY, + NEEDSCALIBRATION, + PREPARECALIBRATION_INSTABLE, + PREPARECALIBRATION_STABLE, + CALIBRATION}; const char *state_names[] = { "INITIAL", "BOOTUP", "READY", "NEEDSCALIBRATION", - "PREPARECALIBRATION", + "PREPARECALIBRATION_INSTABLE", + "PREPARECALIBRATION_STABLE", "CALIBRATION" }; state current_state = INITIAL; @@ -114,19 +121,21 @@ namespace sensor { previous_measurement_at = now; } - void countStableMeasurements() { + bool countStableMeasurements() { + // Returns true, if a sufficient number of stable measurements has been observed. static int16_t previous_co2 = 0; if (co2 > (previous_co2 - config::max_deviation_during_calibration) && co2 < (previous_co2 + config::max_deviation_during_calibration)) { stable_measurements++; Serial.print(F("Number of stable measurements : ")); Serial.println(stable_measurements); - waiting_color = color::green; + switchState(PREPARECALIBRATION_STABLE); } else { stable_measurements = 0; - waiting_color = color::red; + switchState(PREPARECALIBRATION_INSTABLE); } previous_co2 = co2; + return (stable_measurements == config::enough_stable_measurements); } void startCalibrationProcess() { @@ -138,7 +147,7 @@ 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.")); - switchState(PREPARECALIBRATION); + switchState(PREPARECALIBRATION_INSTABLE); } void calibrateAndRestart() { @@ -191,7 +200,8 @@ namespace sensor { // No special signaling, we want to show the CO2 value case READY: break; case NEEDSCALIBRATION: led_effects::showWaitingLED(color::magenta); break; - case PREPARECALIBRATION: led_effects::showWaitingLED(waiting_color); break; + case PREPARECALIBRATION_INSTABLE: led_effects::showWaitingLED(color::red); break; + case PREPARECALIBRATION_STABLE: led_effects::showWaitingLED(color::green); break; // No special signaling here, too. case CALIBRATION: break; // This should not happen. @@ -218,14 +228,17 @@ namespace sensor { // zero ppm but non-zero temperature and non-zero humidity. Serial.println(F("Invalid sensor data - CO2 concentration <= 0 ppm")); switchState(BOOTUP); + } else if ((current_state == PREPARECALIBRATION_INSTABLE) || + (current_state == PREPARECALIBRATION_STABLE)) { + // Check for pre-calibration states first, because we do not want to + // leave them before calibration is done. + bool ready_for_calibration = countStableMeasurements(); + if (ready_for_calibration) { + calibrateAndRestart(); + } } else if (co2 < 250) { // Sensor should be calibrated. switchState(NEEDSCALIBRATION); - } else if (current_state == PREPARECALIBRATION) { - countStableMeasurements(); - if (stable_measurements == config::enough_stable_measurements) { - calibrateAndRestart(); - } } else { switchState(READY); } -- GitLab From 19efaca1d5f4ff2d5ab36ca83a430eca30891f47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ppler?= Date: Thu, 22 Apr 2021 22:47:38 +0200 Subject: [PATCH 8/8] co2_sensor: Disable state logging by default --- ampel-firmware/co2_sensor.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ampel-firmware/co2_sensor.cpp b/ampel-firmware/co2_sensor.cpp index 946e09b..51e5fcc 100644 --- a/ampel-firmware/co2_sensor.cpp +++ b/ampel-firmware/co2_sensor.cpp @@ -15,6 +15,7 @@ namespace config { const float temperature_offset = -3.0; // [K] Temperature measured by sensor is usually at least 3K too high. #endif bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false] + const bool debug_sensor_states = false; // If true, log state transitions over serial console } namespace sensor { @@ -172,10 +173,12 @@ namespace sensor { void switchState(state new_state) { if (new_state == current_state) return; - Serial.print(F("Changing sensor state: ")); - Serial.print(state_names[current_state]); - Serial.print(" -> "); - Serial.println(state_names[new_state]); + if (config::debug_sensor_states) { + Serial.print(F("Changing sensor state: ")); + Serial.print(state_names[current_state]); + Serial.print(" -> "); + Serial.println(state_names[new_state]); + } current_state = new_state; } -- GitLab