co2_sensor.cpp 12.2 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
  uint16_t measurement_timestep = MEASUREMENT_TIMESTEP; // [s] Value between 2 and 1800 (range for SCD30 sensor)
6
  const uint16_t measurement_timestep_bootup = 2; // [s] Measurement timestep during acclimatization
Eric Duminil's avatar
Eric Duminil committed
7
8
  const uint16_t altitude_above_sea_level = ALTITUDE_ABOVE_SEA_LEVEL; // [m]
  uint16_t co2_calibration_level = ATMOSPHERIC_CO2_CONCENTRATION; // [ppm]
Eric Duminil's avatar
Eric Duminil committed
9
10
  int8_t max_deviation_during_calibration = 30; // [ppm]
  int8_t enough_stable_measurements = 60;
11
  const uint8_t max_deviation_during_bootup = 20; // [%]
12
13
14
#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
15
  const float temperature_offset = TEMPERATURE_OFFSET; // [K]
16
17
18
#else
  const float temperature_offset = -3.0;  // [K] Temperature measured by sensor is usually at least 3K too high.
#endif
19
  bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false]
20
  const bool debug_sensor_states = false; // If true, log state transitions over serial console
21
22
23
24
}

namespace sensor {
  SCD30 scd30;
25
  uint16_t co2 = 0;
26
27
  float temperature = 0;
  float humidity = 0;
28
  char timestamp[23];
29
  int16_t stable_measurements = 0;
Käppler's avatar
Käppler committed
30
31
32
33
34
35

