co2_sensor.cpp 9.89 KB
Newer Older
1
2
#include "co2_sensor.h"

Eric Duminil's avatar
Eric Duminil committed
3
#include "web_config.h"
Eric Duminil's avatar
Eric Duminil committed
4
#include "ntp.h"
5
6
7
#include "led_effects.h"
#include "sensor_console.h"

Eric Duminil's avatar
Eric Duminil committed
8
#include "src/lib/S8_UART/s8_uart.h"
Eric Duminil's avatar
Eric Duminil committed
9

10
namespace config {
11
  const uint16_t measurement_timestep_bootup = 5; // [s] Measurement timestep during acclimatization.
12
  const uint8_t max_deviation_during_bootup = 20; // [%]
13
14
15
  const int8_t max_deviation_during_calibration = 30; // [ppm]
  const int16_t timestep_during_calibration = 10; // [s] WARNING: Measurements can be unreliable for timesteps shorter than 10s.
  const int8_t stable_measurements_before_calibration = 120 / timestep_during_calibration; // [-] Stable measurements during at least 2 minutes.
16
  const uint16_t co2_alert_threshold = 2000; // [ppm] Display a flashing led ring, if concentration exceeds this value
17
  const bool debug_sensor_states = false; // If true, log state transitions over serial console
18
19
}

Eric Duminil's avatar
Eric Duminil committed
20
21
22
23
24
25
26
#if defined(ESP8266)
    // ???
#endif
#if defined(ESP32)
// For ESP32 : RX on GPIO17, TX on GPIO16
#   define S8_UART_PORT  2

27
namespace sensor {
Eric Duminil's avatar
Eric Duminil committed
28
  HardwareSerial S8_serial(S8_UART_PORT);
Eric Duminil's avatar
Eric Duminil committed
29
  S8_UART *sensor_S8;
Eric Duminil's avatar
Eric Duminil committed
30
  S8_sensor s8;
31
  uint16_t co2 = 0;
32
33
  float temperature = 0;
  float humidity = 0;
34
  char timestamp[23];
35
  int16_t stable_measurements = 0;
Käppler's avatar
Käppler committed
36
37
38

