diff --git a/ampel-firmware/co2_sensor.cpp b/ampel-firmware/co2_sensor.cpp index 5dff6765fb7c4a6b19d9dc631af15c3474d9840b..51e5fcc68dc34e393fe4ed63e1db92a2bc4818da 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 { @@ -24,8 +25,36 @@ namespace sensor { float humidity = 0; char timestamp[23]; int16_t stable_measurements = 0; - uint32_t waiting_color = color::blue; - bool should_calibrate = false; + + /** + * 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_INSTABLE, + PREPARECALIBRATION_STABLE, + CALIBRATION}; + const char *state_names[] = { + "INITIAL", + "BOOTUP", + "READY", + "NEEDSCALIBRATION", + "PREPARECALIBRATION_INSTABLE", + "PREPARECALIBRATION_STABLE", + "CALIBRATION" + }; + state current_state = INITIAL; + void switchState(state); void initialize() { #if defined(ESP8266) @@ -49,6 +78,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(); @@ -91,19 +122,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() { @@ -115,10 +148,11 @@ 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_INSTABLE); } void calibrateAndRestart() { + switchState(CALIBRATION); Serial.print(F("Calibrating SCD30 now...")); scd30.setAltitudeCompensation(config::altitude_above_sea_level); scd30.setForcedRecalibrationFactor(config::co2_calibration_level); @@ -137,12 +171,18 @@ namespace sensor { Serial.println(humidity, 1); } - void displayCO2OnLedRing() { - if (co2 < 250) { - // Sensor should be calibrated. - led_effects::showWaitingLED(color::magenta); - return; + void switchState(state new_state) { + if (new_state == current_state) return; + 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; + } + + void displayCO2OnLedRing() { /** * Display data, even if it's "old" (with breathing). * A short delay is required in order to let background tasks run on the ESP8266. @@ -157,6 +197,22 @@ 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_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. + 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) */ @@ -169,34 +225,38 @@ 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. - led_effects::showWaitingLED(color::blue); - return false; - } - /** - * Fresh data. Log it and send it if needed. - */ - if (freshData) { - if (should_calibrate) { - countStableMeasurements(); + 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 ((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 { + switchState(READY); } + + // Log every time fresh data is available. logToSerial(); } - if (should_calibrate) { - if (stable_measurements == config::enough_stable_measurements) { - calibrateAndRestart(); - } - led_effects::showWaitingLED(waiting_color); - 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; }