co2_sensor.cpp 10.12 KiB
#include "co2_sensor.h"
#include "web_config.h"
#include "ntp.h"
#include "led_effects.h"
#include "sensor_console.h"
#include "src/lib/S8_UART/s8_uart.h"
namespace config {
  const uint16_t measurement_timestep_bootup = 4; // [s] Measurement timestep during acclimatization.
  const uint8_t max_deviation_during_bootup = 20; // [%]
  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.
  const uint16_t co2_alert_threshold = 2000; // [ppm] Display a flashing led ring, if concentration exceeds this value
  const bool debug_sensor_states = false; // If true, log state transitions over serial console
#if defined(ESP8266)
#  include "src/lib/EspSoftwareSerial/SoftwareSerial.h"
#  define S8_RX_PIN 13         // GPIO13, a.k.a. D7, connected to S8 Tx pin.
#  define S8_TX_PIN 15         // GPIO15, a.k.a. D8, connected to S8 Rx pin.
SoftwareSerial S8_serial(S8_RX_PIN, S8_TX_PIN);
#endif
#if defined(ESP32)
// GPIO16 connected to S8 Tx pin.
// GPIO17 connected to S8 Rx pin.
#  define S8_UART_PORT  2
HardwareSerial S8_serial(S8_UART_PORT);
#endif
namespace sensor {
  S8_UART *sensor_S8;
  S8_sensor s8;
  uint16_t co2 = 0;
  float temperature = 0;
  float humidity = 0;
  char timestamp[23];
  int16_t stable_measurements = 0;
  // I'm not sure it's possible to change S8 measurement interval (constant 4s). But we can check every check_timestep seconds.
  uint16_t check_timestep = 0;
  /**
   * Define sensor states
   * BOOTUP -> initial state, until first >0 ppm values are returned
   * READY -> sensor does output valid information (> 0 ppm) and no other condition takes place
   * NEEDS_CALIBRATION -> sensor measurements are too low (< 250 ppm)
   * 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
  enum state {
    BOOTUP,
    READY,
    NEEDS_CALIBRATION,
    PREPARE_CALIBRATION_UNSTABLE,
    PREPARE_CALIBRATION_STABLE
  const char *state_names[] = {
      "BOOTUP",
      "READY",
      "NEEDS_CALIBRATION",
      "PREPARE_CALIBRATION_UNSTABLE",
      "PREPARE_CALIBRATION_STABLE" };
  state current_state = BOOTUP;
  void switchState(state);
  void setCO2forDebugging(int32_t fakeCo2);
  void calibrateSensorToSpecificPPM(int32_t calibrationLevel);
  void calibrateSensorRightNow(int32_t calibrationLevel);
void setAutoCalibration(int32_t autoCalibration); void setTimer(int32_t timestep); void initialize() { Serial.println(F("Sensor : Senseair S8")); S8_serial.begin(S8_BAUDRATE); sensor_S8 = new S8_UART(S8_serial); Serial.println(); // Check if S8 is available sensor_S8->get_firmware_version(s8.firm_version); int len = strlen(s8.firm_version); if (len == 0) { Serial.println(F("ERROR - Senseair S8 CO2 sensor not detected. Please check wiring!")); led_effects::showKITTWheel(color::red, 30); ESP.restart(); } // Show basic S8 sensor info Serial.print(F("S8 - Firmware : ")); Serial.println(s8.firm_version); s8.sensor_id = sensor_S8->get_sensor_ID(); Serial.print(F("S8 - ID : 0x")); printIntToHex(s8.sensor_id, 4); Serial.println(); //TODO: Auto-calibration on/off? // S8 has its own timer (constant 4s) Serial.println(); Serial.print(F("Setting S8 timestep to ")); Serial.print(config::measurement_timestep_bootup); Serial.println(F(" s during acclimatization.")); check_timestep = config::measurement_timestep_bootup; sensor_console::defineIntCommand("co2", setCO2forDebugging, F("1500 (Sets co2 level, for debugging)")); 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)")); } 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. return (co2 > 0 && delta < ((uint32_t) co2 * config::max_deviation_during_bootup / 100)); } bool enoughStableMeasurements() { static int16_t previous_co2 = 0; if (co2 > (previous_co2 - config::max_deviation_during_calibration) && co2 < (previous_co2 + config::max_deviation_during_calibration)) { stable_measurements++; Serial.print(F("Number of stable measurements : ")); Serial.print(stable_measurements); Serial.print(F(" / ")); Serial.println(config::stable_measurements_before_calibration); switchState(PREPARE_CALIBRATION_STABLE); } else { stable_measurements = 0; switchState(PREPARE_CALIBRATION_UNSTABLE); } previous_co2 = co2;
return (stable_measurements == config::stable_measurements_before_calibration); } void startCalibrationProcess() { Serial.println(F("Implement ME!")); } void calibrate() { Serial.println(F("Implement ME!")); } void logToSerial() { Serial.print(timestamp); Serial.print(F(" - co2(ppm): ")); Serial.print(co2); Serial.println(F(" temp(C): ? humidity(%): ?")); } void switchState(state new_state) { if (new_state == current_state) { return; } if (config::debug_sensor_states) { Serial.print(F("Changing sensor state: ")); Serial.print(state_names[current_state]); Serial.print(F(" -> ")); Serial.println(state_names[new_state]); } current_state = new_state; } void switchStateForCurrentPPM() { if (current_state == BOOTUP) { if (!hasSensorSettled()) { return; } switchState(READY); Serial.println(F("Sensor acclimatization finished.")); Serial.print(F("Setting S8 timestep to ")); Serial.print(config::measurement_timestep); Serial.println(F(" s.")); check_timestep = config::measurement_timestep; // [s] } // Check for pre-calibration states first, because we do not want to // leave them before calibration is done. if ((current_state == PREPARE_CALIBRATION_UNSTABLE) || (current_state == PREPARE_CALIBRATION_STABLE)) { if (enoughStableMeasurements()) { calibrate(); } } else if (co2 < 250) { // Sensor should be calibrated. switchState(NEEDS_CALIBRATION); } else { switchState(READY); } } void displayCO2OnLedRing() { /** * Display data, even if it's "old" (with breathing). * A short delay is required in order to let background tasks run on the ESP8266. * see https://github.com/esp8266/Arduino/issues/3241#issuecomment-301290392 */ if (co2 < config::co2_alert_threshold) { led_effects::displayCO2color(co2); delay(100); } else { // Display a flashing led ring, if concentration exceeds a specific value led_effects::alert(color::red);
} } void showState() { switch (current_state) { case BOOTUP: led_effects::showWaitingLED(color::blue); break; case READY: displayCO2OnLedRing(); break; case NEEDS_CALIBRATION: led_effects::showWaitingLED(color::magenta); break; case PREPARE_CALIBRATION_UNSTABLE: led_effects::showWaitingLED(color::red); break; case PREPARE_CALIBRATION_STABLE: led_effects::showWaitingLED(color::green); break; default: Serial.println(F("Encountered unknown sensor state")); // This should not happen. } } /** Gets fresh data if available, checks calibration status, displays CO2 levels. * Returns true if fresh data is available, for further processing (e.g. MQTT, CSV or LoRa) */ bool processData() { static unsigned long last_measurement = 0; unsigned long now = seconds(); bool freshData = now - last_measurement > check_timestep; if (freshData) { last_measurement = now; ntp::getLocalTime(timestamp); co2 = sensor_S8->get_co2(); //TODO: Check if there's really no temperature info available. temperature = 0.0; humidity = 0.0; switchStateForCurrentPPM(); // Log every time fresh data is available. logToSerial(); } showState(); // 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); } float getTemperatureOffset() { return 0.0; } /***************************************************************** * Callbacks for sensor commands * *****************************************************************/ void setCO2forDebugging(int32_t fakeCo2) { Serial.print(F("DEBUG. Setting CO2 to ")); co2 = fakeCo2; Serial.println(co2); switchStateForCurrentPPM(); } void setAutoCalibration(int32_t autoCalibration) { Serial.println(F("TODO: Implement ME!")); }
void setTimer(int32_t timestep) { if (timestep >= 4) { Serial.print(F("Setting Measurement Interval to : ")); Serial.print(timestep); Serial.println(F("s.")); check_timestep = timestep; config::measurement_timestep = timestep; led_effects::showKITTWheel(color::green, 1); } } void calibrateSensorToSpecificPPM(int32_t calibrationLevel) { Serial.println(F("TODO: Implement ME!")); } void calibrateSensorRightNow(int32_t calibrationLevel) { Serial.println(F("TODO: Implement ME!")); } }