  /**
   * Define sensor states
39
   * BOOTUP -> initial state, until first >0 ppm values are returned
Käppler's avatar
Käppler committed
40
   * READY -> sensor does output valid information (> 0 ppm) and no other condition takes place
Eric Duminil's avatar
Eric Duminil committed
41
   * NEEDS_CALIBRATION -> sensor measurements are too low (< 250 ppm)
42
43
   * PREPARE_CALIBRATION_UNSTABLE -> forced calibration was initiated, last measurements were too far apart
   * PREPARE_CALIBRATION_STABLE -> forced calibration was initiated, last measurements were close to each others
Käppler's avatar
Käppler committed
44
   */
45
46
47
  enum state {
    BOOTUP,
    READY,
48
    NEEDS_CALIBRATION,
49
    PREPARE_CALIBRATION_UNSTABLE,
50
    PREPARE_CALIBRATION_STABLE
Käppler's avatar
Käppler committed
51
  };
52
53
54
  const char *state_names[] = {
      "BOOTUP",
      "READY",
55
      "NEEDS_CALIBRATION",
56
      "PREPARE_CALIBRATION_UNSTABLE",
57
      "PREPARE_CALIBRATION_STABLE" };
58

Eric Duminil's avatar
Eric Duminil committed
59
  state current_state = READY;
Käppler's avatar
Käppler committed
60
  void switchState(state);
Eric Duminil's avatar
Eric Duminil committed
61
62
63
64
  void setCO2forDebugging(int32_t fakeCo2);
  void calibrateSensorToSpecificPPM(int32_t calibrationLevel);
  void calibrateSensorRightNow(int32_t calibrationLevel);
  void setAutoCalibration(int32_t autoCalibration);
Käppler's avatar
Käppler committed
65

66
  void initialize() {
Eric Duminil's avatar
Eric Duminil committed
67
68
    S8_serial.begin(S8_BAUDRATE);
    sensor_S8 = new S8_UART(S8_serial);
69
#endif
70
    Serial.println();
71

Eric Duminil's avatar
Eric Duminil committed
72
    // Check if S8 is available
Eric Duminil's avatar
Eric Duminil committed
73
74
    sensor_S8->get_firmware_version(s8.firm_version);
    int len = strlen(s8.firm_version);
Eric Duminil's avatar
Eric Duminil committed
75
76
    if (len == 0) {
      Serial.println(F("ERROR - Senseair S8 CO2 sensor not detected. Please check wiring!"));
Eric Duminil's avatar
Eric Duminil committed
77
78
      led_effects::showKITTWheel(color::red, 30);
      ESP.restart();
79
80
    }

Eric Duminil's avatar
Eric Duminil committed
81
82
    // Show basic S8 sensor info
    Serial.println(">>> SenseAir S8 NDIR CO2 sensor <<<");
Eric Duminil's avatar
Eric Duminil committed
83
84
    printf("Firmware version: %s\n", s8.firm_version);
    s8.sensor_id = sensor_S8->get_sensor_ID();
Eric Duminil's avatar
Eric Duminil committed
85
    Serial.print("Sensor ID: 0x");
Eric Duminil's avatar
Eric Duminil committed
86
    printIntToHex(s8.sensor_id, 4);
Eric Duminil's avatar
Eric Duminil committed
87
88
89
90
    Serial.println("");

    Serial.println("Setup done!");
    Serial.flush();
91

92
    Serial.print(F("Auto-calibration is "));
93
    Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF.");
94

Eric Duminil's avatar
Eric Duminil committed
95
    sensor_console::defineIntCommand("co2", setCO2forDebugging, F("1500 (Sets co2 level, for debugging)"));
Eric Duminil's avatar
Eric Duminil committed
96
97
98
99
100
101
102
103
//    sensor_console::defineIntCommand("timer", setTimer, F("30 (Sets measurement interval, in s)"));
//    sensor_console::defineCommand("calibrate", startCalibrationProcess, F("(Starts calibration process)"));
//    sensor_console::defineIntCommand("calibrate", calibrateSensorToSpecificPPM,
//        F("600 (Starts calibration process, to given ppm)"));
//    sensor_console::defineIntCommand("calibrate!", calibrateSensorRightNow,
//        F("600 (Calibrates right now, to given ppm)"));
//    sensor_console::defineIntCommand("auto_calibrate", setAutoCalibration, F("0/1 (Disables/enables autocalibration)"));
//    sensor_console::defineCommand("reset_scd", resetSCD, F("(Resets SCD30)"));
104
105
  }

106
107
108
109
110
111
112
  bool hasSensorSettled() {
    static uint16_t last_co2 = 0;
    uint16_t delta;
    delta = abs(co2 - last_co2);
    last_co2 = co2;
    // We assume the sensor has acclimated to the environment if measurements
    // change less than a specified percentage of the current value.
Eric Duminil's avatar
Eric Duminil committed
113
    return (co2 > 0 && delta < ((uint32_t) co2 * config::max_deviation_during_bootup / 100));
114
115
  }

Eric Duminil's avatar
Eric Duminil committed
116
  bool enoughStableMeasurements() {
Eric Duminil's avatar
Eric Duminil committed
117
    static int16_t previous_co2 = 0;
Eric Duminil's avatar
Eric Duminil committed
118
119
    if (co2 > (previous_co2 - config::max_deviation_during_calibration)
        && co2 < (previous_co2 + config::max_deviation_during_calibration)) {
120
      stable_measurements++;
Eric Duminil's avatar
Eric Duminil committed
121
      Serial.print(F("Number of stable measurements : "));
122
123
124
      Serial.print(stable_measurements);
      Serial.print(F(" / "));
      Serial.println(config::stable_measurements_before_calibration);
125
      switchState(PREPARE_CALIBRATION_STABLE);
126
127
    } else {
      stable_measurements = 0;
128
      switchState(PREPARE_CALIBRATION_UNSTABLE);
129
    }
Eric Duminil's avatar
Eric Duminil committed
130
    previous_co2 = co2;
131
    return (stable_measurements == config::stable_measurements_before_calibration);
132
133
134
  }

