Compare commits

..

52 Commits

Author SHA1 Message Date
92e74feabd Merge branch 'develop'
# Conflicts:
#	library.properties
#	src/AirGradient.h
2024-12-05 15:36:32 +07:00
cc0fd88068 Prepared to release 3.1.21 2024-12-05 15:35:37 +07:00
56809a412c Merge branch 'develop' 2024-12-05 15:17:34 +07:00
6a83743e2a Prepared to release 3.1.16 2024-12-05 15:06:10 +07:00
faaf051e39 Prepared to release 3.1.15 2024-12-05 15:00:25 +07:00
5bc1821ef9 Merge pull request #264 from airgradienthq/feat/ssl
Airgradient API calls using https
2024-12-05 13:23:52 +07:00
280ea5e997 Prepared to release 3.1.14 2024-12-04 10:38:13 +07:00
e95627ece6 Merge pull request #270 from airgradienthq/fix/openmetrics-correction-openair
Fix openmetrics pm25 correction on openair
2024-12-03 03:36:25 +07:00
80b9ae11d8 Fix openmetrics pm25 correction on openair 2024-12-03 03:33:46 +07:00
1937e3d59e Merge pull request #267 from jakpor/fix/basic_temp_display
(Basic DIY): Fix temperature parsing in OLED and debug for Basic DYI version
2024-12-03 02:18:07 +07:00
107fb21331 Merge pull request #269 from airgradienthq/fix/openmetrics-correction
PM25 correction for openmetrics
2024-12-03 02:00:16 +07:00
ccc1ab463a PM25 correction for openmetrics 2024-12-03 01:55:11 +07:00
38e792b88d if guard uri for esp8266 2024-11-30 04:45:04 +07:00
aeee0cad01 OTA bin download using https
rename server root ca constant name
2024-11-30 04:33:58 +07:00
401326d00d Move airgradient server CA const to AirGradient.h file
Constant will be used by api client and otahandler
2024-11-30 04:13:25 +07:00
fb0dcad54d Fix CI error esp32 2024-11-30 03:52:32 +07:00
3556e4a96a Fix CI error 2024-11-30 03:42:45 +07:00
283646a699 Move OtaHandler to src
OTA only valid for esp32 based monitor
2024-11-30 02:47:05 +07:00
6312612ada resetReason and freeheap only for esp32 based mcu 2024-11-30 00:45:46 +07:00
c1f22674e2 Add reset reason to transmission payload 2024-11-29 18:01:34 +07:00
40d38a75d8 Add reset reason to transmission payload 2024-11-29 18:01:14 +07:00
39ef69cbdf Fix printing of debug logs throught serial 2024-11-27 17:18:43 +01:00
3473e30e2e Fix temperature float formatting for basic oled display 2024-11-27 17:17:22 +01:00
566f8a63b4 Prepare 3.1.13 release 2024-11-27 13:35:26 +07:00
9e4d52454b Merge pull request #266 from airgradienthq/fix/openmetrics
Fix measurements value in prometheus metrics endpoints
2024-11-27 03:38:57 +07:00
5f5e985309 Fix openmetrics esp8266 based 2024-11-27 00:43:03 +07:00
d638573ca7 Fix open metrics for OneOpenAir 2024-11-27 00:15:32 +07:00
4c165b31f5 Add freeheap to cloud payload 2024-11-21 02:36:56 +07:00
2be91b3968 Boot count using setter and getter 2024-11-21 02:30:03 +07:00
3ca2d1d208 Fix esp8266 build issue 2024-11-21 01:26:05 +07:00
aad12fc868 Using https
For get config and post measurements
2024-11-21 01:18:51 +07:00
79fbd901bd Merge branch 'develop' 2024-11-19 18:45:31 +07:00
3644dc43fe Prepared to release 3.1.12 2024-11-19 18:44:23 +07:00
03fa62d8f0 Merge pull request #263 from airgradienthq/fix/correction
Fix EPA compensated on top of SLR correction
2024-11-19 02:20:51 +07:00
902a768f28 Handle parsing invalid json string 2024-11-19 01:45:50 +07:00
1de9344f43 Fix typo on docs 2024-11-18 22:50:36 +07:00
46f6309b77 Fix use the right function 2024-11-18 22:42:43 +07:00
a6b48acb41 CO2 led bar indicator sync fix 2024-11-18 21:24:47 +07:00
1b4d89e1a1 fix correction on top of compensation 2024-11-18 19:53:28 +07:00
0d2b0fb657 Fix typo on local-server.md
Fix curl correction command example typos
2024-11-17 11:18:39 +07:00
9f08af44b0 Prepared to release 3.1.11 2024-11-11 20:04:47 +07:00
6b661cdeb7 Merge pull request #261 from airgradienthq/feat/allavg
Values on display and led bar using measurement average values
2024-11-11 12:40:48 +07:00
dc299c4b54 Fix pm2.5 not using getAverage for ledbar 2024-11-10 19:20:09 +07:00
2f595b4e41 Update local server docs correction examples 2024-11-10 04:26:54 +07:00
a30535f75f Use avg value for display and led bar 2024-11-10 04:13:58 +07:00
a513943cba New agvalue member to get avg values 2024-11-09 23:27:29 +07:00
96bb6952fb correction function return raw if algorithm is none 2024-11-09 21:28:33 +07:00
10653bfe26 Fix pms5003 correction default value 2024-11-09 20:55:41 +07:00
c7f89fa7b7 Decrease period length moving average to 80% 2024-11-09 20:53:52 +07:00
b11c461b60 Merge pull request #260 from airgradienthq/fix/led-flicker
Fix flicker on led bar led state change
2024-11-09 20:42:01 +07:00
404c14aad2 Fix typo and comment 2024-11-07 22:08:36 +07:00
bfbae680fd Fix led bar flicker when state change
clear only neccessary led
2024-11-07 22:03:01 +07:00
22 changed files with 787 additions and 455 deletions

View File

@ -156,13 +156,13 @@ If the monitor is set up on the AirGradient dashboard, it will also receive the
| `tvocLearningOffset` | Set VOC learning gain offset. | Number | 0-720 (default 12) | `{"tvocLearningOffset": 12}` | | `tvocLearningOffset` | Set VOC learning gain offset. | Number | 0-720 (default 12) | `{"tvocLearningOffset": 12}` |
| `offlineMode` | Set monitor to run without WiFi. | Boolean | `false`: Disabled (default) <br> `true`: Enabled | `{"offlineMode": true}` | | `offlineMode` | Set monitor to run without WiFi. | Boolean | `false`: Disabled (default) <br> `true`: Enabled | `{"offlineMode": true}` |
| `monitorDisplayCompensatedValues` | Set the display show the PM value with/without compensate value (only on [3.1.9]()) | Boolean | `false`: Without compensate (default) <br> `true`: with compensate | `{"monitorDisplayCompensatedValues": false }` | | `monitorDisplayCompensatedValues` | Set the display show the PM value with/without compensate value (only on [3.1.9]()) | Boolean | `false`: Without compensate (default) <br> `true`: with compensate | `{"monitorDisplayCompensatedValues": false }` |
| `corrections` | Sets correction options to display and measurement values on local server response. | Object | _see corretions section_ | _see corretions section_ | | `corrections` | Sets correction options to display and measurement values on local server response. (version >= [3.1.11]()) | Object | _see corrections section_ | _see corrections section_ |
#### Corrections #### Corrections
The `corrections` object allows configuring PM2.5 correction algorithms and parameters. This affects both the display and local server response values. The `corrections` object allows configuring PM2.5 correction algorithms and parameters locally. This affects both the display and local server response values.
Example correction configuration: Example correction configuration:
@ -189,13 +189,28 @@ Example correction configuration:
| PMS5003_20231218 | `"slr_PMS5003_20231218"` | Correction for PMS5003 sensor batch 20231218| Yes | | PMS5003_20231218 | `"slr_PMS5003_20231218"` | Correction for PMS5003 sensor batch 20231218| Yes |
| PMS5003_20231030 | `"slr_PMS5003_20231030"` | Correction for PMS5003 sensor batch 20231030| Yes | | PMS5003_20231030 | `"slr_PMS5003_20231030"` | Correction for PMS5003 sensor batch 20231030| Yes |
**Notes**: **NOTES**:
- Set `useEpa2021` to true if want to apply EPA 2021 correction factors on top of SLR correction value. - Set `useEpa2021` to `true` if want to apply EPA 2021 correction factors on top of SLR correction value, otherwise `false`
- `intercept` and `scalingFactor` values can be obtained from [this article](https://www.airgradient.com/blog/low-readings-from-pms5003/) - `intercept` and `scalingFactor` values can be obtained from [this article](https://www.airgradient.com/blog/low-readings-from-pms5003/)
- If `configurationControl` is set to `local` (eg. when using Home Assistant), correction need to be set manually, see examples below
**Example**: **Examples**:
- PMS5003_20231030
```bash ```bash
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231030","slr":{"intercept":0,"scalingFactor":0.02838,"useEpa2021":false}}}}' curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231030","slr":{"intercept":0,"scalingFactor":0.02838,"useEpa2021":true}}}}'
``` ```
- PMS5003_20231218
```bash
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231218","slr":{"intercept":0,"scalingFactor":0.03525,"useEpa2021":true}}}}'
```
- PMS5003_20240104
```bash
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20240104","slr":{"intercept":0,"scalingFactor":0.02896,"useEpa2021":true}}}}'
```

View File

