co2_sensor.cpp 8.53 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
Eric Duminil's avatar
Eric Duminil committed
17
  const bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false]
18
19
20
21
}

namespace sensor {
  SCD30 scd30;
22
  uint16_t co2 = 0;
23
24
25
  float temperature = 0;
  float humidity = 0;
  String timestamp = "";
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
47
48
    /**
     *  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) {
49
        led_effects::showWaitingLED(color::red);
50
51
52
53
      }
    }

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

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

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

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

Eric Duminil's avatar
Eric Duminil committed
74
75
76
77
    sensor_console::defineIntCommand("co2", setCO2forDebugging, " 1500 (Sets co2 level, for debugging purposes)");
    sensor_console::defineIntCommand("timer", setTimer, " 30 (Sets measurement interval, in s)");
    sensor_console::defineCommand("calibrate", startCalibrationProcess, " (Starts calibration process)");
    sensor_console::defineIntCommand("calibrate", calibrateSensorToSpecificPPM,
78
        " 600 (Starts calibration process, to given ppm)");
Eric Duminil's avatar
Eric Duminil committed
79
    sensor_console::defineIntCommand("calibrate!", calibrateSensorRightNow,
Eric Duminil's avatar
Eric Duminil committed
80
        " 600 (Calibrates right now, to given ppm)");
Eric Duminil's avatar
Eric Duminil committed
81
    sensor_console::defineCommand("reset", []() {
Eric Duminil's avatar
Eric Duminil committed
82
83
      ESP.restart();
    }, " (Restarts the sensor)");
Eric Duminil's avatar
Eric Duminil committed
84
    sensor_console::defineCommand("night_mode", led_effects::toggleNightMode, " (Toggles night mode on/off)");
85
86
  }

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

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

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

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

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

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

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

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

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

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

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

202
    displayCO2OnLedRing();
Eric Duminil's avatar
Eric Duminil committed
203
    return freshData;
204
  }
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239

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

  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);
  }
240
}