  void startCalibrationProcess() {
Eric Duminil's avatar
Eric Duminil committed
135
    Serial.println(F("Implement ME!"));
136
137
  }

138
  void calibrate() {
Eric Duminil's avatar
Eric Duminil committed
139
    Serial.println(F("Implement ME!"));
140
  }
141

142
  void logToSerial() {
Eric Duminil's avatar
Eric Duminil committed
143
144
    Serial.print(timestamp);
    Serial.print(F(" - co2(ppm): "));
145
146
    Serial.print(co2);
    Serial.print(F(" temp(C): "));
147
    Serial.print(temperature, 1);
148
    Serial.print(F(" humidity(%): "));
149
    Serial.println(humidity, 1);
150
151
  }

Käppler's avatar
Käppler committed
152
  void switchState(state new_state) {
153
154
155
    if (new_state == current_state) {
      return;
    }
156
157
158
    if (config::debug_sensor_states) {
      Serial.print(F("Changing sensor state: "));
      Serial.print(state_names[current_state]);
Eric Duminil's avatar
Eric Duminil committed
159
      Serial.print(F(" -> "));
160
161
      Serial.println(state_names[new_state]);
    }
Käppler's avatar
Käppler committed
162
163
164
    current_state = new_state;
  }

165
  void switchStateForCurrentPPM() {
166
    if (current_state == BOOTUP) {
Eric Duminil's avatar
Eric Duminil committed
167
168
169
      if (!hasSensorSettled()) {
        return;
      }
170
171
172
      switchState(READY);
      Serial.println(F("Sensor acclimatization finished."));
    }
Eric Duminil's avatar
Eric Duminil committed
173
174
175

    // Check for pre-calibration states first, because we do not want to
    // leave them before calibration is done.
Käppler's avatar
Käppler committed
176
    if ((current_state == PREPARE_CALIBRATION_UNSTABLE) || (current_state == PREPARE_CALIBRATION_STABLE)) {
Eric Duminil's avatar
Eric Duminil committed
177
      if (enoughStableMeasurements()) {
178
        calibrate();
179
180
181
182
183
184
185
186
187
      }
    } else if (co2 < 250) {
      // Sensor should be calibrated.
      switchState(NEEDS_CALIBRATION);
    } else {
      switchState(READY);
    }
  }

188
189
190
  void displayCO2OnLedRing() {
    /**
     * Display data, even if it's "old" (with breathing).
Eric Duminil's avatar
Eric Duminil committed
191
     * A short delay is required in order to let background tasks run on the ESP8266.
192
     * see https://github.com/esp8266/Arduino/issues/3241#issuecomment-301290392
193
     */
194
    if (co2 < config::co2_alert_threshold) {
195
      led_effects::displayCO2color(co2);
196
      delay(100);
197
    } else {
198
      // Display a flashing led ring, if concentration exceeds a specific value
199
      led_effects::alert(color::red);
200
201
202
    }
  }

203
  void showState() {
204
205
206
207
208
    switch (current_state) {
    case BOOTUP:
      led_effects::showWaitingLED(color::blue);
      break;
    case READY:
209
      displayCO2OnLedRing();
210
      break;
211
    case NEEDS_CALIBRATION:
212
213
      led_effects::showWaitingLED(color::magenta);
      break;
214
    case PREPARE_CALIBRATION_UNSTABLE:
215
216
      led_effects::showWaitingLED(color::red);
      break;
217
    case PREPARE_CALIBRATION_STABLE:
218
219
220
      led_effects::showWaitingLED(color::green);
      break;
    default:
Eric Duminil's avatar
Eric Duminil committed
221
      Serial.println(F("Encountered unknown sensor state")); // This should not happen.
222
223
224
    }
  }

Eric Duminil's avatar
Eric Duminil committed
225
  /** Gets fresh data if available, checks calibration status, displays CO2 levels.
226
   * Returns true if fresh data is available, for further processing (e.g. MQTT, CSV or LoRa)
Eric Duminil's avatar
Eric Duminil committed
227
228
   */
  bool processData() {
Eric Duminil's avatar
Eric Duminil committed
229
230
    static unsigned long last_measurement = 0;
    unsigned long now = seconds();
Eric Duminil's avatar
Eric Duminil committed
231
232
//    bool freshData = now - last_measurement > config::measurement_timestep;
    bool freshData = now - last_measurement > 4;
Eric Duminil's avatar
Eric Duminil committed
233
    if (freshData) {
Eric Duminil's avatar
Eric Duminil committed
234
      Serial.println("FRESH!");
Eric Duminil's avatar
Eric Duminil committed
235
      last_measurement = now;
236
      ntp::getLocalTime(timestamp);
Eric Duminil's avatar
Eric Duminil committed
237
238
239
240
      s8.co2 = sensor_S8->get_co2();
      Serial.print(s8.co2);
      Serial.println(" ppm.");
      co2 = s8.co2;
Eric Duminil's avatar
Eric Duminil committed
241
242
      temperature = 0.0;
      humidity = 0.0;
243

244
      switchStateForCurrentPPM();
245

246
247
      // Log every time fresh data is available.
      logToSerial();
248
249
    }

250
    showState();
251

252
253
254
    // Report data for further processing only if the data is reliable
    // (state 'READY') or manual calibration is necessary (state 'NEEDS_CALIBRATION').
    return freshData && (current_state == READY || current_state == NEEDS_CALIBRATION);
255
  }
256

Eric Duminil's avatar
Eric Duminil committed
257
  float getTemperatureOffset() {
Eric Duminil's avatar
Eric Duminil committed
258
    return 0.0;
Eric Duminil's avatar
Eric Duminil committed
259
260
  }

261
262
263
264
265
266
267
  /*****************************************************************
   * Callbacks for sensor commands                                 *
   *****************************************************************/
  void setCO2forDebugging(int32_t fakeCo2) {
    Serial.print(F("DEBUG. Setting CO2 to "));
    co2 = fakeCo2;
    Serial.println(co2);
268
    switchStateForCurrentPPM();
269
270
  }

271
  void setAutoCalibration(int32_t autoCalibration) {
Eric Duminil's avatar
Eric Duminil committed
272
    Serial.println(F("Implement ME!"));
273
274
  }

275
  void setTimer(int32_t timestep) {
Eric Duminil's avatar
Eric Duminil committed
276
    Serial.println(F("Implement ME!"));
277
278
279
280
281
  }

