-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathCO2_Gadget_Sensors.h
More file actions
416 lines (375 loc) · 17.9 KB
/
Copy pathCO2_Gadget_Sensors.h
File metadata and controls
416 lines (375 loc) · 17.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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
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
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
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
#ifndef CO2_Gadget_Sensors_h
#define CO2_Gadget_Sensors_h
// LOW_POWER mode constants (shared with CO2_Gadget_DeepSleep.h)
#define LOW_POWER 1 // Binary: 0 = HIGH_PERFORMANCE, 1 = LOW_POWER
#include <Sensors.hpp>
#include "CO2_Gadget_Thresholds.h"
volatile bool pendingCalibration = false;
volatile bool newReadingsAvailable = false;
uint16_t calibrationValue = 415;
uint16_t customCalibrationValue = 415;
bool pendingAmbientPressure = false;
uint16_t ambientPressureValue = 0;
uint16_t altitudeMeters = 600;
bool autoSelfCalibration = false;
float tempOffset = 0.0f;
volatile uint16_t co2 = 0;
volatile uint16_t previousCO2Value = 0;
float temp, tempFahrenheit, hum = 0;
uint16_t pm1 = 0; // PM1.0 (µg/m³) — from particulate sensors e.g. SPS30
uint16_t pm25 = 0; // PM2.5 (µg/m³)
uint16_t pm4 = 0; // PM4.0 (µg/m³) — SPS30 specific
uint16_t pm10 = 0; // PM10 (µg/m³)
float pressureHpa = 0; // [BTHOME-SENSEL] barometric pressure (hPa) when a PRESS-capable sensor is present
String mainDeviceSelected = "";
ThresholdManager thresholdsManager;
String sensorsGetMainDeviceSelected() {
return mainDeviceSelected;
}
void printSensorsDetected() {
uint16_t sensors_count = sensors.getSensorsRegisteredCount();
uint16_t units_count = sensors.getUnitsRegisteredCount();
Serial.println("-->[SENS] Sensors detected count\t: " + String(sensors_count));
Serial.println("-->[SENS] Sensors units count \t: " + String(units_count));
Serial.print("-->[SENS] Sensors devices names\t: ");
int i = 0;
while (sensors.getSensorsRegistered()[i++] != 0) {
Serial.print(sensors.getSensorName((SENSORS)sensors.getSensorsRegistered()[i - 1]));
mainDeviceSelected = sensors.getSensorName((SENSORS)sensors.getSensorsRegistered()[i - 1]);
Serial.print(",");
}
Serial.println();
if (mainDeviceSelected == "SCD4X") {
Serial.print("-->[SENS] SCD4X model detected\t: ");
sensors.scd4x.stopPeriodicMeasurement();
mainDeviceSelected = sensors.getSCD4xModel();
sensors.scd4x.startPeriodicMeasurement();
Serial.println(mainDeviceSelected);
}
}
void onSensorDataOk() {
previousCO2Value = co2;
co2 = sensors.getCO2();
hum = sensors.getHumidity();
if (hum == 0.0) hum = sensors.getCO2humi();
temp = sensors.getTemperature();
if (temp == 0.0) temp = sensors.getCO2temp(); // TO-DO: temp could be 0.0
tempFahrenheit = (temp * 1.8 + 32);
deepSleepData.lastCO2Value = co2;
deepSleepData.lastTemperatureValue = temp;
deepSleepData.lastHumidityValue = hum;
if (!inMenu) {
Serial.printf("-->[SENS] CO2: %d CO2temp: %.2f CO2humi: %.2f H: %.2f T: %.2f\n", co2, sensors.getCO2temp(), sensors.getCO2humi(), sensors.getHumidity(), sensors.getTemperature());
}
if (sensors.isUnitRegistered(UNIT::PM25)) {
pm1 = sensors.getPM1();
pm25 = sensors.getPM25();
pm4 = sensors.getPM4();
pm10 = sensors.getPM10();
if (!inMenu) Serial.printf("-->[SENS] PM1: %d PM2.5: %d PM4: %d PM10: %d µg/m³\n", pm1, pm25, pm4, pm10);
}
// [BTHOME-SENSEL] Barometric pressure from BME280-class sensors.
if (sensors.isUnitRegistered(UNIT::PRESS)) {
// The CanAirIO lib is inconsistent: BME280 returns Pa (~101325) while
// BMP280/BME680 already return hPa (~1013). BTHome (and the 300-1100
// validity range) expect hPa, so normalize: values that look like Pa
// are divided by 100. Without this, BME280 pressure always fails the
// validity gate and is never advertised.
float p = sensors.getPressure();
pressureHpa = (p > 10000.0f) ? (p / 100.0f) : p;
}
#ifdef SUPPORT_BTHOME_BLE
// Recompute the runtime freshness mask each read cycle so a sensor that stops
// reporting drops out instead of advertising its last value forever (battery
// and voltage are special-cased as always-fresh in bthomeMeasurementFresh()).
bthomeFreshMeasurements = 0;
if (sensors.isUnitRegistered(UNIT::CO2)) {
bthomeFreshMeasurements |= BTHOME_SEL_CO2;
}
// Mirror bthomeMeasurementAvailable(): only TEMP/CO2TEMP (not bare CO2) means a
// real temperature reading, so CO2-only sensors don't mark temp/hum fresh.
if (sensors.isUnitRegistered(UNIT::TEMP) || sensors.isUnitRegistered(UNIT::CO2TEMP)) {
bthomeFreshMeasurements |= BTHOME_SEL_TEMP;
}
if (sensors.isUnitRegistered(UNIT::HUM) || sensors.isUnitRegistered(UNIT::CO2HUM)) {
bthomeFreshMeasurements |= BTHOME_SEL_HUM;
}
if (sensors.isUnitRegistered(UNIT::PRESS)) {
bthomeFreshMeasurements |= BTHOME_SEL_PRESS;
}
#ifdef SUPPORT_LOW_POWER
// Remember whether a pressure sensor is present so the deep-sleep wake path
// (SUPPORT_LOW_POWER_PRESSURE) knows it can re-read the BME280 on wake.
deepSleepData.hasPressureOnWake = sensors.isUnitRegistered(UNIT::PRESS);
#endif
if (sensors.isUnitRegistered(UNIT::PM25)) {
bthomeFreshMeasurements |= BTHOME_SEL_PM1 | BTHOME_SEL_PM25 | BTHOME_SEL_PM4 | BTHOME_SEL_PM10;
}
#endif
newReadingsAvailable = true;
// Serial.printf("-->[SENS] Free heap: %d\n", ESP.getFreeHeap());
}
void onSensorDataError(const char* msg) {
Serial.println("-->[SENS] " + String(msg));
}
void storeSensorSelectedInRTC() {
if (!sensorsGetMainDeviceSelected().isEmpty()) {
Serial.println("-->[SENS] Storing main device selected to RTC Memory: [" + sensorsGetMainDeviceSelected() + "]");
if (sensorsGetMainDeviceSelected() == "SCD40") {
deepSleepData.co2Sensor = CO2Sensor_SCD40;
Serial.println("-->[SENS][SCD4X] Sensor stored: CO2Sensor_SCD40");
return;
}
if (sensorsGetMainDeviceSelected() == "SCD41") {
deepSleepData.co2Sensor = CO2Sensor_SCD41;
Serial.println("-->[SENS][SCD4X] Sensor stored: CO2Sensor_SCD41");
return;
}
if ((sensorsGetMainDeviceSelected()) == "CM1106") {
char softver[CM1106_LEN_SOFTVER];
sensors.cm1106->get_software_version(softver);
String softverStr(softver);
Serial.println("-->[SENS][CM1106] Sensor stored: CO2Sensor_CM1106");
// Serial.println("-->[SENS-CM1106] CM1106 version detected 1\t: " + softverStr);
if (softverStr.length() >= 10 && softverStr.endsWith("SL-NS")) {
deepSleepData.co2Sensor = CO2Sensor_CM1106SL_NS;
Serial.println("-->[SENS][CM1106] Sensor stored: CO2Sensor_CM1106SL_NS");
} else if (softverStr.endsWith("CM")) {
deepSleepData.co2Sensor = CO2Sensor_CM1106;
Serial.println("-->[SENS][CM1106] Sensor stored: CO2Sensor_CM1106");
} else {
deepSleepData.co2Sensor = CO2Sensor_NONE;
Serial.println("-->[SENS][CM1106] Sensor stored: CO2Sensor_NONE");
}
return;
}
if ((sensorsGetMainDeviceSelected()) == "SCD30") {
deepSleepData.co2Sensor = CO2Sensor_SCD30;
Serial.println("-->[SENS][SCD30] Sensor stored: CO2Sensor_SCD30");
return;
}
if ((sensorsGetMainDeviceSelected()) == "MHZ19") {
deepSleepData.co2Sensor = CO2Sensor_MHZ19;
Serial.println("-->[SENS][MHZ19] Sensor stored: CO2Sensor_MHZ19");
return;
}
if ((sensorsGetMainDeviceSelected()) == "SENSEAIRS8") {
deepSleepData.co2Sensor = CO2Sensor_SENSEAIRS8;
Serial.println("-->[SENS][SENSEAIRS8] Sensor stored: CO2Sensor_SENSEAIRS8");
return;
}
if ((sensorsGetMainDeviceSelected()) == "NONE") {
deepSleepData.co2Sensor = CO2Sensor_NONE;
Serial.println("-->[SENS][NONE][ERROR] Sensor stored: CO2Sensor_NONE");
delay(10000);
return;
}
}
}
void initSCD30SensorLowPower() {
return;
#ifndef Wire1
if (!sensors.scd30.begin()) return;
#else
if (!sensors.scd30.begin() && !sensors.scd30.begin(SCD30_I2CADDR_DEFAULT, &Wire1, SCD30_CHIP_ID)) return;
#endif
}
void initSensorsLowPower() {
const int8_t None = -1, AUTO = 0, MHZ19 = 4, CM1106 = 5, SENSEAIRS8 = 6, DEMO = 127;
if (deepSleepData.lowPowerMode == SENSORS::SSCD30) {
Serial.println("-->[SENS] Trying to init CO2 sensor in Low Power Mode: SCD30");
initSCD30SensorLowPower();
return;
}
if (selectedCO2Sensor == AUTO) {
Serial.println("-->[SENS] Trying to init CO2 sensor in Low Power Mode: AutoSensor (I2C)");
deepSleepData.measurementsStarted = false;
sensors.initCO2LowPowerMode(SENSORS::Auto, (LowPowerModes)LOW_POWER); // Always use LOW_POWER internally regardless of the binary lowPowerMode value
return;
}
Serial.println("-->[SENS] Trying to init CO2 sensor in Low Power Mode: " + sensors.getSensorName(static_cast<SENSORS>(selectedCO2Sensor)));
displayNotification("-->[SENS][ERROR] Init sensors", "Low Power Mode not supported for this sensor", notifyError);
while (1) {
Serial.println("-->[SENS][ERROR] Low Power Mode not supported for this sensor " + sensors.getSensorName(static_cast<SENSORS>(selectedCO2Sensor)));
delay(10000);
}
}
/**
* @brief Apply the current measurementInterval to the active CO2 sensor.
*
* Centralised single source of truth for setting the sampling interval on
* the physical sensor. Call this after any runtime change to
* measurementInterval (web UI, serial menu, JSON import, etc.) and from
* every init path (normal boot, low-power wake).
*
* Per-sensor dispatch:
* - Generic (any sensor): sensors.setSampleTime(measurementInterval)
* - CM1106: additionally set_measurement_period()
* - SCD30 (future, see #247): additionally setMeasurementInterval()
*/
void applyMeasurementIntervalToSensors() {
sensors.setSampleTime(measurementInterval);
Serial.println("-->[SENS] Applied measurementInterval: " + String(measurementInterval) + "s to " + sensors.getSensorName(static_cast<SENSORS>(selectedCO2Sensor)));
// CM1106 and CM1106SL_NS (values 5 and 6) both need the additional
// hardware-level set_measurement_period() call.
if (selectedCO2Sensor == 5 || selectedCO2Sensor == 6) {
#ifdef UART_RX_GPIO
if (sensors.cm1106 != nullptr) {
sensors.cm1106->set_measurement_period(measurementInterval, 1);
int16_t period;
uint8_t smooth;
sensors.cm1106->get_measurement_period(&period, &smooth);
Serial.println("-->[SENS] CM1106 measurement period set to: " + String(period) + "s smooth: " + String(smooth));
}
#endif
}
// SCD30 will need setMeasurementInterval() here once implemented (#247)
}
void initSensors() {
const int8_t None = -1, AUTO = 0, MHZ19 = 4, CM1106 = 5, SENSEAIRS8 = 6, DEMO = 127;
int16_t period;
uint8_t smooth;
// Initialize sensors
#ifdef I2C_SDA&& defined(I2C_SCL)
Wire.begin(I2C_SDA, I2C_SCL);
#else
Wire.begin();
#endif
Wire.setTimeout(2000);
Serial.println("-->[SENS] Detecting sensors...");
sensors.setOnDataCallBack(&onSensorDataOk); // all data read callback
sensors.setOnErrorCallBack(&onSensorDataError); // [optional] error callback
sensors.setDebugMode(debugSensors); // [optional] debug mode
sensors.setTempOffset(tempOffset);
sensors.setCO2AltitudeOffset(altitudeMeters);
// sensors.setAutoSelfCalibration(false); // TO-DO: Implement in CanAirIO Sensors Lib
applyMeasurementIntervalToSensors();
Serial.println("-->[SENS] Selected CO2 Sensor: " + sensors.getSensorName(static_cast<SENSORS>(selectedCO2Sensor)));
Serial.println("-->[SENS] Measurement Interval: " + String(sensors.getSampleTime()));
// setBLEHistoryInterval(sampleInterval);
if ((deepSleepData.lowPowerMode) && (!interactiveMode)) {
displayNotification("Init sensors", "Trying Low Power Mode: " + String(deepSleepData.lowPowerMode), notifyInfo);
Serial.println("-->[SENS] Trying to init sensors in Low Power Mode: " + String(deepSleepData.lowPowerMode));
initSensorsLowPower();
} else {
displayNotification("Init sensors", notifyInfo);
if (selectedCO2Sensor == AUTO) {
Serial.println("-->[SENS] Trying to init CO2 sensor: AutoSensor (I2C)");
sensors.detectI2COnly(true);
sensors.init();
} else if (selectedCO2Sensor == MHZ19) {
Serial.println("-->[SENS] Trying to init CO2 sensor: MHZ19(A/B/C/D)");
sensors.detectI2COnly(false);
sensors.init(MHZ19);
} else if (selectedCO2Sensor == CM1106) {
Serial.println("-->[SENS] Trying to init CO2 sensor: CM1106");
sensors.detectI2COnly(false);
#ifdef CM1106_ENABLE_PIN
pinMode(CM1106_ENABLE_PIN, OUTPUT);
digitalWrite(CM1106_ENABLE_PIN, HIGH);
#endif
#ifdef CM1106_READY_PIN
pinMode(CM1106_READY_PIN, INPUT);
#endif
#if defined(UART_RX_GPIO) && defined(UART_TX_GPIO)
sensors.init(CM1106, UART_RX_GPIO, UART_TX_GPIO);
#else
sensors.init(CM1106);
#endif
sensors.cm1106->set_working_status(CM1106_CONTINUOUS_MEASUREMENT);
sensors.cm1106->get_measurement_period(&period, &smooth);
Serial.println("-->[SENS] CM1106 period before: " + String(period) + " smooth: " + String(smooth));
applyMeasurementIntervalToSensors();
sensors.cm1106->get_measurement_period(&period, &smooth);
Serial.println("-->[SENS] CM1106 period after: " + String(period) + " smooth: " + String(smooth));
// sensors.cm1106->set_working_status(CM1106_SINGLE_MEASUREMENT);
} else if (selectedCO2Sensor == SENSEAIRS8) {
Serial.println("-->[SENS] Trying to init CO2 sensor: SENSEAIRS8");
sensors.detectI2COnly(false);
sensors.init(SENSEAIRS8);
}
}
// Apply auto self-calibration now that the sensor is detected/registered (ASC for
// SCD30/SCD4x, ABC for MH-Z19). Done here, not before detection, so the library's
// isSensorRegistered() checks see the sensor. The sleep interval lets the library
// scale the SCD4x ASC periods for the single-shot idle interval (default periods
// assume 5-min sampling). initSensors() runs on cold boot / mode entry only —
// deep-sleep wakes go through fromDeepSleep() and the ASC setting persists in the
// sensor. See: https://github.com/melkati/CO2-Gadget/issues/250
sensors.setAutoSelfCalibration(autoSelfCalibration, deepSleepData.timeSleeping);
printSensorsDetected();
storeSensorSelectedInRTC();
}
void sensorSCD30LoopLowPower() {
Serial.println("-->[DEEP] " + String(__func__) + "()");
}
void sensorCM1106SL_NSLoopLowPower() {
Serial.println("-->[DEEP] " + String(__func__) + "()");
}
void sensorSCD4XLoopLowPower() {
bool dataReadyFlag = false;
uint16_t error = 0;
uint16_t co2value = 0;
float temperature = 0;
float humidity = 0;
Serial.println("-->[DEEP] " + String(__func__) + "() " + sensors.getSCD4xModel());
// Serial.println("-->[SENS] sensorSCD4XLoopLowPower() " + (sensors.getSCD4xModel() == "SCD40" ? "SCD40" : "SCD41"));
error = sensors.scd4x.getDataReadyFlag(dataReadyFlag);
if (error != 0) {
Serial.printf("-->[DEEP] SCD4X sensorSCD4XLoopLowPower() --> getDataReadyFlag() error: %d\n", error);
}
if (dataReadyFlag) {
error = sensors.scd4x.readMeasurement(co2value, temperature, humidity);
if (error != 0) {
Serial.printf("-->[DEEP] sensorSCD4XLoopLowPower() error: %d\n", error);
} else {
// Serial.println("-->[SENS] sensorSCD4XLoopLowPower() " + (sensors.getSCD4xModel() == "SCD40" ? "SCD40" : "SCD41"));
Serial.printf("-->[SENS] CO2: %d CO2temp: %.2f CO2humi: %.2f H: %.2f T: %.2f Sensor: (%s)\n", co2value, temperature, humidity, humidity, temperature, sensors.getSCD4xModel().c_str());
co2 = co2value;
temp = temperature;
hum = humidity;
}
}
}
void sensorsLoopLowPower() {
if (deepSleepData.co2Sensor == static_cast<CO2SENSORS_t>(CO2Sensor_SCD30)) {
// Serial.println("sensorsLoopLowPower() SCD30");
sensorSCD30LoopLowPower();
} else if (deepSleepData.co2Sensor == static_cast<CO2SENSORS_t>(CO2Sensor_CM1106SL_NS)) {
// Serial.println("sensorsLoopLowPower() CM1106SL_NS");
sensorCM1106SL_NSLoopLowPower();
} else if (deepSleepData.co2Sensor == static_cast<CO2SENSORS_t>(CO2Sensor_SCD41)) {
// Serial.println("sensorsLoopLowPower() SCD41");
sensorSCD4XLoopLowPower();
}
}
void sensorsLoop() {
static unsigned long lastDotPrintTime = 0;
static unsigned long sensorWarmupStart = 0;
static bool sensorWarmupDone = false;
if (isDownloadingBLE) return;
// Warmup: skip first ~5s of sensor.loop() after boot to let the sensor
// produce its first measurement before we ask for data. The SCD41 needs
// ~5s after begin() before the first reading is available; calling loop()
// earlier triggers a "No data from any sensor!" warning from the library.
// This is harmless for other sensor types (they just return sooner).
if (!sensorWarmupDone) {
if (sensorWarmupStart == 0) sensorWarmupStart = millis();
if (millis() - sensorWarmupStart < 5000) return;
sensorWarmupDone = true;
}
if ((!interactiveMode) && (deepSleepData.lowPowerMode != HIGH_PERFORMANCE) && (!deepSleepData.calForceContinuous)) {
if (millis() - lastDotPrintTime >= 100) {
Serial.print("[-]"); // Print a - every loop to show that the device is alive
lastDotPrintTime = millis();
}
sensorsLoopLowPower();
} else if (!buzzerBeeping) { // Avoid affecting beep sound (also continuous reads while a calibration paused deep sleep)
// if (millis() - lastDotPrintTime >= 100) {
// Serial.print("[+] "); // Print a + every loop to show that the device is alive
// lastDotPrintTime = millis();
// }
sensors.loop();
}
}
#endif // CO2_Gadget_Sensors_h