co2_sensor.cpp 6.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]
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

28
29
30
31
32
33
34
  void printCO2(void *data) {
    int parameter = *(uint16_t*) data;
    Serial.print("NICE! HELLO FROM SENSOR.");
    Serial.print("CO2 : ");
    Serial.println(parameter);
  }

35
36
37
38
39
  void initialize() {
#if defined(ESP8266)
    Wire.begin(12, 14);  // ESP8266 - D6, D5;
#endif
#if defined(ESP32)
Eric Duminil's avatar
Eric Duminil committed
40
    Wire.begin(21, 22); // ESP32
41
42
43
44
45
46
47
48
49
50
51
52
53
    /**
     *  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) {
54
        led_effects::showWaitingLED(color::red);
55
56
57
58
      }
    }

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

66
    Serial.print(F("Setting temperature offset to -"));
67
68
69
    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.
70
    delay(100);
71

72
    Serial.print(F("Temperature offset is : -"));
73
74
75
    Serial.print(scd30.getTemperatureOffset());
    Serial.println(" K");

76
    Serial.print(F("Auto-calibration is "));
77
    Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF.");
78
79

    sensor_commands::defineCallback(printCO2, &co2);
80
81
  }

Eric Duminil's avatar
Eric Duminil committed
82
  //NOTE: should timer deviation be used to adjust measurement_timestep?
Eric Duminil's avatar
Eric Duminil committed
83
  void checkTimerDeviation() {
84
    static int32_t previous_measurement_at = 0;
Eric Duminil's avatar
Eric Duminil committed
85
86
87
88
89
90
91
    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;
  }

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

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

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

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

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

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

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

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

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

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

196
    displayCO2OnLedRing();
Eric Duminil's avatar
Eric Duminil committed
197
    return freshData;
198
  }
199
}