#include "co2_sensor.h" 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] #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] #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] } namespace sensor { SCD30 scd30; int16_t co2 = 0; float temperature = 0; float humidity = 0; String timestamp = ""; int16_t stable_measurements = 0; uint32_t waiting_color = color::blue; bool should_calibrate = false; unsigned long time_calaibration_started = millis(); void initialize() { #if defined(ESP8266) Wire.begin(12, 14); // ESP8266 - SDA: D6, SCL: D5; #endif #if defined(ESP32) Wire.begin(21, 22); // ESP32 /** * SCD30 ESP32 * VCC --- 3V3 * GND --- GND * SCL --- SCL (GPIO22) //NOTE: GPIO3 Would be more convenient (right next to GND) * SDA --- SDA (GPIO21) //NOTE: GPIO1 would be more convenient (right next to GPO3) */ #endif // CO2 if (scd30.begin(config::auto_calibrate_sensor) == false) { Serial.println("Air sensor not detected. Please check wiring. Freezing..."); while (1) { led_effects::showWaitingLED(color::red); } } // 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] Serial.print(F("Setting temperature offset to -")); Serial.print(abs(config::temperature_offset)); Serial.println(" K."); scd30.setTemperatureOffset(abs(config::temperature_offset)); // setTemperatureOffset only accepts positive numbers, but shifts the temperature down. delay(100); Serial.print(F("Temperature offset is : -")); Serial.print(scd30.getTemperatureOffset()); Serial.println(" K"); Serial.print(F("Auto-calibration is ")); Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF."); } //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; } 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. */ Serial.println(F("Setting SCD30 timestep to 2s, prior to calibration.")); scd30.setMeasurementInterval(MEASUREMENT_TIMESTEP); // [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; time_calaibration_started = millis(); } void calibrateAndRestart() { Serial.print(F("Calibrating SCD30 now...")); scd30.setAltitudeCompensation(config::altitude_above_sea_level); scd30.setForcedRecalibrationFactor(config::co2_calibration_level); Serial.println(F(" Done!")); Serial.println(F("Sensor calibrated.")); ESP.restart(); // softer than ESP.reset } void logToSerial() { Serial.print(timestamp); Serial.print(F(" - co2(ppm): ")); Serial.print(co2); Serial.print(F(" temp(C): ")); Serial.print(temperature, 1); Serial.print(F(" humidity(%): ")); Serial.println(humidity, 1); } void displayCO2OnLedRing() { int16_t co2_int = co2; if (co2_int < CALIBRATE_LEVEL) { // Sensor should be calibrated. led_effects::showWaitingLED(color::magenta); return; } if(co2_int < 400) { co2_int = 400; } /** * Display data, even if it's "old" (with breathing). * Those effects include a short delay. */ if (co2_int < 2000) { led_effects::displayCO2color(co2_int); led_effects::breathe(co2_int); } else { // >= 2000: entire ring blinks red led_effects::redAlert(); } } /** 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) */ bool processData() { bool freshData = scd30.dataAvailable(); if (freshData) { // checkTimerDeviation(); timestamp = ntp::getLocalTime(); 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) { if(millis() - time_calaibration_started > 60000) { countStableMeasurements(); } } logToSerial(); } if (should_calibrate) { if (stable_measurements == 60) { calibrateAndRestart(); } led_effects::showWaitingLED(waiting_color); return false; } displayCO2OnLedRing(); return freshData; } }