  /**
   * 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
36
   * INVALID -> sensor does output invalid CO2 measurements (== 0 ppm)
Eric Duminil's avatar
Eric Duminil committed
37
38
   * NEEDS_CALIBRATION -> sensor measurements are too low (< 250 ppm)
   * PREPARE_CALIBRATION -> forced calibration was initiated, waiting for stable measurements
Käppler's avatar
Käppler committed
39
40
   * CALIBRATION -> the sensor does calibrate itself
   */
41
42
43
44
  enum state {
    INITIAL,
    BOOTUP,
    READY,
45
    INVALID,
46
    NEEDS_CALIBRATION,
47
    PREPARE_CALIBRATION_UNSTABLE,
48
    PREPARE_CALIBRATION_STABLE,
49
    CALIBRATION
Käppler's avatar
Käppler committed
50
  };
51
52
53
54
  const char *state_names[] = {
      "INITIAL",
      "BOOTUP",
      "READY",
55
      "INVALID",
56
      "NEEDS_CALIBRATION",
57
      "PREPARE_CALIBRATION_UNSTABLE",
58
      "PREPARE_CALIBRATION_STABLE",
59
      "CALIBRATION" };
Käppler's avatar
Käppler committed
60
61
62
  state current_state = INITIAL;
  void switchState(state);

63
64
  void initialize() {
#if defined(ESP8266)
65
    Wire.begin(12, 14); // ESP8266 - D6, D5;
66
67
#endif
#if defined(ESP32)
Eric Duminil's avatar
Eric Duminil committed
68
    Wire.begin(21, 22); // ESP32
69
70
71
72
73
74
75
76
    /**
     *  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
77
78
    Serial.println();
    scd30.enableDebugging(); // Prints firmware version in the console.
79

80
    if (!scd30.begin(config::auto_calibrate_sensor)) {
Eric Duminil's avatar
Eric Duminil committed
81
82
83
      Serial.println(F("ERROR - CO2 sensor not detected. Please check wiring!"));
      led_effects::showKITTWheel(color::red, 30);
      ESP.restart();
84
85
    }

Käppler's avatar
Käppler committed
86
87
    switchState(BOOTUP);

88
    Serial.print(F("Setting temperature offset to -"));
89
90
91
    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.
92
    delay(100);
93

94
    Serial.print(F("Temperature offset is : -"));
95
96
97
    Serial.print(scd30.getTemperatureOffset());
    Serial.println(" K");

98
    Serial.print(F("Auto-calibration is "));
99
    Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF.");
100

101
102
103
104
    // 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 "));
105
106
107
    Serial.print(config::measurement_timestep_bootup);
    Serial.println(" s during acclimatization.");
    scd30.setMeasurementInterval(config::measurement_timestep_bootup); // [s]
108

109
110
111
    sensor_console::defineIntCommand("co2", setCO2forDebugging, F(" 1500 (Sets co2 level, for debugging purposes)"));
    sensor_console::defineIntCommand("timer", setTimer, F(" 30 (Sets measurement interval, in s)"));
    sensor_console::defineCommand("calibrate", startCalibrationProcess, F(" (Starts calibration process)"));
Eric Duminil's avatar
Eric Duminil committed
112
    sensor_console::defineIntCommand("calibrate", calibrateSensorToSpecificPPM,
113
        F(" 600 (Starts calibration process, to given ppm)"));
Eric Duminil's avatar
Eric Duminil committed
114
    sensor_console::defineIntCommand("calibrate!", calibrateSensorRightNow,
115
116
117
        F(" 600 (Calibrates right now, to given ppm)"));
    sensor_console::defineIntCommand("auto_calibrate", setAutoCalibration,
        F(" 0/1 (Disables/enables autocalibration)"));
118
119
  }

Eric Duminil's avatar
Eric Duminil committed
120
  //NOTE: should timer deviation be used to adjust measurement_timestep?
Eric Duminil's avatar
Eric Duminil committed
121
  void checkTimerDeviation() {
122
    static int32_t previous_measurement_at = 0;
Eric Duminil's avatar
Eric Duminil committed
123
    int32_t now = millis();
124
    Serial.print(F("Measurement time offset : "));
Eric Duminil's avatar
Eric Duminil committed
125
126
127
128
129
    Serial.print(now - previous_measurement_at - config::measurement_timestep * 1000);
    Serial.println(" ms.");
    previous_measurement_at = now;
  }

130
131
132
133
134
135
136
137
138
139
  bool hasSensorSettled() {
    static uint16_t last_co2 = 0;
    uint16_t delta;
    delta = abs(co2 - last_co2);
    last_co2 = co2;
    // We assume the sensor has acclimated to the environment if measurements
    // change less than a specified percentage of the current value.
    return (co2 > 0 && delta < ((uint32_t)co2 * config::max_deviation_during_bootup / 100));
  }

140
141
  bool countStableMeasurements() {
    // Returns true, if a sufficient number of stable measurements has been observed.
Eric Duminil's avatar
Eric Duminil committed
142
    static int16_t previous_co2 = 0;
Eric Duminil's avatar
Eric Duminil committed
143
144
    if (co2 > (previous_co2 - config::max_deviation_during_calibration)
        && co2 < (previous_co2 + config::max_deviation_during_calibration)) {
145
      stable_measurements++;
Eric Duminil's avatar
Eric Duminil committed
146
      Serial.print(F("Number of stable measurements : "));
Eric Duminil's avatar
Eric Duminil committed
147
      Serial.println(stable_measurements);
148
      switchState(PREPARE_CALIBRATION_STABLE);
149
150
    } else {
      stable_measurements = 0;
151
      switchState(PREPARE_CALIBRATION_UNSTABLE);
152
    }
Eric Duminil's avatar
Eric Duminil committed
153
    previous_co2 = co2;
154
    return (stable_measurements == config::enough_stable_measurements);
155
156
157
  }

  void startCalibrationProcess() {
158
159
160
161
    /** 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.
     */
162
163
164
165
    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."));
166
    switchState(PREPARE_CALIBRATION_UNSTABLE);
167
168
  }

169
  void calibrateAndRestart() {
Käppler's avatar
Käppler committed
170
    switchState(CALIBRATION);
171
    Serial.print(F("Calibrating SCD30 now..."));
172
173
    scd30.setAltitudeCompensation(config::altitude_above_sea_level);
    scd30.setForcedRecalibrationFactor(config::co2_calibration_level);
174
175
    Serial.println(F(" Done!"));
    Serial.println(F("Sensor calibrated."));
Eric Duminil's avatar
Eric Duminil committed
176
    ESP.restart(); // softer than ESP.reset
177
  }
178

179
  void logToSerial() {
Eric Duminil's avatar
Eric Duminil committed
180
181
    Serial.print(timestamp);
    Serial.print(F(" - co2(ppm): "));
182
183
    Serial.print(co2);
    Serial.print(F(" temp(C): "));
184
    Serial.print(temperature, 1);
185
    Serial.print(F(" humidity(%): "));
186
    Serial.println(humidity, 1);
187
188
  }

Käppler's avatar
Käppler committed
189
  void switchState(state new_state) {
190
191
192
    if (new_state == current_state) {
      return;
    }
193
194
195
196
197
198
    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]);
    }
Käppler's avatar
Käppler committed
199
200
201
    current_state = new_state;
  }