@ -331,7 +331,7 @@ static void sendDataToAg() {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting); stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
delay(1500); delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) { if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount())) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected); stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} else { } else {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed); stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
@ -518,7 +518,8 @@ static void updatePm(void) {
static void sendDataToServer(void) { static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */ /** Increment bootcount when send measurements data is scheduled */
measurements.bootCount++; int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
/** Ignore send data to server if postToAirGradient disabled */ /** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false || if (configuration.isPostDataToAirGradient() == false ||

View File

@ -57,26 +57,20 @@ String OpenMetrics::getPayload(void) {
"gauge", "dbm"); "gauge", "dbm");
add_metric_point("", String(wifiConnector.RSSI())); add_metric_point("", String(wifiConnector.RSSI()));
if (config.hasSensorS8 && measure.CO2 >= 0) { // Initialize default invalid value for each measurements
add_metric("co2",
"Carbon dioxide concentration as measured by the AirGradient S8 "
"sensor, in parts per million",
"gauge", "ppm");
add_metric_point("", String(measure.CO2));
}
float _temp = utils::getInvalidTemperature(); float _temp = utils::getInvalidTemperature();
float _hum = utils::getInvalidHumidity(); float _hum = utils::getInvalidHumidity();
int pm01 = utils::getInvalidPmValue(); int pm01 = utils::getInvalidPmValue();
int pm25 = utils::getInvalidPmValue(); int pm25 = utils::getInvalidPmValue();
int pm10 = utils::getInvalidPmValue(); int pm10 = utils::getInvalidPmValue();
int pm03PCount = utils::getInvalidPmValue(); int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature(); int atmpCompensated = utils::getInvalidTemperature();
int ahumCompensated = utils::getInvalidHumidity(); int ahumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC(); int tvoc = utils::getInvalidVOC();
int tvoc_raw = utils::getInvalidVOC(); int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx(); int nox = utils::getInvalidNOx();
int nox_raw = utils::getInvalidNOx(); int noxRaw = utils::getInvalidNOx();
if (config.hasSensorSHT) { if (config.hasSensorSHT) {
_temp = measure.getFloat(Measurements::Temperature); _temp = measure.getFloat(Measurements::Temperature);
@ -87,16 +81,21 @@ String OpenMetrics::getPayload(void) {
if (config.hasSensorPMS1) { if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01); pm01 = measure.get(Measurements::PM01);
pm25 = measure.get(Measurements::PM25); float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10); pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC); pm03PCount = measure.get(Measurements::PM03_PC);
} }
if (config.hasSensorSGP) { if (config.hasSensorSGP) {
tvoc = measure.get(Measurements::TVOC); tvoc = measure.get(Measurements::TVOC);
tvoc_raw = measure.get(Measurements::TVOCRaw); tvocRaw = measure.get(Measurements::TVOCRaw);
nox = measure.get(Measurements::NOx); nox = measure.get(Measurements::NOx);
nox_raw = measure.get(Measurements::NOxRaw); noxRaw = measure.get(Measurements::NOxRaw);
}
if (config.hasSensorS8) {
co2 = measure.get(Measurements::CO2);
} }
if (config.hasSensorPMS1) { if (config.hasSensorPMS1) {
@ -138,12 +137,12 @@ String OpenMetrics::getPayload(void) {
"gauge"); "gauge");
add_metric_point("", String(tvoc)); add_metric_point("", String(tvoc));
} }
if (utils::isValidVOC(tvoc_raw)) { if (utils::isValidVOC(tvocRaw)) {
add_metric("tvoc_raw", add_metric("tvoc_raw",
"The raw input value to the Total Volatile Organic Compounds " "The raw input value to the Total Volatile Organic Compounds "
"(TVOC) index as measured by the AirGradient SGP sensor", "(TVOC) index as measured by the AirGradient SGP sensor",
"gauge"); "gauge");
add_metric_point("", String(tvoc_raw)); add_metric_point("", String(tvocRaw));
} }
if (utils::isValidNOx(nox)) { if (utils::isValidNOx(nox)) {
add_metric("nox_index", add_metric("nox_index",
@ -152,15 +151,23 @@ String OpenMetrics::getPayload(void) {
"gauge"); "gauge");
add_metric_point("", String(nox)); add_metric_point("", String(nox));
} }
if (utils::isValidNOx(nox_raw)) { if (utils::isValidNOx(noxRaw)) {
add_metric("nox_raw", add_metric("nox_raw",
"The raw input value to the Nitrous Oxide (NOx) index as " "The raw input value to the Nitrous Oxide (NOx) index as "
"measured by the AirGradient SGP sensor", "measured by the AirGradient SGP sensor",
"gauge"); "gauge");
add_metric_point("", String(nox_raw)); add_metric_point("", String(noxRaw));
} }
} }
if (utils::isValidCO2(co2)) {
add_metric("co2",
"Carbon dioxide concentration as measured by the AirGradient S8 "
"sensor, in parts per million",
"gauge", "ppm");
add_metric_point("", String(co2));
}
if (utils::isValidTemperature(_temp)) { if (utils::isValidTemperature(_temp)) {
add_metric( add_metric(
"temperature", "temperature",

View File

@ -388,7 +388,7 @@ static void sendDataToAg() {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting); stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
delay(1500); delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) { if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount())) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected); stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} else { } else {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed); stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
@ -570,7 +570,8 @@ static void updatePm(void) {
static void sendDataToServer(void) { static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */ /** Increment bootcount when send measurements data is scheduled */
measurements.bootCount++; int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
/** Ignore send data to server if postToAirGradient disabled */ /** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false || if (configuration.isPostDataToAirGradient() == false ||

View File

@ -57,26 +57,20 @@ String OpenMetrics::getPayload(void) {
"gauge", "dbm"); "gauge", "dbm");
add_metric_point("", String(wifiConnector.RSSI())); add_metric_point("", String(wifiConnector.RSSI()));
if (config.hasSensorS8 && measure.CO2 >= 0) { // Initialize default invalid value for each measurements
add_metric("co2",
"Carbon dioxide concentration as measured by the AirGradient S8 "
"sensor, in parts per million",
"gauge", "ppm");
add_metric_point("", String(measure.CO2));
}
float _temp = utils::getInvalidTemperature(); float _temp = utils::getInvalidTemperature();
float _hum = utils::getInvalidHumidity(); float _hum = utils::getInvalidHumidity();
int pm01 = utils::getInvalidPmValue(); int pm01 = utils::getInvalidPmValue();
int pm25 = utils::getInvalidPmValue(); int pm25 = utils::getInvalidPmValue();
int pm10 = utils::getInvalidPmValue(); int pm10 = utils::getInvalidPmValue();
int pm03PCount = utils::getInvalidPmValue(); int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature(); int atmpCompensated = utils::getInvalidTemperature();
int ahumCompensated = utils::getInvalidHumidity(); int ahumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC(); int tvoc = utils::getInvalidVOC();
int tvoc_raw = utils::getInvalidVOC(); int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx(); int nox = utils::getInvalidNOx();
int nox_raw = utils::getInvalidNOx(); int noxRaw = utils::getInvalidNOx();
if (config.hasSensorSHT) { if (config.hasSensorSHT) {
_temp = measure.getFloat(Measurements::Temperature); _temp = measure.getFloat(Measurements::Temperature);
@ -87,16 +81,21 @@ String OpenMetrics::getPayload(void) {
if (config.hasSensorPMS1) { if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01); pm01 = measure.get(Measurements::PM01);
pm25 = measure.get(Measurements::PM25); float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10); pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC); pm03PCount = measure.get(Measurements::PM03_PC);
} }
if (config.hasSensorSGP) { if (config.hasSensorSGP) {
tvoc = measure.get(Measurements::TVOC); tvoc = measure.get(Measurements::TVOC);
tvoc_raw = measure.get(Measurements::TVOCRaw); tvocRaw = measure.get(Measurements::TVOCRaw);
nox = measure.get(Measurements::NOx); nox = measure.get(Measurements::NOx);
nox_raw = measure.get(Measurements::NOxRaw); noxRaw = measure.get(Measurements::NOxRaw);
}
if (config.hasSensorS8) {
co2 = measure.get(Measurements::CO2);
} }
if (config.hasSensorPMS1) { if (config.hasSensorPMS1) {
@ -138,12 +137,13 @@ String OpenMetrics::getPayload(void) {
"gauge"); "gauge");
add_metric_point("", String(tvoc)); add_metric_point("", String(tvoc));
} }
if (utils::isValidVOC(tvoc_raw)) {
if (utils::isValidVOC(tvocRaw)) {
add_metric("tvoc_raw", add_metric("tvoc_raw",
"The raw input value to the Total Volatile Organic Compounds " "The raw input value to the Total Volatile Organic Compounds "
"(TVOC) index as measured by the AirGradient SGP sensor", "(TVOC) index as measured by the AirGradient SGP sensor",
"gauge"); "gauge");
add_metric_point("", String(tvoc_raw)); add_metric_point("", String(tvocRaw));
} }
if (utils::isValidNOx(nox)) { if (utils::isValidNOx(nox)) {
add_metric("nox_index", add_metric("nox_index",
@ -152,15 +152,23 @@ String OpenMetrics::getPayload(void) {
"gauge"); "gauge");
add_metric_point("", String(nox)); add_metric_point("", String(nox));
} }
if (utils::isValidNOx(nox_raw)) { if (utils::isValidNOx(noxRaw)) {
add_metric("nox_raw", add_metric("nox_raw",
"The raw input value to the Nitrous Oxide (NOx) index as " "The raw input value to the Nitrous Oxide (NOx) index as "
"measured by the AirGradient SGP sensor", "measured by the AirGradient SGP sensor",
"gauge"); "gauge");
add_metric_point("", String(nox_raw)); add_metric_point("", String(noxRaw));
} }
} }
if (utils::isValidCO2(co2)) {
add_metric("co2",
"Carbon dioxide concentration as measured by the AirGradient S8 "
"sensor, in parts per million",
"gauge", "ppm");
add_metric_point("", String(co2));
}
if (utils::isValidTemperature(_temp)) { if (utils::isValidTemperature(_temp)) {
add_metric( add_metric(
"temperature", "temperature",

View File

@ -411,7 +411,7 @@ static void sendDataToAg() {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting); stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
delay(1500); delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) { if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount())) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected); stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} else { } else {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed); stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
@ -611,7 +611,8 @@ static void updatePm(void) {
static void sendDataToServer(void) { static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */ /** Increment bootcount when send measurements data is scheduled */
measurements.bootCount++; int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
/** Ignore send data to server if postToAirGradient disabled */ /** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false || if (configuration.isPostDataToAirGradient() == false ||

View File

@ -57,26 +57,20 @@ String OpenMetrics::getPayload(void) {
"gauge", "dbm"); "gauge", "dbm");
add_metric_point("", String(wifiConnector.RSSI())); add_metric_point("", String(wifiConnector.RSSI()));
if (config.hasSensorS8 && measure.CO2 >= 0) { // Initialize default invalid value for each measurements
add_metric("co2",
"Carbon dioxide concentration as measured by the AirGradient S8 "
"sensor, in parts per million",
"gauge", "ppm");
add_metric_point("", String(measure.CO2));
}
float _temp = utils::getInvalidTemperature(); float _temp = utils::getInvalidTemperature();
float _hum = utils::getInvalidHumidity(); float _hum = utils::getInvalidHumidity();
int pm01 = utils::getInvalidPmValue(); int pm01 = utils::getInvalidPmValue();
int pm25 = utils::getInvalidPmValue(); int pm25 = utils::getInvalidPmValue();
int pm10 = utils::getInvalidPmValue(); int pm10 = utils::getInvalidPmValue();
int pm03PCount = utils::getInvalidPmValue(); int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature(); int atmpCompensated = utils::getInvalidTemperature();
int ahumCompensated = utils::getInvalidHumidity(); int ahumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC(); int tvoc = utils::getInvalidVOC();
int tvoc_raw = utils::getInvalidVOC(); int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx(); int nox = utils::getInvalidNOx();
int nox_raw = utils::getInvalidNOx(); int noxRaw = utils::getInvalidNOx();
if (config.hasSensorSHT) { if (config.hasSensorSHT) {
_temp = measure.getFloat(Measurements::Temperature); _temp = measure.getFloat(Measurements::Temperature);
@ -87,16 +81,21 @@ String OpenMetrics::getPayload(void) {
if (config.hasSensorPMS1) { if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01); pm01 = measure.get(Measurements::PM01);
pm25 = measure.get(Measurements::PM25); float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10); pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC); pm03PCount = measure.get(Measurements::PM03_PC);
} }
if (config.hasSensorSGP) { if (config.hasSensorSGP) {
tvoc = measure.get(Measurements::TVOC); tvoc = measure.get(Measurements::TVOC);
tvoc_raw = measure.get(Measurements::TVOCRaw); tvocRaw = measure.get(Measurements::TVOCRaw);
nox = measure.get(Measurements::NOx); nox = measure.get(Measurements::NOx);
nox_raw = measure.get(Measurements::NOxRaw); noxRaw = measure.get(Measurements::NOxRaw);
}
if (config.hasSensorS8) {
co2 = measure.get(Measurements::CO2);
} }
if (config.hasSensorPMS1) { if (config.hasSensorPMS1) {
@ -138,12 +137,12 @@ String OpenMetrics::getPayload(void) {
"gauge"); "gauge");
add_metric_point("", String(tvoc)); add_metric_point("", String(tvoc));
} }
if (utils::isValidVOC(tvoc_raw)) { if (utils::isValidVOC(tvocRaw)) {
add_metric("tvoc_raw", add_metric("tvoc_raw",
"The raw input value to the Total Volatile Organic Compounds " "The raw input value to the Total Volatile Organic Compounds "
"(TVOC) index as measured by the AirGradient SGP sensor", "(TVOC) index as measured by the AirGradient SGP sensor",
"gauge"); "gauge");
add_metric_point("", String(tvoc_raw)); add_metric_point("", String(tvocRaw));
} }
if (utils::isValidNOx(nox)) { if (utils::isValidNOx(nox)) {
add_metric("nox_index", add_metric("nox_index",
@ -152,15 +151,23 @@ String OpenMetrics::getPayload(void) {
"gauge"); "gauge");
add_metric_point("", String(nox)); add_metric_point("", String(nox));
} }
if (utils::isValidNOx(nox_raw)) { if (utils::isValidNOx(noxRaw)) {
add_metric("nox_raw", add_metric("nox_raw",
"The raw input value to the Nitrous Oxide (NOx) index as " "The raw input value to the Nitrous Oxide (NOx) index as "
"measured by the AirGradient SGP sensor", "measured by the AirGradient SGP sensor",
"gauge"); "gauge");
add_metric_point("", String(nox_raw)); add_metric_point("", String(noxRaw));
} }
} }
if (utils::isValidCO2(co2)) {
add_metric("co2",
"Carbon dioxide concentration as measured by the AirGradient S8 "
"sensor, in parts per million",
"gauge", "ppm");
add_metric_point("", String(co2));
}
if (utils::isValidTemperature(_temp)) { if (utils::isValidTemperature(_temp)) {
add_metric( add_metric(
"temperature", "temperature",

View File

@ -36,20 +36,21 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
*/ */
#include <HardwareSerial.h>
#include "AirGradient.h"
#include "OtaHandler.h"
#include "AgApiClient.h" #include "AgApiClient.h"
#include "AgConfigure.h" #include "AgConfigure.h"
#include "AgSchedule.h" #include "AgSchedule.h"
#include "AgStateMachine.h" #include "AgStateMachine.h"
#include "AgWiFiConnector.h" #include "AgWiFiConnector.h"
#include "AirGradient.h"
#include "EEPROM.h" #include "EEPROM.h"
#include "ESPmDNS.h" #include "ESPmDNS.h"
#include "LocalServer.h" #include "LocalServer.h"
#include "MqttClient.h" #include "MqttClient.h"
#include "OpenMetrics.h" #include "OpenMetrics.h"
#include "OtaHandler.h"
#include "WebServer.h" #include "WebServer.h"
#include "esp32c3/rom/rtc.h"
#include <HardwareSerial.h>
#include <WebServer.h> #include <WebServer.h>
#include <WiFi.h> #include <WiFi.h>
@ -111,9 +112,8 @@ static void wdgFeedUpdate(void);
static void ledBarEnabledUpdate(void); static void ledBarEnabledUpdate(void);
static bool sgp41Init(void); static bool sgp41Init(void);
static void firmwareCheckForUpdate(void); static void firmwareCheckForUpdate(void);
static void otaHandlerCallback(OtaState state, String mesasge); static void otaHandlerCallback(OtaHandler::OtaState state, String mesasge);
static void displayExecuteOta(OtaState state, String msg, static void displayExecuteOta(OtaHandler::OtaState state, String msg, int processing);
int processing);
static int calculateMaxPeriod(int updateInterval); static int calculateMaxPeriod(int updateInterval);
static void setMeasurementMaxPeriod(); static void setMeasurementMaxPeriod();
@ -136,6 +136,10 @@ void setup() {
/** Print device ID into log */ /** Print device ID into log */
Serial.println("Serial nr: " + ag->deviceId()); Serial.println("Serial nr: " + ag->deviceId());
// Set reason why esp is reset
esp_reset_reason_t reason = esp_reset_reason();
measurements.setResetReason(reason);
/** Initialize local configure */ /** Initialize local configure */
configuration.begin(); configuration.begin();
@ -168,7 +172,7 @@ void setup() {
boardInit(); boardInit();
setMeasurementMaxPeriod(); setMeasurementMaxPeriod();
// Uncomment below line to print every measurements reading update // Comment below line to disable debug measurement readings
measurements.setDebug(true); measurements.setDebug(true);
/** Connecting wifi */ /** Connecting wifi */
@ -514,29 +518,27 @@ static void firmwareCheckForUpdate(void) {
Serial.println(); Serial.println();
} }
static void otaHandlerCallback(OtaState state, String mesasge) { static void otaHandlerCallback(OtaHandler::OtaState state, String message) {
Serial.println("OTA message: " + mesasge); Serial.println("OTA message: " + message);
switch (state) { switch (state) {
case OtaState::OTA_STATE_BEGIN: case OtaHandler::OTA_STATE_BEGIN:
displayExecuteOta(state, fwNewVersion, 0); displayExecuteOta(state, fwNewVersion, 0);
break; break;
case OtaState::OTA_STATE_FAIL: case OtaHandler::OTA_STATE_FAIL:
displayExecuteOta(state, "", 0); displayExecuteOta(state, "", 0);
break; break;
case OtaState::OTA_STATE_PROCESSING: case OtaHandler::OTA_STATE_PROCESSING:
displayExecuteOta(state, "", mesasge.toInt()); case OtaHandler::OTA_STATE_SUCCESS:
break; displayExecuteOta(state, "", message.toInt());
case OtaState::OTA_STATE_SUCCESS:
displayExecuteOta(state, "", mesasge.toInt());
break; break;
default: default:
break; break;
} }
} }
static void displayExecuteOta(OtaState state, String msg, int processing) { static void displayExecuteOta(OtaHandler::OtaState state, String msg, int processing) {
switch (state) { switch (state) {
case OtaState::OTA_STATE_BEGIN: { case OtaHandler::OTA_STATE_BEGIN: {
if (ag->isOne()) { if (ag->isOne()) {
oledDisplay.showFirmwareUpdateVersion(msg); oledDisplay.showFirmwareUpdateVersion(msg);
} else { } else {
@ -545,7 +547,7 @@ static void displayExecuteOta(OtaState state, String msg, int processing) {
delay(2500); delay(2500);
break; break;
} }
case OtaState::OTA_STATE_FAIL: { case OtaHandler::OTA_STATE_FAIL: {
if (ag->isOne()) { if (ag->isOne()) {
oledDisplay.showFirmwareUpdateFailed(); oledDisplay.showFirmwareUpdateFailed();
} else { } else {
@ -555,7 +557,7 @@ static void displayExecuteOta(OtaState state, String msg, int processing) {
delay(2500); delay(2500);
break; break;
} }
case OtaState::OTA_STATE_SKIP: { case OtaHandler::OTA_STATE_SKIP: {
if (ag->isOne()) { if (ag->isOne()) {
oledDisplay.showFirmwareUpdateSkipped(); oledDisplay.showFirmwareUpdateSkipped();
} else { } else {
@ -565,7 +567,7 @@ static void displayExecuteOta(OtaState state, String msg, int processing) {
delay(2500); delay(2500);
break; break;
} }
case OtaState::OTA_STATE_UP_TO_DATE: { case OtaHandler::OTA_STATE_UP_TO_DATE: {
if (ag->isOne()) { if (ag->isOne()) {
oledDisplay.showFirmwareUpdateUpToDate(); oledDisplay.showFirmwareUpdateUpToDate();
} else { } else {
@ -575,7 +577,7 @@ static void displayExecuteOta(OtaState state, String msg, int processing) {
delay(2500); delay(2500);
break; break;
} }
case OtaState::OTA_STATE_PROCESSING: { case OtaHandler::OTA_STATE_PROCESSING: {
if (ag->isOne()) { if (ag->isOne()) {
oledDisplay.showFirmwareUpdateProgress(processing); oledDisplay.showFirmwareUpdateProgress(processing);
} else { } else {
@ -584,7 +586,7 @@ static void displayExecuteOta(OtaState state, String msg, int processing) {
break; break;
} }
case OtaState::OTA_STATE_SUCCESS: { case OtaHandler::OTA_STATE_SUCCESS: {
int i = 6; int i = 6;
while(i != 0) { while(i != 0) {
i = i - 1; i = i - 1;
@ -634,7 +636,7 @@ static void sendDataToAg() {
"task_led", 2048, NULL, 5, NULL); "task_led", 2048, NULL, 5, NULL);
delay(1500); delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) { if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount())) {
if (ag->isOne()) { if (ag->isOne()) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected); stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} }
@ -1135,7 +1137,8 @@ static void updatePm(void) {
static void sendDataToServer(void) { static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */ /** Increment bootcount when send measurements data is scheduled */
measurements.bootCount++; int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
/** Ignore send data to server if postToAirGradient disabled */ /** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false || configuration.isOfflineMode()) { if (configuration.isPostDataToAirGradient() == false || configuration.isOfflineMode()) {
@ -1149,6 +1152,9 @@ static void sendDataToServer(void) {
"Online mode and isPostToAirGradient = true: watchdog reset"); "Online mode and isPostToAirGradient = true: watchdog reset");
Serial.println(); Serial.println();
} }
/** Log current free heap size */
Serial.printf("Free heap: %u\n", ESP.getFreeHeap());
} }
static void tempHumUpdate(void) { static void tempHumUpdate(void) {
@ -1216,6 +1222,6 @@ void setMeasurementMaxPeriod() {
} }
int calculateMaxPeriod(int updateInterval) { int calculateMaxPeriod(int updateInterval) {
// 0.5 is 50% reduced interval for max period // 0.8 is 80% reduced interval for max period
return (SERVER_SYNC_INTERVAL - (SERVER_SYNC_INTERVAL * 0.5)) / updateInterval; return (SERVER_SYNC_INTERVAL - (SERVER_SYNC_INTERVAL * 0.8)) / updateInterval;
} }

View File

@ -57,22 +57,22 @@ String OpenMetrics::getPayload(void) {
"gauge", "dbm"); "gauge", "dbm");
add_metric_point("", String(wifiConnector.RSSI())); add_metric_point("", String(wifiConnector.RSSI()));
if (config.hasSensorS8 && measure.CO2 >= 0) { // Initialize default invalid value for each measurements
add_metric("co2",
"Carbon dioxide concentration as measured by the AirGradient S8 "
"sensor, in parts per million",
"gauge", "ppm");
add_metric_point("", String(measure.CO2));
}
float _temp = utils::getInvalidTemperature(); float _temp = utils::getInvalidTemperature();
float _hum = utils::getInvalidHumidity(); float _hum = utils::getInvalidHumidity();
int pm01 = utils::getInvalidPmValue(); int pm01 = utils::getInvalidPmValue();
int pm25 = utils::getInvalidPmValue(); int pm25 = utils::getInvalidPmValue();
int pm10 = utils::getInvalidPmValue(); int pm10 = utils::getInvalidPmValue();
int pm03PCount = utils::getInvalidPmValue(); int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature(); int atmpCompensated = utils::getInvalidTemperature();
int ahumCompensated = utils::getInvalidHumidity(); int ahumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
int noxRaw = utils::getInvalidNOx();
// Get values
if (config.hasSensorPMS1 && config.hasSensorPMS2) { if (config.hasSensorPMS1 && config.hasSensorPMS2) {
_temp = (measure.getFloat(Measurements::Temperature, 1) + _temp = (measure.getFloat(Measurements::Temperature, 1) +
measure.getFloat(Measurements::Temperature, 2)) / measure.getFloat(Measurements::Temperature, 2)) /
@ -81,7 +81,10 @@ String OpenMetrics::getPayload(void) {
measure.getFloat(Measurements::Humidity, 2)) / measure.getFloat(Measurements::Humidity, 2)) /
2.0f; 2.0f;
pm01 = (measure.get(Measurements::PM01, 1) + measure.get(Measurements::PM01, 2)) / 2.0f; pm01 = (measure.get(Measurements::PM01, 1) + measure.get(Measurements::PM01, 2)) / 2.0f;
pm25 = (measure.get(Measurements::PM25, 1) + measure.get(Measurements::PM25, 2)) / 2.0f; float correctedPm25_1 = measure.getCorrectedPM25(*ag, config, false, 1);
float correctedPm25_2 = measure.getCorrectedPM25(*ag, config, false, 2);
float correctedPm25 = (correctedPm25_1 + correctedPm25_2) / 2.0f;
pm25 = round(correctedPm25);
pm10 = (measure.get(Measurements::PM10, 1) + measure.get(Measurements::PM10, 2)) / 2.0f; pm10 = (measure.get(Measurements::PM10, 1) + measure.get(Measurements::PM10, 2)) / 2.0f;
pm03PCount = pm03PCount =
(measure.get(Measurements::PM03_PC, 1) + measure.get(Measurements::PM03_PC, 2)) / 2.0f; (measure.get(Measurements::PM03_PC, 1) + measure.get(Measurements::PM03_PC, 2)) / 2.0f;
@ -94,7 +97,8 @@ String OpenMetrics::getPayload(void) {
if (config.hasSensorPMS1) { if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01); pm01 = measure.get(Measurements::PM01);
pm25 = measure.get(Measurements::PM25); float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10); pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC); pm03PCount = measure.get(Measurements::PM03_PC);
} }
@ -103,7 +107,8 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature, 1); _temp = measure.getFloat(Measurements::Temperature, 1);
_hum = measure.getFloat(Measurements::Humidity, 1); _hum = measure.getFloat(Measurements::Humidity, 1);
pm01 = measure.get(Measurements::PM01, 1); pm01 = measure.get(Measurements::PM01, 1);
pm25 = measure.get(Measurements::PM25, 1); float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10, 1); pm10 = measure.get(Measurements::PM10, 1);
pm03PCount = measure.get(Measurements::PM03_PC, 1); pm03PCount = measure.get(Measurements::PM03_PC, 1);
} }
@ -111,13 +116,25 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature, 2); _temp = measure.getFloat(Measurements::Temperature, 2);
_hum = measure.getFloat(Measurements::Humidity, 2); _hum = measure.getFloat(Measurements::Humidity, 2);
pm01 = measure.get(Measurements::PM01, 2); pm01 = measure.get(Measurements::PM01, 2);
pm25 = measure.get(Measurements::PM25, 2); float correctedPm = measure.getCorrectedPM25(*ag, config, false, 2);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10, 2); pm10 = measure.get(Measurements::PM10, 2);
pm03PCount = measure.get(Measurements::PM03_PC, 2); pm03PCount = measure.get(Measurements::PM03_PC, 2);
} }
} }
} }
if (config.hasSensorSGP) {
tvoc = measure.get(Measurements::TVOC);
tvocRaw = measure.get(Measurements::TVOCRaw);
nox = measure.get(Measurements::NOx);
noxRaw = measure.get(Measurements::NOxRaw);
}
if (config.hasSensorS8) {
co2 = measure.get(Measurements::CO2);
}
/** Get temperature and humidity compensated */ /** Get temperature and humidity compensated */
if (ag->isOne()) { if (ag->isOne()) {
atmpCompensated = _temp; atmpCompensated = _temp;
@ -127,6 +144,7 @@ String OpenMetrics::getPayload(void) {
ahumCompensated = ag->pms5003t_1.compensateHum(_hum); ahumCompensated = ag->pms5003t_1.compensateHum(_hum);
} }
// Add measurements that valid to the metrics
if (config.hasSensorPMS1 || config.hasSensorPMS2) { if (config.hasSensorPMS1 || config.hasSensorPMS2) {
if (utils::isValidPm(pm01)) { if (utils::isValidPm(pm01)) {
add_metric("pm1", add_metric("pm1",
@ -159,36 +177,44 @@ String OpenMetrics::getPayload(void) {
} }
if (config.hasSensorSGP) { if (config.hasSensorSGP) {
if (utils::isValidVOC(measure.TVOC)) { if (utils::isValidVOC(tvoc)) {
add_metric("tvoc_index", add_metric("tvoc_index",
"The processed Total Volatile Organic Compounds (TVOC) index " "The processed Total Volatile Organic Compounds (TVOC) index "
"as measured by the AirGradient SGP sensor", "as measured by the AirGradient SGP sensor",
"gauge"); "gauge");
add_metric_point("", String(measure.TVOC)); add_metric_point("", String(tvoc));
} }
if (utils::isValidVOC(measure.TVOCRaw)) { if (utils::isValidVOC(tvocRaw)) {
add_metric("tvoc_raw", add_metric("tvoc_raw",
"The raw input value to the Total Volatile Organic Compounds " "The raw input value to the Total Volatile Organic Compounds "
"(TVOC) index as measured by the AirGradient SGP sensor", "(TVOC) index as measured by the AirGradient SGP sensor",
"gauge"); "gauge");
add_metric_point("", String(measure.TVOCRaw)); add_metric_point("", String(tvocRaw));
} }
if (utils::isValidNOx(measure.NOx)) { if (utils::isValidNOx(nox)) {
add_metric("nox_index", add_metric("nox_index",
"The processed Nitrous Oxide (NOx) index as measured by the " "The processed Nitrous Oxide (NOx) index as measured by the "
"AirGradient SGP sensor", "AirGradient SGP sensor",
"gauge"); "gauge");
add_metric_point("", String(measure.NOx)); add_metric_point("", String(nox));
} }
if (utils::isValidNOx(measure.NOxRaw)) { if (utils::isValidNOx(noxRaw)) {
add_metric("nox_raw", add_metric("nox_raw",
"The raw input value to the Nitrous Oxide (NOx) index as " "The raw input value to the Nitrous Oxide (NOx) index as "
"measured by the AirGradient SGP sensor", "measured by the AirGradient SGP sensor",
"gauge"); "gauge");
add_metric_point("", String(measure.NOxRaw)); add_metric_point("", String(noxRaw));
} }
} }
if (utils::isValidCO2(co2)) {
add_metric("co2",
"Carbon dioxide concentration as measured by the AirGradient S8 "
"sensor, in parts per million",
"gauge", "ppm");
add_metric_point("", String(co2));
}
if (utils::isValidTemperature(_temp)) { if (utils::isValidTemperature(_temp)) {
add_metric("temperature", add_metric("temperature",
"The ambient temperature as measured by the AirGradient SHT / PMS " "The ambient temperature as measured by the AirGradient SHT / PMS "
@ -197,25 +223,21 @@ String OpenMetrics::getPayload(void) {
add_metric_point("", String(_temp)); add_metric_point("", String(_temp));
} }
if (utils::isValidTemperature(atmpCompensated)) { if (utils::isValidTemperature(atmpCompensated)) {
add_metric( add_metric("temperature_compensated",
"temperature_compensated", "The compensated ambient temperature as measured by the AirGradient SHT / PMS "
"The compensated ambient temperature as measured by the AirGradient SHT / PMS " "sensor, in degrees Celsius",
"sensor, in degrees Celsius", "gauge", "celsius");
"gauge", "celsius");
add_metric_point("", String(atmpCompensated)); add_metric_point("", String(atmpCompensated));
} }
if (utils::isValidHumidity(_hum)) { if (utils::isValidHumidity(_hum)) {
add_metric( add_metric("humidity", "The relative humidity as measured by the AirGradient SHT sensor",
"humidity", "gauge", "percent");
"The relative humidity as measured by the AirGradient SHT sensor",
"gauge", "percent");
add_metric_point("", String(_hum)); add_metric_point("", String(_hum));
} }
if (utils::isValidHumidity(ahumCompensated)) { if (utils::isValidHumidity(ahumCompensated)) {
add_metric( add_metric("humidity_compensated",
"humidity_compensated", "The compensated relative humidity as measured by the AirGradient SHT / PMS sensor",
"The compensated relative humidity as measured by the AirGradient SHT / PMS sensor", "gauge", "percent");
"gauge", "percent");
add_metric_point("", String(ahumCompensated)); add_metric_point("", String(ahumCompensated));
} }

View File

@ -1,206 +0,0 @@
#ifndef _OTA_HANDLER_H_
#define _OTA_HANDLER_H_
#include <Arduino.h>
#include <esp_err.h>
#include <esp_http_client.h>
#include <esp_ota_ops.h>
#define OTA_BUF_SIZE 1024
#define URL_BUF_SIZE 256
enum OtaUpdateOutcome {
UPDATE_PERFORMED,
ALREADY_UP_TO_DATE,
UPDATE_FAILED,
UDPATE_SKIPPED
};
enum OtaState {
OTA_STATE_BEGIN,
OTA_STATE_FAIL,
OTA_STATE_SKIP,
OTA_STATE_UP_TO_DATE,
OTA_STATE_PROCESSING,
OTA_STATE_SUCCESS
};
typedef void(*OtaHandlerCallback_t)(OtaState state,
String message);
class OtaHandler {
public:
void updateFirmwareIfOutdated(String deviceId) {
String url = "http://hw.airgradient.com/sensors/airgradient:" + deviceId +
"/generic/os/firmware.bin";
url += "?current_firmware=";
url += GIT_VERSION;
char urlAsChar[URL_BUF_SIZE];
url.toCharArray(urlAsChar, URL_BUF_SIZE);
Serial.printf("checking for new OTA update @ %s\n", urlAsChar);
esp_http_client_config_t config = {};
config.url = urlAsChar;
OtaUpdateOutcome ret = attemptToPerformOta(&config);
Serial.println(ret);
if (this->callback) {
switch (ret) {
case OtaUpdateOutcome::UPDATE_PERFORMED:
this->callback(OtaState::OTA_STATE_SUCCESS, "");
break;
case OtaUpdateOutcome::UDPATE_SKIPPED:
this->callback(OtaState::OTA_STATE_SKIP, "");
break;
case OtaUpdateOutcome::ALREADY_UP_TO_DATE:
this->callback(OtaState::OTA_STATE_UP_TO_DATE, "");
break;
case OtaUpdateOutcome::UPDATE_FAILED:
this->callback(OtaState::OTA_STATE_FAIL, "");
break;
default:
break;
}
}
}
void setHandlerCallback(OtaHandlerCallback_t callback) {
this->callback = callback;
}
private:
OtaHandlerCallback_t callback;
OtaUpdateOutcome attemptToPerformOta(const esp_http_client_config_t *config) {
esp_http_client_handle_t client = esp_http_client_init(config);
if (client == NULL) {
Serial.println("Failed to initialize HTTP connection");
return OtaUpdateOutcome::UPDATE_FAILED;
}
esp_err_t err = esp_http_client_open(client, 0);
if (err != ESP_OK) {
esp_http_client_cleanup(client);
Serial.printf("Failed to open HTTP connection: %s\n",
esp_err_to_name(err));
return OtaUpdateOutcome::UPDATE_FAILED;
}
esp_http_client_fetch_headers(client);
int httpStatusCode = esp_http_client_get_status_code(client);
if (httpStatusCode == 304) {
Serial.println("Firmware is already up to date");
cleanupHttp(client);
return OtaUpdateOutcome::ALREADY_UP_TO_DATE;
} else if (httpStatusCode != 200) {
Serial.printf("Firmware update skipped, the server returned %d\n",
httpStatusCode);
cleanupHttp(client);
return OtaUpdateOutcome::UDPATE_SKIPPED;
}
esp_ota_handle_t update_handle = 0;
const esp_partition_t *update_partition = NULL;
Serial.println("Starting OTA update ...");
update_partition = esp_ota_get_next_update_partition(NULL);
if (update_partition == NULL) {
Serial.println("Passive OTA partition not found");
cleanupHttp(client);
return OtaUpdateOutcome::UPDATE_FAILED;
}
Serial.printf("Writing to partition subtype %d at offset 0x%x\n",
update_partition->subtype, update_partition->address);
err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle);
if (err != ESP_OK) {
Serial.printf("esp_ota_begin failed, error=%d\n", err);
cleanupHttp(client);
return OtaUpdateOutcome::UPDATE_FAILED;
}
esp_err_t ota_write_err = ESP_OK;
char *upgrade_data_buf = (char *)malloc(OTA_BUF_SIZE);
if (!upgrade_data_buf) {
Serial.println("Couldn't allocate memory for data buffer");
return OtaUpdateOutcome::UPDATE_FAILED;
}
int binary_file_len = 0;
int totalSize = esp_http_client_get_content_length(client);
Serial.println("File size: " + String(totalSize) + String(" bytes"));
// Show display start update new firmware.
if (this->callback) {
this->callback(OtaState::OTA_STATE_BEGIN, "");
}
// Download file and write new firmware to OTA partition
uint32_t lastUpdate = millis();
while (1) {
int data_read =
esp_http_client_read(client, upgrade_data_buf, OTA_BUF_SIZE);
if (data_read == 0) {
if (this->callback) {
this->callback(OtaState::OTA_STATE_PROCESSING, String(100));
}
Serial.println("Connection closed, all data received");
break;
}
if (data_read < 0) {
Serial.println("Data read error");
if (this->callback) {
this->callback(OtaState::OTA_STATE_FAIL, "");
}
break;
}
if (data_read > 0) {
ota_write_err = esp_ota_write(
update_handle, (const void *)upgrade_data_buf, data_read);
if (ota_write_err != ESP_OK) {
if (this->callback) {
this->callback(OtaState::OTA_STATE_FAIL, "");
}
break;
}
binary_file_len += data_read;
int percent = (binary_file_len * 100) / totalSize;
uint32_t ms = (uint32_t)(millis() - lastUpdate);
if (ms >= 250) {
// sm.executeOTA(StateMachine::OtaState::OTA_STATE_PROCESSING, "",
// percent);
if (this->callback) {
this->callback(OtaState::OTA_STATE_PROCESSING,
String(percent));
}
lastUpdate = millis();
}
}
}
free(upgrade_data_buf);
cleanupHttp(client);
Serial.printf("# of bytes written: %d\n", binary_file_len);
esp_err_t ota_end_err = esp_ota_end(update_handle);
if (ota_write_err != ESP_OK) {
Serial.printf("Error: esp_ota_write failed! err=0x%d\n", err);
return OtaUpdateOutcome::UPDATE_FAILED;
} else if (ota_end_err != ESP_OK) {
Serial.printf("Error: esp_ota_end failed! err=0x%d. Image is invalid",
ota_end_err);
return OtaUpdateOutcome::UPDATE_FAILED;
}
err = esp_ota_set_boot_partition(update_partition);
if (err != ESP_OK) {
Serial.printf("esp_ota_set_boot_partition failed! err=0x%d\n", err);
return OtaUpdateOutcome::UPDATE_FAILED;
}
return OtaUpdateOutcome::UPDATE_PERFORMED;
}
void cleanupHttp(esp_http_client_handle_t client) {
esp_http_client_close(client);
esp_http_client_cleanup(client);
}
};
#endif

View File

@ -1,5 +1,5 @@
name=AirGradient Air Quality Sensor name=AirGradient Air Quality Sensor
version=3.1.10 version=3.1.21
author=AirGradient <support@airgradient.com> author=AirGradient <support@airgradient.com>
maintainer=AirGradient <support@airgradient.com> maintainer=AirGradient <support@airgradient.com>
sentence=ESP32-C3 / ESP8266 library for air quality monitor measuring PM, CO2, Temperature, TVOC and Humidity with OLED display. sentence=ESP32-C3 / ESP8266 library for air quality monitor measuring PM, CO2, Temperature, TVOC and Humidity with OLED display.

View File

@ -59,9 +59,20 @@ bool AgApiClient::fetchServerConfiguration(void) {
#else #else
HTTPClient client; HTTPClient client;
client.setTimeout(timeoutMs); client.setTimeout(timeoutMs);
if (client.begin(uri) == false) { if (apiRootChanged) {
getConfigFailed = true; // If apiRoot is changed, assume not using https
return false; if (client.begin(uri) == false) {
logError("Begin HTTPClient failed (GET)");
getConfigFailed = true;
return false;
}
} else {
// By default, airgradient using https
if (client.begin(uri, AG_SERVER_ROOT_CA) == false) {
logError("Begin HTTPClient using tls failed (GET)");
getConfigFailed = true;
return false;
}
} }
#endif #endif
@ -90,8 +101,6 @@ bool AgApiClient::fetchServerConfiguration(void) {
String respContent = client.getString(); String respContent = client.getString();
client.end(); client.end();
// logInfo("Get configuration: " + respContent);
/** Parse configuration and return result */ /** Parse configuration and return result */
return config.parse(respContent, false); return config.parse(respContent, false);
} }
@ -115,22 +124,37 @@ bool AgApiClient::postToServer(String data) {
} }
String uri = apiRoot + "/sensors/airgradient:" + ag->deviceId() + "/measures"; String uri = apiRoot + "/sensors/airgradient:" + ag->deviceId() + "/measures";
// logInfo("Post uri: " + uri); #ifdef ESP8266
// logInfo("Post data: " + data);
WiFiClient wifiClient;
HTTPClient client; HTTPClient client;
client.setTimeout(timeoutMs); WiFiClient wifiClient;
if (client.begin(wifiClient, uri.c_str()) == false) { if (client.begin(wifiClient, uri) == false) {
logError("Init client failed"); getConfigFailed = true;
return false; return false;
} }
#else
HTTPClient client;
client.setTimeout(timeoutMs);
if (apiRootChanged) {
// If apiRoot is changed, assume not using https
if (client.begin(uri) == false) {
logError("Begin HTTPClient failed (POST)");
getConfigFailed = true;
return false;
}
} else {
// By default, airgradient using https
if (client.begin(uri, AG_SERVER_ROOT_CA) == false) {
logError("Begin HTTPClient using tls failed (POST)");
getConfigFailed = true;
return false;
}
}
#endif
client.addHeader("content-type", "application/json"); client.addHeader("content-type", "application/json");
int retCode = client.POST(data); int retCode = client.POST(data);
client.end(); client.end();
logInfo(String("POST: ") + uri); logInfo(String("POST: ") + uri);
// logInfo(String("DATA: ") + data);
logInfo(String("Return code: ") + String(retCode)); logInfo(String("Return code: ") + String(retCode));
if ((retCode == 200) || (retCode == 429)) { if ((retCode == 200) || (retCode == 429)) {
@ -189,7 +213,10 @@ bool AgApiClient::sendPing(int rssi, int bootCount) {
String AgApiClient::getApiRoot() const { return apiRoot; } String AgApiClient::getApiRoot() const { return apiRoot; }
void AgApiClient::setApiRoot(const String &apiRoot) { this->apiRoot = apiRoot; } void AgApiClient::setApiRoot(const String &apiRoot) {
this->apiRootChanged = true;
this->apiRoot = apiRoot;
}
/** /**
* @brief Set http request timeout. (Default: 10s) * @brief Set http request timeout. (Default: 10s)

View File

@ -20,8 +20,14 @@ class AgApiClient : public PrintLog {
private: private:
Configuration &config; Configuration &config;
AirGradient *ag; AirGradient *ag;
#ifdef ESP8266
// ESP8266 not support HTTPS
String apiRoot = "http://hw.airgradient.com"; String apiRoot = "http://hw.airgradient.com";
#else
String apiRoot = "https://hw.airgradient.com";
#endif
bool apiRootChanged = false; // Indicate if setApiRoot() is called
bool getConfigFailed; bool getConfigFailed;
bool postToServerFailed; bool postToServerFailed;
bool notAvailableOnDashboard = false; // Device not setup on Airgradient cloud dashboard. bool notAvailableOnDashboard = false; // Device not setup on Airgradient cloud dashboard.

View File

@ -292,8 +292,8 @@ void Configuration::defaultConfig(void) {
// PM2.5 correction // PM2.5 correction
pmCorrection.algorithm = None; pmCorrection.algorithm = None;
pmCorrection.changed = false; pmCorrection.changed = false;
pmCorrection.intercept = -1; pmCorrection.intercept = 0;
pmCorrection.scalingFactor = -1; pmCorrection.scalingFactor = 1;
pmCorrection.useEPA = false; pmCorrection.useEPA = false;
saveConfig(); saveConfig();
@ -354,16 +354,16 @@ bool Configuration::begin(void) {
* @return false Failure * @return false Failure
*/ */
bool Configuration::parse(String data, bool isLocal) { bool Configuration::parse(String data, bool isLocal) {
logInfo("Parse configure: " + data); logInfo("Parsing configuration: " + data);
JSONVar root = JSON.parse(data); JSONVar root = JSON.parse(data);
failedMessage = ""; failedMessage = "";
if (root == undefined) { if (root == undefined || JSONVar::typeof_(root) != "object") {
logError("Parse configuration failed, JSON invalid (" + JSONVar::typeof_(root) + ")");
failedMessage = "JSON invalid"; failedMessage = "JSON invalid";
logError(failedMessage);
return false; return false;
} }
logInfo("Parse configure success"); logInfo("Parse configuration success");
/** Is configuration changed */ /** Is configuration changed */
bool changed = false; bool changed = false;
@ -1369,7 +1369,12 @@ bool Configuration::isPMCorrectionChanged(void) {
*/ */
bool Configuration::isPMCorrectionEnabled(void) { bool Configuration::isPMCorrectionEnabled(void) {
PMCorrection pmCorrection = getPMCorrection(); PMCorrection pmCorrection = getPMCorrection();
return pmCorrection.algorithm != PMCorrectionAlgorithm::None; if (pmCorrection.algorithm == PMCorrectionAlgorithm::None ||
pmCorrection.algorithm == PMCorrectionAlgorithm::Unknown) {
return false;
}
return true;
} }
Configuration::PMCorrection Configuration::getPMCorrection(void) { Configuration::PMCorrection Configuration::getPMCorrection(void) {

View File

@ -12,7 +12,7 @@
*/ */
void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) { void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) {
/** Temperature */ /** Temperature */
float temp = value.getFloat(Measurements::Temperature); float temp = value.getAverage(Measurements::Temperature);
if (utils::isValidTemperature(temp)) { if (utils::isValidTemperature(temp)) {
float t = 0.0f; float t = 0.0f;
if (config.isTemperatureUnitInF()) { if (config.isTemperatureUnitInF()) {
@ -44,7 +44,7 @@ void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) {
DISP()->drawUTF8(1, 10, buf); DISP()->drawUTF8(1, 10, buf);
/** Show humidity */ /** Show humidity */
int rhum = (int)value.getFloat(Measurements::Humidity); int rhum = round(value.getAverage(Measurements::Humidity));
if (utils::isValidHumidity(rhum)) { if (utils::isValidHumidity(rhum)) {
snprintf(buf, buf_size, "%d%%", rhum); snprintf(buf, buf_size, "%d%%", rhum);
} else { } else {
@ -292,7 +292,7 @@ void OledDisplay::showDashboard(const char *status) {
DISP()->drawUTF8(1, 27, "CO2"); DISP()->drawUTF8(1, 27, "CO2");
DISP()->setFont(u8g2_font_t0_22b_tf); DISP()->setFont(u8g2_font_t0_22b_tf);
int co2 = value.get(Measurements::CO2); int co2 = round(value.getAverage(Measurements::CO2));
if (utils::isValidCO2(co2)) { if (utils::isValidCO2(co2)) {
sprintf(strBuf, "%d", co2); sprintf(strBuf, "%d", co2);
} else { } else {
@ -313,11 +313,10 @@ void OledDisplay::showDashboard(const char *status) {
DISP()->drawStr(55, 27, "PM2.5"); DISP()->drawStr(55, 27, "PM2.5");
/** Draw PM2.5 value */ /** Draw PM2.5 value */
int pm25 = round(value.getAverage(Measurements::PM25));
int pm25 = value.get(Measurements::PM25);
if (utils::isValidPm(pm25)) { if (utils::isValidPm(pm25)) {
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) { if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25 = (int)value.getCorrectedPM25(*ag, config); pm25 = round(value.getCorrectedPM25(*ag, config, true));
} }
if (config.isPmStandardInUSAQI()) { if (config.isPmStandardInUSAQI()) {
sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(pm25)); sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(pm25));
@ -343,7 +342,7 @@ void OledDisplay::showDashboard(const char *status) {
DISP()->drawStr(100, 27, "VOC:"); DISP()->drawStr(100, 27, "VOC:");
/** Draw tvocIndexvalue */ /** Draw tvocIndexvalue */
int tvoc = value.get(Measurements::TVOC); int tvoc = round(value.getAverage(Measurements::TVOC));
if (utils::isValidVOC(tvoc)) { if (utils::isValidVOC(tvoc)) {
sprintf(strBuf, "%d", tvoc); sprintf(strBuf, "%d", tvoc);
} else { } else {
@ -352,7 +351,7 @@ void OledDisplay::showDashboard(const char *status) {
DISP()->drawStr(100, 39, strBuf); DISP()->drawStr(100, 39, strBuf);
/** Draw NOx label */ /** Draw NOx label */
int nox = value.get(Measurements::NOx); int nox = round(value.getAverage(Measurements::NOx));
DISP()->drawStr(100, 53, "NOx:"); DISP()->drawStr(100, 53, "NOx:");
if (utils::isValidNOx(nox)) { if (utils::isValidNOx(nox)) {
sprintf(strBuf, "%d", nox); sprintf(strBuf, "%d", nox);
@ -365,7 +364,7 @@ void OledDisplay::showDashboard(const char *status) {
ag->display.clear(); ag->display.clear();
/** Set CO2 */ /** Set CO2 */
int co2 = value.get(Measurements::CO2); int co2 = round(value.getAverage(Measurements::CO2));
if (utils::isValidCO2(co2)) { if (utils::isValidCO2(co2)) {
snprintf(strBuf, sizeof(strBuf), "CO2:%d", co2); snprintf(strBuf, sizeof(strBuf), "CO2:%d", co2);
} else { } else {
@ -376,9 +375,9 @@ void OledDisplay::showDashboard(const char *status) {
ag->display.setText(strBuf); ag->display.setText(strBuf);
/** Set PM */ /** Set PM */
int pm25 = value.get(Measurements::PM25); int pm25 = round(value.getAverage(Measurements::PM25));
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) { if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25 = (int)value.getCorrectedPM25(*ag, config); pm25 = round(value.getCorrectedPM25(*ag, config, true));
} }
ag->display.setCursor(0, 12); ag->display.setCursor(0, 12);
@ -390,12 +389,12 @@ void OledDisplay::showDashboard(const char *status) {
ag->display.setText(strBuf); ag->display.setText(strBuf);
/** Set temperature and humidity */ /** Set temperature and humidity */
float temp = value.getFloat(Measurements::Temperature); float temp = value.getAverage(Measurements::Temperature);
if (utils::isValidTemperature(temp)) { if (utils::isValidTemperature(temp)) {
if (config.isTemperatureUnitInF()) { if (config.isTemperatureUnitInF()) {
snprintf(strBuf, sizeof(strBuf), "T:%0.1f F", utils::degreeC_To_F(temp)); snprintf(strBuf, sizeof(strBuf), "T:%0.1f F", utils::degreeC_To_F(temp));
} else { } else {
snprintf(strBuf, sizeof(strBuf), "T:%0.f1 C", temp); snprintf(strBuf, sizeof(strBuf), "T:%0.1f C", temp);
} }
} else { } else {
if (config.isTemperatureUnitInF()) { if (config.isTemperatureUnitInF()) {
@ -408,7 +407,7 @@ void OledDisplay::showDashboard(const char *status) {
ag->display.setCursor(0, 24); ag->display.setCursor(0, 24);
ag->display.setText(strBuf); ag->display.setText(strBuf);
int rhum = (int)value.getFloat(Measurements::Humidity); int rhum = round(value.getAverage(Measurements::Humidity));
if (utils::isValidHumidity(rhum)) { if (utils::isValidHumidity(rhum)) {
snprintf(strBuf, sizeof(strBuf), "H:%d %%", rhum); snprintf(strBuf, sizeof(strBuf), "H:%d %%", rhum);
} else { } else {

View File

@ -13,6 +13,7 @@
#define RGB_COLOR_Y 255, 150, 0 /** Yellow */ #define RGB_COLOR_Y 255, 150, 0 /** Yellow */
#define RGB_COLOR_O 255, 40, 0 /** Orange */ #define RGB_COLOR_O 255, 40, 0 /** Orange */
#define RGB_COLOR_P 180, 0, 255 /** Purple */ #define RGB_COLOR_P 180, 0, 255 /** Purple */
#define RGB_COLOR_CLEAR 0, 0, 0 /** No color */
/** /**
* @brief Animation LED bar with color * @brief Animation LED bar with color
@ -47,55 +48,76 @@ void StateMachine::ledStatusBlinkDelay(uint32_t ms) {
} }
/** /**
* @brief Led bar show led color status * @brief Led bar show PM or CO2 led color status
* *
* @return true if all led bar are used, false othwerwise
*/ */
void StateMachine::sensorhandleLeds(void) { bool StateMachine::sensorhandleLeds(void) {
int totalLedUsed = 0;
switch (config.getLedBarMode()) { switch (config.getLedBarMode()) {
case LedBarMode::LedBarModeCO2: case LedBarMode::LedBarModeCO2:
co2handleLeds(); totalLedUsed = co2handleLeds();
break; break;
case LedBarMode::LedBarModePm: case LedBarMode::LedBarModePm:
pm25handleLeds(); totalLedUsed = pm25handleLeds();
break; break;
default: default:
ag->ledBar.clear(); ag->ledBar.clear();
break; break;
} }
if (totalLedUsed == ag->ledBar.getNumberOfLeds()) {
return true;
}
// Clear the rest of unused led
int startIndex = totalLedUsed + 1;
for (int i = startIndex; i <= ag->ledBar.getNumberOfLeds(); i++) {
ag->ledBar.setColor(RGB_COLOR_CLEAR, ag->ledBar.getNumberOfLeds() - i);
}
return false;
} }
/** /**
* @brief Show CO2 LED status * @brief Show CO2 LED status
* *
* @return return total number of led that are used on the monitor
*/ */
void StateMachine::co2handleLeds(void) { int StateMachine::co2handleLeds(void) {
int co2Value = value.get(Measurements::CO2); int totalUsed = ag->ledBar.getNumberOfLeds();
if (co2Value <= 700) { int co2Value = round(value.getAverage(Measurements::CO2));
if (co2Value <= 600) {
/** G; 1 */ /** G; 1 */
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1); ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
} else if (co2Value <= 1000) { totalUsed = 1;
} else if (co2Value <= 800) {
/** GG; 2 */ /** GG; 2 */
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1); ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 2); ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 2);
} else if (co2Value <= 1333) { totalUsed = 2;
} else if (co2Value <= 1000) {
/** YYY; 3 */ /** YYY; 3 */
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 1); ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 2); ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 3); ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 3);
} else if (co2Value <= 1666) { totalUsed = 3;
} else if (co2Value <= 1250) {
/** OOOO; 4 */ /** OOOO; 4 */
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 1); ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 2); ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 3); ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 4); ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4);
} else if (co2Value <= 2000) { totalUsed = 4;
} else if (co2Value <= 1500) {
/** OOOOO; 5 */ /** OOOOO; 5 */
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 1); ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 2); ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 3); ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 4); ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 5); ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 5);
} else if (co2Value <= 2666) { totalUsed = 5;
} else if (co2Value <= 1750) {
/** RRRRRR; 6 */ /** RRRRRR; 6 */
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
@ -103,7 +125,8 @@ void StateMachine::co2handleLeds(void) {
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 4); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 5); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
} else if (co2Value <= 3333) { totalUsed = 6;
} else if (co2Value <= 2000) {
/** RRRRRRR; 7 */ /** RRRRRRR; 7 */
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
@ -112,17 +135,19 @@ void StateMachine::co2handleLeds(void) {
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 5); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7);
} else if (co2Value <= 4000) { totalUsed = 7;
/** RRRRRRRR; 8 */ } else if (co2Value <= 3000) {
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1); /** PPPPPPPP; 8 */
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2); ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 3); ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 4); ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 5); ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6); ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7); ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8); ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7);
} else { /** > 4000 */ ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 8);
totalUsed = 8;
} else { /** > 3000 */
/* PRPRPRPRP; 9 */ /* PRPRPRPRP; 9 */
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1); ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
@ -133,37 +158,47 @@ void StateMachine::co2handleLeds(void) {
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7); ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9); ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9);
totalUsed = 9;
} }
return totalUsed;
} }
/** /**
* @brief Show PM2.5 LED status * @brief Show PM2.5 LED status
* *
* @return return total number of led that are used on the monitor
*/ */
void StateMachine::pm25handleLeds(void) { int StateMachine::pm25handleLeds(void) {
int pm25Value = value.get(Measurements::PM25); int totalUsed = ag->ledBar.getNumberOfLeds();
int pm25Value = round(value.getAverage(Measurements::PM25));
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) { if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25Value = (int)value.getCorrectedPM25(*ag, config); pm25Value = round(value.getCorrectedPM25(*ag, config, true));
} }
if (pm25Value <= 5) { if (pm25Value <= 5) {
/** G; 1 */ /** G; 1 */
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1); ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
totalUsed = 1;
} else if (pm25Value <= 9) { } else if (pm25Value <= 9) {
/** GG; 2 */ /** GG; 2 */
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1); ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 2); ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 2);
totalUsed = 2;
} else if (pm25Value <= 20) { } else if (pm25Value <= 20) {
/** YYY; 3 */ /** YYY; 3 */
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 1); ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 2); ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 3); ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 3);
totalUsed = 3;
} else if (pm25Value <= 35) { } else if (pm25Value <= 35) {
/** YYYY; 4 */ /** YYYY; 4 */
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 1); ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 2); ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 3); ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 4); ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 4);
totalUsed = 4;
} else if (pm25Value <= 45) { } else if (pm25Value <= 45) {
/** OOOOO; 5 */ /** OOOOO; 5 */
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1); ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1);
@ -171,6 +206,7 @@ void StateMachine::pm25handleLeds(void) {
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 3); ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4); ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 5); ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 5);
totalUsed = 5;
} else if (pm25Value <= 55) { } else if (pm25Value <= 55) {
/** OOOOOO; 6 */ /** OOOOOO; 6 */
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1); ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1);
@ -179,6 +215,7 @@ void StateMachine::pm25handleLeds(void) {
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4); ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 5); ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 6); ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 6);
totalUsed = 6;
} else if (pm25Value <= 100) { } else if (pm25Value <= 100) {
/** RRRRRRR; 7 */ /** RRRRRRR; 7 */
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
@ -188,6 +225,7 @@ void StateMachine::pm25handleLeds(void) {
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 5); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7);
totalUsed = 7;
} else if (pm25Value <= 125) { } else if (pm25Value <= 125) {
/** RRRRRRRR; 8 */ /** RRRRRRRR; 8 */
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
@ -198,6 +236,7 @@ void StateMachine::pm25handleLeds(void) {
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8);
totalUsed = 8;
} else if (pm25Value <= 225) { } else if (pm25Value <= 225) {
/** PPPPPPPPP; 9 */ /** PPPPPPPPP; 9 */
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1); ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1);
@ -209,6 +248,7 @@ void StateMachine::pm25handleLeds(void) {
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7); ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 8); ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 8);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9); ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9);
totalUsed = 9;
} else { /** > 225 */ } else { /** > 225 */
/* PRPRPRPRP; 9 */ /* PRPRPRPRP; 9 */
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1); ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1);
@ -220,7 +260,10 @@ void StateMachine::pm25handleLeds(void) {
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7); ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8); ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9); ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9);
totalUsed = 9;
} }
return totalUsed;
} }
void StateMachine::co2Calibration(void) { void StateMachine::co2Calibration(void) {
@ -311,6 +354,7 @@ void StateMachine::co2Calibration(void) {
void StateMachine::ledBarTest(void) { void StateMachine::ledBarTest(void) {
if (config.isLedBarTestRequested()) { if (config.isLedBarTestRequested()) {
if (ag->isOne()) { if (ag->isOne()) {
ag->ledBar.clear();
if (config.getCountry() == "TH") { if (config.getCountry() == "TH") {
uint32_t tstart = millis(); uint32_t tstart = millis();
logInfo("Start run LED test for 2 min"); logInfo("Start run LED test for 2 min");
@ -332,7 +376,12 @@ void StateMachine::ledBarTest(void) {
} }
} }
void StateMachine::ledBarPowerUpTest(void) { ledBarRunTest(); } void StateMachine::ledBarPowerUpTest(void) {
if (ag->isOne()) {
ag->ledBar.clear();
}
ledBarRunTest();
}
void StateMachine::ledBarRunTest(void) { void StateMachine::ledBarRunTest(void) {
if (ag->isOne()) { if (ag->isOne()) {
@ -585,15 +634,13 @@ void StateMachine::handleLeds(AgStateMachineState state) {
} }
ledState = state; ledState = state;
if (ag->isOne()) {
ag->ledBar.clear(); // Set all LED OFF
}
switch (state) { switch (state) {
case AgStateMachineWiFiManagerMode: { case AgStateMachineWiFiManagerMode: {
/** In WiFi Manager Mode */ /** In WiFi Manager Mode */
/** Turn LED OFF */ /** Turn LED OFF */
/** Turn middle LED Color */ /** Turn middle LED Color */
if (ag->isOne()) { if (ag->isOne()) {
ag->ledBar.clear();
ag->ledBar.setColor(0, 0, 255, ag->ledBar.getNumberOfLeds() / 2); ag->ledBar.setColor(0, 0, 255, ag->ledBar.getNumberOfLeds() / 2);
} else { } else {
ag->statusLed.setToggle(); ag->statusLed.setToggle();
@ -603,6 +650,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
case AgStateMachineWiFiManagerPortalActive: { case AgStateMachineWiFiManagerPortalActive: {
/** WiFi Manager has connected to mobile phone */ /** WiFi Manager has connected to mobile phone */
if (ag->isOne()) { if (ag->isOne()) {
ag->ledBar.clear();
ag->ledBar.setColor(0, 0, 255); ag->ledBar.setColor(0, 0, 255);
} else { } else {
ag->statusLed.setOn(); ag->statusLed.setOn();
@ -613,6 +661,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
/** after SSID and PW entered and OK clicked, connection to WiFI network is /** after SSID and PW entered and OK clicked, connection to WiFI network is
* attempted */ * attempted */
if (ag->isOne()) { if (ag->isOne()) {
ag->ledBar.clear();
ledBarSingleLedAnimation(255, 255, 255); ledBarSingleLedAnimation(255, 255, 255);
} else { } else {
ag->statusLed.setOff(); ag->statusLed.setOff();
@ -622,6 +671,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
case AgStateMachineWiFiManagerStaConnected: { case AgStateMachineWiFiManagerStaConnected: {
/** Connecting to WiFi worked */ /** Connecting to WiFi worked */
if (ag->isOne()) { if (ag->isOne()) {
ag->ledBar.clear();
ag->ledBar.setColor(255, 255, 255); ag->ledBar.setColor(255, 255, 255);
} else { } else {
ag->statusLed.setOff(); ag->statusLed.setOff();
@ -631,6 +681,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
case AgStateMachineWiFiOkServerConnecting: { case AgStateMachineWiFiOkServerConnecting: {
/** once connected to WiFi an attempt to reach the server is performed */ /** once connected to WiFi an attempt to reach the server is performed */
if (ag->isOne()) { if (ag->isOne()) {
ag->ledBar.clear();
ledBarSingleLedAnimation(0, 255, 0); ledBarSingleLedAnimation(0, 255, 0);
} else { } else {
ag->statusLed.setOff(); ag->statusLed.setOff();
@ -640,6 +691,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
case AgStateMachineWiFiOkServerConnected: { case AgStateMachineWiFiOkServerConnected: {
/** Server is reachable, all fine */ /** Server is reachable, all fine */
if (ag->isOne()) { if (ag->isOne()) {
ag->ledBar.clear();
ag->ledBar.setColor(0, 255, 0); ag->ledBar.setColor(0, 255, 0);
} else { } else {
ag->statusLed.setOff(); ag->statusLed.setOff();
@ -656,6 +708,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
case AgStateMachineWiFiManagerConnectFailed: { case AgStateMachineWiFiManagerConnectFailed: {
/** Cannot connect to WiFi (e.g. wrong password, WPA Enterprise etc.) */ /** Cannot connect to WiFi (e.g. wrong password, WPA Enterprise etc.) */
if (ag->isOne()) { if (ag->isOne()) {
ag->ledBar.clear();
ag->ledBar.setColor(255, 0, 0); ag->ledBar.setColor(255, 0, 0);
} else { } else {
ag->statusLed.setOff(); ag->statusLed.setOff();
@ -674,6 +727,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
/** Connected to WiFi but server not reachable, e.g. firewall block/ /** Connected to WiFi but server not reachable, e.g. firewall block/
* whitelisting needed etc. */ * whitelisting needed etc. */
if (ag->isOne()) { if (ag->isOne()) {
ag->ledBar.clear();
ag->ledBar.setColor(233, 183, 54); /** orange */ ag->ledBar.setColor(233, 183, 54); /** orange */
} else { } else {
ag->statusLed.setOff(); ag->statusLed.setOff();
@ -690,6 +744,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
case AgStateMachineWiFiOkServerOkSensorConfigFailed: { case AgStateMachineWiFiOkServerOkSensorConfigFailed: {
/** Server reachable but sensor not configured correctly */ /** Server reachable but sensor not configured correctly */
if (ag->isOne()) { if (ag->isOne()) {
ag->ledBar.clear();
ag->ledBar.setColor(139, 24, 248); /** violet */ ag->ledBar.setColor(139, 24, 248); /** violet */
} else { } else {
ag->statusLed.setOff(); ag->statusLed.setOff();
@ -707,11 +762,10 @@ void StateMachine::handleLeds(AgStateMachineState state) {
/** Connection to WiFi network failed credentials incorrect encryption not /** Connection to WiFi network failed credentials incorrect encryption not
* supported etc. */ * supported etc. */
if (ag->isOne()) { if (ag->isOne()) {
/** WIFI failed status LED color */ bool allUsed = sensorhandleLeds();
ag->ledBar.setColor(255, 0, 0, 0); if (allUsed == false) {
/** Show CO2 or PM color status */ ag->ledBar.setColor(255, 0, 0, 0);
// sensorLedColorHandler(); }
sensorhandleLeds();
} else { } else {
ag->statusLed.setOff(); ag->statusLed.setOff();
} }
@ -721,11 +775,10 @@ void StateMachine::handleLeds(AgStateMachineState state) {
/** Connected to WiFi network but the server cannot be reached through the /** Connected to WiFi network but the server cannot be reached through the
* internet, e.g. blocked by firewall */ * internet, e.g. blocked by firewall */
if (ag->isOne()) { if (ag->isOne()) {
ag->ledBar.setColor(233, 183, 54, 0); bool allUsed = sensorhandleLeds();
if (allUsed == false) {
/** Show CO2 or PM color status */ ag->ledBar.setColor(233, 183, 54, 0);
sensorhandleLeds(); }
// sensorLedColorHandler();
} else { } else {
ag->statusLed.setOff(); ag->statusLed.setOff();
} }
@ -735,10 +788,10 @@ void StateMachine::handleLeds(AgStateMachineState state) {
/** Server is reachable but there is some configuration issue to be fixed on /** Server is reachable but there is some configuration issue to be fixed on
* the server side */ * the server side */
if (ag->isOne()) { if (ag->isOne()) {
ag->ledBar.setColor(139, 24, 248, 0); bool allUsed = sensorhandleLeds();
if (allUsed == false) {
/** Show CO2 or PM color status */ ag->ledBar.setColor(139, 24, 248, 0);
sensorhandleLeds(); }
} else { } else {
ag->statusLed.setOff(); ag->statusLed.setOff();
} }

View File

@ -24,9 +24,9 @@ private:
void ledBarSingleLedAnimation(uint8_t r, uint8_t g, uint8_t b); void ledBarSingleLedAnimation(uint8_t r, uint8_t g, uint8_t b);
void ledStatusBlinkDelay(uint32_t delay); void ledStatusBlinkDelay(uint32_t delay);
void sensorhandleLeds(void); bool sensorhandleLeds(void);
void co2handleLeds(void); int co2handleLeds(void);
void pm25handleLeds(void); int pm25handleLeds(void);
void co2Calibration(void); void co2Calibration(void);
void ledBarTest(void); void ledBarTest(void);
void ledBarPowerUpTest(void); void ledBarPowerUpTest(void);

View File

@ -27,6 +27,12 @@
#define json_prop_noxRaw "noxRaw" #define json_prop_noxRaw "noxRaw"
#define json_prop_co2 "rco2" #define json_prop_co2 "rco2"
Measurements::Measurements() {
#ifndef ESP8266
_resetReason = (int)ESP_RST_UNKNOWN;
#endif
}
void Measurements::maxPeriod(MeasurementType type, int max) { void Measurements::maxPeriod(MeasurementType type, int max) {
switch (type) { switch (type) {
case Temperature: case Temperature:
@ -189,7 +195,7 @@ bool Measurements::update(MeasurementType type, int val, int ch) {
// Sanity check if measurement type is defined for integer data type or not // Sanity check if measurement type is defined for integer data type or not
if (temporary == nullptr) { if (temporary == nullptr) {
Serial.printf("%s is not defined for integer data type\n", measurementTypeStr(type)); Serial.printf("%s is not defined for integer data type\n", measurementTypeStr(type).c_str());
// TODO: Just assert? // TODO: Just assert?
return false; return false;
} }
@ -228,7 +234,7 @@ bool Measurements::update(MeasurementType type, int val, int ch) {
// Calculate average based on how many elements on the list // Calculate average based on how many elements on the list
temporary->update.avg = temporary->sumValues / (float)temporary->listValues.size(); temporary->update.avg = temporary->sumValues / (float)temporary->listValues.size();
if (_debug) { if (_debug) {
Serial.printf("%s{%d}: %.2f\n", measurementTypeStr(type), ch, temporary->update.avg); Serial.printf("%s{%d}: %.2f\n", measurementTypeStr(type).c_str(), ch, temporary->update.avg);
} }
return true; return true;
@ -260,7 +266,7 @@ bool Measurements::update(MeasurementType type, float val, int ch) {
// Sanity check if measurement type is defined for float data type or not // Sanity check if measurement type is defined for float data type or not
if (temporary == nullptr) { if (temporary == nullptr) {
Serial.printf("%s is not defined for float data type\n", measurementTypeStr(type)); Serial.printf("%s is not defined for float data type\n", measurementTypeStr(type).c_str());
// TODO: Just assert? // TODO: Just assert?
return false; return false;
} }
@ -299,7 +305,7 @@ bool Measurements::update(MeasurementType type, float val, int ch) {
// Calculate average based on how many elements on the list // Calculate average based on how many elements on the list
temporary->update.avg = temporary->sumValues / (float)temporary->listValues.size(); temporary->update.avg = temporary->sumValues / (float)temporary->listValues.size();
if (_debug) { if (_debug) {
Serial.printf("%s{%d}: %.2f\n", measurementTypeStr(type), ch, temporary->update.avg); Serial.printf("%s{%d}: %.2f\n", measurementTypeStr(type).c_str(), ch, temporary->update.avg);
} }
return true; return true;
@ -348,7 +354,7 @@ int Measurements::get(MeasurementType type, int ch) {
// Sanity check if measurement type is defined for integer data type or not // Sanity check if measurement type is defined for integer data type or not
if (temporary == nullptr) { if (temporary == nullptr) {
Serial.printf("%s is not defined for integer data type\n", measurementTypeStr(type)); Serial.printf("%s is not defined for integer data type\n", measurementTypeStr(type).c_str());
// TODO: Just assert? // TODO: Just assert?
return false; return false;
} }
@ -383,7 +389,7 @@ float Measurements::getFloat(MeasurementType type, int ch) {
// Sanity check if measurement type is defined for float data type or not // Sanity check if measurement type is defined for float data type or not
if (temporary == nullptr) { if (temporary == nullptr) {
Serial.printf("%s is not defined for float data type\n", measurementTypeStr(type)); Serial.printf("%s is not defined for float data type\n", measurementTypeStr(type).c_str());
// TODO: Just assert? // TODO: Just assert?
return false; return false;
} }
@ -396,6 +402,52 @@ float Measurements::getFloat(MeasurementType type, int ch) {
return temporary->listValues.back(); return temporary->listValues.back();
} }
float Measurements::getAverage(MeasurementType type, int ch) {
// Sanity check to validate channel, assert if invalid
validateChannel(ch);
// Follow array indexing just for get address of the value type
ch = ch - 1;
// Define data point source. Data type doesn't matter because only to get the average value
FloatValue *temporary = nullptr;
Update update;
float measurementAverage;
switch (type) {
case CO2:
measurementAverage = _co2.update.avg;
break;
case TVOC:
measurementAverage = _tvoc.update.avg;
break;
case NOx:
measurementAverage = _nox.update.avg;
break;
case PM25:
measurementAverage = _pm_25[ch].update.avg;
break;
case Temperature:
measurementAverage = _temperature[ch].update.avg;
break;
case Humidity:
measurementAverage = _humidity[ch].update.avg;
break;
default:
// Invalidate, measurements type not handled
measurementAverage = -1000;
break;
};
// Sanity check if measurement type is not defined
if (measurementAverage == -1000) {
Serial.printf("ERROR! %s is not defined on get average value function\n", measurementTypeStr(type).c_str());
delay(1000);
assert(0);
}
return measurementAverage;
}
String Measurements::pms5003FirmwareVersion(int fwCode) { String Measurements::pms5003FirmwareVersion(int fwCode) {
return pms5003FirmwareVersionBase("PMS5003x", fwCode); return pms5003FirmwareVersionBase("PMS5003x", fwCode);
} }
@ -485,11 +537,12 @@ void Measurements::validateChannel(int ch) {
float Measurements::getCorrectedPM25(AirGradient &ag, Configuration &config, bool useAvg, int ch) { float Measurements::getCorrectedPM25(AirGradient &ag, Configuration &config, bool useAvg, int ch) {
float pm25; float pm25;
float corrected;
float humidity; float humidity;
float pm003Count; float pm003Count;
int channel = ch - 1; // Array index
if (useAvg) { if (useAvg) {
// Directly call from the index // Directly call from the index
int channel = ch - 1; // Array index
pm25 = _pm_25[channel].update.avg; pm25 = _pm_25[channel].update.avg;
humidity = _humidity[channel].update.avg; humidity = _humidity[channel].update.avg;
pm003Count = _pm_03_pc[channel].update.avg; pm003Count = _pm_03_pc[channel].update.avg;
@ -500,19 +553,27 @@ float Measurements::getCorrectedPM25(AirGradient &ag, Configuration &config, boo
} }
Configuration::PMCorrection pmCorrection = config.getPMCorrection(); Configuration::PMCorrection pmCorrection = config.getPMCorrection();
if (pmCorrection.algorithm == PMCorrectionAlgorithm::EPA_2021) { switch (pmCorrection.algorithm) {
// EPA correction directly applied case PMCorrectionAlgorithm::Unknown:
pm25 = ag.pms5003.compensate(pm25, humidity); case PMCorrectionAlgorithm::None:
} else { // If correction is Unknown, then default is None
// SLR correction, this is assumes before calling this function, correction algorithm is not None corrected = pm25;
pm25 = ag.pms5003.slrCorrection(pm25, pm003Count, pmCorrection.scalingFactor, pmCorrection.intercept); break;
case PMCorrectionAlgorithm::EPA_2021:
corrected = ag.pms5003.compensate(pm25, humidity);
break;
default: {
// All SLR correction using the same flow, hence default condition
corrected = ag.pms5003.slrCorrection(pm25, pm003Count, pmCorrection.scalingFactor,
pmCorrection.intercept);
if (pmCorrection.useEPA) { if (pmCorrection.useEPA) {
// Add EPA compensation on top of SLR // Add EPA compensation on top of SLR
pm25 = ag.pms5003.compensate(pm25, humidity); corrected = ag.pms5003.compensate(corrected, humidity);
} }
} }
}
return pm25; return corrected;
} }
String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi, AirGradient &ag, String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi, AirGradient &ag,
@ -546,8 +607,8 @@ String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi,
} }
} }
root["boot"] = bootCount; root["boot"] = _bootCount;
root["bootCount"] = bootCount; root["bootCount"] = _bootCount;
root["wifi"] = rssi; root["wifi"] = rssi;
if (localServer) { if (localServer) {
@ -557,6 +618,11 @@ String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi,
root["serialno"] = ag.deviceId(); root["serialno"] = ag.deviceId();
root["firmware"] = ag.getVersion(); root["firmware"] = ag.getVersion();
root["model"] = AgFirmwareModeName(fwMode); root["model"] = AgFirmwareModeName(fwMode);
} else {
#ifndef ESP8266
root["resetReason"] = _resetReason;
root["freeHeap"] = ESP.getFreeHeap();
#endif
} }
String result = JSON.stringify(root); String result = JSON.stringify(root);
@ -841,10 +907,10 @@ JSONVar Measurements::buildPMS(AirGradient &ag, int ch, bool allCh, bool withTem
pms[json_prop_pm03Count] = ag.round2(avg); pms[json_prop_pm03Count] = ag.round2(avg);
pms["channels"]["1"][json_prop_pm03Count] = ag.round2(_pm_03_pc[0].update.avg); pms["channels"]["1"][json_prop_pm03Count] = ag.round2(_pm_03_pc[0].update.avg);
pms["channels"]["2"][json_prop_pm03Count] = ag.round2(_pm_03_pc[1].update.avg); pms["channels"]["2"][json_prop_pm03Count] = ag.round2(_pm_03_pc[1].update.avg);
} else if (utils::isValidPm(_pm_03_pc[0].update.avg)) { } else if (utils::isValidPm03Count(_pm_03_pc[0].update.avg)) {
pms[json_prop_pm03Count] = ag.round2(_pm_03_pc[0].update.avg); pms[json_prop_pm03Count] = ag.round2(_pm_03_pc[0].update.avg);
pms["channels"]["1"][json_prop_pm03Count] = ag.round2(_pm_03_pc[0].update.avg); pms["channels"]["1"][json_prop_pm03Count] = ag.round2(_pm_03_pc[0].update.avg);
} else if (utils::isValidPm(_pm_03_pc[1].update.avg)) { } else if (utils::isValidPm03Count(_pm_03_pc[1].update.avg)) {
pms[json_prop_pm03Count] = ag.round2(_pm_03_pc[1].update.avg); pms[json_prop_pm03Count] = ag.round2(_pm_03_pc[1].update.avg);
pms["channels"]["2"][json_prop_pm03Count] = ag.round2(_pm_03_pc[1].update.avg); pms["channels"]["2"][json_prop_pm03Count] = ag.round2(_pm_03_pc[1].update.avg);
} }
@ -856,10 +922,10 @@ JSONVar Measurements::buildPMS(AirGradient &ag, int ch, bool allCh, bool withTem
pms[json_prop_pm05Count] = ag.round2(avg); pms[json_prop_pm05Count] = ag.round2(avg);
pms["channels"]["1"][json_prop_pm05Count] = ag.round2(_pm_05_pc[0].update.avg); pms["channels"]["1"][json_prop_pm05Count] = ag.round2(_pm_05_pc[0].update.avg);
pms["channels"]["2"][json_prop_pm05Count] = ag.round2(_pm_05_pc[1].update.avg); pms["channels"]["2"][json_prop_pm05Count] = ag.round2(_pm_05_pc[1].update.avg);
} else if (utils::isValidPm(_pm_05_pc[0].update.avg)) { } else if (utils::isValidPm03Count(_pm_05_pc[0].update.avg)) {
pms[json_prop_pm05Count] = ag.round2(_pm_05_pc[0].update.avg); pms[json_prop_pm05Count] = ag.round2(_pm_05_pc[0].update.avg);
pms["channels"]["1"][json_prop_pm05Count] = ag.round2(_pm_05_pc[0].update.avg); pms["channels"]["1"][json_prop_pm05Count] = ag.round2(_pm_05_pc[0].update.avg);
} else if (utils::isValidPm(_pm_05_pc[1].update.avg)) { } else if (utils::isValidPm03Count(_pm_05_pc[1].update.avg)) {
pms[json_prop_pm05Count] = ag.round2(_pm_05_pc[1].update.avg); pms[json_prop_pm05Count] = ag.round2(_pm_05_pc[1].update.avg);
pms["channels"]["2"][json_prop_pm05Count] = ag.round2(_pm_05_pc[1].update.avg); pms["channels"]["2"][json_prop_pm05Count] = ag.round2(_pm_05_pc[1].update.avg);
} }
@ -870,10 +936,10 @@ JSONVar Measurements::buildPMS(AirGradient &ag, int ch, bool allCh, bool withTem
pms[json_prop_pm1Count] = ag.round2(avg); pms[json_prop_pm1Count] = ag.round2(avg);
pms["channels"]["1"][json_prop_pm1Count] = ag.round2(_pm_01_pc[0].update.avg); pms["channels"]["1"][json_prop_pm1Count] = ag.round2(_pm_01_pc[0].update.avg);
pms["channels"]["2"][json_prop_pm1Count] = ag.round2(_pm_01_pc[1].update.avg); pms["channels"]["2"][json_prop_pm1Count] = ag.round2(_pm_01_pc[1].update.avg);
} else if (utils::isValidPm(_pm_01_pc[0].update.avg)) { } else if (utils::isValidPm03Count(_pm_01_pc[0].update.avg)) {
pms[json_prop_pm1Count] = ag.round2(_pm_01_pc[0].update.avg); pms[json_prop_pm1Count] = ag.round2(_pm_01_pc[0].update.avg);
pms["channels"]["1"][json_prop_pm1Count] = ag.round2(_pm_01_pc[0].update.avg); pms["channels"]["1"][json_prop_pm1Count] = ag.round2(_pm_01_pc[0].update.avg);
} else if (utils::isValidPm(_pm_01_pc[1].update.avg)) { } else if (utils::isValidPm03Count(_pm_01_pc[1].update.avg)) {
pms[json_prop_pm1Count] = ag.round2(_pm_01_pc[1].update.avg); pms[json_prop_pm1Count] = ag.round2(_pm_01_pc[1].update.avg);
pms["channels"]["2"][json_prop_pm1Count] = ag.round2(_pm_01_pc[1].update.avg); pms["channels"]["2"][json_prop_pm1Count] = ag.round2(_pm_01_pc[1].update.avg);
} }
@ -885,10 +951,10 @@ JSONVar Measurements::buildPMS(AirGradient &ag, int ch, bool allCh, bool withTem
pms[json_prop_pm25Count] = ag.round2(avg); pms[json_prop_pm25Count] = ag.round2(avg);
pms["channels"]["1"][json_prop_pm25Count] = ag.round2(_pm_25_pc[0].update.avg); pms["channels"]["1"][json_prop_pm25Count] = ag.round2(_pm_25_pc[0].update.avg);
pms["channels"]["2"][json_prop_pm25Count] = ag.round2(_pm_25_pc[1].update.avg); pms["channels"]["2"][json_prop_pm25Count] = ag.round2(_pm_25_pc[1].update.avg);
} else if (utils::isValidPm(_pm_25_pc[0].update.avg)) { } else if (utils::isValidPm03Count(_pm_25_pc[0].update.avg)) {
pms[json_prop_pm25Count] = ag.round2(_pm_25_pc[0].update.avg); pms[json_prop_pm25Count] = ag.round2(_pm_25_pc[0].update.avg);
pms["channels"]["1"][json_prop_pm25Count] = ag.round2(_pm_25_pc[0].update.avg); pms["channels"]["1"][json_prop_pm25Count] = ag.round2(_pm_25_pc[0].update.avg);
} else if (utils::isValidPm(_pm_25_pc[1].update.avg)) { } else if (utils::isValidPm03Count(_pm_25_pc[1].update.avg)) {
pms[json_prop_pm25Count] = ag.round2(_pm_25_pc[1].update.avg); pms[json_prop_pm25Count] = ag.round2(_pm_25_pc[1].update.avg);
pms["channels"]["2"][json_prop_pm25Count] = ag.round2(_pm_25_pc[1].update.avg); pms["channels"]["2"][json_prop_pm25Count] = ag.round2(_pm_25_pc[1].update.avg);
} }
@ -1010,4 +1076,50 @@ JSONVar Measurements::buildPMS(AirGradient &ag, int ch, bool allCh, bool withTem
return pms; return pms;
} }
void Measurements::setDebug(bool debug) { _debug = debug; } void Measurements::setDebug(bool debug) { _debug = debug; }
int Measurements::bootCount() { return _bootCount; }
void Measurements::setBootCount(int bootCount) { _bootCount = bootCount; }
#ifndef ESP8266
void Measurements::setResetReason(esp_reset_reason_t reason) {
switch (reason) {
case ESP_RST_UNKNOWN:
Serial.println("Reset reason: ESP_RST_UNKNOWN");
break;
case ESP_RST_POWERON:
Serial.println("Reset reason: ESP_RST_POWERON");
break;
case ESP_RST_EXT:
Serial.println("Reset reason: ESP_RST_EXT");
break;
case ESP_RST_SW:
Serial.println("Reset reason: ESP_RST_SW");
break;
case ESP_RST_PANIC:
Serial.println("Reset reason: ESP_RST_PANIC");
break;
case ESP_RST_INT_WDT:
Serial.println("Reset reason: ESP_RST_INT_WDT");
break;
case ESP_RST_TASK_WDT:
Serial.println("Reset reason: ESP_RST_TASK_WDT");
break;
case ESP_RST_WDT:
Serial.println("Reset reason: ESP_RST_WDT");
break;
case ESP_RST_BROWNOUT:
Serial.println("Reset reason: ESP_RST_BROWNOUT");
break;
case ESP_RST_SDIO:
Serial.println("Reset reason: ESP_RST_SDIO");
break;
default:
Serial.println("Reset reason: unknown");
break;
}
_resetReason = (int)reason;
}
#endif

View File

@ -34,7 +34,7 @@ private:
}; };
public: public:
Measurements() {} Measurements();
~Measurements() {} ~Measurements() {}
// Enumeration for every AG measurements // Enumeration for every AG measurements
@ -114,9 +114,19 @@ public:
*/ */
float getFloat(MeasurementType type, int ch = 1); float getFloat(MeasurementType type, int ch = 1);
/**
* @brief Get the target measurement average value
*
* @param type measurement type that will be retrieve
* @param ch target type value channel
* @return moving average value of target measurements type
*/
float getAverage(MeasurementType type, int ch = 1);
/** /**
* @brief Get the Corrected PM25 object based on the correction algorithm from configuration * @brief Get the Corrected PM25 object based on the correction algorithm from configuration
*
* If correction is not enabled, then will return the raw value (either average or last value)
* *
* @param ag AirGradient instance * @param ag AirGradient instance
* @param config Configuration instance * @param config Configuration instance
@ -137,8 +147,12 @@ public:
*/ */
void setDebug(bool debug); void setDebug(bool debug);
// TODO: update this to use setter int bootCount();
int bootCount; void setBootCount(int bootCount);
#ifndef ESP8266
void setResetReason(esp_reset_reason_t reason);
#endif
private: private:
// Some declared as an array (channel), because FW_MODE_O_1PPx has two PMS5003T // Some declared as an array (channel), because FW_MODE_O_1PPx has two PMS5003T
@ -161,7 +175,8 @@ private:
IntegerValue _pm_25_pc[2]; // particle count 2.5 IntegerValue _pm_25_pc[2]; // particle count 2.5
IntegerValue _pm_5_pc[2]; // particle count 5.0 IntegerValue _pm_5_pc[2]; // particle count 5.0
IntegerValue _pm_10_pc[2]; // particle count 10 IntegerValue _pm_10_pc[2]; // particle count 10
int _bootCount;
int _resetReason;
bool _debug = false; bool _debug = false;
/** /**

View File

@ -15,7 +15,46 @@
#include "Main/utils.h" #include "Main/utils.h"
#ifndef GIT_VERSION #ifndef GIT_VERSION
#define GIT_VERSION "3.1.10-snap" #define GIT_VERSION "3.1.21-snap"
#endif
#ifndef ESP8266
// Airgradient server root ca certificate
const char *const AG_SERVER_ROOT_CA =
"-----BEGIN CERTIFICATE-----\n"
"MIIF4jCCA8oCCQD7MgvcaVWxkTANBgkqhkiG9w0BAQsFADCBsjELMAkGA1UEBhMC\n"
"VEgxEzARBgNVBAgMCkNoaWFuZyBNYWkxEDAOBgNVBAcMB01hZSBSaW0xGTAXBgNV\n"
"BAoMEEFpckdyYWRpZW50IEx0ZC4xFDASBgNVBAsMC1NlbnNvciBMYWJzMSgwJgYD\n"
"VQQDDB9BaXJHcmFkaWVudCBTZW5zb3IgTGFicyBSb290IENBMSEwHwYJKoZIhvcN\n"
"AQkBFhJjYUBhaXJncmFkaWVudC5jb20wHhcNMjEwOTE3MTE0NDE3WhcNNDEwOTEy\n"
"MTE0NDE3WjCBsjELMAkGA1UEBhMCVEgxEzARBgNVBAgMCkNoaWFuZyBNYWkxEDAO\n"
"BgNVBAcMB01hZSBSaW0xGTAXBgNVBAoMEEFpckdyYWRpZW50IEx0ZC4xFDASBgNV\n"
"BAsMC1NlbnNvciBMYWJzMSgwJgYDVQQDDB9BaXJHcmFkaWVudCBTZW5zb3IgTGFi\n"
"cyBSb290IENBMSEwHwYJKoZIhvcNAQkBFhJjYUBhaXJncmFkaWVudC5jb20wggIi\n"
"MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC6XkVQ4O9d5GcUjPYRgF/uaY6O\n"
"5ry1xCGvotxkEeKkBk99lB1oNUUfNsP5bwuDci4XKfY9Ro6/jmkfHSVcPAwUnjAt\n"
"BcHqZtA/cMXykaynf9yXPxPQN7XLu/Rk32RIfb90sIGS318xgNziCYvzWZmlxpxc\n"
"3gUcAgGtamlgZ6wD3yOHVo8B9aFNvmP16QwkUm8fKDHunJG+iX2Bxa4ka5FJovhG\n"
"TnUwtso6Vrn0JaWF9qWcPZE0JZMjFW8PYRriyJmHwr/nAXfPPKphD1oRO+oA7/jq\n"
"dYkrJw6+OHfFXnPB1xkeh4OPBzcCZHT5XWNfwBYazYpjcJa9ngGFSmg8lX1ac23C\n"
"zea1XJmSrPwbZbWxoQznnf7Y78mRjruYKgSP8rf74KYvBe/HGPL5NQyXQ3l6kwmu\n"
"CCUqfcC0wCWEtWESxwSdFE2qQii8CZ12kQExzvR2PrOIyKQYSdkGx9/RBZtAVPXP\n"
"hmLuRBQYHrF5Cxf1oIbBK8OMoNVgBm6ftt15t9Sq9dH5Aup2YR6WEJkVaYkYzZzK\n"
"X7M+SQcdbXp+hAO8PFpABJxkaDAO2kiB5Ov7pDYPAcmNFqnJT48AY0TZJeVeCa5W\n"
"sIv3lPvB/XcFjP0+aZxxNSEEwpGPUYgvKUYUUmb0NammlYQwZHKaShPEmZ3UZ0bp\n"
"VNt4p6374nzO376sSwIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQB/LfBPgTx7xKQB\n"
"JNMUhah17AFAn050NiviGJOHdPQely6u3DmJGg+ijEVlPWO1FEW3it+LOuNP5zOu\n"
"bhq8paTYIxPxtALIxw5ksykX9woDuX3H6FF9mPdQIbL7ft+3ZtZ4FWPui9dUtaPe\n"
"ZBmDFDi4U29nhWZK68JSp5QkWjfaYLV/vtag7120eVyGEPFZ0UAuTUNqpw+stOt9\n"
"gJ2ZxNx13xJ8ZnLK7qz1crPe8/8IVAdxbVLoY7JaWPLc//+VF+ceKicy8+4gV7zN\n"
"Gnq2IyM+CHFz8VYMLbW+3eVp4iJjTa72vae116kozboEIUVN9rgLqIKyVqQXiuoN\n"
"g3xY+yfncPB2+H/+lfyy6mepPIfgksd3+KeNxFADSc5EVY2JKEdorRodnAh7a8K6\n"
"WjTYgq+GjWXU2uQW2SyPt6Tu33OT8nBnu3NB80eT8WXgdVCkgsuyCuLvNRf1Xmze\n"
"igvurpU6JmQ1GlLgLJo8omJHTh1zIbkR9injPYne2v9ciHCoP6+LDEqe+rOsvPCB\n"
"C/o/iZ4svmYX4fWGuU7GgqZE8hhrC3+GdOTf2ADC752cYCZxBidXGtkrGNoHQKmQ\n"
"KCOMFBxZIvWteB3tUo3BKYz1D2CvKWz1wV4moc5JHkOgS+jqxhvOkQ/vfQBQ1pUY\n"
"TMui9BSwU7B1G2XjdLbfF3Dc67zaSg==\n"
"-----END CERTIFICATE-----\n";
#endif #endif
/** /**

171
src/OtaHandler.cpp Normal file
View File

@ -0,0 +1,171 @@
#include "OtaHandler.h"
#ifndef ESP8266 // Only for esp32 based mcu
#include "AirGradient.h"
void OtaHandler::setHandlerCallback(OtaHandlerCallback_t callback) { _callback = callback; }
void OtaHandler::updateFirmwareIfOutdated(String deviceId) {
String url =
"https://hw.airgradient.com/sensors/airgradient:" + deviceId + "/generic/os/firmware.bin";
url += "?current_firmware=";
url += GIT_VERSION;
char urlAsChar[URL_BUF_SIZE];
url.toCharArray(urlAsChar, URL_BUF_SIZE);
Serial.printf("checking for new OTA update @ %s\n", urlAsChar);
esp_http_client_config_t config = {};
config.url = urlAsChar;
config.cert_pem = AG_SERVER_ROOT_CA;
OtaUpdateOutcome ret = attemptToPerformOta(&config);
Serial.println(ret);
if (_callback) {
switch (ret) {
case OtaUpdateOutcome::UPDATE_PERFORMED:
_callback(OtaState::OTA_STATE_SUCCESS, "");
break;
case OtaUpdateOutcome::UPDATE_SKIPPED:
_callback(OtaState::OTA_STATE_SKIP, "");
break;
case OtaUpdateOutcome::ALREADY_UP_TO_DATE:
_callback(OtaState::OTA_STATE_UP_TO_DATE, "");
break;
case OtaUpdateOutcome::UPDATE_FAILED:
_callback(OtaState::OTA_STATE_FAIL, "");
break;
default:
break;
}
}
}
OtaHandler::OtaUpdateOutcome
OtaHandler::attemptToPerformOta(const esp_http_client_config_t *config) {
esp_http_client_handle_t client = esp_http_client_init(config);
if (client == NULL) {
Serial.println("Failed to initialize HTTP connection");
return OtaUpdateOutcome::UPDATE_FAILED;
}
esp_err_t err = esp_http_client_open(client, 0);
if (err != ESP_OK) {
esp_http_client_cleanup(client);
Serial.printf("Failed to open HTTP connection: %s\n", esp_err_to_name(err));
return OtaUpdateOutcome::UPDATE_FAILED;
}
esp_http_client_fetch_headers(client);
int httpStatusCode = esp_http_client_get_status_code(client);
if (httpStatusCode == 304) {
Serial.println("Firmware is already up to date");
cleanupHttp(client);
return OtaUpdateOutcome::ALREADY_UP_TO_DATE;
} else if (httpStatusCode != 200) {
Serial.printf("Firmware update skipped, the server returned %d\n", httpStatusCode);
cleanupHttp(client);
return OtaUpdateOutcome::UPDATE_SKIPPED;
}
esp_ota_handle_t update_handle = 0;
const esp_partition_t *update_partition = NULL;
Serial.println("Starting OTA update ...");
update_partition = esp_ota_get_next_update_partition(NULL);
if (update_partition == NULL) {
Serial.println("Passive OTA partition not found");
cleanupHttp(client);
return OtaUpdateOutcome::UPDATE_FAILED;
}
Serial.printf("Writing to partition subtype %d at offset 0x%x\n", update_partition->subtype,
update_partition->address);
err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle);
if (err != ESP_OK) {
Serial.printf("esp_ota_begin failed, error=%d\n", err);
cleanupHttp(client);
return OtaUpdateOutcome::UPDATE_FAILED;
}
esp_err_t ota_write_err = ESP_OK;
char *upgrade_data_buf = (char *)malloc(OTA_BUF_SIZE);
if (!upgrade_data_buf) {
Serial.println("Couldn't allocate memory for data buffer");
return OtaUpdateOutcome::UPDATE_FAILED;
}
int binary_file_len = 0;
int totalSize = esp_http_client_get_content_length(client);
Serial.println("File size: " + String(totalSize) + String(" bytes"));
// Show display start update new firmware.
if (_callback) {
_callback(OtaState::OTA_STATE_BEGIN, "");
}
// Download file and write new firmware to OTA partition
uint32_t lastUpdate = millis();
while (1) {
int data_read = esp_http_client_read(client, upgrade_data_buf, OTA_BUF_SIZE);
if (data_read == 0) {
if (_callback) {
_callback(OtaState::OTA_STATE_PROCESSING, String(100));
}
Serial.println("Connection closed, all data received");
break;
}
if (data_read < 0) {
Serial.println("Data read error");
if (_callback) {
_callback(OtaState::OTA_STATE_FAIL, "");
}
break;
}
if (data_read > 0) {
ota_write_err = esp_ota_write(update_handle, (const void *)upgrade_data_buf, data_read);
if (ota_write_err != ESP_OK) {
if (_callback) {
_callback(OtaState::OTA_STATE_FAIL, "");
}
break;
}
binary_file_len += data_read;
int percent = (binary_file_len * 100) / totalSize;
uint32_t ms = (uint32_t)(millis() - lastUpdate);
if (ms >= 250) {
// sm.executeOTA(StateMachine::OtaState::OTA_STATE_PROCESSING, "",
// percent);
if (_callback) {
_callback(OtaState::OTA_STATE_PROCESSING, String(percent));
}
lastUpdate = millis();
}
}
}
free(upgrade_data_buf);
cleanupHttp(client);
Serial.printf("# of bytes written: %d\n", binary_file_len);
esp_err_t ota_end_err = esp_ota_end(update_handle);
if (ota_write_err != ESP_OK) {
Serial.printf("Error: esp_ota_write failed! err=0x%d\n", err);
return OtaUpdateOutcome::UPDATE_FAILED;
} else if (ota_end_err != ESP_OK) {
Serial.printf("Error: esp_ota_end failed! err=0x%d. Image is invalid", ota_end_err);
return OtaUpdateOutcome::UPDATE_FAILED;
}
err = esp_ota_set_boot_partition(update_partition);
if (err != ESP_OK) {
Serial.printf("esp_ota_set_boot_partition failed! err=0x%d\n", err);
return OtaUpdateOutcome::UPDATE_FAILED;
}
return OtaUpdateOutcome::UPDATE_PERFORMED;
}
void OtaHandler::cleanupHttp(esp_http_client_handle_t client) {
esp_http_client_close(client);
esp_http_client_cleanup(client);
}
#endif

43
src/OtaHandler.h Normal file
View File

@ -0,0 +1,43 @@
#ifndef OTA_HANDLER_H
#define OTA_HANDLER_H
#ifndef ESP8266 // Only for esp32 based mcu
#include <Arduino.h>
#include <esp_err.h>
#include <esp_http_client.h>
#include <esp_ota_ops.h>
#define OTA_BUF_SIZE 1024
#define URL_BUF_SIZE 256
class OtaHandler {
public:
enum OtaState {
OTA_STATE_BEGIN,
OTA_STATE_FAIL,
OTA_STATE_SKIP,
OTA_STATE_UP_TO_DATE,
OTA_STATE_PROCESSING,
OTA_STATE_SUCCESS
};
typedef void (*OtaHandlerCallback_t)(OtaState state, String message);
void setHandlerCallback(OtaHandlerCallback_t callback);
void updateFirmwareIfOutdated(String deviceId);
private:
OtaHandlerCallback_t _callback;
enum OtaUpdateOutcome {
UPDATE_PERFORMED = 0,
ALREADY_UP_TO_DATE,
UPDATE_FAILED,
UPDATE_SKIPPED
}; // Internal use
OtaUpdateOutcome attemptToPerformOta(const esp_http_client_config_t *config);
void cleanupHttp(esp_http_client_handle_t client);
};
#endif // ESP8266
#endif // OTA_HANDLER_H