-
Eric Duminil authoredda3bcf5a
#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);
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
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;
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
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);
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
}
}
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!"));
}
}