202
  void switchStateForCurrentPPM() {
203
204
205
206
207
208
209
210
211
    if (current_state == BOOTUP) {
      if (!hasSensorSettled()) return;
      switchState(READY);
      Serial.println(F("Sensor acclimatization finished."));
      Serial.print(F("Setting SCD30 timestep to "));
      Serial.print(config::measurement_timestep);
      Serial.println(" s.");
      scd30.setMeasurementInterval(config::measurement_timestep); // [s]
    }
212
213
214
215
    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"));
216
      switchState(INVALID);
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
    } 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);
    }
  }

232
233
234
  void displayCO2OnLedRing() {
    /**
     * Display data, even if it's "old" (with breathing).
Eric Duminil's avatar
Eric Duminil committed
235
     * A short delay is required in order to let background tasks run on the ESP8266.
236
     * see https://github.com/esp8266/Arduino/issues/3241#issuecomment-301290392
237
238
     */
    if (co2 < 2000) {
239
      led_effects::displayCO2color(co2);
240
      delay(100);
241
242
    } else {
      // >= 2000: entire ring blinks red
243
      led_effects::redAlert();
244
245
246
    }
  }

247
  void showState() {
248
249
250
251
252
    switch (current_state) {
    case BOOTUP:
      led_effects::showWaitingLED(color::blue);
      break;
    case READY:
253
      displayCO2OnLedRing();
254
      break;
255
256
257
    case INVALID:
      led_effects::showWaitingLED(color::red);
      break;
258
    case NEEDS_CALIBRATION:
259
260
      led_effects::showWaitingLED(color::magenta);
      break;
261
    case PREPARE_CALIBRATION_UNSTABLE:
262
263
      led_effects::showWaitingLED(color::red);
      break;
264
    case PREPARE_CALIBRATION_STABLE:
265
266
      led_effects::showWaitingLED(color::green);
      break;
Eric Duminil's avatar
Eric Duminil committed
267
    case CALIBRATION: // Nothing to do, will restart soon.
268
269
      break;
    default:
Eric Duminil's avatar
Eric Duminil committed
270
      Serial.println(F("Encountered unknown sensor state")); // This should not happen.
271
272
273
    }
  }

Eric Duminil's avatar
Eric Duminil committed
274
  /** Gets fresh data if available, checks calibration status, displays CO2 levels.
275
   * Returns true if fresh data is available, for further processing (e.g. MQTT, CSV or LoRa)
Eric Duminil's avatar
Eric Duminil committed
276
277
278
279
280
281
   */
  bool processData() {
    bool freshData = scd30.dataAvailable();

    if (freshData) {
      // checkTimerDeviation();
282
      ntp::getLocalTime(timestamp);
Eric Duminil's avatar
Eric Duminil committed
283
284
285
      co2 = scd30.getCO2();
      temperature = scd30.getTemperature();
      humidity = scd30.getHumidity();
286

287
      switchStateForCurrentPPM();
288

289
290
      // Log every time fresh data is available.
      logToSerial();
291
292
    }

293
    showState();
294

Eric Duminil's avatar
Eric Duminil committed
295
    return freshData;
296
  }
297
298
299
300
301
302
303
304

  /*****************************************************************
   * Callbacks for sensor commands                                 *
   *****************************************************************/
  void setCO2forDebugging(int32_t fakeCo2) {
    Serial.print(F("DEBUG. Setting CO2 to "));
    co2 = fakeCo2;
    Serial.println(co2);
305
    switchStateForCurrentPPM();
306
307
  }

308
309
310
311
312
313
314
  void setAutoCalibration(int32_t autoCalibration) {
    config::auto_calibrate_sensor = autoCalibration;
    scd30.setAutoSelfCalibration(autoCalibration);
    Serial.print(F("Setting auto-calibration to : "));
    Serial.println(autoCalibration ? F("On.") : F("Off."));
  }

315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
  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);
    }
  }

  void calibrateSensorToSpecificPPM(int32_t calibrationLevel) {
    if (calibrationLevel >= 400 && calibrationLevel <= 2000) {
      Serial.print(F("Force calibration, at "));
      config::co2_calibration_level = calibrationLevel;
      Serial.print(config::co2_calibration_level);
      Serial.println(" ppm.");
      sensor::startCalibrationProcess();
    }
  }

  void calibrateSensorRightNow(int32_t calibrationLevel) {
    stable_measurements = config::enough_stable_measurements;
    calibrateSensorToSpecificPPM(calibrationLevel);
  }
340
}