co2_sensor.cpp 6.91 KB
Newer Older
1
2
3
4
#include "co2_sensor.h"

namespace config {
  // Values should be defined in config.h
Eric Duminil's avatar
Eric Duminil committed
5
6
7
  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]
8
9
10
#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.
Eric Duminil's avatar
Eric Duminil committed
11
  const float temperature_offset = TEMPERATURE_OFFSET; // [K]
12
13
14
#else
  const float temperature_offset = -3.0;  // [K] Temperature measured by sensor is usually at least 3K too high.
#endif
Eric Duminil's avatar
Eric Duminil committed
15
  const bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false]
16
17
18
19
}

namespace sensor {
  SCD30 scd30;
20
  uint16_t co2 = 0;
21
22
23
  float temperature = 0;
  float humidity = 0;
  String timestamp = "";
24
25
26
  int16_t stable_measurements = 0;
  uint32_t waiting_color = color::blue;
  bool should_calibrate = false;
27

Eric Duminil's avatar
Eric Duminil committed
28
29
30
31
  void setCO2forDebugging(int32_t fakeCo2) {
    Serial.print(F("DEBUG. Setting CO2 to "));
    co2 = fakeCo2;
    Serial.println(co2);
32
33
  }

Eric Duminil's avatar
Timer    
Eric Duminil committed
34
35
36
37
38
39
40
41
42
43
44
  void setTimer(int32_t timestep) {
    if (timestep >= 2 && timestep <= 1800) {
      Serial.print(F("Setting Measurement Interval to : "));
      Serial.print(timestep);
      Serial.println("s.");
      sensor::scd30.setMeasurementInterval(timestep);
      config::measurement_timestep = timestep;
      led_effects::showKITTWheel(color::green, 1);
    }
  }

45
46
47
48
49
  void initialize() {
#if defined(ESP8266)
    Wire.begin(12, 14);  // ESP8266 - D6, D5;
#endif
#if defined(ESP32)
Eric Duminil's avatar
Eric Duminil committed
50
    Wire.begin(21, 22); // ESP32
51
52
53
54
55
56
57
58
59
60
61
62
63
    /**
     *  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) {
64
        led_effects::showWaitingLED(color::red);
65
66
67
68
      }
    }

    // SCD30 has its own timer.
Eric Duminil's avatar
Eric Duminil committed
69
    //NOTE: The timer seems to be inaccurate, though, possibly depending on voltage. Should it be offset?
70
71
72
73
    Serial.println();
    Serial.print(F("Setting SCD30 timestep to "));
    Serial.print(config::measurement_timestep);
    Serial.println(" s.");
Eric Duminil's avatar
Eric Duminil committed
74
    scd30.setMeasurementInterval(config::measurement_timestep); // [s]
75

76
    Serial.print(F("Setting temperature offset to -"));
77
78
79
    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.
80
    delay(100);
81

82
    Serial.print(F("Temperature offset is : -"));
83
84
85
    Serial.print(scd30.getTemperatureOffset());
    Serial.println(" K");

86
    Serial.print(F("Auto-calibration is "));
87
    Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF.");
88

Eric Duminil's avatar
Eric Duminil committed
89
    sensor_commands::defineCallback("co2", setCO2forDebugging);
Eric Duminil's avatar
Timer    
Eric Duminil committed
90
    sensor_commands::defineCallback("timer", setTimer);
91
92
  }

Eric Duminil's avatar
Eric Duminil committed
93
  //NOTE: should timer deviation be used to adjust measurement_timestep?
Eric Duminil's avatar
Eric Duminil committed
94
  void checkTimerDeviation() {
95
    static int32_t previous_measurement_at = 0;
Eric Duminil's avatar
Eric Duminil committed
96
97
98
99
100
101
102
    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;
  }

103
  void countStableMeasurements() {
Eric Duminil's avatar
Eric Duminil committed
104
    static int16_t previous_co2 = 0;
105
106
    if (co2 > (previous_co2 - 30) && co2 < (previous_co2 + 30)) {
      stable_measurements++;
Eric Duminil's avatar
Eric Duminil committed
107
      Serial.print(F("Number of stable measurements : "));
Eric Duminil's avatar
Eric Duminil committed
108
      Serial.println(stable_measurements);
109
110
111
112
113
      waiting_color = color::green;
    } else {
      stable_measurements = 0;
      waiting_color = color::red;
    }
Eric Duminil's avatar
Eric Duminil committed
114
    previous_co2 = co2;
115
116
117
  }

  void startCalibrationProcess() {
118
119
120
121
    /** 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.
     */
122
123
124
125
    Serial.println(F("Setting SCD30 timestep to 2s, prior to calibration."));
    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."));
126
    should_calibrate = true;
127
128
  }

129
130
  void calibrateAndRestart() {
    Serial.print(F("Calibrating SCD30 now..."));
131
132
    scd30.setAltitudeCompensation(config::altitude_above_sea_level);
    scd30.setForcedRecalibrationFactor(config::co2_calibration_level);
133
134
    Serial.println(F(" Done!"));
    Serial.println(F("Sensor calibrated."));
Eric Duminil's avatar
Eric Duminil committed
135
    ESP.restart(); // softer than ESP.reset
136
  }
137

138
  void logToSerial() {
Eric Duminil's avatar
Eric Duminil committed
139
140
    Serial.print(timestamp);
    Serial.print(F(" - co2(ppm): "));
141
142
    Serial.print(co2);
    Serial.print(F(" temp(C): "));
143
    Serial.print(temperature, 1);
144
    Serial.print(F(" humidity(%): "));
145
    Serial.println(humidity, 1);
146
147
148
149
150
  }

