2021-05-09 18:45:59 +00:00
|
|
|
#include "co2_sensor.h"
|
|
|
|
|
|
|
|
namespace config {
|
|
|
|
// 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]
|
|
|
|
#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
|
|
|
|
const bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false]
|
|
|
|
}
|
|
|
|
|
|
|
|
namespace sensor {
|
|
|
|
SCD30 scd30;
|
|
|
|
int16_t co2 = 0;
|
|
|
|
float temperature = 0;
|
|
|
|
float humidity = 0;
|
|
|
|
String timestamp = "";
|
|
|
|
int16_t stable_measurements = 0;
|
|
|
|
uint32_t waiting_color = color::blue;
|
|
|
|
bool should_calibrate = false;
|
2021-05-10 16:42:08 +00:00
|
|
|
unsigned long time_calaibration_started = millis();
|
2021-05-09 18:45:59 +00:00
|
|
|
|
|
|
|
void initialize() {
|
|
|
|
#if defined(ESP8266)
|
|
|
|
Wire.begin(12, 14); // ESP8266 - SDA: D6, SCL: 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
|
|
|
|
|
|
|
|
// CO2
|
|
|
|
if (scd30.begin(config::auto_calibrate_sensor) == false) {
|
|
|
|
Serial.println("Air sensor not detected. Please check wiring. Freezing...");
|
|
|
|
while (1) {
|
|
|
|
led_effects::showWaitingLED(color::red);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
Serial.println(" s.");
|
|
|
|
scd30.setMeasurementInterval(config::measurement_timestep); // [s]
|
|
|
|
|
|
|
|
Serial.print(F("Setting temperature offset to -"));
|
|
|
|
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.
|
|
|
|
delay(100);
|
|
|
|
|
|
|
|
Serial.print(F("Temperature offset is : -"));
|
|
|
|
Serial.print(scd30.getTemperatureOffset());
|
|
|
|
Serial.println(" K");
|
|
|
|
|
|
|
|
Serial.print(F("Auto-calibration is "));
|
|
|
|
Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF.");
|
|
|
|
}
|
|
|
|
|
|
|
|
//NOTE: should timer deviation be used to adjust measurement_timestep?
|
|
|
|
void checkTimerDeviation() {
|
|
|
|
static int32_t previous_measurement_at = 0;
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
void countStableMeasurements() {
|
|
|
|
static int16_t previous_co2 = 0;
|
|
|
|
if (co2 > (previous_co2 - 30) && co2 < (previous_co2 + 30)) {
|
|
|
|
stable_measurements++;
|
|
|
|
Serial.print(F("Number of stable measurements : "));
|
|
|
|
Serial.println(stable_measurements);
|
|
|
|
waiting_color = color::green;
|
|
|
|
} else {
|
|
|
|
stable_measurements = 0;
|
|
|
|
waiting_color = color::red;
|
|
|
|
}
|
|
|
|
previous_co2 = co2;
|
|
|
|
}
|
|
|
|
|
|
|
|
void startCalibrationProcess() {
|
|
|
|
/** 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.
|
|
|
|
*/
|
|
|
|
Serial.println(F("Setting SCD30 timestep to 2s, prior to calibration."));
|
2021-05-10 16:42:08 +00:00
|
|
|
scd30.setMeasurementInterval(MEASUREMENT_TIMESTEP); // [s] The change will only take effect after next measurement.
|
2021-05-09 18:45:59 +00:00
|
|
|
Serial.println(F("Waiting until the measurements are stable for at least 2 minutes."));
|
|
|
|
Serial.println(F("It could take a very long time."));
|
|
|
|
should_calibrate = true;
|
2021-05-10 16:42:08 +00:00
|
|
|
time_calaibration_started = millis();
|
2021-05-09 18:45:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void calibrateAndRestart() {
|
|
|
|
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."));
|
|
|
|
ESP.restart(); // softer than ESP.reset
|
|
|
|
}
|
|
|
|
|
|
|
|
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 displayCO2OnLedRing() {
|
2021-05-10 16:42:08 +00:00
|
|
|
int16_t co2_int = co2;
|
|
|
|
if (co2_int < CALIBRATE_LEVEL) {
|
2021-05-09 18:45:59 +00:00
|
|
|
// Sensor should be calibrated.
|
|
|
|
led_effects::showWaitingLED(color::magenta);
|
|
|
|
return;
|
|
|
|
}
|
2021-05-10 16:42:08 +00:00
|
|
|
if(co2_int < 400) {
|
|
|
|
co2_int = 400;
|
|
|
|
}
|
2021-05-09 18:45:59 +00:00
|
|
|
/**
|
|
|
|
* Display data, even if it's "old" (with breathing).
|
|
|
|
* Those effects include a short delay.
|
|
|
|
*/
|
2021-05-10 16:42:08 +00:00
|
|
|
if (co2_int < 2000) {
|
|
|
|
led_effects::displayCO2color(co2_int);
|
|
|
|
led_effects::breathe(co2_int);
|
2021-05-09 18:45:59 +00:00
|
|
|
} else {
|
|
|
|
// >= 2000: entire ring blinks red
|
|
|
|
led_effects::redAlert();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** 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) {
|
|
|
|
// checkTimerDeviation();
|
|
|
|
timestamp = ntp::getLocalTime();
|
|
|
|
co2 = scd30.getCO2();
|
|
|
|
temperature = scd30.getTemperature();
|
|
|
|
humidity = scd30.getHumidity();
|
|
|
|
}
|
|
|
|
|
|
|
|
//NOTE: Data is available, but it's sometimes erroneous: the sensor outputs zero ppm but non-zero temperature and non-zero humidity.
|
|
|
|
if (co2 <= 0) {
|
|
|
|
// No measurement yet. Waiting.
|
|
|
|
led_effects::showWaitingLED(color::blue);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fresh data. Log it and send it if needed.
|
|
|
|
*/
|
|
|
|
if (freshData) {
|
|
|
|
if (should_calibrate) {
|
2021-05-10 16:42:08 +00:00
|
|
|
if(millis() - time_calaibration_started > 60000)
|
|
|
|
{
|
|
|
|
countStableMeasurements();
|
|
|
|
}
|
2021-05-09 18:45:59 +00:00
|
|
|
}
|
|
|
|
logToSerial();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (should_calibrate) {
|
|
|
|
if (stable_measurements == 60) {
|
|
|
|
calibrateAndRestart();
|
|
|
|
}
|
|
|
|
led_effects::showWaitingLED(waiting_color);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
displayCO2OnLedRing();
|
|
|
|
return freshData;
|
|
|
|
}
|
|
|
|
}
|