mirror of
https://github.com/airgradienthq/arduino.git
synced 2025-06-29 17:50:58 +02:00
Compare commits
69 Commits
3.1.10
...
feat/local
Author | SHA1 | Date | |
---|---|---|---|
227bd518c9 | |||
d0caee99aa | |||
3162030800 | |||
6b6116ab6d | |||
15dec1713d | |||
70e626cbc9 | |||
c003912d7a | |||
902797ceb0 | |||
430e908d88 | |||
6cb06986c3 | |||
e3156d438c | |||
4ae0206e6b | |||
83a4eddc37 | |||
67b71f583b | |||
e2798f1193 | |||
f4357cca7e | |||
20dcea20ad | |||
cfe6fa9fd5 | |||
391186dd59 | |||
a9f7f72871 | |||
9a3f71b33c | |||
d8f433bd3e | |||
da414bf3fc | |||
d225af623a | |||
b7d22c2136 | |||
6cd5e9f4b8 | |||
0cec71ceb6 | |||
424d1d89fa | |||
6186e3eca0 | |||
b79c4e74e2 | |||
baa8601b5c | |||
a9fa7b6e63 | |||
1034f1892a | |||
859c1a7e92 | |||
bce46445d6 | |||
be7ca28a0e | |||
12e6f72b85 | |||
e95627ece6 | |||
80b9ae11d8 | |||
1937e3d59e | |||
107fb21331 | |||
ccc1ab463a | |||
39ef69cbdf | |||
3473e30e2e | |||
566f8a63b4 | |||
9e4d52454b | |||
5f5e985309 | |||
d638573ca7 | |||
79fbd901bd | |||
3644dc43fe | |||
03fa62d8f0 | |||
902a768f28 | |||
1de9344f43 | |||
46f6309b77 | |||
a6b48acb41 | |||
1b4d89e1a1 | |||
0d2b0fb657 | |||
9f08af44b0 | |||
6b661cdeb7 | |||
dc299c4b54 | |||
2f595b4e41 | |||
a30535f75f | |||
a513943cba | |||
96bb6952fb | |||
10653bfe26 | |||
c7f89fa7b7 | |||
b11c461b60 | |||
404c14aad2 | |||
bfbae680fd |
BIN
docs/epoch.png
Normal file
BIN
docs/epoch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 221 KiB |
@ -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}` |
|
||||
| `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 }` |
|
||||
| `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
|
||||
|
||||
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:
|
||||
|
||||
@ -189,13 +189,28 @@ Example correction configuration:
|
||||
| PMS5003_20231218 | `"slr_PMS5003_20231218"` | Correction for PMS5003 sensor batch 20231218| 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/)
|
||||
- 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
|
||||
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}}}}'
|
||||
```
|
56
docs/local-storage-experimental.md
Normal file
56
docs/local-storage-experimental.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
```
|
||||
|
@ -57,26 +57,20 @@ String OpenMetrics::getPayload(void) {
|
||||
"gauge", "dbm");
|
||||
add_metric_point("", String(wifiConnector.RSSI()));
|
||||
|
||||
if (config.hasSensorS8 && measure.CO2 >= 0) {
|
||||
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));
|
||||
}
|
||||
|
||||
// Initialize default invalid value for each measurements
|
||||
float _temp = utils::getInvalidTemperature();
|
||||
float _hum = utils::getInvalidHumidity();
|
||||
int pm01 = utils::getInvalidPmValue();
|
||||
int pm25 = utils::getInvalidPmValue();
|
||||
int pm10 = utils::getInvalidPmValue();
|
||||
int pm03PCount = utils::getInvalidPmValue();
|
||||
int co2 = utils::getInvalidCO2();
|
||||
int atmpCompensated = utils::getInvalidTemperature();
|
||||
int ahumCompensated = utils::getInvalidHumidity();
|
||||
int tvoc = utils::getInvalidVOC();
|
||||
int tvoc_raw = utils::getInvalidVOC();
|
||||
int tvocRaw = utils::getInvalidVOC();
|
||||
int nox = utils::getInvalidNOx();
|
||||
int nox_raw = utils::getInvalidNOx();
|
||||
int noxRaw = utils::getInvalidNOx();
|
||||
|
||||
if (config.hasSensorSHT) {
|
||||
_temp = measure.getFloat(Measurements::Temperature);
|
||||
@ -87,16 +81,21 @@ String OpenMetrics::getPayload(void) {
|
||||
|
||||
if (config.hasSensorPMS1) {
|
||||
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);
|
||||
pm03PCount = measure.get(Measurements::PM03_PC);
|
||||
}
|
||||
|
||||
if (config.hasSensorSGP) {
|
||||
tvoc = measure.get(Measurements::TVOC);
|
||||
tvoc_raw = measure.get(Measurements::TVOCRaw);
|
||||
tvocRaw = measure.get(Measurements::TVOCRaw);
|
||||
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) {
|
||||
@ -138,12 +137,12 @@ String OpenMetrics::getPayload(void) {
|
||||
"gauge");
|
||||
add_metric_point("", String(tvoc));
|
||||
}
|
||||
if (utils::isValidVOC(tvoc_raw)) {
|
||||
if (utils::isValidVOC(tvocRaw)) {
|
||||
add_metric("tvoc_raw",
|
||||
"The raw input value to the Total Volatile Organic Compounds "
|
||||
"(TVOC) index as measured by the AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(tvoc_raw));
|
||||
add_metric_point("", String(tvocRaw));
|
||||
}
|
||||
if (utils::isValidNOx(nox)) {
|
||||
add_metric("nox_index",
|
||||
@ -152,15 +151,23 @@ String OpenMetrics::getPayload(void) {
|
||||
"gauge");
|
||||
add_metric_point("", String(nox));
|
||||
}
|
||||
if (utils::isValidNOx(nox_raw)) {
|
||||
if (utils::isValidNOx(noxRaw)) {
|
||||
add_metric("nox_raw",
|
||||
"The raw input value to the Nitrous Oxide (NOx) index as "
|
||||
"measured by the AirGradient SGP sensor",
|
||||
"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)) {
|
||||
add_metric(
|
||||
"temperature",
|
||||
|
@ -57,26 +57,20 @@ String OpenMetrics::getPayload(void) {
|
||||
"gauge", "dbm");
|
||||
add_metric_point("", String(wifiConnector.RSSI()));
|
||||
|
||||
if (config.hasSensorS8 && measure.CO2 >= 0) {
|
||||
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));
|
||||
}
|
||||
|
||||
// Initialize default invalid value for each measurements
|
||||
float _temp = utils::getInvalidTemperature();
|
||||
float _hum = utils::getInvalidHumidity();
|
||||
int pm01 = utils::getInvalidPmValue();
|
||||
int pm25 = utils::getInvalidPmValue();
|
||||
int pm10 = utils::getInvalidPmValue();
|
||||
int pm03PCount = utils::getInvalidPmValue();
|
||||
int co2 = utils::getInvalidCO2();
|
||||
int atmpCompensated = utils::getInvalidTemperature();
|
||||
int ahumCompensated = utils::getInvalidHumidity();
|
||||
int tvoc = utils::getInvalidVOC();
|
||||
int tvoc_raw = utils::getInvalidVOC();
|
||||
int tvocRaw = utils::getInvalidVOC();
|
||||
int nox = utils::getInvalidNOx();
|
||||
int nox_raw = utils::getInvalidNOx();
|
||||
int noxRaw = utils::getInvalidNOx();
|
||||
|
||||
if (config.hasSensorSHT) {
|
||||
_temp = measure.getFloat(Measurements::Temperature);
|
||||
@ -87,16 +81,21 @@ String OpenMetrics::getPayload(void) {
|
||||
|
||||
if (config.hasSensorPMS1) {
|
||||
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);
|
||||
pm03PCount = measure.get(Measurements::PM03_PC);
|
||||
}
|
||||
|
||||
if (config.hasSensorSGP) {
|
||||
tvoc = measure.get(Measurements::TVOC);
|
||||
tvoc_raw = measure.get(Measurements::TVOCRaw);
|
||||
tvocRaw = measure.get(Measurements::TVOCRaw);
|
||||
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) {
|
||||
@ -138,12 +137,13 @@ String OpenMetrics::getPayload(void) {
|
||||
"gauge");
|
||||
add_metric_point("", String(tvoc));
|
||||
}
|
||||
if (utils::isValidVOC(tvoc_raw)) {
|
||||
|
||||
if (utils::isValidVOC(tvocRaw)) {
|
||||
add_metric("tvoc_raw",
|
||||
"The raw input value to the Total Volatile Organic Compounds "
|
||||
"(TVOC) index as measured by the AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(tvoc_raw));
|
||||
add_metric_point("", String(tvocRaw));
|
||||
}
|
||||
if (utils::isValidNOx(nox)) {
|
||||
add_metric("nox_index",
|
||||
@ -152,15 +152,23 @@ String OpenMetrics::getPayload(void) {
|
||||
"gauge");
|
||||
add_metric_point("", String(nox));
|
||||
}
|
||||
if (utils::isValidNOx(nox_raw)) {
|
||||
if (utils::isValidNOx(noxRaw)) {
|
||||
add_metric("nox_raw",
|
||||
"The raw input value to the Nitrous Oxide (NOx) index as "
|
||||
"measured by the AirGradient SGP sensor",
|
||||
"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)) {
|
||||
add_metric(
|
||||
"temperature",
|
||||
|
@ -57,26 +57,20 @@ String OpenMetrics::getPayload(void) {
|
||||
"gauge", "dbm");
|
||||
add_metric_point("", String(wifiConnector.RSSI()));
|
||||
|
||||
if (config.hasSensorS8 && measure.CO2 >= 0) {
|
||||
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));
|
||||
}
|
||||
|
||||
// Initialize default invalid value for each measurements
|
||||
float _temp = utils::getInvalidTemperature();
|
||||
float _hum = utils::getInvalidHumidity();
|
||||
int pm01 = utils::getInvalidPmValue();
|
||||
int pm25 = utils::getInvalidPmValue();
|
||||
int pm10 = utils::getInvalidPmValue();
|
||||
int pm03PCount = utils::getInvalidPmValue();
|
||||
int co2 = utils::getInvalidCO2();
|
||||
int atmpCompensated = utils::getInvalidTemperature();
|
||||
int ahumCompensated = utils::getInvalidHumidity();
|
||||
int tvoc = utils::getInvalidVOC();
|
||||
int tvoc_raw = utils::getInvalidVOC();
|
||||
int tvocRaw = utils::getInvalidVOC();
|
||||
int nox = utils::getInvalidNOx();
|
||||
int nox_raw = utils::getInvalidNOx();
|
||||
int noxRaw = utils::getInvalidNOx();
|
||||
|
||||
if (config.hasSensorSHT) {
|
||||
_temp = measure.getFloat(Measurements::Temperature);
|
||||
@ -87,16 +81,21 @@ String OpenMetrics::getPayload(void) {
|
||||
|
||||
if (config.hasSensorPMS1) {
|
||||
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);
|
||||
pm03PCount = measure.get(Measurements::PM03_PC);
|
||||
}
|
||||
|
||||
if (config.hasSensorSGP) {
|
||||
tvoc = measure.get(Measurements::TVOC);
|
||||
tvoc_raw = measure.get(Measurements::TVOCRaw);
|
||||
tvocRaw = measure.get(Measurements::TVOCRaw);
|
||||
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) {
|
||||
@ -138,12 +137,12 @@ String OpenMetrics::getPayload(void) {
|
||||
"gauge");
|
||||
add_metric_point("", String(tvoc));
|
||||
}
|
||||
if (utils::isValidVOC(tvoc_raw)) {
|
||||
if (utils::isValidVOC(tvocRaw)) {
|
||||
add_metric("tvoc_raw",
|
||||
"The raw input value to the Total Volatile Organic Compounds "
|
||||
"(TVOC) index as measured by the AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(tvoc_raw));
|
||||
add_metric_point("", String(tvocRaw));
|
||||
}
|
||||
if (utils::isValidNOx(nox)) {
|
||||
add_metric("nox_index",
|
||||
@ -152,15 +151,23 @@ String OpenMetrics::getPayload(void) {
|
||||
"gauge");
|
||||
add_metric_point("", String(nox));
|
||||
}
|
||||
if (utils::isValidNOx(nox_raw)) {
|
||||
if (utils::isValidNOx(noxRaw)) {
|
||||
add_metric("nox_raw",
|
||||
"The raw input value to the Nitrous Oxide (NOx) index as "
|
||||
"measured by the AirGradient SGP sensor",
|
||||
"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)) {
|
||||
add_metric(
|
||||
"temperature",
|
||||
|
@ -9,10 +9,16 @@ LocalServer::LocalServer(Stream &log, OpenMetrics &openMetrics,
|
||||
LocalServer::~LocalServer() {}
|
||||
|
||||
bool LocalServer::begin(void) {
|
||||
server.on("/", HTTP_GET, [this]() { _GET_root(); });
|
||||
server.on("/measures/current", HTTP_GET, [this]() { _GET_measure(); });
|
||||
server.on(openMetrics.getApi(), HTTP_GET, [this]() { _GET_metrics(); });
|
||||
server.on("/config", HTTP_GET, [this]() { _GET_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();
|
||||
|
||||
if (xTaskCreate(
|
||||
@ -38,6 +44,13 @@ String LocalServer::getHostname(void) {
|
||||
|
||||
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) {
|
||||
if(ag->isOne()) {
|
||||
server.send(200, "application/json", config.toString());
|
||||
@ -68,4 +81,174 @@ void LocalServer::_GET_measure(void) {
|
||||
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; }
|
||||
|
||||
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;
|
||||
}
|
@ -19,6 +19,9 @@ private:
|
||||
WebServer server;
|
||||
AgFirmwareMode fwMode;
|
||||
|
||||
String htmlDashboard(String timestamp);
|
||||
String htmlResponse(String body, bool redirect);
|
||||
|
||||
public:
|
||||
LocalServer(Stream &log, OpenMetrics &openMetrics, Measurements &measure,
|
||||
Configuration &config, WifiConnector& wifiConnector);
|
||||
@ -29,10 +32,15 @@ public:
|
||||
String getHostname(void);
|
||||
void setFwMode(AgFirmwareMode fwMode);
|
||||
void _handle(void);
|
||||
void _GET_root(void);
|
||||
void _GET_config(void);
|
||||
void _PUT_config(void);
|
||||
void _GET_metrics(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_ */
|
||||
|
@ -93,6 +93,7 @@ static AgFirmwareMode fwMode = FW_MODE_I_9PSL;
|
||||
|
||||
static bool ledBarButtonTest = false;
|
||||
static String fwNewVersion;
|
||||
static bool isLocalServerInitialized = false;
|
||||
|
||||
static void boardInit(void);
|
||||
static void failedHandler(String msg);
|
||||
@ -116,23 +117,29 @@ static void displayExecuteOta(OtaState state, String msg,
|
||||
int processing);
|
||||
static int calculateMaxPeriod(int updateInterval);
|
||||
static void setMeasurementMaxPeriod();
|
||||
static void offlineStorageUpdate();
|
||||
|
||||
AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, updateDisplayAndLedBar);
|
||||
AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL,
|
||||
configurationUpdateSchedule);
|
||||
AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
|
||||
// AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL,
|
||||
// configurationUpdateSchedule);
|
||||
// AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
|
||||
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update);
|
||||
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, updatePm);
|
||||
AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate);
|
||||
AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc);
|
||||
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() {
|
||||
/** Serial for print debug message */
|
||||
Serial.begin(115200);
|
||||
delay(100); /** For bester show log */
|
||||
|
||||
// Set timezone to UTC
|
||||
setenv("TZ", "UTC", 1);
|
||||
tzset();
|
||||
|
||||
/** Print device ID into log */
|
||||
Serial.println("Serial nr: " + ag->deviceId());
|
||||
|
||||
@ -168,102 +175,35 @@ void setup() {
|
||||
boardInit();
|
||||
setMeasurementMaxPeriod();
|
||||
|
||||
// Uncomment below line to print every measurements reading update
|
||||
measurements.setDebug(true);
|
||||
// Comment below line to disable debug measurement readings
|
||||
measurements.setDebug(false);
|
||||
|
||||
/** Connecting wifi */
|
||||
bool connectToWifi = false;
|
||||
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);
|
||||
}
|
||||
// Force to offline mode
|
||||
configuration.setOfflineMode(true);
|
||||
|
||||
/** Show display Warning up */
|
||||
if (ag->isOne()) {
|
||||
oledDisplay.setText("Warming Up", "Serial Number:", ag->deviceId().c_str());
|
||||
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
|
||||
updateDisplayAndLedBar();
|
||||
}
|
||||
@ -271,8 +211,9 @@ void setup() {
|
||||
void loop() {
|
||||
/** Handle schedule */
|
||||
dispLedSchedule.run();
|
||||
configSchedule.run();
|
||||
agApiPostSchedule.run();
|
||||
// configSchedule.run();
|
||||
// agApiPostSchedule.run();
|
||||
offlineStorage.run();
|
||||
|
||||
if (configuration.hasSensorS8) {
|
||||
co2Schedule.run();
|
||||
@ -308,8 +249,8 @@ void loop() {
|
||||
|
||||
watchdogFeedSchedule.run();
|
||||
|
||||
/** Check for handle WiFi reconnect */
|
||||
wifiConnector.handle();
|
||||
// /** Check for handle WiFi reconnect */
|
||||
// wifiConnector.handle();
|
||||
|
||||
/** factory reset handle */
|
||||
factoryConfigReset();
|
||||
@ -318,7 +259,7 @@ void loop() {
|
||||
configUpdateHandle();
|
||||
|
||||
/** Firmware check for update handle */
|
||||
checkForUpdateSchedule.run();
|
||||
// checkForUpdateSchedule.run();
|
||||
}
|
||||
|
||||
static void co2Update(void) {
|
||||
@ -436,7 +377,7 @@ static void factoryConfigReset(void) {
|
||||
WiFi.disconnect(true, true);
|
||||
|
||||
/** Reset local config */
|
||||
configuration.reset();
|
||||
// configuration.reset();
|
||||
|
||||
if (ag->isOne()) {
|
||||
oledDisplay.setText("Factory reset", "successful", "");
|
||||
@ -470,6 +411,8 @@ static void factoryConfigReset(void) {
|
||||
static void wdgFeedUpdate(void) {
|
||||
ag->watchdog.reset();
|
||||
Serial.println("External watchdog feed!");
|
||||
/** Log current free heap size */
|
||||
Serial.printf("Free heap: %u\n", ESP.getFreeHeap());
|
||||
}
|
||||
|
||||
static void ledBarEnabledUpdate(void) {
|
||||
@ -660,6 +603,7 @@ static void oneIndoorInit(void) {
|
||||
|
||||
/** Display init */
|
||||
oledDisplay.begin();
|
||||
oledDisplay.setBrightness(40);
|
||||
|
||||
/** Show boot display */
|
||||
Serial.println("Firmware Version: " + ag->getVersion());
|
||||
@ -963,7 +907,7 @@ static void updateDisplayAndLedBar(void) {
|
||||
if (configuration.isOfflineMode()) {
|
||||
// Ignore network related status when in offline mode
|
||||
stateMachine.displayHandle(AgStateMachineNormal);
|
||||
stateMachine.handleLeds(AgStateMachineNormal);
|
||||
// stateMachine.handleLeds(AgStateMachineNormal);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1216,6 +1160,15 @@ void setMeasurementMaxPeriod() {
|
||||
}
|
||||
|
||||
int calculateMaxPeriod(int updateInterval) {
|
||||
// 0.5 is 50% reduced interval for max period
|
||||
return (SERVER_SYNC_INTERVAL - (SERVER_SYNC_INTERVAL * 0.5)) / updateInterval;
|
||||
// 0.8 is 80% reduced interval for max period
|
||||
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);
|
||||
}
|
@ -57,22 +57,22 @@ String OpenMetrics::getPayload(void) {
|
||||
"gauge", "dbm");
|
||||
add_metric_point("", String(wifiConnector.RSSI()));
|
||||
|
||||
if (config.hasSensorS8 && measure.CO2 >= 0) {
|
||||
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));
|
||||
}
|
||||
|
||||
// Initialize default invalid value for each measurements
|
||||
float _temp = utils::getInvalidTemperature();
|
||||
float _hum = utils::getInvalidHumidity();
|
||||
int pm01 = utils::getInvalidPmValue();
|
||||
int pm25 = utils::getInvalidPmValue();
|
||||
int pm10 = utils::getInvalidPmValue();
|
||||
int pm03PCount = utils::getInvalidPmValue();
|
||||
int co2 = utils::getInvalidCO2();
|
||||
int atmpCompensated = utils::getInvalidTemperature();
|
||||
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) {
|
||||
_temp = (measure.getFloat(Measurements::Temperature, 1) +
|
||||
measure.getFloat(Measurements::Temperature, 2)) /
|
||||
@ -81,7 +81,10 @@ String OpenMetrics::getPayload(void) {
|
||||
measure.getFloat(Measurements::Humidity, 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;
|
||||
pm03PCount =
|
||||
(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) {
|
||||
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);
|
||||
pm03PCount = measure.get(Measurements::PM03_PC);
|
||||
}
|
||||
@ -103,7 +107,8 @@ String OpenMetrics::getPayload(void) {
|
||||
_temp = measure.getFloat(Measurements::Temperature, 1);
|
||||
_hum = measure.getFloat(Measurements::Humidity, 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);
|
||||
pm03PCount = measure.get(Measurements::PM03_PC, 1);
|
||||
}
|
||||
@ -111,13 +116,25 @@ String OpenMetrics::getPayload(void) {
|
||||
_temp = measure.getFloat(Measurements::Temperature, 2);
|
||||
_hum = measure.getFloat(Measurements::Humidity, 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);
|
||||
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 */
|
||||
if (ag->isOne()) {
|
||||
atmpCompensated = _temp;
|
||||
@ -127,6 +144,7 @@ String OpenMetrics::getPayload(void) {
|
||||
ahumCompensated = ag->pms5003t_1.compensateHum(_hum);
|
||||
}
|
||||
|
||||
// Add measurements that valid to the metrics
|
||||
if (config.hasSensorPMS1 || config.hasSensorPMS2) {
|
||||
if (utils::isValidPm(pm01)) {
|
||||
add_metric("pm1",
|
||||
@ -159,36 +177,44 @@ String OpenMetrics::getPayload(void) {
|
||||
}
|
||||
|
||||
if (config.hasSensorSGP) {
|
||||
if (utils::isValidVOC(measure.TVOC)) {
|
||||
if (utils::isValidVOC(tvoc)) {
|
||||
add_metric("tvoc_index",
|
||||
"The processed Total Volatile Organic Compounds (TVOC) index "
|
||||
"as measured by the AirGradient SGP sensor",
|
||||
"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",
|
||||
"The raw input value to the Total Volatile Organic Compounds "
|
||||
"(TVOC) index as measured by the AirGradient SGP sensor",
|
||||
"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",
|
||||
"The processed Nitrous Oxide (NOx) index as measured by the "
|
||||
"AirGradient SGP sensor",
|
||||
"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",
|
||||
"The raw input value to the Nitrous Oxide (NOx) index as "
|
||||
"measured by the AirGradient SGP sensor",
|
||||
"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)) {
|
||||
add_metric("temperature",
|
||||
"The ambient temperature as measured by the AirGradient SHT / PMS "
|
||||
@ -197,25 +223,21 @@ String OpenMetrics::getPayload(void) {
|
||||
add_metric_point("", String(_temp));
|
||||
}
|
||||
if (utils::isValidTemperature(atmpCompensated)) {
|
||||
add_metric(
|
||||
"temperature_compensated",
|
||||
"The compensated ambient temperature as measured by the AirGradient SHT / PMS "
|
||||
"sensor, in degrees Celsius",
|
||||
"gauge", "celsius");
|
||||
add_metric("temperature_compensated",
|
||||
"The compensated ambient temperature as measured by the AirGradient SHT / PMS "
|
||||
"sensor, in degrees Celsius",
|
||||
"gauge", "celsius");
|
||||
add_metric_point("", String(atmpCompensated));
|
||||
}
|
||||
if (utils::isValidHumidity(_hum)) {
|
||||
add_metric(
|
||||
"humidity",
|
||||
"The relative humidity as measured by the AirGradient SHT sensor",
|
||||
"gauge", "percent");
|
||||
add_metric("humidity", "The relative humidity as measured by the AirGradient SHT sensor",
|
||||
"gauge", "percent");
|
||||
add_metric_point("", String(_hum));
|
||||
}
|
||||
if (utils::isValidHumidity(ahumCompensated)) {
|
||||
add_metric(
|
||||
"humidity_compensated",
|
||||
"The compensated relative humidity as measured by the AirGradient SHT / PMS sensor",
|
||||
"gauge", "percent");
|
||||
add_metric("humidity_compensated",
|
||||
"The compensated relative humidity as measured by the AirGradient SHT / PMS sensor",
|
||||
"gauge", "percent");
|
||||
add_metric_point("", String(ahumCompensated));
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
name=AirGradient Air Quality Sensor
|
||||
version=3.1.10
|
||||
version=3.1.13
|
||||
author=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.
|
||||
|
@ -253,7 +253,7 @@ void Configuration::loadConfig(void) {
|
||||
}
|
||||
file.close();
|
||||
} else {
|
||||
SPIFFS.format();
|
||||
// SPIFFS.format();
|
||||
}
|
||||
#endif
|
||||
toConfig(buf);
|
||||
@ -292,8 +292,8 @@ void Configuration::defaultConfig(void) {
|
||||
// PM2.5 correction
|
||||
pmCorrection.algorithm = None;
|
||||
pmCorrection.changed = false;
|
||||
pmCorrection.intercept = -1;
|
||||
pmCorrection.scalingFactor = -1;
|
||||
pmCorrection.intercept = 0;
|
||||
pmCorrection.scalingFactor = 1;
|
||||
pmCorrection.useEPA = false;
|
||||
|
||||
saveConfig();
|
||||
@ -354,16 +354,16 @@ bool Configuration::begin(void) {
|
||||
* @return false Failure
|
||||
*/
|
||||
bool Configuration::parse(String data, bool isLocal) {
|
||||
logInfo("Parse configure: " + data);
|
||||
logInfo("Parsing configuration: " + data);
|
||||
|
||||
JSONVar root = JSON.parse(data);
|
||||
failedMessage = "";
|
||||
if (root == undefined) {
|
||||
if (root == undefined || JSONVar::typeof_(root) != "object") {
|
||||
logError("Parse configuration failed, JSON invalid (" + JSONVar::typeof_(root) + ")");
|
||||
failedMessage = "JSON invalid";
|
||||
logError(failedMessage);
|
||||
return false;
|
||||
}
|
||||
logInfo("Parse configure success");
|
||||
logInfo("Parse configuration success");
|
||||
|
||||
/** Is configuration changed */
|
||||
bool changed = false;
|
||||
@ -1369,7 +1369,12 @@ bool Configuration::isPMCorrectionChanged(void) {
|
||||
*/
|
||||
bool Configuration::isPMCorrectionEnabled(void) {
|
||||
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) {
|
||||
|
@ -12,7 +12,7 @@
|
||||
*/
|
||||
void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) {
|
||||
/** Temperature */
|
||||
float temp = value.getFloat(Measurements::Temperature);
|
||||
float temp = value.getAverage(Measurements::Temperature);
|
||||
if (utils::isValidTemperature(temp)) {
|
||||
float t = 0.0f;
|
||||
if (config.isTemperatureUnitInF()) {
|
||||
@ -44,7 +44,7 @@ void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) {
|
||||
DISP()->drawUTF8(1, 10, buf);
|
||||
|
||||
/** Show humidity */
|
||||
int rhum = (int)value.getFloat(Measurements::Humidity);
|
||||
int rhum = round(value.getAverage(Measurements::Humidity));
|
||||
if (utils::isValidHumidity(rhum)) {
|
||||
snprintf(buf, buf_size, "%d%%", rhum);
|
||||
} else {
|
||||
@ -292,7 +292,7 @@ void OledDisplay::showDashboard(const char *status) {
|
||||
DISP()->drawUTF8(1, 27, "CO2");
|
||||
|
||||
DISP()->setFont(u8g2_font_t0_22b_tf);
|
||||
int co2 = value.get(Measurements::CO2);
|
||||
int co2 = round(value.getAverage(Measurements::CO2));
|
||||
if (utils::isValidCO2(co2)) {
|
||||
sprintf(strBuf, "%d", co2);
|
||||
} else {
|
||||
@ -313,11 +313,10 @@ void OledDisplay::showDashboard(const char *status) {
|
||||
DISP()->drawStr(55, 27, "PM2.5");
|
||||
|
||||
/** Draw PM2.5 value */
|
||||
|
||||
int pm25 = value.get(Measurements::PM25);
|
||||
int pm25 = round(value.getAverage(Measurements::PM25));
|
||||
if (utils::isValidPm(pm25)) {
|
||||
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
|
||||
pm25 = (int)value.getCorrectedPM25(*ag, config);
|
||||
pm25 = round(value.getCorrectedPM25(*ag, config, true));
|
||||
}
|
||||
if (config.isPmStandardInUSAQI()) {
|
||||
sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(pm25));
|
||||
@ -343,7 +342,7 @@ void OledDisplay::showDashboard(const char *status) {
|
||||
DISP()->drawStr(100, 27, "VOC:");
|
||||
|
||||
/** Draw tvocIndexvalue */
|
||||
int tvoc = value.get(Measurements::TVOC);
|
||||
int tvoc = round(value.getAverage(Measurements::TVOC));
|
||||
if (utils::isValidVOC(tvoc)) {
|
||||
sprintf(strBuf, "%d", tvoc);
|
||||
} else {
|
||||
@ -352,7 +351,7 @@ void OledDisplay::showDashboard(const char *status) {
|
||||
DISP()->drawStr(100, 39, strBuf);
|
||||
|
||||
/** Draw NOx label */
|
||||
int nox = value.get(Measurements::NOx);
|
||||
int nox = round(value.getAverage(Measurements::NOx));
|
||||
DISP()->drawStr(100, 53, "NOx:");
|
||||
if (utils::isValidNOx(nox)) {
|
||||
sprintf(strBuf, "%d", nox);
|
||||
@ -365,7 +364,7 @@ void OledDisplay::showDashboard(const char *status) {
|
||||
ag->display.clear();
|
||||
|
||||
/** Set CO2 */
|
||||
int co2 = value.get(Measurements::CO2);
|
||||
int co2 = round(value.getAverage(Measurements::CO2));
|
||||
if (utils::isValidCO2(co2)) {
|
||||
snprintf(strBuf, sizeof(strBuf), "CO2:%d", co2);
|
||||
} else {
|
||||
@ -376,9 +375,9 @@ void OledDisplay::showDashboard(const char *status) {
|
||||
ag->display.setText(strBuf);
|
||||
|
||||
/** Set PM */
|
||||
int pm25 = value.get(Measurements::PM25);
|
||||
int pm25 = round(value.getAverage(Measurements::PM25));
|
||||
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
|
||||
pm25 = (int)value.getCorrectedPM25(*ag, config);
|
||||
pm25 = round(value.getCorrectedPM25(*ag, config, true));
|
||||
}
|
||||
|
||||
ag->display.setCursor(0, 12);
|
||||
@ -390,12 +389,12 @@ void OledDisplay::showDashboard(const char *status) {
|
||||
ag->display.setText(strBuf);
|
||||
|
||||
/** Set temperature and humidity */
|
||||
float temp = value.getFloat(Measurements::Temperature);
|
||||
float temp = value.getAverage(Measurements::Temperature);
|
||||
if (utils::isValidTemperature(temp)) {
|
||||
if (config.isTemperatureUnitInF()) {
|
||||
snprintf(strBuf, sizeof(strBuf), "T:%0.1f F", utils::degreeC_To_F(temp));
|
||||
} else {
|
||||
snprintf(strBuf, sizeof(strBuf), "T:%0.f1 C", temp);
|
||||
snprintf(strBuf, sizeof(strBuf), "T:%0.1f C", temp);
|
||||
}
|
||||
} else {
|
||||
if (config.isTemperatureUnitInF()) {
|
||||
@ -408,7 +407,7 @@ void OledDisplay::showDashboard(const char *status) {
|
||||
ag->display.setCursor(0, 24);
|
||||
ag->display.setText(strBuf);
|
||||
|
||||
int rhum = (int)value.getFloat(Measurements::Humidity);
|
||||
int rhum = round(value.getAverage(Measurements::Humidity));
|
||||
if (utils::isValidHumidity(rhum)) {
|
||||
snprintf(strBuf, sizeof(strBuf), "H:%d %%", rhum);
|
||||
} else {
|
||||
|
@ -13,6 +13,7 @@
|
||||
#define RGB_COLOR_Y 255, 150, 0 /** Yellow */
|
||||
#define RGB_COLOR_O 255, 40, 0 /** Orange */
|
||||
#define RGB_COLOR_P 180, 0, 255 /** Purple */
|
||||
#define RGB_COLOR_CLEAR 0, 0, 0 /** No 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()) {
|
||||
case LedBarMode::LedBarModeCO2:
|
||||
co2handleLeds();
|
||||
totalLedUsed = co2handleLeds();
|
||||
break;
|
||||
case LedBarMode::LedBarModePm:
|
||||
pm25handleLeds();
|
||||
totalLedUsed = pm25handleLeds();
|
||||
break;
|
||||
default:
|
||||
ag->ledBar.clear();
|
||||
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
|
||||
*
|
||||
* @return return total number of led that are used on the monitor
|
||||
*/
|
||||
void StateMachine::co2handleLeds(void) {
|
||||
int co2Value = value.get(Measurements::CO2);
|
||||
if (co2Value <= 700) {
|
||||
int StateMachine::co2handleLeds(void) {
|
||||
int totalUsed = ag->ledBar.getNumberOfLeds();
|
||||
int co2Value = round(value.getAverage(Measurements::CO2));
|
||||
if (co2Value <= 600) {
|
||||
/** G; 1 */
|
||||
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
|
||||
} else if (co2Value <= 1000) {
|
||||
totalUsed = 1;
|
||||
} else if (co2Value <= 800) {
|
||||
/** GG; 2 */
|
||||
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
|
||||
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 2);
|
||||
} else if (co2Value <= 1333) {
|
||||
totalUsed = 2;
|
||||
} else if (co2Value <= 1000) {
|
||||
/** YYY; 3 */
|
||||
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() - 3);
|
||||
} else if (co2Value <= 1666) {
|
||||
totalUsed = 3;
|
||||
} else if (co2Value <= 1250) {
|
||||
/** OOOO; 4 */
|
||||
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() - 3);
|
||||
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 4);
|
||||
} else if (co2Value <= 2000) {
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1);
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 2);
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 3);
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4);
|
||||
totalUsed = 4;
|
||||
} else if (co2Value <= 1500) {
|
||||
/** OOOOO; 5 */
|
||||
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() - 3);
|
||||
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 4);
|
||||
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 5);
|
||||
} else if (co2Value <= 2666) {
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1);
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 2);
|
||||
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() - 5);
|
||||
totalUsed = 5;
|
||||
} else if (co2Value <= 1750) {
|
||||
/** RRRRRR; 6 */
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
|
||||
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() - 5);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
|
||||
} else if (co2Value <= 3333) {
|
||||
totalUsed = 6;
|
||||
} else if (co2Value <= 2000) {
|
||||
/** RRRRRRR; 7 */
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
|
||||
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() - 6);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7);
|
||||
} else if (co2Value <= 4000) {
|
||||
/** RRRRRRRR; 8 */
|
||||
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() - 3);
|
||||
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() - 6);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8);
|
||||
} else { /** > 4000 */
|
||||
totalUsed = 7;
|
||||
} else if (co2Value <= 3000) {
|
||||
/** PPPPPPPP; 8 */
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1);
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 2);
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 3);
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 4);
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 5);
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 6);
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7);
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 8);
|
||||
totalUsed = 8;
|
||||
} else { /** > 3000 */
|
||||
/* PRPRPRPRP; 9 */
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1);
|
||||
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_R, ag->ledBar.getNumberOfLeds() - 8);
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9);
|
||||
totalUsed = 9;
|
||||
}
|
||||
|
||||
return totalUsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Show PM2.5 LED status
|
||||
*
|
||||
* @return return total number of led that are used on the monitor
|
||||
*/
|
||||
void StateMachine::pm25handleLeds(void) {
|
||||
int pm25Value = value.get(Measurements::PM25);
|
||||
int StateMachine::pm25handleLeds(void) {
|
||||
int totalUsed = ag->ledBar.getNumberOfLeds();
|
||||
|
||||
int pm25Value = round(value.getAverage(Measurements::PM25));
|
||||
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
|
||||
pm25Value = (int)value.getCorrectedPM25(*ag, config);
|
||||
pm25Value = round(value.getCorrectedPM25(*ag, config, true));
|
||||
}
|
||||
|
||||
if (pm25Value <= 5) {
|
||||
/** G; 1 */
|
||||
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
|
||||
totalUsed = 1;
|
||||
} else if (pm25Value <= 9) {
|
||||
/** GG; 2 */
|
||||
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
|
||||
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 2);
|
||||
totalUsed = 2;
|
||||
} else if (pm25Value <= 20) {
|
||||
/** YYY; 3 */
|
||||
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() - 3);
|
||||
totalUsed = 3;
|
||||
} else if (pm25Value <= 35) {
|
||||
/** YYYY; 4 */
|
||||
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() - 3);
|
||||
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 4);
|
||||
totalUsed = 4;
|
||||
} else if (pm25Value <= 45) {
|
||||
/** OOOOO; 5 */
|
||||
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() - 4);
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 5);
|
||||
totalUsed = 5;
|
||||
} else if (pm25Value <= 55) {
|
||||
/** OOOOOO; 6 */
|
||||
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() - 5);
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 6);
|
||||
totalUsed = 6;
|
||||
} else if (pm25Value <= 100) {
|
||||
/** RRRRRRR; 7 */
|
||||
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() - 6);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7);
|
||||
totalUsed = 7;
|
||||
} else if (pm25Value <= 125) {
|
||||
/** RRRRRRRR; 8 */
|
||||
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() - 7);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8);
|
||||
totalUsed = 8;
|
||||
} else if (pm25Value <= 225) {
|
||||
/** PPPPPPPPP; 9 */
|
||||
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() - 8);
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9);
|
||||
totalUsed = 9;
|
||||
} else { /** > 225 */
|
||||
/* PRPRPRPRP; 9 */
|
||||
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_R, ag->ledBar.getNumberOfLeds() - 8);
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9);
|
||||
totalUsed = 9;
|
||||
}
|
||||
|
||||
return totalUsed;
|
||||
}
|
||||
|
||||
void StateMachine::co2Calibration(void) {
|
||||
@ -311,6 +354,7 @@ void StateMachine::co2Calibration(void) {
|
||||
void StateMachine::ledBarTest(void) {
|
||||
if (config.isLedBarTestRequested()) {
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
if (config.getCountry() == "TH") {
|
||||
uint32_t tstart = millis();
|
||||
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) {
|
||||
if (ag->isOne()) {
|
||||
@ -585,15 +634,13 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
}
|
||||
|
||||
ledState = state;
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear(); // Set all LED OFF
|
||||
}
|
||||
switch (state) {
|
||||
case AgStateMachineWiFiManagerMode: {
|
||||
/** In WiFi Manager Mode */
|
||||
/** Turn LED OFF */
|
||||
/** Turn middle LED Color */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
ag->ledBar.setColor(0, 0, 255, ag->ledBar.getNumberOfLeds() / 2);
|
||||
} else {
|
||||
ag->statusLed.setToggle();
|
||||
@ -603,6 +650,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
case AgStateMachineWiFiManagerPortalActive: {
|
||||
/** WiFi Manager has connected to mobile phone */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
ag->ledBar.setColor(0, 0, 255);
|
||||
} else {
|
||||
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
|
||||
* attempted */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
ledBarSingleLedAnimation(255, 255, 255);
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
@ -622,6 +671,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
case AgStateMachineWiFiManagerStaConnected: {
|
||||
/** Connecting to WiFi worked */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
ag->ledBar.setColor(255, 255, 255);
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
@ -631,6 +681,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
case AgStateMachineWiFiOkServerConnecting: {
|
||||
/** once connected to WiFi an attempt to reach the server is performed */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
ledBarSingleLedAnimation(0, 255, 0);
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
@ -640,6 +691,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
case AgStateMachineWiFiOkServerConnected: {
|
||||
/** Server is reachable, all fine */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
ag->ledBar.setColor(0, 255, 0);
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
@ -656,6 +708,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
case AgStateMachineWiFiManagerConnectFailed: {
|
||||
/** Cannot connect to WiFi (e.g. wrong password, WPA Enterprise etc.) */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
ag->ledBar.setColor(255, 0, 0);
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
@ -674,6 +727,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
/** Connected to WiFi but server not reachable, e.g. firewall block/
|
||||
* whitelisting needed etc. */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
ag->ledBar.setColor(233, 183, 54); /** orange */
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
@ -690,6 +744,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
case AgStateMachineWiFiOkServerOkSensorConfigFailed: {
|
||||
/** Server reachable but sensor not configured correctly */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
ag->ledBar.setColor(139, 24, 248); /** violet */
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
@ -707,11 +762,10 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
/** Connection to WiFi network failed credentials incorrect encryption not
|
||||
* supported etc. */
|
||||
if (ag->isOne()) {
|
||||
/** WIFI failed status LED color */
|
||||
ag->ledBar.setColor(255, 0, 0, 0);
|
||||
/** Show CO2 or PM color status */
|
||||
// sensorLedColorHandler();
|
||||
sensorhandleLeds();
|
||||
bool allUsed = sensorhandleLeds();
|
||||
if (allUsed == false) {
|
||||
ag->ledBar.setColor(255, 0, 0, 0);
|
||||
}
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
}
|
||||
@ -721,11 +775,10 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
/** Connected to WiFi network but the server cannot be reached through the
|
||||
* internet, e.g. blocked by firewall */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.setColor(233, 183, 54, 0);
|
||||
|
||||
/** Show CO2 or PM color status */
|
||||
sensorhandleLeds();
|
||||
// sensorLedColorHandler();
|
||||
bool allUsed = sensorhandleLeds();
|
||||
if (allUsed == false) {
|
||||
ag->ledBar.setColor(233, 183, 54, 0);
|
||||
}
|
||||
} else {
|
||||
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
|
||||
* the server side */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.setColor(139, 24, 248, 0);
|
||||
|
||||
/** Show CO2 or PM color status */
|
||||
sensorhandleLeds();
|
||||
bool allUsed = sensorhandleLeds();
|
||||
if (allUsed == false) {
|
||||
ag->ledBar.setColor(139, 24, 248, 0);
|
||||
}
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
}
|
||||
|
@ -24,9 +24,9 @@ private:
|
||||
|
||||
void ledBarSingleLedAnimation(uint8_t r, uint8_t g, uint8_t b);
|
||||
void ledStatusBlinkDelay(uint32_t delay);
|
||||
void sensorhandleLeds(void);
|
||||
void co2handleLeds(void);
|
||||
void pm25handleLeds(void);
|
||||
bool sensorhandleLeds(void);
|
||||
int co2handleLeds(void);
|
||||
int pm25handleLeds(void);
|
||||
void co2Calibration(void);
|
||||
void ledBarTest(void);
|
||||
void ledBarPowerUpTest(void);
|
||||
|
195
src/AgValue.cpp
195
src/AgValue.cpp
@ -2,6 +2,7 @@
|
||||
#include "AgConfigure.h"
|
||||
#include "AirGradient.h"
|
||||
#include "App/AppDef.h"
|
||||
#include "SPIFFS.h"
|
||||
|
||||
#define json_prop_pmFirmware "firmware"
|
||||
#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
|
||||
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?
|
||||
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
|
||||
temporary->update.avg = temporary->sumValues / (float)temporary->listValues.size();
|
||||
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;
|
||||
@ -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
|
||||
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?
|
||||
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
|
||||
temporary->update.avg = temporary->sumValues / (float)temporary->listValues.size();
|
||||
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;
|
||||
@ -348,7 +349,7 @@ int Measurements::get(MeasurementType type, int ch) {
|
||||
|
||||
// Sanity check if measurement type is defined for integer data type or not
|
||||
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?
|
||||
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
|
||||
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?
|
||||
return false;
|
||||
}
|
||||
@ -396,6 +397,52 @@ float Measurements::getFloat(MeasurementType type, int ch) {
|
||||
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) {
|
||||
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 pm25;
|
||||
float corrected;
|
||||
float humidity;
|
||||
float pm003Count;
|
||||
int channel = ch - 1; // Array index
|
||||
if (useAvg) {
|
||||
// Directly call from the index
|
||||
int channel = ch - 1; // Array index
|
||||
pm25 = _pm_25[channel].update.avg;
|
||||
humidity = _humidity[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();
|
||||
if (pmCorrection.algorithm == PMCorrectionAlgorithm::EPA_2021) {
|
||||
// EPA correction directly applied
|
||||
pm25 = ag.pms5003.compensate(pm25, humidity);
|
||||
} else {
|
||||
// SLR correction, this is assumes before calling this function, correction algorithm is not None
|
||||
pm25 = ag.pms5003.slrCorrection(pm25, pm003Count, pmCorrection.scalingFactor, pmCorrection.intercept);
|
||||
switch (pmCorrection.algorithm) {
|
||||
case PMCorrectionAlgorithm::Unknown:
|
||||
case PMCorrectionAlgorithm::None:
|
||||
// If correction is Unknown, then default is None
|
||||
corrected = pm25;
|
||||
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) {
|
||||
// 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,
|
||||
@ -841,10 +897,10 @@ JSONVar Measurements::buildPMS(AirGradient &ag, int ch, bool allCh, bool withTem
|
||||
pms[json_prop_pm03Count] = ag.round2(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);
|
||||
} 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["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["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["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);
|
||||
} 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["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["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["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);
|
||||
} 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["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["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["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);
|
||||
} 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["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["channels"]["2"][json_prop_pm25Count] = ag.round2(_pm_25_pc[1].update.avg);
|
||||
}
|
||||
@ -1011,3 +1067,96 @@ JSONVar Measurements::buildPMS(AirGradient &ag, int ch, bool allCh, bool withTem
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
@ -114,10 +114,20 @@ public:
|
||||
*/
|
||||
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
|
||||
*
|
||||
* If correction is not enabled, then will return the raw value (either average or last value)
|
||||
*
|
||||
* @param ag AirGradient instance
|
||||
* @param config Configuration instance
|
||||
* @param useAvg Use moving average value if true, otherwise use latest value
|
||||
@ -132,6 +142,10 @@ public:
|
||||
String toString(bool localServer, AgFirmwareMode fwMode, int rssi, AirGradient &ag,
|
||||
Configuration &config);
|
||||
|
||||
bool resetLocalStorage();
|
||||
bool saveLocalStorage(AirGradient &ag, Configuration &config);
|
||||
char *getLocalStorage();
|
||||
|
||||
/**
|
||||
* Set to true if want to debug every update value
|
||||
*/
|
||||
@ -163,6 +177,7 @@ private:
|
||||
IntegerValue _pm_10_pc[2]; // particle count 10
|
||||
|
||||
bool _debug = false;
|
||||
const char *FILE_PATH = "/measurements.csv"; // Local storage file path
|
||||
|
||||
/**
|
||||
* @brief Get PMS5003 firmware version string
|
||||
|
@ -85,3 +85,25 @@ String AirGradient::deviceId(void) {
|
||||
mac.toLowerCase();
|
||||
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);
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
#include "Main/utils.h"
|
||||
|
||||
#ifndef GIT_VERSION
|
||||
#define GIT_VERSION "3.1.10-snap"
|
||||
#define GIT_VERSION "3.1.13-snap"
|
||||
#endif
|
||||
|
||||
/**
|
||||
@ -173,6 +173,9 @@ public:
|
||||
*/
|
||||
String deviceId(void);
|
||||
|
||||
void setCurrentTime(long epochTime);
|
||||
String getCurrentTime();
|
||||
|
||||
private:
|
||||
BoardType boardType;
|
||||
};
|
||||
|
Reference in New Issue
Block a user