Compare commits

...

69 Commits

Author SHA1 Message Date
227bd518c9 Fix response data is big 2024-12-09 18:22:05 +07:00
d0caee99aa Fix close file after write
Better error handling when write and load measurement file
comment out spiffs format
2024-12-08 03:09:28 +07:00
3162030800 Increase html font text size 2024-12-08 01:24:29 +07:00
6b6116ab6d Format csv file
remove pm1.0 and pm10
Add tvoc raw and nox raw
2024-12-08 01:23:51 +07:00
15dec1713d Display serial number to dashboard 2024-12-08 01:06:28 +07:00
70e626cbc9 Display serial number to dashboard 2024-12-08 01:05:43 +07:00
c003912d7a post storage and time return html 2024-12-08 01:05:21 +07:00
902797ceb0 Redirect root path to dashboard 2024-12-08 01:03:53 +07:00
430e908d88 Downloaded filename and AP ssid
ap ssid format have serial number
filname have last 4 digit serial number
2024-12-07 23:51:54 +07:00
6cb06986c3 Remove set time reduce by 17 minutes 2024-12-07 23:32:38 +07:00
e3156d438c Hotspot mode 2024-12-07 05:41:39 +07:00
4ae0206e6b Switch button position 2024-12-07 05:40:43 +07:00
83a4eddc37 Local storage mode using esp32 as AP 2024-12-07 05:39:59 +07:00
67b71f583b Add esp32 timestamp to dashboard page
Hotfix timestamp off by 17 minutes when set system time
2024-12-07 05:13:19 +07:00
e2798f1193 Dashboard page 2024-12-07 04:59:25 +07:00
f4357cca7e Fix timezone 2024-12-07 04:16:00 +07:00
20dcea20ad Notify write succes on oled
Disable led bar
Decrease oled brightness
2024-12-07 02:21:11 +07:00
cfe6fa9fd5 Seperate reset and set time endpoints 2024-12-07 02:05:43 +07:00
391186dd59 PM2.5 correction 2024-12-06 20:00:43 +07:00
a9f7f72871 Fix typo 2024-12-06 19:40:32 +07:00
9a3f71b33c Fix typo 2024-12-06 19:39:32 +07:00
d8f433bd3e WiFi reconnection with indicator 2024-12-06 19:38:34 +07:00
da414bf3fc Add tips to docs 2024-12-06 19:14:39 +07:00
d225af623a Add local storage docs 2024-12-06 19:06:28 +07:00
b7d22c2136 Fix SPIFFS usage percentage 2024-12-06 15:19:03 +07:00
6cd5e9f4b8 Handle if spiffs full 2024-12-06 04:26:18 +07:00
0cec71ceb6 Attempt connect to default wifi on boot
notify led when new measurement inserted to local storage
2024-12-06 03:57:49 +07:00
424d1d89fa Init timezone on boot 2024-12-06 03:55:55 +07:00
6186e3eca0 Fix get storage allocate based on size 2024-12-06 03:12:37 +07:00
b79c4e74e2 Add timestamp to local storage measurements 2024-12-06 03:00:23 +07:00
baa8601b5c Reset storage endpoints 2024-12-06 02:20:46 +07:00
a9fa7b6e63 Delete local storage function 2024-12-06 02:20:07 +07:00
1034f1892a Set and get system time 2024-12-06 02:19:25 +07:00
859c1a7e92 Local server to get local storage measurements 2024-12-05 04:09:17 +07:00
bce46445d6 Disable unnecessary scheduler 2024-12-05 04:07:57 +07:00
be7ca28a0e Scheduler to run save measurements to local storage 2024-12-05 04:07:00 +07:00
12e6f72b85 Save and get function local storage measurements 2024-12-05 04:05:42 +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
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
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
19 changed files with 803 additions and 298 deletions

BIN
docs/epoch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

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

