340 lines
13 KiB
C++
340 lines
13 KiB
C++
#include "co2_sensor.h"
|
|
|
|
namespace config {
|
|
// UPPERCASE values should be defined in config.h
|
|
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]
|
|
const uint16_t measurement_timestep_bootup = 5; // [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
|
|
|
|
#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.
|
|
const float temperature_offset = TEMPERATURE_OFFSET; // [K]
|
|
#else
|
|
const float temperature_offset = -3.0; // [K] Temperature measured by sensor is usually at least 3K too high.
|
|
#endif
|
|
bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false]
|
|
const bool debug_sensor_states = false; // If true, log state transitions over serial console
|
|
}
|
|
|
|
namespace sensor {
|
|
SCD30 scd30;
|
|
uint16_t co2 = 0;
|
|
float temperature = 0;
|
|
float humidity = 0;
|
|
char timestamp[23];
|
|
int16_t stable_measurements = 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 initialize() {
|
|
#if defined(ESP8266)
|
|
Wire.begin(12, 14); // ESP8266 - D6, D5;
|
|
#endif
|
|
#if defined(ESP32)
|
|
Wire.begin(21, 22); // ESP32
|
|
/**
|
|
* 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
|
|
Serial.println();
|
|
scd30.enableDebugging(); // Prints firmware version in the console.
|
|
|
|
if (!scd30.begin(config::auto_calibrate_sensor)) {
|
|
Serial.println(F("ERROR - CO2 sensor not detected. Please check wiring!"));
|
|
led_effects::showKITTWheel(color::red, 30);
|
|
ESP.restart();
|
|
}
|
|
|
|
// Changes of the SCD30's measurement timestep do not come into effect
|
|
// before the next measurement takes place. That means that after a hard reset
|
|
// of the ESP the SCD30 sometimes needs a long time until switching back to 2 s
|
|
// for acclimatization. Resetting it after startup seems to fix this behaviour.
|
|
scd30.reset();
|
|
|
|
Serial.print(F("Setting temperature offset to -"));
|
|
Serial.print(abs(config::temperature_offset));
|
|
Serial.println(F(" K."));
|
|
scd30.setTemperatureOffset(abs(config::temperature_offset)); // setTemperatureOffset only accepts positive numbers, but shifts the temperature down.
|
|
delay(100);
|
|
|
|
Serial.print(F("Temperature offset is : -"));
|
|
Serial.print(scd30.getTemperatureOffset());
|
|
Serial.println(F(" K"));
|
|
|
|
Serial.print(F("Auto-calibration is "));
|
|
Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF.");
|
|
|
|
// SCD30 has its own timer.
|
|
//NOTE: The timer seems to be inaccurate, though, possibly depending on voltage. Should it be offset?
|
|
Serial.println();
|
|
Serial.print(F("Setting SCD30 timestep to "));
|
|
Serial.print(config::measurement_timestep_bootup);
|
|
Serial.println(F(" s during acclimatization."));
|
|
scd30.setMeasurementInterval(config::measurement_timestep_bootup); // [s]
|
|
|
|
sensor_console::defineIntCommand("co2", setCO2forDebugging, F("1500 (Sets co2 level, for debugging purposes)"));
|
|
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)"));
|
|
}
|
|
|
|
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() {
|
|
/** From the sensor documentation:
|
|
* Before applying FRC, SCD30 needs to be operated for 2 minutes with the desired measurement period in continuous mode.
|
|
*/
|
|
Serial.print(F("Setting SCD30 timestep to "));
|
|
Serial.print(config::timestep_during_calibration);
|
|
Serial.println(F("s, prior to calibration."));
|
|
scd30.setMeasurementInterval(config::timestep_during_calibration); // [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."));
|
|
switchState(PREPARE_CALIBRATION_UNSTABLE);
|
|
}
|
|
|
|
void calibrate() {
|
|
Serial.print(F("Calibrating SCD30 now..."));
|
|
scd30.setAltitudeCompensation(config::altitude_above_sea_level);
|
|
scd30.setForcedRecalibrationFactor(config::co2_calibration_level);
|
|
Serial.println(F(" Done!"));
|
|
Serial.println(F("Sensor calibrated."));
|
|
switchState(BOOTUP); // In order to stop the calibration and select the desired timestep.
|
|
//WARNING: Do not reset the ampel or the SCD30!
|
|
//At least one measurement needs to happen in order for the calibration to be correctly applied.
|
|
}
|
|
|
|
void logToSerial() {
|
|
Serial.print(timestamp);
|
|
Serial.print(F(" - co2(ppm): "));
|
|
Serial.print(co2);
|
|
Serial.print(F(" temp(C): "));
|
|
Serial.print(temperature, 1);
|
|
Serial.print(F(" humidity(%): "));
|
|
Serial.println(humidity, 1);
|
|
}
|
|
|
|
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 SCD30 timestep to "));
|
|
Serial.print(config::measurement_timestep);
|
|
Serial.println(F(" s."));
|
|
if (config::measurement_timestep < 10) {
|
|
Serial.println(F("WARNING: Timesteps shorter than 10s can lead to unreliable measurements!"));
|
|
}
|
|
scd30.setMeasurementInterval(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::redAlert();
|
|
}
|
|
}
|
|
|
|
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() {
|
|
bool freshData = scd30.dataAvailable();
|
|
|
|
if (freshData) {
|
|
ntp::getLocalTime(timestamp);
|
|
co2 = scd30.getCO2();
|
|
temperature = scd30.getTemperature();
|
|
humidity = scd30.getHumidity();
|
|
|
|
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);
|
|
}
|
|
|
|
/*****************************************************************
|
|
* 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) {
|
|
config::auto_calibrate_sensor = autoCalibration;
|
|
scd30.setAutoSelfCalibration(autoCalibration);
|
|
Serial.print(F("Setting auto-calibration to : "));
|
|
Serial.println(autoCalibration ? F("On.") : F("Off."));
|
|
}
|
|
|
|
void setTimer(int32_t timestep) {
|
|
if (timestep >= 2 && timestep <= 1800) {
|
|
Serial.print(F("Setting Measurement Interval to : "));
|
|
Serial.print(timestep);
|
|
Serial.println(F("s (change will only be applied after next measurement)."));
|
|
scd30.setMeasurementInterval(timestep);
|
|
config::measurement_timestep = timestep;
|
|
led_effects::showKITTWheel(color::green, 1);
|
|
}
|
|
}
|
|
|
|
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(F(" ppm."));
|
|
startCalibrationProcess();
|
|
}
|
|
}
|
|
|
|
void calibrateSensorRightNow(int32_t calibrationLevel) {
|
|
if (calibrationLevel >= 400 && calibrationLevel <= 2000) {
|
|
Serial.print(F("Force calibration, right now, at "));
|
|
config::co2_calibration_level = calibrationLevel;
|
|
Serial.print(config::co2_calibration_level);
|
|
Serial.println(F(" ppm."));
|
|
calibrate();
|
|
}
|
|
}
|
|
|
|
void resetSCD() {
|
|
Serial.print(F("Resetting SCD30..."));
|
|
scd30.reset();
|
|
Serial.println(F("done."));
|
|
}
|
|
}
|