co2_sensor.cpp 8.71 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]
Eric Duminil's avatar
Eric Duminil committed
8
9
  int8_t max_deviation_during_calibration = 30; // [ppm]
  int8_t enough_stable_measurements = 60;
10
11
12
#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
13
  const float temperature_offset = TEMPERATURE_OFFSET; // [K]
14
15
16
#else
  const float temperature_offset = -3.0;  // [K] Temperature measured by sensor is usually at least 3K too high.
#endif
17
  bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false]
18
19
20
21
}

namespace sensor {
  SCD30 scd30;
22
  uint16_t co2 = 0;
23
24
  float temperature = 0;
  float humidity = 0;
25
  char timestamp[23];
26
27
28
  int16_t stable_measurements = 0;
  uint32_t waiting_color = color::blue;
  bool should_calibrate = false;
29
30
31
32
33
34

  void initialize() {
#if defined(ESP8266)
    Wire.begin(12, 14);  // ESP8266 - D6, D5;
#endif
#if defined(ESP32)
Eric Duminil's avatar
Eric Duminil committed
35
    Wire.begin(21, 22); // ESP32
36
37
38
39
40
41
42
43
44
45
46
    /**
     *  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) {
Eric Duminil's avatar
Eric Duminil committed
47
48
49
      Serial.println(F("ERROR - CO2 sensor not detected. Please check wiring!"));
      led_effects::showKITTWheel(color::red, 30);
      ESP.restart();
50
51
52
    }

    // SCD30 has its own timer.
Eric Duminil's avatar
Eric Duminil committed
53
    //NOTE: The timer seems to be inaccurate, though, possibly depending on voltage. Should it be offset?
54
55
56
57
    Serial.println();
    Serial.print(F("Setting SCD30 timestep to "));
    Serial.print(config::measurement_timestep);
    Serial.println(" s.");
Eric Duminil's avatar
Eric Duminil committed
58
    scd30.setMeasurementInterval(config::measurement_timestep); // [s]
59

60
    Serial.print(F("Setting temperature offset to -"));
61
62
63
    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.
64
    delay(100);
65

66
    Serial.print(F("Temperature offset is : -"));
67
68
69
    Serial.print(scd30.getTemperatureOffset());
    Serial.println(" K");

70
    Serial.print(F("Auto-calibration is "));
71
    Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF.");
72

73
74
75
    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
76
    sensor_console::defineIntCommand("calibrate", calibrateSensorToSpecificPPM,
77
        F(" 600 (Starts calibration process, to given ppm)"));
Eric Duminil's avatar
Eric Duminil committed
78
    sensor_console::defineIntCommand("calibrate!", calibrateSensorRightNow,
79
80
81
        F(" 600 (Calibrates right now, to given ppm)"));
    sensor_console::defineIntCommand("auto_calibrate", setAutoCalibration,
        F(" 0/1 (Disables/enables autocalibration)"));
82
83
  }

Eric Duminil's avatar
Eric Duminil committed
84
  //NOTE: should timer deviation be used to adjust measurement_timestep?
Eric Duminil's avatar
Eric Duminil committed
85
  void checkTimerDeviation() {
86
    static int32_t previous_measurement_at = 0;
Eric Duminil's avatar
Eric Duminil committed
87
    int32_t now = millis();
88
    Serial.print(F("Measurement time offset : "));
Eric Duminil's avatar
Eric Duminil committed
89
90
91
92
93
    Serial.print(now - previous_measurement_at - config::measurement_timestep * 1000);
    Serial.println(" ms.");
    previous_measurement_at = now;
  }

94
  void countStableMeasurements() {
Eric Duminil's avatar
Eric Duminil committed
95
    static int16_t previous_co2 = 0;
Eric Duminil's avatar
Eric Duminil committed
96
97
    if (co2 > (previous_co2 - config::max_deviation_during_calibration)
        && co2 < (previous_co2 + config::max_deviation_during_calibration)) {
98
      stable_measurements++;
Eric Duminil's avatar
Eric Duminil committed
99
      Serial.print(F("Number of stable measurements : "));
Eric Duminil's avatar
Eric Duminil committed
100
      Serial.println(stable_measurements);
101
102
103
104
105
      waiting_color = color::green;
    } else {
      stable_measurements = 0;
      waiting_color = color::red;
    }
Eric Duminil's avatar
Eric Duminil committed
106
    previous_co2 = co2;
107
108
109
  }

  void startCalibrationProcess() {
110
111
112
113
    /** 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.
     */
114
115
116
117
    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."));
118
    should_calibrate = true;
119
120
  }

121
122
  void calibrateAndRestart() {
    Serial.print(F("Calibrating SCD30 now..."));
123
124
    scd30.setAltitudeCompensation(config::altitude_above_sea_level);
    scd30.setForcedRecalibrationFactor(config::co2_calibration_level);
125
126
    Serial.println(F(" Done!"));
    Serial.println(F("Sensor calibrated."));
Eric Duminil's avatar
Eric Duminil committed
127
    ESP.restart(); // softer than ESP.reset
128
  }
129

130
  void logToSerial() {
Eric Duminil's avatar
Eric Duminil committed
131
132
    Serial.print(timestamp);
    Serial.print(F(" - co2(ppm): "));
133
134
    Serial.print(co2);
    Serial.print(F(" temp(C): "));
135
    Serial.print(temperature, 1);
136
    Serial.print(F(" humidity(%): "));
137
    Serial.println(humidity, 1);
138
139
140
141
142
  }

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

Eric Duminil's avatar
Eric Duminil committed
160
  /** Gets fresh data if available, checks calibration status, displays CO2 levels.
161
   * Returns true if fresh data is available, for further processing (e.g. MQTT, CSV or LoRa)
Eric Duminil's avatar
Eric Duminil committed
162
163
164
165
166
167
   */
  bool processData() {
    bool freshData = scd30.dataAvailable();

    if (freshData) {
      // checkTimerDeviation();
168
      ntp::getLocalTime(timestamp);
Eric Duminil's avatar
Eric Duminil committed
169
170
171
172
      co2 = scd30.getCO2();
      temperature = scd30.getTemperature();
      humidity = scd30.getHumidity();
    }
173
174

    //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
175
    if (co2 <= 0) {
176
      // No measurement yet. Waiting.
177
      led_effects::showWaitingLED(color::blue);
Eric Duminil's avatar
Eric Duminil committed
178
      return false;
179
180
181
    }

    /**
182
     * Fresh data. Log it and send it if needed.
183
184
     */
    if (freshData) {
Eric Duminil's avatar
Eric Duminil committed
185
186
187
      if (should_calibrate) {
        countStableMeasurements();
      }
188
      logToSerial();
189
190
191
    }

    if (should_calibrate) {
Eric Duminil's avatar
Eric Duminil committed
192
      if (stable_measurements == config::enough_stable_measurements) {
193
194
        calibrateAndRestart();
      }
195
      led_effects::showWaitingLED(waiting_color);
Eric Duminil's avatar
Eric Duminil committed
196
      return false;
197
198
    }

199
    displayCO2OnLedRing();
Eric Duminil's avatar
Eric Duminil committed
200
    return freshData;
201
  }
202
203
204
205
206
207
208
209
210
211

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

212
213
214
215
216
217
218
  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."));
  }

219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
  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);
  }
244
}