  void calibrateSensorToSpecificPPM(int32_t calibrationLevel) {
    if (calibrationLevel >= 400 && calibrationLevel <= 2000) {
      Serial.print(F("Force calibration, at "));
282
283
      config::co2_calibration_level = calibrationLevel;
      Serial.print(config::co2_calibration_level);
Eric Duminil's avatar
Eric Duminil committed
284
      Serial.println(F(" ppm."));
Eric Duminil's avatar
Eric Duminil committed
285
      startCalibrationProcess();
286
287
288
289
    }
  }

  void calibrateSensorRightNow(int32_t calibrationLevel) {
Eric Duminil's avatar
Eric Duminil committed
290
291
    if (calibrationLevel >= 400 && calibrationLevel <= 2000) {
      Serial.print(F("Force calibration, right now, at "));
292
293
      config::co2_calibration_level = calibrationLevel;
      Serial.print(config::co2_calibration_level);
Eric Duminil's avatar
Eric Duminil committed
294
      Serial.println(F(" ppm."));
295
      calibrate();
Eric Duminil's avatar
Eric Duminil committed
296
    }
297
  }
298
299

  void resetSCD() {
Eric Duminil's avatar
Eric Duminil committed
300
    Serial.println(F("Implement ME!"));
301
  }
302
}