@ -0,0 +1,56 @@
*This document to explain local storage mode - experimental*
## How it works?
1. Monitor directly goes to local storage mode
2. On boot, monitor will attempt to connect to default wifi. And if connected, mdns and local server will be enabled, otherwise it will ignore and continues the measurements
3. On display, when boot it will show the mode ("local storage mode") and wifi related scenario. After that, monitor will show the measurements dashboard
4. Measurement records to the local storage every two minutes that saved on CSV file in SPIFFs partition
5. Every successful writes, monitor will blink the most left led bar to *blue* twice, but if failed it will blink *red* twice. There are two possibilities for failed write, SPIFFs partition already full or out of heap memory when load the file.
6. There are 2 endpoinds added for this mode, download measurements from local storage and reset measurement (delete old measurements file and create new one) with new timestamp. Timestamp here to set the monitor system time.
**Notes**
1. Default wifi
- ssid ➝ `airgradient`
- password ➝ `cleanair`
2. Maximum measurements file is around 113kb. If assume each measurements is 60 bytes, with write schedule 2 minutes, SPIFFS will be full in around 5 days
3. WiFi connection attempt on boot wait for 10s before considering timeout
4. Tips. If monitor not connected to wifi on boot, no need to restart the monitor for reconnection, it will automatically connect to AP once it is available
### Local Storage Endpoinds
*Make sure monitor is connected to AP, and client also connect to it. And change the serial number on the url*
**Download measurements file**
To download measurements file from local storage, just directly access following url on the browser `http://airgradient_aaaaaaaa.local/storage`, and browser should automatically download the file.
**Reset measurements**
Execute below command in terminal
```sh
curl -X PUT -H "Content-Type: text/plain" -d '1733431986' http://airgradient_aaaaaaa.local/storage/reset
```
`1733431986` this data is the time that we want to set monitor system time to. Its in epoch time format and expecting UTC+0 timezone.
To get epoch time, access this url [https://www.unixtimestamp.com/](https://www.unixtimestamp.com/), and click copy button.
![unixtimestamp website](epoch.png)
### Example measurements file content
```csv
datetime,pm0.3 count,pm1,pm2.5,pm10,temp,rhum,co2,tvoc,nox
05/12 21:10:59,869.67,11.17,20.33,21.83,26.69,72.93,417,40,1
05/12 21:11:30,834.83,11.50,19.33,20.33,26.68,73.08,413,79,1
05/12 21:12:01,829.67,10.33,19.33,22.00,26.64,73.09,412,90,1
05/12 21:12:32,831.50,10.33,18.33,20.83,26.62,73.21,411,97,1
05/12 21:13:02,887.50,12.00,20.33,21.67,26.59,73.33,412,95,1
05/12 21:13:33,785.17,8.67,18.50,19.50,26.56,73.43,414,92,1
05/12 21:14:04,827.50,10.50,18.50,19.50,26.54,73.43,415,98,1
05/12 21:14:35,815.83,10.50,19.50,19.83,26.49,73.47,413,99,1
```

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

@ -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

@ -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

@ -9,10 +9,16 @@ LocalServer::LocalServer(Stream &log, OpenMetrics &openMetrics,
LocalServer::~LocalServer() {} LocalServer::~LocalServer() {}
bool LocalServer::begin(void) { bool LocalServer::begin(void) {
server.on("/", HTTP_GET, [this]() { _GET_root(); });
server.on("/measures/current", HTTP_GET, [this]() { _GET_measure(); }); server.on("/measures/current", HTTP_GET, [this]() { _GET_measure(); });
server.on(openMetrics.getApi(), HTTP_GET, [this]() { _GET_metrics(); }); server.on(openMetrics.getApi(), HTTP_GET, [this]() { _GET_metrics(); });
server.on("/config", HTTP_GET, [this]() { _GET_config(); }); server.on("/config", HTTP_GET, [this]() { _GET_config(); });
server.on("/config", HTTP_PUT, [this]() { _PUT_config(); }); server.on("/config", HTTP_PUT, [this]() { _PUT_config(); });
server.on("/dashboard", HTTP_GET, [this]() { _GET_dashboard(); });
server.on("/storage/download", HTTP_GET, [this]() { _GET_storage(); });
server.on("/storage/reset", HTTP_POST, [this]() { _POST_storage(); });
server.on("/timestamp", HTTP_POST, [this]() { _POST_time(); });
server.begin(); server.begin();
if (xTaskCreate( if (xTaskCreate(
@ -38,6 +44,13 @@ String LocalServer::getHostname(void) {
void LocalServer::_handle(void) { server.handleClient(); } void LocalServer::_handle(void) { server.handleClient(); }
void LocalServer::_GET_root(void) {
String body = "If you are not redirected automatically, go to <a "
"href='http://192.168.4.1/dashboard'>dashboard</a>.";
server.send(302, "text/html", htmlResponse(body, true));
}
void LocalServer::_GET_config(void) { void LocalServer::_GET_config(void) {
if(ag->isOne()) { if(ag->isOne()) {
server.send(200, "application/json", config.toString()); server.send(200, "application/json", config.toString());
@ -68,4 +81,174 @@ void LocalServer::_GET_measure(void) {
server.send(200, "application/json", toSend); server.send(200, "application/json", toSend);
} }
void LocalServer::_GET_dashboard(void) {
String timestamp = ag->getCurrentTime();
server.send(200, "text/html", htmlDashboard(timestamp));
}
void LocalServer::_GET_storage(void) {
char *data = measure.getLocalStorage();
if (data != nullptr) {
String filename =
"measurements-" + ag->deviceId().substring(8) + ".csv"; // measurements-fdsa.csv
server.sendHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
server.send_P(200, "text/plain", data);
free(data);
} else {
server.send(204, "text/plain", "No data");
}
}
void LocalServer::_POST_storage(void) {
String body;
int statusCode = 200;
if (measure.resetLocalStorage()) {
body = "Success reset storage";
} else {
body = "Failed reset local storage, unknown error";
statusCode = 500;
}
body += ". Go to <a href='http://192.168.4.1/dashboard'>dashboard</a>.";
server.send(statusCode, "text/html", htmlResponse(body, false));
}
void LocalServer::_POST_time(void) {
String epochTime = server.arg(0);
Serial.printf("Received epoch: %s \n", epochTime.c_str());
if (epochTime.isEmpty()) {
server.send(400, "text/plain", "Time query not provided");
return;
}
long _epochTime = epochTime.toInt();
if (_epochTime == 0) {
server.send(400, "text/plain", "Time format is not in epoch time");
return;
}
ag->setCurrentTime(_epochTime);
String body = "Success set new time. Go to <a href='http://192.168.4.1/dashboard'>dashboard</a>.";
server.send(200, "text/html", htmlResponse(body, false));
}
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; } void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }
String LocalServer::htmlDashboard(String timestamp) {
String page = "";
page += "<!DOCTYPE html>";
page += "<html lang=\"en\">";
page += "<head>";
page += " <meta charset=\"UTF-8\">";
page += " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">";
page += " <title>AirGradient Local Storage Mode</title>";
page += " <style>";
page += " body {";
page += " font-family: Arial, sans-serif;";
page += " display: flex;";
page += " flex-direction: column;";
page += " align-items: center;";
page += " margin-top: 50px;";
page += " }";
page += "";
page += " button {";
page += " display: block;";
page += " margin: 10px 0;";
page += " padding: 10px 20px;";
page += " font-size: 16px;";
page += " cursor: pointer;";
page += " }";
page += " .datetime-container {";
page += " display: flex;";
page += " align-items: center;";
page += " margin: 10px 0;";
page += " }";
page += " .datetime-container input[type=\"datetime-local\"] {";
page += " margin-left: 10px;";
page += " padding: 5px;";
page += " font-size: 16px;";
page += " }";
page += " button.reset-button {";
page += " background-color: red;";
page += " color: white;";
page += " border: none;";
page += " padding: 10px 20px;";
page += " font-size: 16px;";
page += " cursor: pointer;";
page += " }";
page += " .spacer {";
page += " height: 50px;";
page += " }";
page += " </style>";
page += "</head>";
page += "<body>";
page += " <h2>";
page += " Device Time: ";
page += timestamp;
page += " </h2>";
page += " <h2>";
page += " Serial Number: ";
page += ag->deviceId();
page += " </h2>";
page += " <form action=\"/storage/download\" method=\"GET\">";
page += " <button type=\"submit\">Download Measurements</button>";
page += " </form>";
page += " <form id=\"timestampForm\" method=\"POST\" action=\"/timestamp\">";
page += " <input type=\"datetime-local\" id=\"timestampInput\" required>";
page += " <button type=\"submit\">Set Timestamp</button>";
page += " <input type=\"hidden\" name=\"timestamp\" id=\"epochInput\">";
page += " </form>";
page += " <div class=\"spacer\"></div>";
page += " <form action=\"/storage/reset\" method=\"POST\"";
page += " onsubmit=\"return confirm('Are you sure you want to reset the measurements? "
"This action will permanently delete the existing measurement files!');\">";
page += " <button class=\"reset-button\" type=\"submit\">Reset Measurements</button>";
page += " </form>";
page += "</body>";
page += "<script>";
page += " document.querySelector('#timestampForm').onsubmit = function (event) {";
page += " const datetimeInput = document.querySelector('#timestampInput').value;";
page += " const localDate = new Date(datetimeInput);";
page += " const epochTimeUTC = Math.floor(Date.UTC(";
page += " localDate.getFullYear(),";
page += " localDate.getMonth(),";
page += " localDate.getDate(),";
page += " localDate.getHours(),";
page += " localDate.getMinutes()";
page += " ) / 1000);";
page += " document.querySelector('#epochInput').value = epochTimeUTC;";
page += " return true;";
page += " };";
page += "</script>";
page += "</html>";
return page;
}
String LocalServer::htmlResponse(String body, bool redirect) {
String page = "";
page += "<!DOCTYPE HTML>";
page += "<html lang=\"en-US\">";
page += " <head>";
page += "<style>";
page += "p { font-size: 22px; }";
page += "</style>";
page += " <meta charset=\"UTF-8\">";
if (redirect) {
page += " <meta http-equiv=\"refresh\" content=\"0;url=/dashboard\">";
}
page += " <title>Page Redirection</title>";
page += " </head>";
page += " <body>";
page += " <p>";
page += body;
page += " </p>";
page += " </body>";
page += "</html>";
return page;
}

View File

@ -19,6 +19,9 @@ private:
WebServer server; WebServer server;
AgFirmwareMode fwMode; AgFirmwareMode fwMode;
String htmlDashboard(String timestamp);
String htmlResponse(String body, bool redirect);
public: public:
LocalServer(Stream &log, OpenMetrics &openMetrics, Measurements &measure, LocalServer(Stream &log, OpenMetrics &openMetrics, Measurements &measure,
Configuration &config, WifiConnector& wifiConnector); Configuration &config, WifiConnector& wifiConnector);
@ -29,10 +32,15 @@ public:
String getHostname(void); String getHostname(void);
void setFwMode(AgFirmwareMode fwMode); void setFwMode(AgFirmwareMode fwMode);
void _handle(void); void _handle(void);
void _GET_root(void);
void _GET_config(void); void _GET_config(void);
void _PUT_config(void); void _PUT_config(void);
void _GET_metrics(void); void _GET_metrics(void);
void _GET_measure(void); void _GET_measure(void);
void _GET_dashboard(void);
void _GET_storage(void);
void _POST_storage(void);
void _POST_time(void);
}; };
#endif /** _LOCAL_SERVER_H_ */ #endif /** _LOCAL_SERVER_H_ */

View File

@ -93,6 +93,7 @@ static AgFirmwareMode fwMode = FW_MODE_I_9PSL;
static bool ledBarButtonTest = false; static bool ledBarButtonTest = false;
static String fwNewVersion; static String fwNewVersion;
static bool isLocalServerInitialized = false;
static void boardInit(void); static void boardInit(void);
static void failedHandler(String msg); static void failedHandler(String msg);
@ -116,23 +117,29 @@ static void displayExecuteOta(OtaState state, String msg,
int processing); int processing);
static int calculateMaxPeriod(int updateInterval); static int calculateMaxPeriod(int updateInterval);
static void setMeasurementMaxPeriod(); static void setMeasurementMaxPeriod();
static void offlineStorageUpdate();
AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, updateDisplayAndLedBar); AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, updateDisplayAndLedBar);
AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL, // AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL,
configurationUpdateSchedule); // configurationUpdateSchedule);
AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer); // AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update); AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update);
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, updatePm); AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, updatePm);
AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate); AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate);
AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc); AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc);
AgSchedule watchdogFeedSchedule(60000, wdgFeedUpdate); AgSchedule watchdogFeedSchedule(60000, wdgFeedUpdate);
AgSchedule checkForUpdateSchedule(FIRMWARE_CHECK_FOR_UPDATE_MS, firmwareCheckForUpdate); AgSchedule offlineStorage((2 * 60000), offlineStorageUpdate);
// AgSchedule checkForUpdateSchedule(FIRMWARE_CHECK_FOR_UPDATE_MS, firmwareCheckForUpdate);
void setup() { void setup() {
/** Serial for print debug message */ /** Serial for print debug message */
Serial.begin(115200); Serial.begin(115200);
delay(100); /** For bester show log */ delay(100); /** For bester show log */
// Set timezone to UTC
setenv("TZ", "UTC", 1);
tzset();
/** Print device ID into log */ /** Print device ID into log */
Serial.println("Serial nr: " + ag->deviceId()); Serial.println("Serial nr: " + ag->deviceId());
@ -168,102 +175,35 @@ 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(false);
/** Connecting wifi */ // Force to offline mode
bool connectToWifi = false; configuration.setOfflineMode(true);
if (ag->isOne()) {
/** Show message confirm offline mode, should me perform if LED bar button
* test pressed */
if (ledBarButtonTest == false) {
oledDisplay.setText(
"Press now for",
configuration.isOfflineMode() ? "online mode" : "offline mode", "");
uint32_t startTime = millis();
while (true) {
if (ag->button.getState() == ag->button.BUTTON_PRESSED) {
configuration.setOfflineMode(!configuration.isOfflineMode());
oledDisplay.setText(
"Offline Mode",
configuration.isOfflineMode() ? " = True" : " = False", "");
delay(1000);
break;
}
uint32_t periodMs = (uint32_t)(millis() - startTime);
if (periodMs >= 3000) {
break;
}
}
connectToWifi = !configuration.isOfflineMode();
} else {
configuration.setOfflineModeWithoutSave(true);
}
} else {
connectToWifi = true;
}
if (connectToWifi) {
apiClient.begin();
if (wifiConnector.connect()) {
if (wifiConnector.isConnected()) {
mdnsInit();
localServer.begin();
initMqtt();
sendDataToAg();
#ifdef ESP8266
// ota not supported
#else
firmwareCheckForUpdate();
checkForUpdateSchedule.update();
#endif
apiClient.fetchServerConfiguration();
configSchedule.update();
if (apiClient.isFetchConfigureFailed()) {
if (ag->isOne()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
AgStateMachineWiFiOkServerOkSensorConfigFailed);
} else {
stateMachine.displayClearAddToDashBoard();
}
}
stateMachine.handleLeds(
AgStateMachineWiFiOkServerOkSensorConfigFailed);
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
} else {
ledBarEnabledUpdate();
}
} else {
if (wifiConnector.isConfigurePorttalTimeout()) {
oledDisplay.showRebooting();
delay(2500);
oledDisplay.setText("", "", "");
ESP.restart();
}
}
}
}
/** Set offline mode without saving, cause wifi is not configured */
if (wifiConnector.hasConfigurated() == false) {
Serial.println("Set offline mode cause wifi is not configurated");
configuration.setOfflineModeWithoutSave(true);
}
/** Show display Warning up */ /** Show display Warning up */
if (ag->isOne()) { if (ag->isOne()) {
oledDisplay.setText("Warming Up", "Serial Number:", ag->deviceId().c_str()); oledDisplay.setText("Warming Up", "Serial Number:", ag->deviceId().c_str());
delay(DISPLAY_DELAY_SHOW_CONTENT_MS); delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
Serial.println("Display brightness: " + String(configuration.getDisplayBrightness()));
oledDisplay.setBrightness(configuration.getDisplayBrightness());
} }
String deviceId = ag->deviceId();
// Connect to Wi-Fi network with SSID and password
Serial.print("Setting AP (Access Point)…");
// Remove the password parameter, if you want the AP (Access Point) to be open
WiFi.softAP("ag_" + deviceId, "cleanair");
IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(IP);
Serial.printf("SSID: ag_%s\n", deviceId.c_str());
oledDisplay.setText("", "Offline Storage Mode", "");
delay(3000);
// mdnsInit();
localServer.begin();
// Update display and led bar after finishing setup to show dashboard // Update display and led bar after finishing setup to show dashboard
updateDisplayAndLedBar(); updateDisplayAndLedBar();
} }
@ -271,8 +211,9 @@ void setup() {
void loop() { void loop() {
/** Handle schedule */ /** Handle schedule */
dispLedSchedule.run(); dispLedSchedule.run();
configSchedule.run(); // configSchedule.run();
agApiPostSchedule.run(); // agApiPostSchedule.run();
offlineStorage.run();
if (configuration.hasSensorS8) { if (configuration.hasSensorS8) {
co2Schedule.run(); co2Schedule.run();
@ -308,8 +249,8 @@ void loop() {
watchdogFeedSchedule.run(); watchdogFeedSchedule.run();
/** Check for handle WiFi reconnect */ // /** Check for handle WiFi reconnect */
wifiConnector.handle(); // wifiConnector.handle();
/** factory reset handle */ /** factory reset handle */
factoryConfigReset(); factoryConfigReset();
@ -318,7 +259,7 @@ void loop() {
configUpdateHandle(); configUpdateHandle();
/** Firmware check for update handle */ /** Firmware check for update handle */
checkForUpdateSchedule.run(); // checkForUpdateSchedule.run();
} }
static void co2Update(void) { static void co2Update(void) {
@ -436,7 +377,7 @@ static void factoryConfigReset(void) {
WiFi.disconnect(true, true); WiFi.disconnect(true, true);
/** Reset local config */ /** Reset local config */
configuration.reset(); // configuration.reset();
if (ag->isOne()) { if (ag->isOne()) {
oledDisplay.setText("Factory reset", "successful", ""); oledDisplay.setText("Factory reset", "successful", "");
@ -470,6 +411,8 @@ static void factoryConfigReset(void) {
static void wdgFeedUpdate(void) { static void wdgFeedUpdate(void) {
ag->watchdog.reset(); ag->watchdog.reset();
Serial.println("External watchdog feed!"); Serial.println("External watchdog feed!");
/** Log current free heap size */
Serial.printf("Free heap: %u\n", ESP.getFreeHeap());
} }
static void ledBarEnabledUpdate(void) { static void ledBarEnabledUpdate(void) {
@ -660,6 +603,7 @@ static void oneIndoorInit(void) {
/** Display init */ /** Display init */
oledDisplay.begin(); oledDisplay.begin();
oledDisplay.setBrightness(40);
/** Show boot display */ /** Show boot display */
Serial.println("Firmware Version: " + ag->getVersion()); Serial.println("Firmware Version: " + ag->getVersion());
@ -963,7 +907,7 @@ static void updateDisplayAndLedBar(void) {
if (configuration.isOfflineMode()) { if (configuration.isOfflineMode()) {
// Ignore network related status when in offline mode // Ignore network related status when in offline mode
stateMachine.displayHandle(AgStateMachineNormal); stateMachine.displayHandle(AgStateMachineNormal);
stateMachine.handleLeds(AgStateMachineNormal); // stateMachine.handleLeds(AgStateMachineNormal);
return; return;
} }
@ -1216,6 +1160,15 @@ 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;
} }
void offlineStorageUpdate() {
if (measurements.saveLocalStorage(*ag, configuration)) {
oledDisplay.setText("", "New Measurements", "");
} else {
oledDisplay.setText("Failed write", "Measurements", "");
}
delay(1200);
}

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,5 +1,5 @@
name=AirGradient Air Quality Sensor name=AirGradient Air Quality Sensor
version=3.1.10 version=3.1.13
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

@ -253,7 +253,7 @@ void Configuration::loadConfig(void) {
} }
file.close(); file.close();
} else { } else {
SPIFFS.format(); // SPIFFS.format();
} }
#endif #endif
toConfig(buf); toConfig(buf);
@ -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

@ -2,6 +2,7 @@
#include "AgConfigure.h" #include "AgConfigure.h"
#include "AirGradient.h" #include "AirGradient.h"
#include "App/AppDef.h" #include "App/AppDef.h"
#include "SPIFFS.h"
#define json_prop_pmFirmware "firmware" #define json_prop_pmFirmware "firmware"
#define json_prop_pm01Ae "pm01" #define json_prop_pm01Ae "pm01"
@ -189,7 +190,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 +229,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 +261,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 +300,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 +349,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 +384,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 +397,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 +532,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 +548,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,
@ -841,10 +897,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 +912,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 +926,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 +941,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 +1066,97 @@ 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; }
bool Measurements::resetLocalStorage() {
if (!SPIFFS.remove(FILE_PATH)) {
Serial.println("Failed reset local storage");
return false;
}
Serial.println("Success reset local storage");
return true;
}
bool Measurements::saveLocalStorage(AirGradient &ag, Configuration &config) {
int spiffUsed = ((float)SPIFFS.usedBytes() / (float)SPIFFS.totalBytes()) * 100.0;
Serial.printf("%d | %d\n", SPIFFS.totalBytes(), SPIFFS.usedBytes());
Serial.printf("SPIFF used %d%%\n", spiffUsed);
if (spiffUsed > 98) {
Serial.println("SPIFF used already on maximum");
return false;
}
File file;
if (!SPIFFS.exists(FILE_PATH)) {
file = SPIFFS.open(FILE_PATH, FILE_APPEND, true);
file.println(
"datetime,pm0.3 count,pm2.5,temp,rhum,co2,tvoc,tvoc raw,nox,nox raw"); // csv header
Serial.println("New measurements file created");
} else {
file = SPIFFS.open(FILE_PATH, FILE_APPEND, false);
}
float pm25 = getCorrectedPM25(ag, config, true);
// Save new measurements
char buff[100] = {0};
sprintf(buff, "%s,%.2f,%.2f,%.2f,%.2f,%d,%d,%d,%d,%d\n\0", ag.getCurrentTime().c_str(),
ag.round2(_pm_03_pc[0].update.avg), ag.round2(pm25),
ag.round2(_temperature[0].update.avg), ag.round2(_humidity[0].update.avg),
(int)round(_co2.update.avg), (int)round(_tvoc.update.avg),
(int)round(_tvoc_raw.update.avg), (int)round(_nox.update.avg),
(int)round(_nox_raw.update.avg));
size_t len = strlen(buff);
if (file.write((const uint8_t *)buff, len) != len) {
Serial.println("Write new measurements failed!");
file.close();
return false;
}
file.close();
Serial.println("Success save measurements to local storage");
return true;
}
char *Measurements::getLocalStorage() {
char *buf = nullptr;
bool success = false;
if (!SPIFFS.exists(FILE_PATH)) {
Serial.println("No measurements file exists yet");
return nullptr;
}
File file = SPIFFS.open(FILE_PATH);
if (file && !file.isDirectory()) {
// Allocate memory
buf = new char[file.size() + 1];
if (buf == nullptr) {
return nullptr;
}
memset(buf, 0, file.size() + 1);
// Retrieve data from the file
if (file.readBytes(buf, file.size()) != file.size()) {
Serial.println("Reading measurements file: failed - size not match");
} else {
Serial.println("Reading measurements file: success");
success = true;
}
file.close();
}
if (!success) {
Serial.println("Reading measurements file failed");
if (buf != nullptr) {
delete buf;
}
return nullptr;
}
// NOTE: Don't forget to free
return buf;
}

View File

@ -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
@ -132,6 +142,10 @@ public:
String toString(bool localServer, AgFirmwareMode fwMode, int rssi, AirGradient &ag, String toString(bool localServer, AgFirmwareMode fwMode, int rssi, AirGradient &ag,
Configuration &config); Configuration &config);
bool resetLocalStorage();
bool saveLocalStorage(AirGradient &ag, Configuration &config);
char *getLocalStorage();
/** /**
* Set to true if want to debug every update value * Set to true if want to debug every update value
*/ */
@ -163,6 +177,7 @@ private:
IntegerValue _pm_10_pc[2]; // particle count 10 IntegerValue _pm_10_pc[2]; // particle count 10
bool _debug = false; bool _debug = false;
const char *FILE_PATH = "/measurements.csv"; // Local storage file path
/** /**
* @brief Get PMS5003 firmware version string * @brief Get PMS5003 firmware version string

View File

@ -85,3 +85,25 @@ String AirGradient::deviceId(void) {
mac.toLowerCase(); mac.toLowerCase();
return mac; return mac;
} }
void AirGradient::setCurrentTime(long epochTime) {
// set current day/time
struct timeval tv;
tv.tv_sec = epochTime; // - 1020; // 17 minutes // don't know why it always off by 17 minutes
settimeofday(&tv, NULL);
Serial.println(epochTime);
Serial.printf("Set current time to %s\n", getCurrentTime().c_str());
}
String AirGradient::getCurrentTime() {
// Get time
time_t now;
char strftime_buf[64];
struct tm timeinfo;
time(&now);
// Format
localtime_r(&now, &timeinfo);
strftime(strftime_buf, sizeof(strftime_buf), "%d/%m %H:%M:%S", &timeinfo);
return String(strftime_buf);
}

View File

@ -15,7 +15,7 @@
#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.13-snap"
#endif #endif
/** /**
@ -173,6 +173,9 @@ public:
*/ */
String deviceId(void); String deviceId(void);
void setCurrentTime(long epochTime);
String getCurrentTime();
private: private:
BoardType boardType; BoardType boardType;
}; };