diff --git a/ampel-firmware/co2_sensor.cpp b/ampel-firmware/co2_sensor.cpp index 5dff6765fb7c4a6b19d9dc631af15c3474d9840b..b573c4b49d2bc03d08cfafad6d78ef6277ae439a 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,12 +25,39 @@ 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 + * NEEDS_CALIBRATION -> sensor measurements are too low (< 250 ppm) + * PREPARE_CALIBRATION -> forced calibration was initiated, waiting for stable measurements + * CALIBRATION -> the sensor does calibrate itself + */ + enum state { + INITIAL, + BOOTUP, + READY, + NEEDS_CALIBRATION, + PREPARE_CALIBRATION_UNSTABLE, + PREPARE_CALIBRATION_STABLE, + CALIBRATION + }; + const char *state_names[] = { + "INITIAL", + "BOOTUP", + "READY", + "NEEDS_CALIBRATION", + "PREPARE_CALIBRATION_UNSTABLE", + "PREPARE_CALIBRATION_STABLE", + "CALIBRATION" }; + state current_state = INITIAL; + void switchState(state); void initialize() { #if defined(ESP8266) - Wire.begin(12, 14); // ESP8266 - D6, D5; + Wire.begin(12, 14); // ESP8266 - D6, D5; #endif #if defined(ESP32) Wire.begin(21, 22); // ESP32 @@ -49,6 +77,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 +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(PREPARE_CALIBRATION_STABLE); } else { stable_measurements = 0; - waiting_color = color::red; + switchState(PREPARE_CALIBRATION_UNSTABLE); } previous_co2 = co2; + return (stable_measurements == config::enough_stable_measurements); } void startCalibrationProcess() { @@ -115,10 +147,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(PREPARE_CALIBRATION_UNSTABLE); } 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 +170,41 @@ namespace sensor { Serial.println(humidity, 1); } - void displayCO2OnLedRing() { - if (co2 < 250) { - // Sensor should be calibrated. - led_effects::showWaitingLED(color::magenta); + 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 switchStateForCurrentPPM() { + 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 supposedly 0 ppm")); + switchState(BOOTUP); + } else if ((current_state == PREPARE_CALIBRATION_UNSTABLE) || (current_state == PREPARE_CALIBRATION_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(NEEDS_CALIBRATION); + } else { + switchState(READY); + } + } + + 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 +219,30 @@ namespace sensor { } } + void showState() { + switch (current_state) { + case BOOTUP: + led_effects::showWaitingLED(color::blue); + break; + case READY: + displayCO2OnLedRing(); + break; + case NEEDS_CALIBRATION: + led_effects::showWaitingLED(color::magenta); + break; + case PREPARE_CALIBRATION_UNSTABLE: + led_effects::showWaitingLED(color::red); + break; + case PREPARE_CALIBRATION_STABLE: + led_effects::showWaitingLED(color::green); + break; + case CALIBRATION: // Nothing to do, will restart soon. + break; + default: + Serial.println(F("Encountered unknown sensor state")); // This should not happen. + } + } + /** 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 +255,15 @@ 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; - } + switchStateForCurrentPPM(); - /** - * Fresh data. Log it and send it if needed. - */ - if (freshData) { - if (should_calibrate) { - countStableMeasurements(); - } + // 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; - } + showState(); - displayCO2OnLedRing(); return freshData; } @@ -207,6 +274,7 @@ namespace sensor { Serial.print(F("DEBUG. Setting CO2 to ")); co2 = fakeCo2; Serial.println(co2); + switchStateForCurrentPPM(); } void setAutoCalibration(int32_t autoCalibration) {