  void displayCO2OnLedRing() {
    if (co2 < 250) {
      // Sensor should be calibrated.
151
      led_effects::showWaitingLED(color::magenta);
152
153
154
155
      return;
    }
    /**
     * Display data, even if it's "old" (with breathing).
Eric Duminil's avatar
Eric Duminil committed
156
     * A short delay is required in order to let background tasks run on the ESP8266.
157
     * see https://github.com/esp8266/Arduino/issues/3241#issuecomment-301290392
158
159
     */
    if (co2 < 2000) {
160
      led_effects::displayCO2color(co2);
161
      delay(100);
162
163
    } else {
      // >= 2000: entire ring blinks red
164
      led_effects::redAlert();
165
166
167
    }
  }

Eric Duminil's avatar
Eric Duminil committed
168
  /** Gets fresh data if available, checks calibration status, displays CO2 levels.
169
   * Returns true if fresh data is available, for further processing (e.g. MQTT, CSV or LoRa)
Eric Duminil's avatar
Eric Duminil committed
170
171
172
173
174
175
176
177
178
179
180
   */
  bool processData() {
    bool freshData = scd30.dataAvailable();

    if (freshData) {
      // checkTimerDeviation();
      timestamp = ntp::getLocalTime();
      co2 = scd30.getCO2();
      temperature = scd30.getTemperature();
      humidity = scd30.getHumidity();
    }
181
182

    //NOTE: Data is available, but it's sometimes erroneous: the sensor outputs zero ppm but non-zero temperature and non-zero humidity.
Eric Duminil's avatar
Eric Duminil committed
183
    if (co2 <= 0) {
184
      // No measurement yet. Waiting.
185
      led_effects::showWaitingLED(color::blue);
Eric Duminil's avatar
Eric Duminil committed
186
      return false;
187
188
189
    }

    /**
190
     * Fresh data. Log it and send it if needed.
191
192
     */
    if (freshData) {
Eric Duminil's avatar
Eric Duminil committed
193
194
195
      if (should_calibrate) {
        countStableMeasurements();
      }
196
      logToSerial();
197
198
199
200
201
202
    }

    if (should_calibrate) {
      if (stable_measurements == 60) {
        calibrateAndRestart();
      }
203
      led_effects::showWaitingLED(waiting_color);
Eric Duminil's avatar
Eric Duminil committed
204
      return false;
205
206
    }

207
    displayCO2OnLedRing();
Eric Duminil's avatar
Eric Duminil committed
208
    return freshData;
209
  }
210
}