co2_sensor.cpp 6.12 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
20
21
22
23
}

namespace sensor {
  SCD30 scd30;
  int16_t co2 = 0;
  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

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

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

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

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

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

Eric Duminil's avatar
Eric Duminil committed
73
74
  //NOTE: should time offset be used to reset measurement_timestep?
  void checkTimerDeviation() {
75
    static int32_t previous_measurement_at = 0;
Eric Duminil's avatar
Eric Duminil committed
76
77
78
79
80
81
82
    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;
  }

83
  void countStableMeasurements() {
Eric Duminil's avatar
Eric Duminil committed
84
    static int16_t previous_co2 = 0;
85
86
87
88
89
90
91
    if (co2 > (previous_co2 - 30) && co2 < (previous_co2 + 30)) {
      stable_measurements++;
      waiting_color = color::green;
    } else {
      stable_measurements = 0;
      waiting_color = color::red;
    }
Eric Duminil's avatar
Eric Duminil committed
92
    previous_co2 = co2;
93
94
95
  }

  bool updateDataIfAvailable() {
Eric Duminil's avatar
Eric Duminil committed
96
    if (scd30.dataAvailable()) {
Eric Duminil's avatar
Eric Duminil committed
97
      // checkTimerDeviation();
Eric Duminil's avatar
Eric Duminil committed
98
99
100
101
102
103
104
105
      co2 = scd30.getCO2();
      temperature = scd30.getTemperature();
      humidity = scd30.getHumidity();
      return true;
    }
    return false;
  }

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
124
    Serial.println(F(" Done!"));
    Serial.println(F("Sensor calibrated."));
    Serial.println(F("Sensor will now restart."));
125
    LedEffects::showKITTWheel(color::green, 5);
Eric Duminil's avatar
TODOs    
Eric Duminil committed
126
    //TODO: Add LEDs off and move to util::reset()
127
128
129
    FS_LIB.end();
    ESP.restart();
  }
130
131

  void getAndSendData() {
Eric Duminil's avatar
Eric Duminil committed
132
    bool freshData = updateDataIfAvailable();
133
134

    //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
135
    if (co2 <= 0) {
136
137
138
139
140
141
142
143
144
145
      // No measurement yet. Waiting.
      LedEffects::showWaitingLED(color::blue);
      return;
    }

    /**
     * Fresh data. Show it and send it if needed.
     */
    if (freshData) {
      countStableMeasurements();
Eric Duminil's avatar
Eric Duminil committed
146
147
      timestamp = ntp::getLocalTime();
      Serial.println(timestamp);
148
149

      Serial.print(F("co2(ppm): "));
Eric Duminil's avatar
Eric Duminil committed
150
      Serial.print(co2);
151
      Serial.print(F(" temp(C): "));
Eric Duminil's avatar
Eric Duminil committed
152
      Serial.print(temperature);
153
      Serial.print(F(" humidity(%): "));
Eric Duminil's avatar
Eric Duminil committed
154
      Serial.println(humidity);
155

Eric Duminil's avatar
Eric Duminil committed
156
      csv_writer::logIfTimeHasCome(timestamp, co2, temperature, humidity);
157
158

#ifdef MQTT
Eric Duminil's avatar
Eric Duminil committed
159
      mqtt::publishIfTimeHasCome(timestamp, co2, temperature, humidity);
160
161
162
163
164
165
166
167
168
169
170
#endif
    }

    if (should_calibrate) {
      if (stable_measurements == 60) {
        calibrateAndRestart();
      }
      LedEffects::showWaitingLED(waiting_color);
      return;
    }

Eric Duminil's avatar
Eric Duminil committed
171
    if (co2 < 250) {
172
173
174
175
176
177
178
179
180
181
      // Sensor should be calibrated.
      LedEffects::showWaitingLED(color::magenta);
      return;
    }

    /**
     * Display data, even if it's "old" (with breathing).
     * Those effects include a short delay.
     */

Eric Duminil's avatar
Eric Duminil committed
182
183
184
    if (co2 < 2000) {
      LedEffects::displayCO2color(co2);
      LedEffects::breathe(co2);
185
186
187
188
    } else { // >= 2000: entire ring blinks red
      LedEffects::redAlert();
    }
  }
189
}