co2_sensor.cpp 8 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

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

Eric Duminil's avatar
Timer    
Eric Duminil committed
36
37
38
39
40
41
42
43
44
45
46
  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);
    }
  }

Eric Duminil's avatar
Eric Duminil committed
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
  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);
  }

62
63
64
65
66
  void initialize() {
#if defined(ESP8266)
    Wire.begin(12, 14);  // ESP8266 - D6, D5;
#endif
#if defined(ESP32)
Eric Duminil's avatar
Eric Duminil committed
67
    Wire.begin(21, 22); // ESP32
68
69
70
71
72
73
74
75
76
77
78
79
80
    /**
     *  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) {
81
        led_effects::showWaitingLED(color::red);
82
83
84
85
      }
    }

    // SCD30 has its own timer.
Eric Duminil's avatar
Eric Duminil committed
86
    //NOTE: The timer seems to be inaccurate, though, possibly depending on voltage. Should it be offset?
87
88
89
90
    Serial.println();
    Serial.print(F("Setting SCD30 timestep to "));
    Serial.print(config::measurement_timestep);
    Serial.println(" s.");
Eric Duminil's avatar
Eric Duminil committed
91
    scd30.setMeasurementInterval(config::measurement_timestep); // [s]
92

93
    Serial.print(F("Setting temperature offset to -"));
94
95
96
    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.
97
    delay(100);
98

99
    Serial.print(F("Temperature offset is : -"));
100
101
102
    Serial.print(scd30.getTemperatureOffset());
    Serial.println(" K");

103
    Serial.print(F("Auto-calibration is "));
104
    Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF.");
105

106
    sensor_commands::defineCallback("co2", setCO2forDebugging, " 1500 (Sets co2 level, for debugging purposes)");
Eric Duminil's avatar
Eric Duminil committed
107
    sensor_commands::defineCallback("timer", setTimer, " 30 (Sets measurement interval, in s)");
108
109
110
    sensor_commands::defineCallback("calibrate", calibrateSensorToSpecificPPM,
        " 600 (Starts calibration process, to given ppm)");
    sensor_commands::defineCallback("calibrate!", calibrateSensorRightNow, " 600 (Calibrates right now, to given ppm)");
111
112
  }

Eric Duminil's avatar
Eric Duminil committed
113
  //NOTE: should timer deviation be used to adjust measurement_timestep?
Eric Duminil's avatar
Eric Duminil committed
114
  void checkTimerDeviation() {
115
    static int32_t previous_measurement_at = 0;
Eric Duminil's avatar
Eric Duminil committed
116
    int32_t now = millis();
117
    Serial.print(F("Measurement time offset : "));
Eric Duminil's avatar
Eric Duminil committed
118
119
120
121
122
    Serial.print(now - previous_measurement_at - config::measurement_timestep * 1000);
    Serial.println(" ms.");
    previous_measurement_at = now;
  }

123
  void countStableMeasurements() {
Eric Duminil's avatar
Eric Duminil committed
124
    static int16_t previous_co2 = 0;
Eric Duminil's avatar
Eric Duminil committed
125
126
    if (co2 > (previous_co2 - config::max_deviation_during_calibration)
        && co2 < (previous_co2 + config::max_deviation_during_calibration)) {
127
      stable_measurements++;
Eric Duminil's avatar
Eric Duminil committed
128
      Serial.print(F("Number of stable measurements : "));
Eric Duminil's avatar
Eric Duminil committed
129
      Serial.println(stable_measurements);
130
131
132
133
134
      waiting_color = color::green;
    } else {
      stable_measurements = 0;
      waiting_color = color::red;
    }
Eric Duminil's avatar
Eric Duminil committed
135
    previous_co2 = co2;
136
137
138
  }

  void startCalibrationProcess() {
139
140
141
142
    /** 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.
     */
143
144
145
146
    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."));
147
    should_calibrate = true;
148
149
  }

150
151
  void calibrateAndRestart() {
    Serial.print(F("Calibrating SCD30 now..."));
152
153
    scd30.setAltitudeCompensation(config::altitude_above_sea_level);
    scd30.setForcedRecalibrationFactor(config::co2_calibration_level);
154
155
    Serial.println(F(" Done!"));
    Serial.println(F("Sensor calibrated."));
Eric Duminil's avatar
Eric Duminil committed
156
    ESP.restart(); // softer than ESP.reset
157
  }
158

159
  void logToSerial() {
Eric Duminil's avatar
Eric Duminil committed
160
161
    Serial.print(timestamp);
    Serial.print(F(" - co2(ppm): "));
162
163
    Serial.print(co2);
    Serial.print(F(" temp(C): "));
164
    Serial.print(temperature, 1);
165
    Serial.print(F(" humidity(%): "));
166
    Serial.println(humidity, 1);
167
168
169
170
171
  }

  void displayCO2OnLedRing() {
    if (co2 < 250) {
      // Sensor should be calibrated.
172
      led_effects::showWaitingLED(color::magenta);
173
174
175
176
      return;
    }
    /**
     * Display data, even if it's "old" (with breathing).
Eric Duminil's avatar
Eric Duminil committed
177
     * A short delay is required in order to let background tasks run on the ESP8266.
178
     * see https://github.com/esp8266/Arduino/issues/3241#issuecomment-301290392
179
180
     */
    if (co2 < 2000) {
181
      led_effects::displayCO2color(co2);
182
      delay(100);
183
184
    } else {
      // >= 2000: entire ring blinks red
185
      led_effects::redAlert();
186
187
188
    }
  }

Eric Duminil's avatar
Eric Duminil committed
189
  /** Gets fresh data if available, checks calibration status, displays CO2 levels.
190
   * Returns true if fresh data is available, for further processing (e.g. MQTT, CSV or LoRa)
Eric Duminil's avatar
Eric Duminil committed
191
192
193
194
195
196
197
198
199
200
201
   */
  bool processData() {
    bool freshData = scd30.dataAvailable();

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

    //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
204
    if (co2 <= 0) {
205
      // No measurement yet. Waiting.
206
      led_effects::showWaitingLED(color::blue);
Eric Duminil's avatar
Eric Duminil committed
207
      return false;
208
209
210
    }

    /**
211
     * Fresh data. Log it and send it if needed.
212
213
     */
    if (freshData) {
Eric Duminil's avatar
Eric Duminil committed
214
215
216
      if (should_calibrate) {
        countStableMeasurements();
      }
217
      logToSerial();
218
219
220
    }

    if (should_calibrate) {
Eric Duminil's avatar
Eric Duminil committed
221
      if (stable_measurements == config::enough_stable_measurements) {
222
223
        calibrateAndRestart();
      }
224
      led_effects::showWaitingLED(waiting_color);
Eric Duminil's avatar
Eric Duminil committed
225
      return false;
226
227
    }

228
    displayCO2OnLedRing();
Eric Duminil's avatar
Eric Duminil committed
229
    return freshData;
230
  }
231
}