diff --git a/README.md b/README.md index 744c18fd15511019511627056c40b7f2b2ab7b31..09bcf3a3850bf2f4ff6606508b5ff05a1c07c805 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CO<sub>2</sub> Ampel -*CO<sub>2</sub> Ampel* is an open-source project, written in C++ for ESP8266 or ESP32. +*CO<sub>2</sub> Ampel* is an open-source project, written in C++ for [ESP8266](https://en.wikipedia.org/wiki/ESP8266) or [ESP32](https://en.wikipedia.org/wiki/ESP32). It measures the current CO<sub>2</sub> concentration (in ppm), and displays it on an LED ring. @@ -12,11 +12,11 @@ The *CO<sub>2</sub> Ampel* can: * Display CO2 concentration on LED ring. * Allow calibration. -* Get current time over NTP -* Send data over MQTT. -* Send data over LoRaWAN. +* Get current time over [NTP](https://en.wikipedia.org/wiki/Network_Time_Protocol) +* Send data over [MQTT](https://en.wikipedia.org/wiki/MQTT). +* Send data over [LoRaWAN](https://en.wikipedia.org/wiki/LoRa#LoRaWAN). * Display measurements and configuration on a small website. -* Log data to a CSV file, directly on the ESP flash memory. +* Log data to a [CSV](https://en.wikipedia.org/wiki/Comma-separated_values) file, directly on the ESP flash memory. ## Hardware Requirements @@ -85,5 +85,4 @@ make upload board=esp32 && make monitor # For ESP32 ## License Copyright © 2021, [HfT Stuttgart](https://www.hft-stuttgart.de/) - [GPLv3](https://choosealicense.com/licenses/gpl-3.0/) diff --git a/ampel-firmware/co2_sensor.cpp b/ampel-firmware/co2_sensor.cpp index 5dff6765fb7c4a6b19d9dc631af15c3474d9840b..5795697b05eb325ae2f4e8822f5f994bcffa043a 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 @@ -41,17 +69,19 @@ namespace sensor { * SDA --- SDA (GPIO21) //NOTE: GPIO1 would be more convenient (right next to GPO3) */ #endif + Serial.println(); + scd30.enableDebugging(); // Prints firmware version in the console. - // CO2 - if (scd30.begin(config::auto_calibrate_sensor) == false) { + if (!scd30.begin(config::auto_calibrate_sensor)) { Serial.println(F("ERROR - CO2 sensor not detected. Please check wiring!")); led_effects::showKITTWheel(color::red, 30); 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(); Serial.print(F("Setting SCD30 timestep to ")); Serial.print(config::measurement_timestep); Serial.println(" s."); @@ -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) { diff --git a/ampel-firmware/csv_writer.cpp b/ampel-firmware/csv_writer.cpp index 7e30d44da9531d5a4022ce3aec2a3a5a9d51c987..a83bed666455545d2d5cb7c68a3d583d88f7b40a 100644 --- a/ampel-firmware/csv_writer.cpp +++ b/ampel-firmware/csv_writer.cpp @@ -87,6 +87,7 @@ namespace csv_writer { void initialize(const char *sensorId) { snprintf(filename, sizeof(filename), "/%s.csv", sensorId); + Serial.println(); Serial.print(F("Initializing FS...")); if (mountFS()) { Serial.println(F("done."));