Compare commits

...

74 Commits

Author SHA1 Message Date
54808ac076 Merge remote-tracking branch 'origin/develop' 2025-02-10 01:37:41 +07:00
063bb2a227 Prepare release 3.2.0 2025-02-10 01:36:41 +07:00
93f79173b2 Release 3.2.0-alpha 2025-02-07 20:06:10 +07:00
615c2389e7 Prepare 3.2.0 alpha release 2025-02-07 19:16:16 +07:00
f972637cca Merge pull request #283 from airgradienthq/feat/update-pm-correction
Apply PM corrections to all models
2025-02-07 19:05:20 +07:00
4c7e72b8e7 Better logging
Fix notif when wifi not connect
2025-02-07 10:45:14 +07:00
d4b4f51c3c Map batch PM correction as custom enum 2025-02-06 17:35:01 +07:00
1c42ff083d Make PM correction applied for all model 2025-02-06 15:58:15 +07:00
17d2e62b15 Remove delayed oled display 2025-02-06 15:38:36 +07:00
38aebeb50a Reformat pm correction enum naming 2025-02-06 12:49:41 +07:00
b0f5263829 Merge pull request #277 from airgradienthq/feat/correction-temp-hum
Apply temperature and humidity correction based on configuration
2025-02-06 10:14:38 +07:00
3226c14b6d Merge pull request #278 from airgradienthq/feat/disable-cloud
Fully disable cloud connection to airgradient server option
2025-02-06 10:13:50 +07:00
0e26aa1b5d Improve comments 2025-02-05 14:06:33 +07:00
830f652bf9 Remove unnecessary todo 2025-02-05 13:52:38 +07:00
bd9dbec663 Rename functions 2025-02-05 13:46:10 +07:00
29d701780a Improve logging 2025-02-05 11:37:16 +07:00
15869be234 Rename prefix temp hum correction enum member 2025-02-05 11:05:36 +07:00
4b09b98524 Fix variable naming ahum to rhum 2025-02-05 10:57:02 +07:00
afd498074b Fix grammar error 2025-02-05 10:43:56 +07:00
e851d0781c Merge branch 'develop' into feat/disable-cloud 2025-02-05 10:06:39 +07:00
2c27c6904c Merge pull request #282 from airgradienthq/fix/extend-connect-timeout
HTTP client failed/timeout to establish connection to airgradient server
2025-02-05 10:01:24 +07:00
03f1b969c2 Add comment describe two timeout functions call 2025-02-05 01:24:59 +07:00
85ba13de12 Set default ag client timeout to 15s 2025-02-05 01:18:46 +07:00
6ec545b00e Merge branch 'develop' into fix/extend-connect-timeout 2025-02-05 01:16:14 +07:00
05dbe60db2 Merge pull request #281 from airgradienthq/fix/zero-display
Fix display 0 measurements value on boot
2025-02-03 18:47:13 +07:00
d2ee3a5d24 Set default value for each measurements value to invalid 2025-02-03 01:28:42 +07:00
1839664137 Extend connect to server timeout
Default 5s from HTTPClient
2025-02-01 14:20:54 +07:00
154f3ecf8a Fix display 0 measurements on boot 2025-02-01 13:52:12 +07:00
b75e40b800 Rename variable for readability 2025-01-30 15:09:27 +07:00
84a358291b Rename function
from configure to configuration
2025-01-30 14:23:39 +07:00
0e41b2d630 Rename function from initiateNetwork to initializeNetwork 2025-01-30 10:01:15 +07:00
3e48a562e7 Change comment of sendDataToAg function call 2025-01-26 22:50:17 +07:00
f0c4df42b7 Fix wording on local-server 2025-01-26 13:02:42 +07:00
c8f0e6a0d2 Update diy samples
That accomodate ApiClient changes
Fix apiClient begin on OneOpenAir
2025-01-25 04:08:17 +07:00
1537d5d480 Update docs
Notes about offlineMode and disableCloudConnection
2025-01-25 03:10:31 +07:00
32c78f6018 Apply disableCloudConnection
fetch configuration, post data and ota will be ignored
2025-01-25 03:01:32 +07:00
c5c0dae4bb Add config to disableCloudConnection functions 2025-01-25 01:51:20 +07:00
4bb97fc8be Fix changes to other examples 2025-01-24 18:17:41 +07:00
a28931493a Add docs about how to set correction from local server 2025-01-24 18:03:13 +07:00
af16c1c060 Apply temphum correction 2025-01-24 09:56:01 +07:00
1666923ab3 Add AirGradient and Configuration object to AgValue 2025-01-23 03:33:47 +07:00
89475ddf95 Get correction of temp and hum based on configuration 2025-01-23 01:21:27 +07:00
20db9d699b Retrieve temp hum correction object 2025-01-23 01:16:49 +07:00
88c2437907 Handle correction configuration for atmp and rhum
From local and cloud
2025-01-22 01:53:55 +07:00
e9b27185b4 Fix spaces 2025-01-22 01:46:08 +07:00
4534f7319a Merge pull request #275 from airgradienthq/fix/invalid-average
Fix invalidate value check for getAverage function
2025-01-19 15:50:47 +07:00
c842346724 Fix invalidate value check for getAverage 2025-01-19 15:45:09 +07:00
92e74feabd Merge branch 'develop'
# Conflicts:
#	library.properties
#	src/AirGradient.h
2024-12-05 15:36:32 +07:00
cc0fd88068 Prepared to release 3.1.21 2024-12-05 15:35:37 +07:00
56809a412c Merge branch 'develop' 2024-12-05 15:17:34 +07:00
6a83743e2a Prepared to release 3.1.16 2024-12-05 15:06:10 +07:00
faaf051e39 Prepared to release 3.1.15 2024-12-05 15:00:25 +07:00
5bc1821ef9 Merge pull request #264 from airgradienthq/feat/ssl
Airgradient API calls using https
2024-12-05 13:23:52 +07:00
280ea5e997 Prepared to release 3.1.14 2024-12-04 10:38:13 +07:00
e95627ece6 Merge pull request #270 from airgradienthq/fix/openmetrics-correction-openair
Fix openmetrics pm25 correction on openair
2024-12-03 03:36:25 +07:00
80b9ae11d8 Fix openmetrics pm25 correction on openair 2024-12-03 03:33:46 +07:00
1937e3d59e Merge pull request #267 from jakpor/fix/basic_temp_display
(Basic DIY): Fix temperature parsing in OLED and debug for Basic DYI version
2024-12-03 02:18:07 +07:00
107fb21331 Merge pull request #269 from airgradienthq/fix/openmetrics-correction
PM25 correction for openmetrics
2024-12-03 02:00:16 +07:00
ccc1ab463a PM25 correction for openmetrics 2024-12-03 01:55:11 +07:00
38e792b88d if guard uri for esp8266 2024-11-30 04:45:04 +07:00
aeee0cad01 OTA bin download using https
rename server root ca constant name
2024-11-30 04:33:58 +07:00
401326d00d Move airgradient server CA const to AirGradient.h file
Constant will be used by api client and otahandler
2024-11-30 04:13:25 +07:00
fb0dcad54d Fix CI error esp32 2024-11-30 03:52:32 +07:00
3556e4a96a Fix CI error 2024-11-30 03:42:45 +07:00
283646a699 Move OtaHandler to src
OTA only valid for esp32 based monitor
2024-11-30 02:47:05 +07:00
6312612ada resetReason and freeheap only for esp32 based mcu 2024-11-30 00:45:46 +07:00
c1f22674e2 Add reset reason to transmission payload 2024-11-29 18:01:34 +07:00
40d38a75d8 Add reset reason to transmission payload 2024-11-29 18:01:14 +07:00
39ef69cbdf Fix printing of debug logs throught serial 2024-11-27 17:18:43 +01:00
3473e30e2e Fix temperature float formatting for basic oled display 2024-11-27 17:17:22 +01:00
4c165b31f5 Add freeheap to cloud payload 2024-11-21 02:36:56 +07:00
2be91b3968 Boot count using setter and getter 2024-11-21 02:30:03 +07:00
3ca2d1d208 Fix esp8266 build issue 2024-11-21 01:26:05 +07:00
aad12fc868 Using https
For get config and post measurements
2024-11-21 01:18:51 +07:00
28 changed files with 1373 additions and 786 deletions

View File

@ -2,7 +2,7 @@
From [firmware version 3.0.10](firmwares) onwards, the AirGradient ONE and Open Air monitors have below API available.
#### Discovery
### Discovery
The monitors run a mDNS discovery. So within the same network, the monitor can be accessed through:
@ -11,7 +11,7 @@ http://airgradient_{{serialnumber}}.local
The following requests are possible:
#### Get Current Air Quality (GET)
### Get Current Air Quality (GET)
With the path "/measures/current" you can get the current air quality data.
@ -80,7 +80,7 @@ You get the following response:
Compensated values apply correction algorithms to make the sensor values more accurate. Temperature and relative humidity correction is only applied on the outdoor monitor Open Air but the properties _compensated will still be send also for the indoor monitor AirGradient ONE.
#### Get Configuration Parameters (GET)
### Get Configuration Parameters (GET)
"/config" path returns the current configuration of the monitor.
@ -111,7 +111,7 @@ Compensated values apply correction algorithms to make the sensor values more ac
}
```
#### Set Configuration Parameters (PUT)
### Set Configuration Parameters (PUT)
Configuration parameters can be changed with a PUT request to the monitor, e.g.
@ -131,11 +131,11 @@ Example to set monitor to Celsius
``` -d "{\"param\":\"value\"}" ```
#### Avoiding Conflicts with Configuration on AirGradient Server
### Avoiding Conflicts with Configuration on AirGradient Server
If the monitor is set up on the AirGradient dashboard, it will also receive the configuration parameters from there. In case you do not want this, please set `configurationControl` to `local`. In case you set it to `cloud` and want to change it to `local`, you need to make a factory reset.
#### Configuration Parameters (GET/PUT)
### Configuration Parameters (GET/PUT)
| Properties | Description | Type | Accepted Values | Example |
|-----------------------------------|:-----------------------------------------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------|
@ -154,15 +154,18 @@ If the monitor is set up on the AirGradient dashboard, it will also receive the
| `ledBarTestRequested` | Can be set to trigger a test. | Boolean | `true` : LEDs will run test sequence | `{"ledBarTestRequested": true}` |
| `noxLearningOffset` | Set NOx learning gain offset. | Number | 0-720 (default 12) | `{"noxLearningOffset": 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}` |
| `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. (version >= [3.1.11]()) | Object | _see corrections section_ | _see corrections section_ |
**Notes**
#### Corrections
- `offlineMode` : the device will disable all network operation, and only show measurements on the display and ledbar; Read-Only; Change can be apply using reset button on boot.
- `disableCloudConnection` : disable every request to AirGradient server, means features like post data to AirGradient server, configuration from AirGradient server and automatic firmware updates are disabled. This configuration overrides `configurationControl` and `postDataToAirGradient`; Read-Only; Change can be apply from wifi setup webpage.
The `corrections` object allows configuring PM2.5 correction algorithms and parameters locally. This affects both the display and local server response values.
### Corrections
The `corrections` object allows configuring PM2.5, Temperature and Humidity correction algorithms and parameters locally. This affects both the display, local server response and open metrics values.
Example correction configuration:
@ -176,11 +179,29 @@ Example correction configuration:
"scalingFactor": 0,
"useEpa2021": false
}
}
},
"atmp": {
"correctionAlgorithm": "<Option In String>",
"slr": {
"intercept": 0,
"scalingFactor": 0
}
},
"rhum": {
"correctionAlgorithm": "<Option In String>",
"slr": {
"intercept": 0,
"scalingFactor": 0
}
},
}
}
```
#### PM 2.5
Field Name: `pm02`
| Algorithm | Value | Description | SLR required |
|------------|-------------|------|---------|
| Raw | `"none"` | No correction (default) | No |
@ -214,3 +235,23 @@ curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header '
```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}}}}'
```
#### Temperature & Humidity
Field Name:
- Temperature: `atmp`
- Humidity: `rhum`
| Algorithm | Value | Description | SLR required |
|------------|-------------|------|---------|
| Raw | `"none"` | No correction (default) | No |
| AirGradient Standard Correction | `"ag_pms5003t_2024"` | Using standard airgradient correction (for outdoor monitor)| No |
| Custom | `"custom"` | custom corrections constant, set `intercept` and `scalingFactor` manually | Yes |
*Table above apply for both Temperature and Humidity*
**Example**
```bash
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"atmp":{"correctionAlgorithm":"custom","slr":{"intercept":0.2,"scalingFactor":1.1}}}}'
```

View File

@ -55,7 +55,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
static AirGradient ag(DIY_BASIC);
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static Measurements measurements;
static Measurements measurements(configuration);
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
@ -124,6 +124,7 @@ void setup() {
apiClient.setAirGradient(&ag);
openMetrics.setAirGradient(&ag);
localServer.setAirGraident(&ag);
measurements.setAirGradient(&ag);
/** Example set custom API root URL */
// apiClient.setApiRoot("https://example.custom.api");
@ -149,9 +150,12 @@ void setup() {
initMqtt();
sendDataToAg();
apiClient.fetchServerConfiguration();
if (configuration.getConfigurationControl() !=
ConfigurationControl::ConfigurationControlLocal) {
apiClient.fetchServerConfiguration();
}
configSchedule.update();
if (apiClient.isFetchConfigureFailed()) {
if (apiClient.isFetchConfigurationFailed()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
@ -316,7 +320,7 @@ static void mqttHandle(void) {
}
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(), ag, configuration);
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
String topic = "airgradient/readings/" + ag.deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
@ -331,7 +335,7 @@ static void sendDataToAg() {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) {
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount())) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} else {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
@ -415,6 +419,14 @@ static void failedHandler(String msg) {
}
static void configurationUpdateSchedule(void) {
if (configuration.isOfflineMode() ||
configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
Serial.println("Ignore fetch server configuration. Either mode is offline "
"or configurationControl set to local");
apiClient.resetFetchConfigurationStatus();
return;
}
if (apiClient.fetchServerConfiguration()) {
configUpdateHandle();
}
@ -472,7 +484,7 @@ static void appDispHandler(void) {
if (configuration.isOfflineMode() == false) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigureFailed()) {
} else if (apiClient.isFetchConfigurationFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
@ -518,19 +530,24 @@ static void updatePm(void) {
static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */
measurements.bootCount++;
int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
/** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false ||
configuration.isOfflineMode()) {
if (configuration.isOfflineMode() || !configuration.isPostDataToAirGradient()) {
Serial.println("Skipping transmission of data to AG server. Either mode is offline "
"or post data to server disabled");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(), ag, configuration);
if (wifiConnector.isConnected() == false) {
Serial.println("WiFi not connected, skipping data transmission to AG server");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
if (apiClient.postToServer(syncData)) {
Serial.println();
Serial.println(
"Online mode and isPostToAirGradient = true: watchdog reset");
Serial.println("Online mode and isPostToAirGradient = true");
Serial.println();
}
}

View File

@ -53,7 +53,7 @@ void LocalServer::_GET_metrics(void) {
}
void LocalServer::_GET_measure(void) {
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI());
server.send(200, "application/json", toSend);
}

View File

@ -43,7 +43,7 @@ String OpenMetrics::getPayload(void) {
"1 if the AirGradient device was able to successfully fetch its "
"configuration from the server",
"gauge");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "0" : "1");
add_metric_point("", apiClient.isFetchConfigurationFailed() ? "0" : "1");
add_metric(
"post_ok",
@ -66,7 +66,7 @@ String OpenMetrics::getPayload(void) {
int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature();
int ahumCompensated = utils::getInvalidHumidity();
int rhumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
@ -76,12 +76,13 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature);
_hum = measure.getFloat(Measurements::Humidity);
atmpCompensated = _temp;
ahumCompensated = _hum;
rhumCompensated = _hum;
}
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
pm25 = measure.get(Measurements::PM25);
float correctedPm = measure.getCorrectedPM25(false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
}
@ -190,12 +191,12 @@ String OpenMetrics::getPayload(void) {
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(ahumCompensated)) {
if (utils::isValidHumidity(rhumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the "
"AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(ahumCompensated));
add_metric_point("", String(rhumCompensated));
}
response += "# EOF\n";

View File

@ -55,7 +55,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
static AirGradient ag(DIY_PRO_INDOOR_V3_3);
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static Measurements measurements;
static Measurements measurements(configuration);
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
@ -124,6 +124,7 @@ void setup() {
apiClient.setAirGradient(&ag);
openMetrics.setAirGradient(&ag);
localServer.setAirGraident(&ag);
measurements.setAirGradient(&ag);
/** Example set custom API root URL */
// apiClient.setApiRoot("https://example.custom.api");
@ -149,9 +150,12 @@ void setup() {
initMqtt();
sendDataToAg();
apiClient.fetchServerConfiguration();
if (configuration.getConfigurationControl() !=
ConfigurationControl::ConfigurationControlLocal) {
apiClient.fetchServerConfiguration();
}
configSchedule.update();
if (apiClient.isFetchConfigureFailed()) {
if (apiClient.isFetchConfigurationFailed()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
@ -373,7 +377,7 @@ static void mqttHandle(void) {
}
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(), ag, configuration);
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
String topic = "airgradient/readings/" + ag.deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
@ -388,7 +392,7 @@ static void sendDataToAg() {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) {
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount())) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} else {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
@ -467,6 +471,14 @@ static void failedHandler(String msg) {
}
static void configurationUpdateSchedule(void) {
if (configuration.isOfflineMode() ||
configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
Serial.println("Ignore fetch server configuration. Either mode is offline "
"or configurationControl set to local");
apiClient.resetFetchConfigurationStatus();
return;
}
if (apiClient.fetchServerConfiguration()) {
configUpdateHandle();
}
@ -524,7 +536,7 @@ static void appDispHandler(void) {
if (configuration.isOfflineMode() == false) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigureFailed()) {
} else if (apiClient.isFetchConfigurationFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
@ -570,19 +582,24 @@ static void updatePm(void) {
static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */
measurements.bootCount++;
int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
/** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false ||
configuration.isOfflineMode()) {
if (configuration.isOfflineMode() || !configuration.isPostDataToAirGradient()) {
Serial.println("Skipping transmission of data to AG server. Either mode is offline "
"or post data to server disabled");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(), ag, configuration);
if (wifiConnector.isConnected() == false) {
Serial.println("WiFi not connected, skipping data transmission to AG server");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
if (apiClient.postToServer(syncData)) {
Serial.println();
Serial.println(
"Online mode and isPostToAirGradient = true: watchdog reset");
Serial.println("Online mode and isPostToAirGradient = true");
Serial.println();
}
}

View File

@ -53,7 +53,7 @@ void LocalServer::_GET_metrics(void) {
}
void LocalServer::_GET_measure(void) {
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI());
server.send(200, "application/json", toSend);
}

View File

@ -43,7 +43,7 @@ String OpenMetrics::getPayload(void) {
"1 if the AirGradient device was able to successfully fetch its "
"configuration from the server",
"gauge");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "0" : "1");
add_metric_point("", apiClient.isFetchConfigurationFailed() ? "0" : "1");
add_metric(
"post_ok",
@ -66,7 +66,7 @@ String OpenMetrics::getPayload(void) {
int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature();
int ahumCompensated = utils::getInvalidHumidity();
int rhumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
@ -76,12 +76,13 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature);
_hum = measure.getFloat(Measurements::Humidity);
atmpCompensated = _temp;
ahumCompensated = _hum;
rhumCompensated = _hum;
}
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
pm25 = measure.get(Measurements::PM25);
float correctedPm = measure.getCorrectedPM25(false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
}
@ -191,12 +192,12 @@ String OpenMetrics::getPayload(void) {
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(ahumCompensated)) {
if (utils::isValidHumidity(rhumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the "
"AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(ahumCompensated));
add_metric_point("", String(rhumCompensated));
}
response += "# EOF\n";

View File

@ -55,7 +55,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
static AirGradient ag(DIY_PRO_INDOOR_V4_2);
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static Measurements measurements;
static Measurements measurements(configuration);
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
@ -125,6 +125,7 @@ void setup() {
apiClient.setAirGradient(&ag);
openMetrics.setAirGradient(&ag);
localServer.setAirGraident(&ag);
measurements.setAirGradient(&ag);
/** Example set custom API root URL */
// apiClient.setApiRoot("https://example.custom.api");
@ -176,9 +177,12 @@ void setup() {
initMqtt();
sendDataToAg();
apiClient.fetchServerConfiguration();
if (configuration.getConfigurationControl() !=
ConfigurationControl::ConfigurationControlLocal) {
apiClient.fetchServerConfiguration();
}
configSchedule.update();
if (apiClient.isFetchConfigureFailed()) {
if (apiClient.isFetchConfigurationFailed()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
@ -396,7 +400,7 @@ static void mqttHandle(void) {
}
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(), ag, configuration);
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
String topic = "airgradient/readings/" + ag.deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
@ -411,7 +415,7 @@ static void sendDataToAg() {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) {
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount())) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} else {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
@ -507,6 +511,14 @@ static void failedHandler(String msg) {
}
static void configurationUpdateSchedule(void) {
if (configuration.isOfflineMode() ||
configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
Serial.println("Ignore fetch server configuration. Either mode is offline "
"or configurationControl set to local");
apiClient.resetFetchConfigurationStatus();
return;
}
if (apiClient.fetchServerConfiguration()) {
configUpdateHandle();
}
@ -564,7 +576,7 @@ static void appDispHandler(void) {
if (configuration.isOfflineMode() == false) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigureFailed()) {
} else if (apiClient.isFetchConfigurationFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
@ -611,19 +623,24 @@ static void updatePm(void) {
static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */
measurements.bootCount++;
int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
/** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false ||
configuration.isOfflineMode()) {
if (configuration.isOfflineMode() || !configuration.isPostDataToAirGradient()) {
Serial.println("Skipping transmission of data to AG server. Either mode is offline "
"or post data to server disabled");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(), ag, configuration);
if (wifiConnector.isConnected() == false) {
Serial.println("WiFi not connected, skipping data transmission to AG server");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
if (apiClient.postToServer(syncData)) {
Serial.println();
Serial.println(
"Online mode and isPostToAirGradient = true: watchdog reset");
Serial.println("Online mode and isPostToAirGradient = true");
Serial.println();
}
}

View File

@ -53,7 +53,7 @@ void LocalServer::_GET_metrics(void) {
}
void LocalServer::_GET_measure(void) {
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI());
server.send(200, "application/json", toSend);
}

View File

@ -43,7 +43,7 @@ String OpenMetrics::getPayload(void) {
"1 if the AirGradient device was able to successfully fetch its "
"configuration from the server",
"gauge");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "0" : "1");
add_metric_point("", apiClient.isFetchConfigurationFailed() ? "0" : "1");
add_metric(
"post_ok",
@ -66,7 +66,7 @@ String OpenMetrics::getPayload(void) {
int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature();
int ahumCompensated = utils::getInvalidHumidity();
int rhumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
@ -76,12 +76,13 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature);
_hum = measure.getFloat(Measurements::Humidity);
atmpCompensated = _temp;
ahumCompensated = _hum;
rhumCompensated = _hum;
}
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
pm25 = measure.get(Measurements::PM25);
float correctedPm = measure.getCorrectedPM25(false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
}
@ -190,12 +191,12 @@ String OpenMetrics::getPayload(void) {
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(ahumCompensated)) {
if (utils::isValidHumidity(rhumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the "
"AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(ahumCompensated));
add_metric_point("", String(rhumCompensated));
}
response += "# EOF\n";

View File

@ -64,7 +64,7 @@ void LocalServer::_GET_metrics(void) {
}
void LocalServer::_GET_measure(void) {
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI());
server.send(200, "application/json", toSend);
}

View File

@ -36,20 +36,21 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
*/
#include <HardwareSerial.h>
#include "AirGradient.h"
#include "OtaHandler.h"
#include "AgApiClient.h"
#include "AgConfigure.h"
#include "AgSchedule.h"
#include "AgStateMachine.h"
#include "AgWiFiConnector.h"
#include "AirGradient.h"
#include "EEPROM.h"
#include "ESPmDNS.h"
#include "LocalServer.h"
#include "MqttClient.h"
#include "OpenMetrics.h"
#include "OtaHandler.h"
#include "WebServer.h"
#include "esp32c3/rom/rtc.h"
#include <HardwareSerial.h>
#include <WebServer.h>
#include <WiFi.h>
@ -75,7 +76,7 @@ static MqttClient mqttClient(Serial);
static TaskHandle_t mqttTask = NULL;
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static Measurements measurements;
static Measurements measurements(configuration);
static AirGradient *ag;
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
@ -95,6 +96,7 @@ static bool ledBarButtonTest = false;
static String fwNewVersion;
static void boardInit(void);
static void initializeNetwork(void);
static void failedHandler(String msg);
static void configurationUpdateSchedule(void);
static void updateDisplayAndLedBar(void);
@ -110,10 +112,9 @@ static void factoryConfigReset(void);
static void wdgFeedUpdate(void);
static void ledBarEnabledUpdate(void);
static bool sgp41Init(void);
static void firmwareCheckForUpdate(void);
static void otaHandlerCallback(OtaState state, String mesasge);
static void displayExecuteOta(OtaState state, String msg,
int processing);
static void checkForFirmwareUpdate(void);
static void otaHandlerCallback(OtaHandler::OtaState state, String mesasge);
static void displayExecuteOta(OtaHandler::OtaState state, String msg, int processing);
static int calculateMaxPeriod(int updateInterval);
static void setMeasurementMaxPeriod();
@ -126,7 +127,7 @@ 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 checkForUpdateSchedule(FIRMWARE_CHECK_FOR_UPDATE_MS, checkForFirmwareUpdate);
void setup() {
/** Serial for print debug message */
@ -136,6 +137,10 @@ void setup() {
/** Print device ID into log */
Serial.println("Serial nr: " + ag->deviceId());
// Set reason why esp is reset
esp_reset_reason_t reason = esp_reset_reason();
measurements.setResetReason(reason);
/** Initialize local configure */
configuration.begin();
@ -160,6 +165,7 @@ void setup() {
apiClient.setAirGradient(ag);
openMetrics.setAirGradient(ag);
localServer.setAirGraident(ag);
measurements.setAirGradient(ag);
/** Example set custom API root URL */
// apiClient.setApiRoot("https://example.custom.api");
@ -204,51 +210,11 @@ void setup() {
connectToWifi = true;
}
// Initialize networking configuration
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();
}
}
}
initializeNetwork();
}
/** Set offline mode without saving, cause wifi is not configured */
if (wifiConnector.hasConfigurated() == false) {
Serial.println("Set offline mode cause wifi is not configurated");
@ -264,15 +230,16 @@ void setup() {
oledDisplay.setBrightness(configuration.getDisplayBrightness());
}
// Update display and led bar after finishing setup to show dashboard
updateDisplayAndLedBar();
// Reset post schedulers to make sure measurements value already available
agApiPostSchedule.update();
}
void loop() {
/** Handle schedule */
/** Run schedulers */
dispLedSchedule.run();
configSchedule.run();
agApiPostSchedule.run();
watchdogFeedSchedule.run();
if (configuration.hasSensorS8) {
co2Schedule.run();
@ -306,15 +273,13 @@ void loop() {
}
}
watchdogFeedSchedule.run();
/** Check for handle WiFi reconnect */
wifiConnector.handle();
/** factory reset handle */
factoryConfigReset();
/** check that local configura changed then do some action */
/** check that local configuration changed then do some action */
configUpdateHandle();
/** Firmware check for update handle */
@ -364,8 +329,7 @@ static void createMqttTask(void) {
/** Send data */
if (mqttClient.isConnected()) {
String payload =
measurements.toString(true, fwMode, wifiConnector.RSSI(), *ag, configuration);
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
String topic = "airgradient/readings/" + ag->deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(),
@ -500,43 +464,47 @@ static bool sgp41Init(void) {
return false;
}
static void firmwareCheckForUpdate(void) {
static void checkForFirmwareUpdate(void) {
Serial.println();
Serial.println("firmwareCheckForUpdate:");
Serial.print("checkForFirmwareUpdate: ");
if (wifiConnector.isConnected()) {
Serial.println("firmwareCheckForUpdate: Perform");
otaHandler.setHandlerCallback(otaHandlerCallback);
otaHandler.updateFirmwareIfOutdated(ag->deviceId());
} else {
Serial.println("firmwareCheckForUpdate: Ignored");
if (configuration.isOfflineMode() || configuration.isCloudConnectionDisabled()) {
Serial.println("mode is offline or cloud connection disabled, ignored");
return;
}
if (!wifiConnector.isConnected()) {
Serial.println("wifi not connected, ignored");
return;
}
Serial.println("perform");
otaHandler.setHandlerCallback(otaHandlerCallback);
otaHandler.updateFirmwareIfOutdated(ag->deviceId());
Serial.println();
}
static void otaHandlerCallback(OtaState state, String mesasge) {
Serial.println("OTA message: " + mesasge);
static void otaHandlerCallback(OtaHandler::OtaState state, String message) {
Serial.println("OTA message: " + message);
switch (state) {
case OtaState::OTA_STATE_BEGIN:
case OtaHandler::OTA_STATE_BEGIN:
displayExecuteOta(state, fwNewVersion, 0);
break;
case OtaState::OTA_STATE_FAIL:
case OtaHandler::OTA_STATE_FAIL:
displayExecuteOta(state, "", 0);
break;
case OtaState::OTA_STATE_PROCESSING:
displayExecuteOta(state, "", mesasge.toInt());
break;
case OtaState::OTA_STATE_SUCCESS:
displayExecuteOta(state, "", mesasge.toInt());
case OtaHandler::OTA_STATE_PROCESSING:
case OtaHandler::OTA_STATE_SUCCESS:
displayExecuteOta(state, "", message.toInt());
break;
default:
break;
}
}
static void displayExecuteOta(OtaState state, String msg, int processing) {
static void displayExecuteOta(OtaHandler::OtaState state, String msg, int processing) {
switch (state) {
case OtaState::OTA_STATE_BEGIN: {
case OtaHandler::OTA_STATE_BEGIN: {
if (ag->isOne()) {
oledDisplay.showFirmwareUpdateVersion(msg);
} else {
@ -545,7 +513,7 @@ static void displayExecuteOta(OtaState state, String msg, int processing) {
delay(2500);
break;
}
case OtaState::OTA_STATE_FAIL: {
case OtaHandler::OTA_STATE_FAIL: {
if (ag->isOne()) {
oledDisplay.showFirmwareUpdateFailed();
} else {
@ -555,7 +523,7 @@ static void displayExecuteOta(OtaState state, String msg, int processing) {
delay(2500);
break;
}
case OtaState::OTA_STATE_SKIP: {
case OtaHandler::OTA_STATE_SKIP: {
if (ag->isOne()) {
oledDisplay.showFirmwareUpdateSkipped();
} else {
@ -565,7 +533,7 @@ static void displayExecuteOta(OtaState state, String msg, int processing) {
delay(2500);
break;
}
case OtaState::OTA_STATE_UP_TO_DATE: {
case OtaHandler::OTA_STATE_UP_TO_DATE: {
if (ag->isOne()) {
oledDisplay.showFirmwareUpdateUpToDate();
} else {
@ -575,7 +543,7 @@ static void displayExecuteOta(OtaState state, String msg, int processing) {
delay(2500);
break;
}
case OtaState::OTA_STATE_PROCESSING: {
case OtaHandler::OTA_STATE_PROCESSING: {
if (ag->isOne()) {
oledDisplay.showFirmwareUpdateProgress(processing);
} else {
@ -584,7 +552,7 @@ static void displayExecuteOta(OtaState state, String msg, int processing) {
break;
}
case OtaState::OTA_STATE_SUCCESS: {
case OtaHandler::OTA_STATE_SUCCESS: {
int i = 6;
while(i != 0) {
i = i - 1;
@ -634,7 +602,7 @@ static void sendDataToAg() {
"task_led", 2048, NULL, 5, NULL);
delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) {
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount())) {
if (ag->isOne()) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
}
@ -645,7 +613,7 @@ static void sendDataToAg() {
}
stateMachine.handleLeds(AgStateMachineWiFiOkServerConnectFailed);
}
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
stateMachine.handleLeds(AgStateMachineNormal);
}
@ -869,7 +837,82 @@ static void failedHandler(String msg) {
}
}
void initializeNetwork() {
if (!wifiConnector.connect()) {
Serial.println("Cannot initiate wifi connection");
return;
}
if (!wifiConnector.isConnected()) {
Serial.println("Failed connect to WiFi");
if (wifiConnector.isConfigurePorttalTimeout()) {
oledDisplay.showRebooting();
delay(2500);
oledDisplay.setText("", "", "");
ESP.restart();
}
// Directly return because the rest of the function applied if wifi is connect only
return;
}
// Initiate local network configuration
mdnsInit();
localServer.begin();
// Apply mqtt connection if configured
initMqtt();
// Ignore the rest if cloud connection to AirGradient is disabled
if (configuration.isCloudConnectionDisabled()) {
return;
}
// Initialize api client
apiClient.begin();
// Send data for the first time to AG server at boot
sendDataToAg();
// OTA check
#ifdef ESP8266
// ota not supported
#else
checkForFirmwareUpdate();
checkForUpdateSchedule.update();
#endif
apiClient.fetchServerConfiguration();
configSchedule.update();
if (apiClient.isFetchConfigurationFailed()) {
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();
}
}
static void configurationUpdateSchedule(void) {
if (configuration.isOfflineMode() || configuration.isCloudConnectionDisabled() ||
configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
Serial.println("Ignore fetch server configuration. Either mode is offline or cloud connection "
"disabled or configurationControl set to local");
apiClient.resetFetchConfigurationStatus();
return;
}
if (wifiConnector.isConnected() == false) {
Serial.println(" WiFi not connected, skipping fetch configuration from AG server");
return;
}
if (apiClient.fetchServerConfiguration()) {
configUpdateHandle();
}
@ -967,10 +1010,21 @@ static void updateDisplayAndLedBar(void) {
return;
}
AgStateMachineState state = AgStateMachineNormal;
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigureFailed()) {
stateMachine.displayHandle(AgStateMachineWiFiLost);
stateMachine.handleLeds(AgStateMachineWiFiLost);
return;
}
if (configuration.isCloudConnectionDisabled()) {
// Ignore API related check since cloud is disabled
stateMachine.displayHandle(AgStateMachineNormal);
stateMachine.handleLeds(AgStateMachineNormal);
return;
}
AgStateMachineState state = AgStateMachineNormal;
if (apiClient.isFetchConfigurationFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
@ -1135,20 +1189,30 @@ static void updatePm(void) {
static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */
measurements.bootCount++;
int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
/** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false || configuration.isOfflineMode()) {
if (configuration.isOfflineMode() || configuration.isCloudConnectionDisabled() ||
!configuration.isPostDataToAirGradient()) {
Serial.println("Skipping transmission of data to AG server. Either mode is offline or cloud connection is "
"disabled or post data to server disabled");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(), *ag, configuration);
if (wifiConnector.isConnected() == false) {
Serial.println("WiFi not connected, skipping data transmission to AG server");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
if (apiClient.postToServer(syncData)) {
Serial.println();
Serial.println(
"Online mode and isPostToAirGradient = true: watchdog reset");
Serial.println("Online mode and isPostToAirGradient = true");
Serial.println();
}
/** Log current free heap size */
Serial.printf("Free heap: %u\n", ESP.getFreeHeap());
}
static void tempHumUpdate(void) {

View File

@ -43,7 +43,7 @@ String OpenMetrics::getPayload(void) {
"1 if the AirGradient device was able to successfully fetch its "
"configuration from the server",
"gauge");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "0" : "1");
add_metric_point("", apiClient.isFetchConfigurationFailed() ? "0" : "1");
add_metric(
"post_ok",
@ -66,7 +66,7 @@ String OpenMetrics::getPayload(void) {
int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature();
int ahumCompensated = utils::getInvalidHumidity();
int rhumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
@ -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(false, 1);
float correctedPm25_2 = measure.getCorrectedPM25(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(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(false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10, 1);
pm03PCount = measure.get(Measurements::PM03_PC, 1);
}
@ -111,7 +116,8 @@ 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(false, 2);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10, 2);
pm03PCount = measure.get(Measurements::PM03_PC, 2);
}
@ -131,11 +137,15 @@ String OpenMetrics::getPayload(void) {
/** Get temperature and humidity compensated */
if (ag->isOne()) {
atmpCompensated = _temp;
ahumCompensated = _hum;
atmpCompensated = round(measure.getCorrectedTempHum(Measurements::Temperature));
rhumCompensated = round(measure.getCorrectedTempHum(Measurements::Humidity));
} else {
atmpCompensated = ag->pms5003t_1.compensateTemp(_temp);
ahumCompensated = ag->pms5003t_1.compensateHum(_hum);
atmpCompensated = round((measure.getCorrectedTempHum(Measurements::Temperature, 1) +
measure.getCorrectedTempHum(Measurements::Temperature, 2)) /
2.0f);
rhumCompensated = round((measure.getCorrectedTempHum(Measurements::Humidity, 1) +
measure.getCorrectedTempHum(Measurements::Humidity, 2)) /
2.0f);
}
// Add measurements that valid to the metrics
@ -228,11 +238,11 @@ String OpenMetrics::getPayload(void) {
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(ahumCompensated)) {
if (utils::isValidHumidity(rhumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(ahumCompensated));
add_metric_point("", String(rhumCompensated));
}
response += "# EOF\n";

View File

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

View File

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

View File

@ -34,17 +34,6 @@ void AgApiClient::begin(void) {
* @return false Failure
*/
bool AgApiClient::fetchServerConfiguration(void) {
if (config.getConfigurationControl() ==
ConfigurationControl::ConfigurationControlLocal ||
config.isOfflineMode()) {
logWarning("Ignore fetch server configuration");
// Clear server configuration failed flag, cause it's ignore but not
// really failed
getConfigFailed = false;
return false;
}
String uri = apiRoot + "/sensors/airgradient:" +
ag->deviceId() + "/one/config";
@ -58,10 +47,22 @@ bool AgApiClient::fetchServerConfiguration(void) {
}
#else
HTTPClient client;
client.setTimeout(timeoutMs);
if (client.begin(uri) == false) {
getConfigFailed = true;
return false;
client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server
client.setTimeout(timeoutMs); // Timeout when waiting for response from AG server
if (apiRootChanged) {
// If apiRoot is changed, assume not using https
if (client.begin(uri) == false) {
logError("Begin HTTPClient failed (GET)");
getConfigFailed = true;
return false;
}
} else {
// By default, airgradient using https
if (client.begin(uri, AG_SERVER_ROOT_CA) == false) {
logError("Begin HTTPClient using tls failed (GET)");
getConfigFailed = true;
return false;
}
}
#endif
@ -90,8 +91,6 @@ bool AgApiClient::fetchServerConfiguration(void) {
String respContent = client.getString();
client.end();
// logInfo("Get configuration: " + respContent);
/** Parse configuration and return result */
return config.parse(respContent, false);
}
@ -105,32 +104,39 @@ bool AgApiClient::fetchServerConfiguration(void) {
* @return false Failure
*/
bool AgApiClient::postToServer(String data) {
if (config.isPostDataToAirGradient() == false) {
logWarning("Ignore post data to server");
return true;
}
if (WiFi.isConnected() == false) {
return false;
}
String uri = apiRoot + "/sensors/airgradient:" + ag->deviceId() + "/measures";
// logInfo("Post uri: " + uri);
// logInfo("Post data: " + data);
WiFiClient wifiClient;
#ifdef ESP8266
HTTPClient client;
client.setTimeout(timeoutMs);
if (client.begin(wifiClient, uri.c_str()) == false) {
logError("Init client failed");
WiFiClient wifiClient;
if (client.begin(wifiClient, uri) == false) {
getConfigFailed = true;
return false;
}
#else
HTTPClient client;
client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server
client.setTimeout(timeoutMs); // Timeout when waiting for response from AG server
if (apiRootChanged) {
// If apiRoot is changed, assume not using https
if (client.begin(uri) == false) {
logError("Begin HTTPClient failed (POST)");
getConfigFailed = true;
return false;
}
} else {
// By default, airgradient using https
if (client.begin(uri, AG_SERVER_ROOT_CA) == false) {
logError("Begin HTTPClient using tls failed (POST)");
getConfigFailed = true;
return false;
}
}
#endif
client.addHeader("content-type", "application/json");
int retCode = client.POST(data);
client.end();
logInfo(String("POST: ") + uri);
// logInfo(String("DATA: ") + data);
logInfo(String("Return code: ") + String(retCode));
if ((retCode == 200) || (retCode == 429)) {
@ -149,7 +155,12 @@ bool AgApiClient::postToServer(String data) {
* @return true Success
* @return false Failure
*/
bool AgApiClient::isFetchConfigureFailed(void) { return getConfigFailed; }
bool AgApiClient::isFetchConfigurationFailed(void) { return getConfigFailed; }
/**
* @brief Reset status of get configuration from AirGradient cloud
*/
void AgApiClient::resetFetchConfigurationStatus(void) { getConfigFailed = false; }
/**
* @brief Get failed status when post data to AirGradient cloud
@ -189,7 +200,10 @@ bool AgApiClient::sendPing(int rssi, int bootCount) {
String AgApiClient::getApiRoot() const { return apiRoot; }
void AgApiClient::setApiRoot(const String &apiRoot) { this->apiRoot = apiRoot; }
void AgApiClient::setApiRoot(const String &apiRoot) {
this->apiRootChanged = true;
this->apiRoot = apiRoot;
}
/**
* @brief Set http request timeout. (Default: 10s)

View File

@ -20,12 +20,18 @@ class AgApiClient : public PrintLog {
private:
Configuration &config;
AirGradient *ag;
#ifdef ESP8266
// ESP8266 not support HTTPS
String apiRoot = "http://hw.airgradient.com";
#else
String apiRoot = "https://hw.airgradient.com";
#endif
bool apiRootChanged = false; // Indicate if setApiRoot() is called
bool getConfigFailed;
bool postToServerFailed;
bool notAvailableOnDashboard = false; // Device not setup on Airgradient cloud dashboard.
uint16_t timeoutMs = 10000; // Default set to 10s
uint16_t timeoutMs = 15000; // Default set to 15s
public:
AgApiClient(Stream &stream, Configuration &config);
@ -34,7 +40,8 @@ public:
void begin(void);
bool fetchServerConfiguration(void);
bool postToServer(String data);
bool isFetchConfigureFailed(void);
bool isFetchConfigurationFailed(void);
void resetFetchConfigurationStatus(void);
bool isPostToServerFailed(void);
bool isNotAvailableOnDashboard(void);
void setAirGradient(AirGradient *ag);

View File

@ -22,15 +22,17 @@ const char *LED_BAR_MODE_NAMES[] = {
};
const char *PM_CORRECTION_ALGORITHM_NAMES[] = {
[Unknown] = "-", // This is only to pass "non-trivial designated initializers" error
[None] = "none",
[EPA_2021] = "epa_2021",
[SLR_PMS5003_20220802] = "slr_PMS5003_20220802",
[SLR_PMS5003_20220803] = "slr_PMS5003_20220803",
[SLR_PMS5003_20220824] = "slr_PMS5003_20220824",
[SLR_PMS5003_20231030] = "slr_PMS5003_20231030",
[SLR_PMS5003_20231218] = "slr_PMS5003_20231218",
[SLR_PMS5003_20240104] = "slr_PMS5003_20240104",
[COR_ALGO_PM_UNKNOWN] = "-", // This is only to pass "non-trivial designated initializers" error
[COR_ALGO_PM_NONE] = "none",
[COR_ALGO_PM_EPA_2021] = "epa_2021",
[COR_ALGO_PM_SLR_CUSTOM] = "custom",
};
const char *TEMP_HUM_CORRECTION_ALGORITHM_NAMES[] = {
[COR_ALGO_TEMP_HUM_UNKNOWN] = "-", // This is only to pass "non-trivial designated initializers" error
[COR_ALGO_TEMP_HUM_NONE] = "none",
[COR_ALGO_TEMP_HUM_AG_PMS5003T_2024] = "ag_pms5003t_2024",
[COR_ALGO_TEMP_HUM_SLR_CUSTOM] = "custom",
};
#define JSON_PROP_NAME(name) jprop_##name
@ -47,6 +49,7 @@ JSON_PROP_DEF(mqttBrokerUrl);
JSON_PROP_DEF(temperatureUnit);
JSON_PROP_DEF(configurationControl);
JSON_PROP_DEF(postDataToAirGradient);
JSON_PROP_DEF(disableCloudConnection);
JSON_PROP_DEF(ledBarBrightness);
JSON_PROP_DEF(displayBrightness);
JSON_PROP_DEF(co2CalibrationRequested);
@ -54,6 +57,8 @@ JSON_PROP_DEF(ledBarTestRequested);
JSON_PROP_DEF(offlineMode);
JSON_PROP_DEF(monitorDisplayCompensatedValues);
JSON_PROP_DEF(corrections);
JSON_PROP_DEF(atmp);
JSON_PROP_DEF(rhum);
#define jprop_model_default ""
#define jprop_country_default "TH"
@ -66,6 +71,7 @@ JSON_PROP_DEF(corrections);
#define jprop_temperatureUnit_default "c"
#define jprop_configurationControl_default String(CONFIGURATION_CONTROL_NAME[ConfigurationControl::ConfigurationControlBoth])
#define jprop_postDataToAirGradient_default true
#define jprop_disableCloudConnection_default false
#define jprop_ledBarBrightness_default 100
#define jprop_displayBrightness_default 100
#define jprop_offlineMode_default false
@ -104,8 +110,8 @@ PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) {
// If the input string matches an algorithm name, return the corresponding enum value
// Else return Unknown
const size_t enumSize = SLR_PMS5003_20240104 + 1; // Get the actual size of the enum
PMCorrectionAlgorithm result = PMCorrectionAlgorithm::Unknown;
const size_t enumSize = COR_ALGO_PM_SLR_CUSTOM + 1; // Get the actual size of the enum
PMCorrectionAlgorithm result = COR_ALGO_PM_UNKNOWN;;
// Loop through enum values
for (size_t enumVal = 0; enumVal < enumSize; enumVal++) {
@ -114,42 +120,63 @@ PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) {
}
}
// If string not match from enum, check if correctionAlgorithm is one of the PM batch corrections
if (result == COR_ALGO_PM_UNKNOWN) {
// Check the substring "slr_PMS5003_xxxxxxxx"
if (algorithm.substring(0, 11) == "slr_PMS5003") {
// If it is, then its a custom correction
result = COR_ALGO_PM_SLR_CUSTOM;
}
}
return result;
}
TempHumCorrectionAlgorithm Configuration::matchTempHumAlgorithm(String algorithm) {
// Get the actual size of the enum
const int enumSize = static_cast<int>(COR_ALGO_TEMP_HUM_SLR_CUSTOM);
TempHumCorrectionAlgorithm result = COR_ALGO_TEMP_HUM_UNKNOWN;
// Loop through enum values
for (size_t enumVal = 0; enumVal <= enumSize; enumVal++) {
if (algorithm == TEMP_HUM_CORRECTION_ALGORITHM_NAMES[enumVal]) {
result = static_cast<TempHumCorrectionAlgorithm>(enumVal);
}
}
return result;
}
bool Configuration::updatePmCorrection(JSONVar &json) {
if (!json.hasOwnProperty("corrections")) {
// TODO: need to response message?
Serial.println("corrections not found");
logInfo("corrections not found");
return false;
}
JSONVar corrections = json["corrections"];
if (!corrections.hasOwnProperty("pm02")) {
Serial.println("pm02 not found");
logWarning("pm02 not found");
return false;
}
JSONVar pm02 = corrections["pm02"];
if (!pm02.hasOwnProperty("correctionAlgorithm")) {
Serial.println("correctionAlgorithm not found");
logWarning("pm02 correctionAlgorithm not found");
return false;
}
// TODO: Need to have data type check, with error message response if invalid
// Check algorithm
String algorithm = pm02["correctionAlgorithm"];
PMCorrectionAlgorithm algo = matchPmAlgorithm(algorithm);
if (algo == Unknown) {
logInfo("Unknown algorithm");
if (algo == COR_ALGO_PM_UNKNOWN) {
logWarning("Unknown algorithm");
return false;
}
logInfo("Correction algorithm: " + algorithm);
// If algo is None or EPA_2021, no need to check slr
// But first check if pmCorrection different from algo
if (algo == None || algo == EPA_2021) {
if (algo == COR_ALGO_PM_NONE || algo == COR_ALGO_PM_EPA_2021) {
if (pmCorrection.algorithm != algo) {
// Deep copy corrections from root to jconfig, so it will be saved later
jconfig[jprop_corrections]["pm02"]["correctionAlgorithm"] = algorithm;
@ -166,7 +193,7 @@ bool Configuration::updatePmCorrection(JSONVar &json) {
// Check if pm02 has slr object
if (!pm02.hasOwnProperty("slr")) {
Serial.println("slr not found");
logWarning("slr not found");
return false;
}
@ -175,7 +202,7 @@ bool Configuration::updatePmCorrection(JSONVar &json) {
// Validate required slr properties exist
if (!slr.hasOwnProperty("intercept") || !slr.hasOwnProperty("scalingFactor") ||
!slr.hasOwnProperty("useEpa2021")) {
Serial.println("Missing required slr properties");
logWarning("Missing required slr properties");
return false;
}
@ -205,6 +232,87 @@ bool Configuration::updatePmCorrection(JSONVar &json) {
return true;
}
bool Configuration::updateTempHumCorrection(JSONVar &json, TempHumCorrection &target,
const char *correctionName) {
if (!json.hasOwnProperty(jprop_corrections)) {
return false;
}
JSONVar corrections = json[jprop_corrections];
if (!corrections.hasOwnProperty(correctionName)) {
logWarning(String(correctionName) + " correction field not found on configuration");
return false;
}
JSONVar correctionTarget = corrections[correctionName];
if (!correctionTarget.hasOwnProperty("correctionAlgorithm")) {
Serial.println("correctionAlgorithm not found");
return false;
}
String algorithm = correctionTarget["correctionAlgorithm"];
TempHumCorrectionAlgorithm algo = matchTempHumAlgorithm(algorithm);
if (algo == COR_ALGO_TEMP_HUM_UNKNOWN) {
logInfo("Uknown temp/hum algorithm");
return false;
}
logInfo(String(correctionName) + " correction algorithm: " + algorithm);
// If algo is None or Standard, then no need to check slr
// But first check if target correction different from algo
if (algo == COR_ALGO_TEMP_HUM_NONE || algo == COR_ALGO_TEMP_HUM_AG_PMS5003T_2024) {
if (target.algorithm != algo) {
// Deep copy corrections from root to jconfig, so it will be saved later
jconfig[jprop_corrections][correctionName]["correctionAlgorithm"] = algorithm;
jconfig[jprop_corrections][correctionName]["slr"] = JSON.parse("{}"); // Clear slr
// Update pmCorrection with new values
target.algorithm = algo;
target.changed = true;
logInfo(String(correctionName) + " correction updated");
return true;
}
return false;
}
// Check if correction.target (atmp or rhum) has slr object
if (!correctionTarget.hasOwnProperty("slr")) {
logWarning(String(correctionName) + " slr not found");
return false;
}
JSONVar slr = correctionTarget["slr"];
// Validate required slr properties exist
if (!slr.hasOwnProperty("intercept") || !slr.hasOwnProperty("scalingFactor")) {
Serial.println("Missing required slr properties");
return false;
}
// arduino_json doesn't support float type, need to cast to double first
float intercept = (float)((double)slr["intercept"]);
float scalingFactor = (float)((double)slr["scalingFactor"]);
// Compare with current target correciont
if (target.algorithm == algo && target.intercept == intercept &&
target.scalingFactor == scalingFactor) {
return false; // No changes needed
}
// Deep copy corrections from root to jconfig, so it will be saved later
jconfig[jprop_corrections] = corrections;
// Update target with new values
target.algorithm = algo;
target.intercept = intercept;
target.scalingFactor = scalingFactor;
target.changed = true;
// Correction values were updated
logInfo(String(correctionName) + " correction updated");
return true;
}
/**
* @brief Save configure to device storage (EEPROM)
*
@ -272,6 +380,7 @@ void Configuration::defaultConfig(void) {
jconfig[jprop_configurationControl] = jprop_configurationControl_default;
jconfig[jprop_pmStandard] = jprop_pmStandard_default;
jconfig[jprop_temperatureUnit] = jprop_temperatureUnit_default;
jconfig[jprop_disableCloudConnection] = jprop_disableCloudConnection_default;
jconfig[jprop_postDataToAirGradient] = jprop_postDataToAirGradient_default;
if (ag->isOne()) {
jconfig[jprop_ledBarBrightness] = jprop_ledBarBrightness_default;
@ -289,8 +398,8 @@ void Configuration::defaultConfig(void) {
jconfig[jprop_offlineMode] = jprop_offlineMode_default;
jconfig[jprop_monitorDisplayCompensatedValues] = jprop_monitorDisplayCompensatedValues_default;
// PM2.5 correction
pmCorrection.algorithm = None;
// PM2.5 default correction
pmCorrection.algorithm = COR_ALGO_PM_NONE;
pmCorrection.changed = false;
pmCorrection.intercept = 0;
pmCorrection.scalingFactor = 1;
@ -792,11 +901,21 @@ bool Configuration::parse(String data, bool isLocal) {
}
}
// Corrections
// PM2.5 Corrections
if (updatePmCorrection(root)) {
changed = true;
}
// Temperature correction
if (updateTempHumCorrection(root, tempCorrection, jprop_atmp)) {
changed = true;
}
// Relative humidity correction
if (updateTempHumCorrection(root, rhumCorrection, jprop_rhum)) {
changed = true;
}
if (changed) {
updated = true;
saveConfig();
@ -1036,20 +1155,20 @@ void Configuration::toConfig(const char *buf) {
}
bool changed = false;
bool isInvalid = false;
bool isConfigFieldInvalid = false;
/** Validate country */
if (JSON.typeof_(jconfig[jprop_country]) != "string") {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
String country = jconfig[jprop_country];
if (country.length() != 2) {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
isInvalid = false;
isConfigFieldInvalid = false;
}
}
if (isInvalid) {
if (isConfigFieldInvalid) {
jconfig[jprop_country] = jprop_country_default;
changed = true;
logInfo("toConfig: country changed");
@ -1057,17 +1176,17 @@ void Configuration::toConfig(const char *buf) {
/** validate: PM standard */
if (JSON.typeof_(jconfig[jprop_pmStandard]) != "string") {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
String standard = jconfig[jprop_pmStandard];
if (standard != getPMStandardString(true) &&
standard != getPMStandardString(false)) {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
isInvalid = false;
isConfigFieldInvalid = false;
}
}
if (isInvalid) {
if (isConfigFieldInvalid) {
jconfig[jprop_pmStandard] = jprop_pmStandard_default;
changed = true;
logInfo("toConfig: pmStandard changed");
@ -1075,18 +1194,18 @@ void Configuration::toConfig(const char *buf) {
/** validate led bar mode */
if (JSON.typeof_(jconfig[jprop_ledBarMode]) != "string") {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
String mode = jconfig[jprop_ledBarMode];
if (mode != getLedBarModeName(LedBarMode::LedBarModeCO2) &&
mode != getLedBarModeName(LedBarMode::LedBarModeOff) &&
mode != getLedBarModeName(LedBarMode::LedBarModePm)) {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
isInvalid = false;
isConfigFieldInvalid = false;
}
}
if (isInvalid) {
if (isConfigFieldInvalid) {
jconfig[jprop_ledBarMode] = jprop_ledBarMode_default;
changed = true;
logInfo("toConfig: ledBarMode changed");
@ -1094,11 +1213,11 @@ void Configuration::toConfig(const char *buf) {
/** validate abcday */
if (JSON.typeof_(jconfig[jprop_abcDays]) != "number") {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
isInvalid = false;
isConfigFieldInvalid = false;
}
if (isInvalid) {
if (isConfigFieldInvalid) {
jconfig[jprop_abcDays] = jprop_abcDays_default;
changed = true;
logInfo("toConfig: abcDays changed");
@ -1106,16 +1225,16 @@ void Configuration::toConfig(const char *buf) {
/** validate tvoc learning offset */
if (JSON.typeof_(jconfig[jprop_tvocLearningOffset]) != "number") {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
int value = jconfig[jprop_tvocLearningOffset];
if (value < 0) {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
isInvalid = false;
isConfigFieldInvalid = false;
}
}
if (isInvalid) {
if (isConfigFieldInvalid) {
jconfig[jprop_tvocLearningOffset] = jprop_tvocLearningOffset_default;
changed = true;
logInfo("toConfig: tvocLearningOffset changed");
@ -1123,16 +1242,16 @@ void Configuration::toConfig(const char *buf) {
/** validate nox learning offset */
if (JSON.typeof_(jconfig[jprop_noxLearningOffset]) != "number") {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
int value = jconfig[jprop_noxLearningOffset];
if (value < 0) {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
isInvalid = false;
isConfigFieldInvalid = false;
}
}
if (isInvalid) {
if (isConfigFieldInvalid) {
jconfig[jprop_noxLearningOffset] = jprop_noxLearningOffset_default;
changed = true;
logInfo("toConfig: noxLearningOffset changed");
@ -1140,11 +1259,11 @@ void Configuration::toConfig(const char *buf) {
/** validate mqtt broker */
if (JSON.typeof_(jconfig[jprop_mqttBrokerUrl]) != "string") {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
isInvalid = false;
isConfigFieldInvalid = false;
}
if (isInvalid) {
if (isConfigFieldInvalid) {
changed = true;
jconfig[jprop_mqttBrokerUrl] = jprop_mqttBrokerUrl_default;
logInfo("toConfig: mqttBroker changed");
@ -1152,24 +1271,36 @@ void Configuration::toConfig(const char *buf) {
/** Validate temperature unit */
if (JSON.typeof_(jconfig[jprop_temperatureUnit]) != "string") {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
String unit = jconfig[jprop_temperatureUnit];
if (unit != "c" && unit != "f") {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
isInvalid = false;
isConfigFieldInvalid = false;
}
}
if (isInvalid) {
if (isConfigFieldInvalid) {
jconfig[jprop_temperatureUnit] = jprop_temperatureUnit_default;
changed = true;
logInfo("toConfig: temperatureUnit changed");
}
/** validate disableCloudConnection configuration */
if (JSON.typeof_(jconfig[jprop_disableCloudConnection]) != "boolean") {
isConfigFieldInvalid = true;
} else {
isConfigFieldInvalid = false;
}
if (isConfigFieldInvalid) {
jconfig[jprop_disableCloudConnection] = jprop_disableCloudConnection_default;
changed = true;
logInfo("toConfig: disableCloudConnection changed");
}
/** validate configuration control */
if (JSON.typeof_(jprop_configurationControl) != "string") {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
String ctrl = jconfig[jprop_configurationControl];
if (ctrl != String(CONFIGURATION_CONTROL_NAME
@ -1178,12 +1309,12 @@ void Configuration::toConfig(const char *buf) {
[ConfigurationControl::ConfigurationControlLocal]) &&
ctrl != String(CONFIGURATION_CONTROL_NAME
[ConfigurationControl::ConfigurationControlCloud])) {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
isInvalid = false;
isConfigFieldInvalid = false;
}
}
if (isInvalid) {
if (isConfigFieldInvalid) {
jconfig[jprop_configurationControl] =jprop_configurationControl_default;
changed = true;
logInfo("toConfig: configurationControl changed");
@ -1191,11 +1322,11 @@ void Configuration::toConfig(const char *buf) {
/** Validate post to airgradient cloud */
if (JSON.typeof_(jconfig[jprop_postDataToAirGradient]) != "boolean") {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
isInvalid = false;
isConfigFieldInvalid = false;
}
if (isInvalid) {
if (isConfigFieldInvalid) {
jconfig[jprop_postDataToAirGradient] = jprop_postDataToAirGradient_default;
changed = true;
logInfo("toConfig: postToAirGradient changed");
@ -1203,16 +1334,16 @@ void Configuration::toConfig(const char *buf) {
/** validate led bar brightness */
if (JSON.typeof_(jconfig[jprop_ledBarBrightness]) != "number") {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
int value = jconfig[jprop_ledBarBrightness];
if (value < 0 || value > 100) {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
isInvalid = false;
isConfigFieldInvalid = false;
}
}
if (isInvalid) {
if (isConfigFieldInvalid) {
jconfig[jprop_ledBarBrightness] = jprop_ledBarBrightness_default;
changed = true;
logInfo("toConfig: ledBarBrightness changed");
@ -1220,16 +1351,16 @@ void Configuration::toConfig(const char *buf) {
/** Validate display brightness */
if (JSON.typeof_(jconfig[jprop_displayBrightness]) != "number") {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
int value = jconfig[jprop_displayBrightness];
if (value < 0 || value > 100) {
isInvalid = true;
isConfigFieldInvalid = true;
} else {
isInvalid = false;
isConfigFieldInvalid = false;
}
}
if (isInvalid) {
if (isConfigFieldInvalid) {
jconfig[jprop_displayBrightness] = jprop_displayBrightness_default;
changed = true;
logInfo("toConfig: displayBrightness changed");
@ -1248,15 +1379,31 @@ void Configuration::toConfig(const char *buf) {
jprop_monitorDisplayCompensatedValues_default;
}
// Set default first before parsing local config
pmCorrection.algorithm = PMCorrectionAlgorithm::None;
// PM2.5 correction
/// Set default first before parsing local config
pmCorrection.algorithm = COR_ALGO_PM_NONE;
pmCorrection.intercept = 0;
pmCorrection.scalingFactor = 0;
pmCorrection.useEPA = false;
// Load correction from saved config
/// Load correction from saved config
updatePmCorrection(jconfig);
// Temperature correction
/// Set default first before parsing local config
tempCorrection.algorithm = COR_ALGO_TEMP_HUM_NONE;
tempCorrection.intercept = 0;
tempCorrection.scalingFactor = 0;
/// Load correction from saved config
updateTempHumCorrection(jconfig, tempCorrection, jprop_atmp);
// Relative humidity correction
/// Set default first before parsing local config
rhumCorrection.algorithm = COR_ALGO_TEMP_HUM_NONE;
rhumCorrection.intercept = 0;
rhumCorrection.scalingFactor = 0;
/// Load correction from saved config
updateTempHumCorrection(jconfig, rhumCorrection, jprop_rhum);
if (changed) {
saveConfig();
}
@ -1334,6 +1481,17 @@ void Configuration::setOfflineModeWithoutSave(bool offline) {
_offlineMode = offline;
}
bool Configuration::isCloudConnectionDisabled(void) {
bool disabled = jconfig[jprop_disableCloudConnection];
return disabled;
}
void Configuration::setDisableCloudConnection(bool disable) {
logInfo("Set DisableCloudConnection to " + String(disable ? "True" : "False"));
jconfig[jprop_disableCloudConnection] = disable;
saveConfig();
}
bool Configuration::isLedBarModeChanged(void) {
bool changed = _ledBarModeChanged;
_ledBarModeChanged = false;
@ -1369,14 +1527,16 @@ bool Configuration::isPMCorrectionChanged(void) {
*/
bool Configuration::isPMCorrectionEnabled(void) {
PMCorrection pmCorrection = getPMCorrection();
if (pmCorrection.algorithm == PMCorrectionAlgorithm::None ||
pmCorrection.algorithm == PMCorrectionAlgorithm::Unknown) {
if (pmCorrection.algorithm == COR_ALGO_PM_NONE ||
pmCorrection.algorithm == COR_ALGO_PM_UNKNOWN) {
return false;
}
return true;
}
Configuration::PMCorrection Configuration::getPMCorrection(void) {
return pmCorrection;
}
Configuration::PMCorrection Configuration::getPMCorrection(void) { return pmCorrection; }
Configuration::TempHumCorrection Configuration::getTempCorrection(void) { return tempCorrection; }
Configuration::TempHumCorrection Configuration::getHumCorrection(void) { return rhumCorrection; }

View File

@ -17,6 +17,13 @@ public:
bool changed;
};
struct TempHumCorrection {
TempHumCorrectionAlgorithm algorithm;
float intercept;
float scalingFactor;
bool changed;
};
private:
bool co2CalibrationRequested;
bool ledBarTestRequested;
@ -30,12 +37,17 @@ private:
bool _offlineMode = false;
bool _ledBarModeChanged = false;
PMCorrection pmCorrection;
TempHumCorrection tempCorrection;
TempHumCorrection rhumCorrection;
AirGradient* ag;
String getLedBarModeName(LedBarMode mode);
PMCorrectionAlgorithm matchPmAlgorithm(String algorithm);
TempHumCorrectionAlgorithm matchTempHumAlgorithm(String algorithm);
bool updatePmCorrection(JSONVar &json);
bool updateTempHumCorrection(JSONVar &json, TempHumCorrection &target,
const char *correctionName);
void saveConfig(void);
void loadConfig(void);
void defaultConfig(void);
@ -46,7 +58,7 @@ private:
void configLogInfo(String name, String fromValue, String toValue);
String getPMStandardString(bool usaqi);
String getAbcDayString(int value);
void toConfig(const char* buf);
void toConfig(const char *buf);
public:
Configuration(Stream &debugLog);
@ -94,11 +106,15 @@ public:
bool isOfflineMode(void);
void setOfflineMode(bool offline);
void setOfflineModeWithoutSave(bool offline);
bool isCloudConnectionDisabled(void);
void setDisableCloudConnection(bool disable);
bool isLedBarModeChanged(void);
bool isMonitorDisplayCompensatedValues(void);
bool isPMCorrectionChanged(void);
bool isPMCorrectionEnabled(void);
PMCorrection getPMCorrection(void);
TempHumCorrection getTempCorrection(void);
TempHumCorrection getHumCorrection(void);
};
#endif /** _AG_CONFIG_H_ */

View File

@ -12,7 +12,7 @@
*/
void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) {
/** Temperature */
float temp = value.getAverage(Measurements::Temperature);
float temp = value.getCorrectedTempHum(Measurements::Temperature, 1);
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 = round(value.getAverage(Measurements::Humidity));
int rhum = round(value.getCorrectedTempHum(Measurements::Humidity, 1));
if (utils::isValidHumidity(rhum)) {
snprintf(buf, buf_size, "%d%%", rhum);
} else {
@ -316,7 +316,7 @@ void OledDisplay::showDashboard(const char *status) {
int pm25 = round(value.getAverage(Measurements::PM25));
if (utils::isValidPm(pm25)) {
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25 = round(value.getCorrectedPM25(*ag, config, true));
pm25 = round(value.getCorrectedPM25(true));
}
if (config.isPmStandardInUSAQI()) {
sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(pm25));
@ -377,7 +377,7 @@ void OledDisplay::showDashboard(const char *status) {
/** Set PM */
int pm25 = round(value.getAverage(Measurements::PM25));
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25 = round(value.getCorrectedPM25(*ag, config, true));
pm25 = round(value.getCorrectedPM25(true));
}
ag->display.setCursor(0, 12);
@ -389,12 +389,12 @@ void OledDisplay::showDashboard(const char *status) {
ag->display.setText(strBuf);
/** Set temperature and humidity */
float temp = value.getAverage(Measurements::Temperature);
float temp = value.getCorrectedTempHum(Measurements::Temperature, 1);
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()) {
@ -407,7 +407,7 @@ void OledDisplay::showDashboard(const char *status) {
ag->display.setCursor(0, 24);
ag->display.setText(strBuf);
int rhum = round(value.getAverage(Measurements::Humidity));
int rhum = round(value.getCorrectedTempHum(Measurements::Humidity, 1));
if (utils::isValidHumidity(rhum)) {
snprintf(strBuf, sizeof(strBuf), "H:%d %%", rhum);
} else {

View File

@ -174,7 +174,7 @@ int StateMachine::pm25handleLeds(void) {
int pm25Value = round(value.getAverage(Measurements::PM25));
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25Value = round(value.getCorrectedPM25(*ag, config, true));
pm25Value = round(value.getCorrectedPM25(true));
}
if (pm25Value <= 5) {

View File

@ -27,6 +27,53 @@
#define json_prop_noxRaw "noxRaw"
#define json_prop_co2 "rco2"
Measurements::Measurements(Configuration &config) : config(config) {
#ifndef ESP8266
_resetReason = (int)ESP_RST_UNKNOWN;
#endif
/* Set invalid value for each measurements as default value when initialized*/
_temperature[0].update.avg = utils::getInvalidTemperature();
_temperature[1].update.avg = utils::getInvalidTemperature();
_humidity[0].update.avg = utils::getInvalidHumidity();
_humidity[1].update.avg = utils::getInvalidHumidity();
_co2.update.avg = utils::getInvalidCO2();
_tvoc.update.avg = utils::getInvalidVOC();
_tvoc_raw.update.avg = utils::getInvalidVOC();
_nox.update.avg = utils::getInvalidNOx();
_nox_raw.update.avg = utils::getInvalidNOx();
_pm_03_pc[0].update.avg = utils::getInvalidPmValue();
_pm_03_pc[1].update.avg = utils::getInvalidPmValue();
_pm_05_pc[0].update.avg = utils::getInvalidPmValue();
_pm_05_pc[1].update.avg = utils::getInvalidPmValue();
_pm_5_pc[0].update.avg = utils::getInvalidPmValue();
_pm_5_pc[1].update.avg = utils::getInvalidPmValue();
_pm_01[0].update.avg = utils::getInvalidPmValue();
_pm_01_sp[0].update.avg = utils::getInvalidPmValue();
_pm_01_pc[0].update.avg = utils::getInvalidPmValue();
_pm_01[1].update.avg = utils::getInvalidPmValue();
_pm_01_sp[1].update.avg = utils::getInvalidPmValue();
_pm_01_pc[1].update.avg = utils::getInvalidPmValue();
_pm_25[0].update.avg = utils::getInvalidPmValue();
_pm_25_sp[0].update.avg = utils::getInvalidPmValue();
_pm_25_pc[0].update.avg = utils::getInvalidPmValue();
_pm_25[1].update.avg = utils::getInvalidPmValue();
_pm_25_sp[1].update.avg = utils::getInvalidPmValue();
_pm_25_pc[1].update.avg = utils::getInvalidPmValue();
_pm_10[0].update.avg = utils::getInvalidPmValue();
_pm_10_sp[0].update.avg = utils::getInvalidPmValue();
_pm_10_pc[0].update.avg = utils::getInvalidPmValue();
_pm_10[1].update.avg = utils::getInvalidPmValue();
_pm_10_sp[1].update.avg = utils::getInvalidPmValue();
_pm_10_pc[1].update.avg = utils::getInvalidPmValue();
}
void Measurements::setAirGradient(AirGradient *ag) { this->ag = ag; }
void Measurements::maxPeriod(MeasurementType type, int max) {
switch (type) {
case Temperature:
@ -189,7 +236,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 +275,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 +307,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 +346,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 +395,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 +430,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;
}
@ -400,12 +447,12 @@ float Measurements::getAverage(MeasurementType type, int ch) {
// Sanity check to validate channel, assert if invalid
validateChannel(ch);
bool undefined = false;
// 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:
@ -428,13 +475,13 @@ float Measurements::getAverage(MeasurementType type, int ch) {
break;
default:
// Invalidate, measurements type not handled
measurementAverage = -1000;
undefined = true;
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));
// Sanity check if measurement type is not defined
if (undefined) {
Serial.printf("ERROR! %s is not defined on get average value function\n", measurementTypeStr(type).c_str());
delay(1000);
assert(0);
}
@ -529,7 +576,70 @@ void Measurements::validateChannel(int ch) {
}
}
float Measurements::getCorrectedPM25(AirGradient &ag, Configuration &config, bool useAvg, int ch) {
float Measurements::getCorrectedTempHum(MeasurementType type, int ch, bool forceCorrection) {
// Sanity check to validate channel, assert if invalid
validateChannel(ch);
// Follow array indexing just for get address of the value type
ch = ch - 1;
bool undefined = false;
float rawValue;
Configuration::TempHumCorrection correction;
switch (type) {
case Temperature: {
rawValue = _temperature[ch].update.avg;
Configuration::TempHumCorrection tmp = config.getTempCorrection();
// Apply 'standard' correction if its defined or correction forced
if (tmp.algorithm == TempHumCorrectionAlgorithm::COR_ALGO_TEMP_HUM_AG_PMS5003T_2024) {
return ag->pms5003t_1.compensateTemp(rawValue);
} else if (tmp.algorithm == TempHumCorrectionAlgorithm::COR_ALGO_TEMP_HUM_NONE && forceCorrection) {
return ag->pms5003t_1.compensateTemp(rawValue);
}
correction.algorithm = tmp.algorithm;
correction.intercept = tmp.intercept;
correction.scalingFactor = tmp.scalingFactor;
break;
}
case Humidity: {
rawValue = _humidity[ch].update.avg;
Configuration::TempHumCorrection tmp = config.getHumCorrection();
// Apply 'standard' correction if its defined or correction forced
if (tmp.algorithm == TempHumCorrectionAlgorithm::COR_ALGO_TEMP_HUM_AG_PMS5003T_2024) {
return ag->pms5003t_1.compensateHum(rawValue);
} else if (tmp.algorithm == TempHumCorrectionAlgorithm::COR_ALGO_TEMP_HUM_NONE && forceCorrection) {
return ag->pms5003t_1.compensateHum(rawValue);
}
correction.algorithm = tmp.algorithm;
correction.intercept = tmp.intercept;
correction.scalingFactor = tmp.scalingFactor;
break;
}
default:
// Should not be called for other measurements
delay(1000);
assert(0);
}
// Use raw if correction not defined
if (correction.algorithm == TempHumCorrectionAlgorithm::COR_ALGO_TEMP_HUM_NONE ||
correction.algorithm == TempHumCorrectionAlgorithm::COR_ALGO_TEMP_HUM_UNKNOWN) {
return rawValue;
}
// Custom correction constants
float corrected = (rawValue * correction.scalingFactor) + correction.intercept;
Serial.println("Custom correction applied");
return corrected;
}
float Measurements::getCorrectedPM25(bool useAvg, int ch, bool forceCorrection) {
float pm25;
float corrected;
float humidity;
@ -548,21 +658,27 @@ float Measurements::getCorrectedPM25(AirGradient &ag, Configuration &config, boo
Configuration::PMCorrection pmCorrection = config.getPMCorrection();
switch (pmCorrection.algorithm) {
case PMCorrectionAlgorithm::Unknown:
case PMCorrectionAlgorithm::None:
// If correction is Unknown, then default is None
corrected = pm25;
case PMCorrectionAlgorithm::COR_ALGO_PM_UNKNOWN:
case PMCorrectionAlgorithm::COR_ALGO_PM_NONE: {
// If correction is Unknown or None, then default is None
// Unless forceCorrection enabled
if (forceCorrection) {
corrected = ag->pms5003.compensate(pm25, humidity);
} else {
corrected = pm25;
}
break;
case PMCorrectionAlgorithm::EPA_2021:
corrected = ag.pms5003.compensate(pm25, humidity);
}
case PMCorrectionAlgorithm::COR_ALGO_PM_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);
corrected = ag->pms5003.slrCorrection(pm25, pm003Count, pmCorrection.scalingFactor,
pmCorrection.intercept);
if (pmCorrection.useEPA) {
// Add EPA compensation on top of SLR
corrected = ag.pms5003.compensate(corrected, humidity);
corrected = ag->pms5003.compensate(corrected, humidity);
}
}
}
@ -570,48 +686,52 @@ float Measurements::getCorrectedPM25(AirGradient &ag, Configuration &config, boo
return corrected;
}
String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi, AirGradient &ag,
Configuration &config) {
String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi) {
JSONVar root;
if (ag.isOne() || (ag.isPro4_2()) || ag.isPro3_3() || ag.isBasic()) {
root = buildIndoor(localServer, ag, config);
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
root = buildIndoor(localServer);
} else {
root = buildOutdoor(localServer, fwMode, ag, config);
root = buildOutdoor(localServer, fwMode);
}
// CO2
if (config.hasSensorS8 && utils::isValidCO2(_co2.update.avg)) {
root[json_prop_co2] = ag.round2(_co2.update.avg);
root[json_prop_co2] = ag->round2(_co2.update.avg);
}
/// TVOx and NOx
if (config.hasSensorSGP) {
if (utils::isValidVOC(_tvoc.update.avg)) {
root[json_prop_tvoc] = ag.round2(_tvoc.update.avg);
root[json_prop_tvoc] = ag->round2(_tvoc.update.avg);
}
if (utils::isValidVOC(_tvoc_raw.update.avg)) {
root[json_prop_tvocRaw] = ag.round2(_tvoc_raw.update.avg);
root[json_prop_tvocRaw] = ag->round2(_tvoc_raw.update.avg);
}
if (utils::isValidNOx(_nox.update.avg)) {
root[json_prop_nox] = ag.round2(_nox.update.avg);
root[json_prop_nox] = ag->round2(_nox.update.avg);
}
if (utils::isValidNOx(_nox_raw.update.avg)) {
root[json_prop_noxRaw] = ag.round2(_nox_raw.update.avg);
root[json_prop_noxRaw] = ag->round2(_nox_raw.update.avg);
}
}
root["boot"] = bootCount;
root["bootCount"] = bootCount;
root["boot"] = _bootCount;
root["bootCount"] = _bootCount;
root["wifi"] = rssi;
if (localServer) {
if (ag.isOne()) {
if (ag->isOne()) {
root["ledMode"] = config.getLedBarModeName();
}
root["serialno"] = ag.deviceId();
root["firmware"] = ag.getVersion();
root["serialno"] = ag->deviceId();
root["firmware"] = ag->getVersion();
root["model"] = AgFirmwareModeName(fwMode);
} else {
#ifndef ESP8266
root["resetReason"] = _resetReason;
root["freeHeap"] = ESP.getFreeHeap();
#endif
}
String result = JSON.stringify(root);
@ -619,8 +739,7 @@ String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi,
return result;
}
JSONVar Measurements::buildOutdoor(bool localServer, AgFirmwareMode fwMode, AirGradient &ag,
Configuration &config) {
JSONVar Measurements::buildOutdoor(bool localServer, AgFirmwareMode fwMode) {
JSONVar outdoor;
if (fwMode == FW_MODE_O_1P || fwMode == FW_MODE_O_1PS || fwMode == FW_MODE_O_1PST) {
// buildPMS params:
@ -629,14 +748,16 @@ JSONVar Measurements::buildOutdoor(bool localServer, AgFirmwareMode fwMode, AirG
/// compensated values if requested by local server
/// Set ch based on hasSensorPMSx
if (config.hasSensorPMS1) {
outdoor = buildPMS(ag, 1, false, true, localServer);
outdoor = buildPMS(1, false, true, localServer);
if (!localServer) {
outdoor[json_prop_pmFirmware] = pms5003TFirmwareVersion(ag.pms5003t_1.getFirmwareVersion());
outdoor[json_prop_pmFirmware] =
pms5003TFirmwareVersion(ag->pms5003t_1.getFirmwareVersion());
}
} else {
outdoor = buildPMS(ag, 2, false, true, localServer);
outdoor = buildPMS(2, false, true, localServer);
if (!localServer) {
outdoor[json_prop_pmFirmware] = pms5003TFirmwareVersion(ag.pms5003t_2.getFirmwareVersion());
outdoor[json_prop_pmFirmware] =
pms5003TFirmwareVersion(ag->pms5003t_2.getFirmwareVersion());
}
}
} else {
@ -645,65 +766,55 @@ JSONVar Measurements::buildOutdoor(bool localServer, AgFirmwareMode fwMode, AirG
/// Have 2 PMS sensor, allCh is set to true (ch params ignored)
/// Enable temp hum from PMS
/// compensated values if requested by local server
outdoor = buildPMS(ag, 1, true, true, localServer);
outdoor = buildPMS(1, true, true, localServer);
// PMS5003T version
if (!localServer) {
outdoor["channels"]["1"][json_prop_pmFirmware] =
pms5003TFirmwareVersion(ag.pms5003t_1.getFirmwareVersion());
pms5003TFirmwareVersion(ag->pms5003t_1.getFirmwareVersion());
outdoor["channels"]["2"][json_prop_pmFirmware] =
pms5003TFirmwareVersion(ag.pms5003t_2.getFirmwareVersion());
pms5003TFirmwareVersion(ag->pms5003t_2.getFirmwareVersion());
}
}
return outdoor;
}
JSONVar Measurements::buildIndoor(bool localServer, AirGradient &ag, Configuration &config) {
JSONVar Measurements::buildIndoor(bool localServer) {
JSONVar indoor;
if (config.hasSensorPMS1) {
// buildPMS params:
/// PMS channel 1 (indoor only have 1 PMS; hence allCh false)
/// Not include temperature and humidity from PMS sensor
/// Not include compensated calculation
indoor = buildPMS(ag, 1, false, false, false);
/// Include compensated calculation
indoor = buildPMS(1, false, false, true);
if (!localServer) {
// Indoor is using PMS5003
indoor[json_prop_pmFirmware] = this->pms5003FirmwareVersion(ag.pms5003.getFirmwareVersion());
indoor[json_prop_pmFirmware] = this->pms5003FirmwareVersion(ag->pms5003.getFirmwareVersion());
}
}
if (config.hasSensorSHT) {
// Add temperature
if (utils::isValidTemperature(_temperature[0].update.avg)) {
indoor[json_prop_temp] = ag.round2(_temperature[0].update.avg);
indoor[json_prop_temp] = ag->round2(_temperature[0].update.avg);
if (localServer) {
indoor[json_prop_tempCompensated] = ag.round2(_temperature[0].update.avg);
indoor[json_prop_tempCompensated] = ag->round2(getCorrectedTempHum(Temperature));
}
}
// Add humidity
if (utils::isValidHumidity(_humidity[0].update.avg)) {
indoor[json_prop_rhum] = ag.round2(_humidity[0].update.avg);
indoor[json_prop_rhum] = ag->round2(_humidity[0].update.avg);
if (localServer) {
indoor[json_prop_rhumCompensated] = ag.round2(_humidity[0].update.avg);
indoor[json_prop_rhumCompensated] = ag->round2(getCorrectedTempHum(Humidity));
}
}
}
// Add pm25 compensated value only if PM2.5 and humidity value is valid
if (config.hasSensorPMS1 && utils::isValidPm(_pm_25[0].update.avg)) {
if (config.hasSensorSHT && utils::isValidHumidity(_humidity[0].update.avg)) {
// Correction using moving average value
float tmp = getCorrectedPM25(ag, config, true);
indoor[json_prop_pm25Compensated] = ag.round2(tmp);
}
}
return indoor;
}
JSONVar Measurements::buildPMS(AirGradient &ag, int ch, bool allCh, bool withTempHum,
bool compensate) {
JSONVar Measurements::buildPMS(int ch, bool allCh, bool withTempHum, bool compensate) {
JSONVar pms;
// When only one of the channel
@ -712,89 +823,86 @@ JSONVar Measurements::buildPMS(AirGradient &ag, int ch, bool allCh, bool withTem
validateChannel(ch);
// Follow array indexing just for get address of the value type
ch = ch - 1;
int chIndex = ch - 1;
if (utils::isValidPm(_pm_01[ch].update.avg)) {
pms[json_prop_pm01Ae] = ag.round2(_pm_01[ch].update.avg);
if (utils::isValidPm(_pm_01[chIndex].update.avg)) {
pms[json_prop_pm01Ae] = ag->round2(_pm_01[chIndex].update.avg);
}
if (utils::isValidPm(_pm_25[ch].update.avg)) {
pms[json_prop_pm25Ae] = ag.round2(_pm_25[ch].update.avg);
if (utils::isValidPm(_pm_25[chIndex].update.avg)) {
pms[json_prop_pm25Ae] = ag->round2(_pm_25[chIndex].update.avg);
}
if (utils::isValidPm(_pm_10[ch].update.avg)) {
pms[json_prop_pm10Ae] = ag.round2(_pm_10[ch].update.avg);
if (utils::isValidPm(_pm_10[chIndex].update.avg)) {
pms[json_prop_pm10Ae] = ag->round2(_pm_10[chIndex].update.avg);
}
if (utils::isValidPm(_pm_01_sp[ch].update.avg)) {
pms[json_prop_pm01Sp] = ag.round2(_pm_01_sp[ch].update.avg);
if (utils::isValidPm(_pm_01_sp[chIndex].update.avg)) {
pms[json_prop_pm01Sp] = ag->round2(_pm_01_sp[chIndex].update.avg);
}
if (utils::isValidPm(_pm_25_sp[ch].update.avg)) {
pms[json_prop_pm25Sp] = ag.round2(_pm_25_sp[ch].update.avg);
if (utils::isValidPm(_pm_25_sp[chIndex].update.avg)) {
pms[json_prop_pm25Sp] = ag->round2(_pm_25_sp[chIndex].update.avg);
}
if (utils::isValidPm(_pm_10_sp[ch].update.avg)) {
pms[json_prop_pm10Sp] = ag.round2(_pm_10_sp[ch].update.avg);
if (utils::isValidPm(_pm_10_sp[chIndex].update.avg)) {
pms[json_prop_pm10Sp] = ag->round2(_pm_10_sp[chIndex].update.avg);
}
if (utils::isValidPm03Count(_pm_03_pc[ch].update.avg)) {
pms[json_prop_pm03Count] = ag.round2(_pm_03_pc[ch].update.avg);
if (utils::isValidPm03Count(_pm_03_pc[chIndex].update.avg)) {
pms[json_prop_pm03Count] = ag->round2(_pm_03_pc[chIndex].update.avg);
}
if (utils::isValidPm03Count(_pm_05_pc[ch].update.avg)) {
pms[json_prop_pm05Count] = ag.round2(_pm_05_pc[ch].update.avg);
if (utils::isValidPm03Count(_pm_05_pc[chIndex].update.avg)) {
pms[json_prop_pm05Count] = ag->round2(_pm_05_pc[chIndex].update.avg);
}
if (utils::isValidPm03Count(_pm_01_pc[ch].update.avg)) {
pms[json_prop_pm1Count] = ag.round2(_pm_01_pc[ch].update.avg);
if (utils::isValidPm03Count(_pm_01_pc[chIndex].update.avg)) {
pms[json_prop_pm1Count] = ag->round2(_pm_01_pc[chIndex].update.avg);
}
if (utils::isValidPm03Count(_pm_25_pc[ch].update.avg)) {
pms[json_prop_pm25Count] = ag.round2(_pm_25_pc[ch].update.avg);
if (utils::isValidPm03Count(_pm_25_pc[chIndex].update.avg)) {
pms[json_prop_pm25Count] = ag->round2(_pm_25_pc[chIndex].update.avg);
}
if (_pm_5_pc[ch].listValues.empty() == false) {
if (_pm_5_pc[chIndex].listValues.empty() == false) {
// Only include pm5.0 count when values available on its list
// If not, means no pm5_pc available from the sensor
if (utils::isValidPm03Count(_pm_5_pc[ch].update.avg)) {
pms[json_prop_pm5Count] = ag.round2(_pm_5_pc[ch].update.avg);
if (utils::isValidPm03Count(_pm_5_pc[chIndex].update.avg)) {
pms[json_prop_pm5Count] = ag->round2(_pm_5_pc[chIndex].update.avg);
}
}
if (_pm_10_pc[ch].listValues.empty() == false) {
if (_pm_10_pc[chIndex].listValues.empty() == false) {
// Only include pm10 count when values available on its list
// If not, means no pm10_pc available from the sensor
if (utils::isValidPm03Count(_pm_10_pc[ch].update.avg)) {
pms[json_prop_pm10Count] = ag.round2(_pm_10_pc[ch].update.avg);
if (utils::isValidPm03Count(_pm_10_pc[chIndex].update.avg)) {
pms[json_prop_pm10Count] = ag->round2(_pm_10_pc[chIndex].update.avg);
}
}
if (withTempHum) {
float _vc;
// Set temperature if valid
if (utils::isValidTemperature(_temperature[ch].update.avg)) {
pms[json_prop_temp] = ag.round2(_temperature[ch].update.avg);
if (utils::isValidTemperature(_temperature[chIndex].update.avg)) {
pms[json_prop_temp] = ag->round2(_temperature[chIndex].update.avg);
// Compensate temperature when flag is set
if (compensate) {
_vc = ag.pms5003t_1.compensateTemp(_temperature[ch].update.avg);
_vc = getCorrectedTempHum(Temperature, ch, true);
if (utils::isValidTemperature(_vc)) {
pms[json_prop_tempCompensated] = ag.round2(_vc);
pms[json_prop_tempCompensated] = ag->round2(_vc);
}
}
}
// Set humidity if valid
if (utils::isValidHumidity(_humidity[ch].update.avg)) {
pms[json_prop_rhum] = ag.round2(_humidity[ch].update.avg);
if (utils::isValidHumidity(_humidity[chIndex].update.avg)) {
pms[json_prop_rhum] = ag->round2(_humidity[chIndex].update.avg);
// Compensate relative humidity when flag is set
if (compensate) {
_vc = ag.pms5003t_1.compensateHum(_humidity[ch].update.avg);
if (utils::isValidTemperature(_vc)) {
pms[json_prop_rhumCompensated] = ag.round2(_vc);
_vc = getCorrectedTempHum(Humidity, ch, true);
if (utils::isValidHumidity(_vc)) {
pms[json_prop_rhumCompensated] = ag->round2(_vc);
}
}
}
// Add pm25 compensated value only if PM2.5 and humidity value is valid
if (compensate) {
if (utils::isValidPm(_pm_25[ch].update.avg) &&
utils::isValidHumidity(_humidity[ch].update.avg)) {
// Note: the pms5003t object is not matter either for channel 1 or 2, compensate points to
// the same base function
float pm25 = ag.pms5003t_1.compensate(_pm_25[ch].update.avg, _humidity[ch].update.avg);
if (utils::isValidPm(pm25)) {
pms[json_prop_pm25Compensated] = ag.round2(pm25);
}
}
}
// Add pm25 compensated value only if PM2.5 and humidity value is valid
if (compensate) {
if (utils::isValidPm(_pm_25[chIndex].update.avg) &&
utils::isValidHumidity(_humidity[chIndex].update.avg)) {
float pm25 = getCorrectedPM25(true, ch, true);
pms[json_prop_pm25Compensated] = ag->round2(pm25);
}
}
@ -808,144 +916,144 @@ JSONVar Measurements::buildPMS(AirGradient &ag, int ch, bool allCh, bool withTem
/// PM1.0 atmospheric environment
if (utils::isValidPm(_pm_01[0].update.avg) && utils::isValidPm(_pm_01[1].update.avg)) {
float avg = (_pm_01[0].update.avg + _pm_01[1].update.avg) / 2.0f;
pms[json_prop_pm01Ae] = ag.round2(avg);
pms["channels"]["1"][json_prop_pm01Ae] = ag.round2(_pm_01[0].update.avg);
pms["channels"]["2"][json_prop_pm01Ae] = ag.round2(_pm_01[1].update.avg);
pms[json_prop_pm01Ae] = ag->round2(avg);
pms["channels"]["1"][json_prop_pm01Ae] = ag->round2(_pm_01[0].update.avg);
pms["channels"]["2"][json_prop_pm01Ae] = ag->round2(_pm_01[1].update.avg);
} else if (utils::isValidPm(_pm_01[0].update.avg)) {
pms[json_prop_pm01Ae] = ag.round2(_pm_01[0].update.avg);
pms["channels"]["1"][json_prop_pm01Ae] = ag.round2(_pm_01[0].update.avg);
pms[json_prop_pm01Ae] = ag->round2(_pm_01[0].update.avg);
pms["channels"]["1"][json_prop_pm01Ae] = ag->round2(_pm_01[0].update.avg);
} else if (utils::isValidPm(_pm_01[1].update.avg)) {
pms[json_prop_pm01Ae] = ag.round2(_pm_01[1].update.avg);
pms["channels"]["2"][json_prop_pm01Ae] = ag.round2(_pm_01[1].update.avg);
pms[json_prop_pm01Ae] = ag->round2(_pm_01[1].update.avg);
pms["channels"]["2"][json_prop_pm01Ae] = ag->round2(_pm_01[1].update.avg);
}
/// PM2.5 atmospheric environment
if (utils::isValidPm(_pm_25[0].update.avg) && utils::isValidPm(_pm_25[1].update.avg)) {
float avg = (_pm_25[0].update.avg + _pm_25[1].update.avg) / 2.0f;
pms[json_prop_pm25Ae] = ag.round2(avg);
pms["channels"]["1"][json_prop_pm25Ae] = ag.round2(_pm_25[0].update.avg);
pms["channels"]["2"][json_prop_pm25Ae] = ag.round2(_pm_25[1].update.avg);
pms[json_prop_pm25Ae] = ag->round2(avg);
pms["channels"]["1"][json_prop_pm25Ae] = ag->round2(_pm_25[0].update.avg);
pms["channels"]["2"][json_prop_pm25Ae] = ag->round2(_pm_25[1].update.avg);
} else if (utils::isValidPm(_pm_25[0].update.avg)) {
pms[json_prop_pm25Ae] = ag.round2(_pm_25[0].update.avg);
pms["channels"]["1"][json_prop_pm25Ae] = ag.round2(_pm_25[0].update.avg);
pms[json_prop_pm25Ae] = ag->round2(_pm_25[0].update.avg);
pms["channels"]["1"][json_prop_pm25Ae] = ag->round2(_pm_25[0].update.avg);
} else if (utils::isValidPm(_pm_25[1].update.avg)) {
pms[json_prop_pm25Ae] = ag.round2(_pm_25[1].update.avg);
pms["channels"]["2"][json_prop_pm25Ae] = ag.round2(_pm_25[1].update.avg);
pms[json_prop_pm25Ae] = ag->round2(_pm_25[1].update.avg);
pms["channels"]["2"][json_prop_pm25Ae] = ag->round2(_pm_25[1].update.avg);
}
/// PM10 atmospheric environment
if (utils::isValidPm(_pm_10[0].update.avg) && utils::isValidPm(_pm_10[1].update.avg)) {
float avg = (_pm_10[0].update.avg + _pm_10[1].update.avg) / 2.0f;
pms[json_prop_pm10Ae] = ag.round2(avg);
pms["channels"]["1"][json_prop_pm10Ae] = ag.round2(_pm_10[0].update.avg);
pms["channels"]["2"][json_prop_pm10Ae] = ag.round2(_pm_10[1].update.avg);
pms[json_prop_pm10Ae] = ag->round2(avg);
pms["channels"]["1"][json_prop_pm10Ae] = ag->round2(_pm_10[0].update.avg);
pms["channels"]["2"][json_prop_pm10Ae] = ag->round2(_pm_10[1].update.avg);
} else if (utils::isValidPm(_pm_10[0].update.avg)) {
pms[json_prop_pm10Ae] = ag.round2(_pm_10[0].update.avg);
pms["channels"]["1"][json_prop_pm10Ae] = ag.round2(_pm_10[0].update.avg);
pms[json_prop_pm10Ae] = ag->round2(_pm_10[0].update.avg);
pms["channels"]["1"][json_prop_pm10Ae] = ag->round2(_pm_10[0].update.avg);
} else if (utils::isValidPm(_pm_10[1].update.avg)) {
pms[json_prop_pm10Ae] = ag.round2(_pm_10[1].update.avg);
pms["channels"]["2"][json_prop_pm10Ae] = ag.round2(_pm_10[1].update.avg);
pms[json_prop_pm10Ae] = ag->round2(_pm_10[1].update.avg);
pms["channels"]["2"][json_prop_pm10Ae] = ag->round2(_pm_10[1].update.avg);
}
/// PM1.0 standard particle
if (utils::isValidPm(_pm_01_sp[0].update.avg) && utils::isValidPm(_pm_01_sp[1].update.avg)) {
float avg = (_pm_01_sp[0].update.avg + _pm_01_sp[1].update.avg) / 2.0f;
pms[json_prop_pm01Sp] = ag.round2(avg);
pms["channels"]["1"][json_prop_pm01Sp] = ag.round2(_pm_01_sp[0].update.avg);
pms["channels"]["2"][json_prop_pm01Sp] = ag.round2(_pm_01_sp[1].update.avg);
pms[json_prop_pm01Sp] = ag->round2(avg);
pms["channels"]["1"][json_prop_pm01Sp] = ag->round2(_pm_01_sp[0].update.avg);
pms["channels"]["2"][json_prop_pm01Sp] = ag->round2(_pm_01_sp[1].update.avg);
} else if (utils::isValidPm(_pm_01_sp[0].update.avg)) {
pms[json_prop_pm01Sp] = ag.round2(_pm_01_sp[0].update.avg);
pms["channels"]["1"][json_prop_pm01Sp] = ag.round2(_pm_01_sp[0].update.avg);
pms[json_prop_pm01Sp] = ag->round2(_pm_01_sp[0].update.avg);
pms["channels"]["1"][json_prop_pm01Sp] = ag->round2(_pm_01_sp[0].update.avg);
} else if (utils::isValidPm(_pm_01_sp[1].update.avg)) {
pms[json_prop_pm01Sp] = ag.round2(_pm_01_sp[1].update.avg);
pms["channels"]["2"][json_prop_pm01Sp] = ag.round2(_pm_01_sp[1].update.avg);
pms[json_prop_pm01Sp] = ag->round2(_pm_01_sp[1].update.avg);
pms["channels"]["2"][json_prop_pm01Sp] = ag->round2(_pm_01_sp[1].update.avg);
}
/// PM2.5 standard particle
if (utils::isValidPm(_pm_25_sp[0].update.avg) && utils::isValidPm(_pm_25_sp[1].update.avg)) {
float avg = (_pm_25_sp[0].update.avg + _pm_25_sp[1].update.avg) / 2.0f;
pms[json_prop_pm25Sp] = ag.round2(avg);
pms["channels"]["1"][json_prop_pm25Sp] = ag.round2(_pm_25_sp[0].update.avg);
pms["channels"]["2"][json_prop_pm25Sp] = ag.round2(_pm_25_sp[1].update.avg);
pms[json_prop_pm25Sp] = ag->round2(avg);
pms["channels"]["1"][json_prop_pm25Sp] = ag->round2(_pm_25_sp[0].update.avg);
pms["channels"]["2"][json_prop_pm25Sp] = ag->round2(_pm_25_sp[1].update.avg);
} else if (utils::isValidPm(_pm_25_sp[0].update.avg)) {
pms[json_prop_pm25Sp] = ag.round2(_pm_25_sp[0].update.avg);
pms["channels"]["1"][json_prop_pm25Sp] = ag.round2(_pm_25_sp[0].update.avg);
pms[json_prop_pm25Sp] = ag->round2(_pm_25_sp[0].update.avg);
pms["channels"]["1"][json_prop_pm25Sp] = ag->round2(_pm_25_sp[0].update.avg);
} else if (utils::isValidPm(_pm_25_sp[1].update.avg)) {
pms[json_prop_pm25Sp] = ag.round2(_pm_25_sp[1].update.avg);
pms["channels"]["2"][json_prop_pm25Sp] = ag.round2(_pm_25_sp[1].update.avg);
pms[json_prop_pm25Sp] = ag->round2(_pm_25_sp[1].update.avg);
pms["channels"]["2"][json_prop_pm25Sp] = ag->round2(_pm_25_sp[1].update.avg);
}
/// PM10 standard particle
if (utils::isValidPm(_pm_10_sp[0].update.avg) && utils::isValidPm(_pm_10_sp[1].update.avg)) {
float avg = (_pm_10_sp[0].update.avg + _pm_10_sp[1].update.avg) / 2.0f;
pms[json_prop_pm10Sp] = ag.round2(avg);
pms["channels"]["1"][json_prop_pm10Sp] = ag.round2(_pm_10_sp[0].update.avg);
pms["channels"]["2"][json_prop_pm10Sp] = ag.round2(_pm_10_sp[1].update.avg);
pms[json_prop_pm10Sp] = ag->round2(avg);
pms["channels"]["1"][json_prop_pm10Sp] = ag->round2(_pm_10_sp[0].update.avg);
pms["channels"]["2"][json_prop_pm10Sp] = ag->round2(_pm_10_sp[1].update.avg);
} else if (utils::isValidPm(_pm_10_sp[0].update.avg)) {
pms[json_prop_pm10Sp] = ag.round2(_pm_10_sp[0].update.avg);
pms["channels"]["1"][json_prop_pm10Sp] = ag.round2(_pm_10_sp[0].update.avg);
pms[json_prop_pm10Sp] = ag->round2(_pm_10_sp[0].update.avg);
pms["channels"]["1"][json_prop_pm10Sp] = ag->round2(_pm_10_sp[0].update.avg);
} else if (utils::isValidPm(_pm_10_sp[1].update.avg)) {
pms[json_prop_pm10Sp] = ag.round2(_pm_10_sp[1].update.avg);
pms["channels"]["2"][json_prop_pm10Sp] = ag.round2(_pm_10_sp[1].update.avg);
pms[json_prop_pm10Sp] = ag->round2(_pm_10_sp[1].update.avg);
pms["channels"]["2"][json_prop_pm10Sp] = ag->round2(_pm_10_sp[1].update.avg);
}
/// PM003 particle count
if (utils::isValidPm03Count(_pm_03_pc[0].update.avg) &&
utils::isValidPm03Count(_pm_03_pc[1].update.avg)) {
float avg = (_pm_03_pc[0].update.avg + _pm_03_pc[1].update.avg) / 2.0f;
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);
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::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);
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::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);
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);
}
/// PM0.5 particle count
if (utils::isValidPm03Count(_pm_05_pc[0].update.avg) &&
utils::isValidPm03Count(_pm_05_pc[1].update.avg)) {
float avg = (_pm_05_pc[0].update.avg + _pm_05_pc[1].update.avg) / 2.0f;
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);
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::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);
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::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);
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);
}
/// PM1.0 particle count
if (utils::isValidPm03Count(_pm_01_pc[0].update.avg) &&
utils::isValidPm03Count(_pm_01_pc[1].update.avg)) {
float avg = (_pm_01_pc[0].update.avg + _pm_01_pc[1].update.avg) / 2.0f;
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);
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::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);
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::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);
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);
}
/// PM2.5 particle count
if (utils::isValidPm03Count(_pm_25_pc[0].update.avg) &&
utils::isValidPm03Count(_pm_25_pc[1].update.avg)) {
float avg = (_pm_25_pc[0].update.avg + _pm_25_pc[1].update.avg) / 2.0f;
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);
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::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);
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::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);
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);
}
// NOTE: No need for particle count 5.0 and 10. When allCh is true, basically monitor using
@ -957,40 +1065,40 @@ JSONVar Measurements::buildPMS(AirGradient &ag, int ch, bool allCh, bool withTem
utils::isValidTemperature(_temperature[1].update.avg)) {
float temperature = (_temperature[0].update.avg + _temperature[1].update.avg) / 2.0f;
pms[json_prop_temp] = ag.round2(temperature);
pms["channels"]["1"][json_prop_temp] = ag.round2(_temperature[0].update.avg);
pms["channels"]["2"][json_prop_temp] = ag.round2(_temperature[1].update.avg);
pms[json_prop_temp] = ag->round2(temperature);
pms["channels"]["1"][json_prop_temp] = ag->round2(_temperature[0].update.avg);
pms["channels"]["2"][json_prop_temp] = ag->round2(_temperature[1].update.avg);
if (compensate) {
// Compensate both temperature channel
float temp = ag.pms5003t_1.compensateTemp(temperature);
float temp1 = ag.pms5003t_1.compensateTemp(_temperature[0].update.avg);
float temp2 = ag.pms5003t_2.compensateTemp(_temperature[1].update.avg);
pms[json_prop_tempCompensated] = ag.round2(temp);
pms["channels"]["1"][json_prop_tempCompensated] = ag.round2(temp1);
pms["channels"]["2"][json_prop_tempCompensated] = ag.round2(temp2);
float temp1 = getCorrectedTempHum(Temperature, 1, true);
float temp2 = getCorrectedTempHum(Temperature, 2, true);
float tempAverage = (temp1 + temp2) / 2.0f;
pms[json_prop_tempCompensated] = ag->round2(tempAverage);
pms["channels"]["1"][json_prop_tempCompensated] = ag->round2(temp1);
pms["channels"]["2"][json_prop_tempCompensated] = ag->round2(temp2);
}
} else if (utils::isValidTemperature(_temperature[0].update.avg)) {
pms[json_prop_temp] = ag.round2(_temperature[0].update.avg);
pms["channels"]["1"][json_prop_temp] = ag.round2(_temperature[0].update.avg);
pms[json_prop_temp] = ag->round2(_temperature[0].update.avg);
pms["channels"]["1"][json_prop_temp] = ag->round2(_temperature[0].update.avg);
if (compensate) {
// Compensate channel 1
float temp1 = ag.pms5003t_1.compensateTemp(_temperature[0].update.avg);
pms[json_prop_tempCompensated] = ag.round2(temp1);
pms["channels"]["1"][json_prop_tempCompensated] = ag.round2(temp1);
float temp1 = getCorrectedTempHum(Temperature, 1, true);
pms[json_prop_tempCompensated] = ag->round2(temp1);
pms["channels"]["1"][json_prop_tempCompensated] = ag->round2(temp1);
}
} else if (utils::isValidTemperature(_temperature[1].update.avg)) {
pms[json_prop_temp] = ag.round2(_temperature[1].update.avg);
pms["channels"]["2"][json_prop_temp] = ag.round2(_temperature[1].update.avg);
pms[json_prop_temp] = ag->round2(_temperature[1].update.avg);
pms["channels"]["2"][json_prop_temp] = ag->round2(_temperature[1].update.avg);
if (compensate) {
// Compensate channel 2
float temp2 = ag.pms5003t_2.compensateTemp(_temperature[1].update.avg);
pms[json_prop_tempCompensated] = ag.round2(temp2);
pms["channels"]["2"][json_prop_tempCompensated] = ag.round2(temp2);
float temp2 = getCorrectedTempHum(Temperature, 2, true);
pms[json_prop_tempCompensated] = ag->round2(temp2);
pms["channels"]["2"][json_prop_tempCompensated] = ag->round2(temp2);
}
}
@ -998,40 +1106,40 @@ JSONVar Measurements::buildPMS(AirGradient &ag, int ch, bool allCh, bool withTem
if (utils::isValidHumidity(_humidity[0].update.avg) &&
utils::isValidHumidity(_humidity[1].update.avg)) {
float humidity = (_humidity[0].update.avg + _humidity[1].update.avg) / 2.0f;
pms[json_prop_rhum] = ag.round2(humidity);
pms["channels"]["1"][json_prop_rhum] = ag.round2(_humidity[0].update.avg);
pms["channels"]["2"][json_prop_rhum] = ag.round2(_humidity[1].update.avg);
pms[json_prop_rhum] = ag->round2(humidity);
pms["channels"]["1"][json_prop_rhum] = ag->round2(_humidity[0].update.avg);
pms["channels"]["2"][json_prop_rhum] = ag->round2(_humidity[1].update.avg);
if (compensate) {
// Compensate both humidity channel
float hum = ag.pms5003t_1.compensateHum(humidity);
float hum1 = ag.pms5003t_1.compensateHum(_humidity[0].update.avg);
float hum2 = ag.pms5003t_2.compensateHum(_humidity[1].update.avg);
pms[json_prop_rhumCompensated] = ag.round2(hum);
pms["channels"]["1"][json_prop_rhumCompensated] = ag.round2(hum1);
pms["channels"]["2"][json_prop_rhumCompensated] = ag.round2(hum2);
float hum1 = getCorrectedTempHum(Humidity, 1, true);
float hum2 = getCorrectedTempHum(Humidity, 2, true);
float humAverage = (hum1 + hum2) / 2.0f;
pms[json_prop_rhumCompensated] = ag->round2(humAverage);
pms["channels"]["1"][json_prop_rhumCompensated] = ag->round2(hum1);
pms["channels"]["2"][json_prop_rhumCompensated] = ag->round2(hum2);
}
} else if (utils::isValidHumidity(_humidity[0].update.avg)) {
pms[json_prop_rhum] = ag.round2(_humidity[0].update.avg);
pms["channels"]["1"][json_prop_rhum] = ag.round2(_humidity[0].update.avg);
pms[json_prop_rhum] = ag->round2(_humidity[0].update.avg);
pms["channels"]["1"][json_prop_rhum] = ag->round2(_humidity[0].update.avg);
if (compensate) {
// Compensate humidity channel 1
float hum1 = ag.pms5003t_1.compensateHum(_humidity[0].update.avg);
pms[json_prop_rhumCompensated] = ag.round2(hum1);
pms["channels"]["1"][json_prop_rhumCompensated] = ag.round2(hum1);
float hum1 = getCorrectedTempHum(Humidity, 1, true);
pms[json_prop_rhumCompensated] = ag->round2(hum1);
pms["channels"]["1"][json_prop_rhumCompensated] = ag->round2(hum1);
}
} else if (utils::isValidHumidity(_humidity[1].update.avg)) {
pms[json_prop_rhum] = ag.round2(_humidity[1].update.avg);
pms["channels"]["2"][json_prop_rhum] = ag.round2(_humidity[1].update.avg);
pms[json_prop_rhum] = ag->round2(_humidity[1].update.avg);
pms["channels"]["2"][json_prop_rhum] = ag->round2(_humidity[1].update.avg);
if (compensate) {
// Compensate humidity channel 2
float hum2 = ag.pms5003t_2.compensateHum(_humidity[1].update.avg);
pms[json_prop_rhumCompensated] = ag.round2(hum2);
pms["channels"]["2"][json_prop_rhumCompensated] = ag.round2(hum2);
float hum2 = getCorrectedTempHum(Humidity, 2, true);
pms[json_prop_rhumCompensated] = ag->round2(hum2);
pms["channels"]["2"][json_prop_rhumCompensated] = ag->round2(hum2);
}
}
@ -1042,22 +1150,22 @@ JSONVar Measurements::buildPMS(AirGradient &ag, int ch, bool allCh, bool withTem
float pm25_comp2 = utils::getInvalidPmValue();
if (utils::isValidPm(_pm_25[0].update.avg) &&
utils::isValidHumidity(_humidity[0].update.avg)) {
pm25_comp1 = ag.pms5003t_1.compensate(_pm_25[0].update.avg, _humidity[0].update.avg);
pms["channels"]["1"][json_prop_pm25Compensated] = ag.round2(pm25_comp1);
pm25_comp1 = getCorrectedPM25(true, 1, true);
pms["channels"]["1"][json_prop_pm25Compensated] = ag->round2(pm25_comp1);
}
if (utils::isValidPm(_pm_25[1].update.avg) &&
utils::isValidHumidity(_humidity[1].update.avg)) {
pm25_comp2 = ag.pms5003t_2.compensate(_pm_25[1].update.avg, _humidity[1].update.avg);
pms["channels"]["2"][json_prop_pm25Compensated] = ag.round2(pm25_comp2);
pm25_comp2 = getCorrectedPM25(true, 2, true);
pms["channels"]["2"][json_prop_pm25Compensated] = ag->round2(pm25_comp2);
}
/// Get average or one of the channel compensated value if only one channel is valid
if (utils::isValidPm(pm25_comp1) && utils::isValidPm(pm25_comp2)) {
pms[json_prop_pm25Compensated] = ag.round2((pm25_comp1 + pm25_comp2) / 2.0f);
pms[json_prop_pm25Compensated] = ag->round2((pm25_comp1 + pm25_comp2) / 2.0f);
} else if (utils::isValidPm(pm25_comp1)) {
pms[json_prop_pm25Compensated] = ag.round2(pm25_comp1);
pms[json_prop_pm25Compensated] = ag->round2(pm25_comp1);
} else if (utils::isValidPm(pm25_comp2)) {
pms[json_prop_pm25Compensated] = ag.round2(pm25_comp2);
pms[json_prop_pm25Compensated] = ag->round2(pm25_comp2);
}
}
}
@ -1065,4 +1173,50 @@ JSONVar Measurements::buildPMS(AirGradient &ag, int ch, bool allCh, bool withTem
return pms;
}
void Measurements::setDebug(bool debug) { _debug = debug; }
void Measurements::setDebug(bool debug) { _debug = debug; }
int Measurements::bootCount() { return _bootCount; }
void Measurements::setBootCount(int bootCount) { _bootCount = bootCount; }
#ifndef ESP8266
void Measurements::setResetReason(esp_reset_reason_t reason) {
switch (reason) {
case ESP_RST_UNKNOWN:
Serial.println("Reset reason: ESP_RST_UNKNOWN");
break;
case ESP_RST_POWERON:
Serial.println("Reset reason: ESP_RST_POWERON");
break;
case ESP_RST_EXT:
Serial.println("Reset reason: ESP_RST_EXT");
break;
case ESP_RST_SW:
Serial.println("Reset reason: ESP_RST_SW");
break;
case ESP_RST_PANIC:
Serial.println("Reset reason: ESP_RST_PANIC");
break;
case ESP_RST_INT_WDT:
Serial.println("Reset reason: ESP_RST_INT_WDT");
break;
case ESP_RST_TASK_WDT:
Serial.println("Reset reason: ESP_RST_TASK_WDT");
break;
case ESP_RST_WDT:
Serial.println("Reset reason: ESP_RST_WDT");
break;
case ESP_RST_BROWNOUT:
Serial.println("Reset reason: ESP_RST_BROWNOUT");
break;
case ESP_RST_SDIO:
Serial.println("Reset reason: ESP_RST_SDIO");
break;
default:
Serial.println("Reset reason: unknown");
break;
}
_resetReason = (int)reason;
}
#endif

View File

@ -34,9 +34,11 @@ private:
};
public:
Measurements() {}
Measurements(Configuration &config);
~Measurements() {}
void setAirGradient(AirGradient *ag);
// Enumeration for every AG measurements
enum MeasurementType {
Temperature,
@ -123,34 +125,51 @@ public:
*/
float getAverage(MeasurementType type, int ch = 1);
/**
* @brief Get Temperature or Humidity correction value
* Only if correction is applied from configuration or forceCorrection is True
*
* @param type measurement type either Temperature or Humidity
* @param ch target type value channel
* @param forceCorrection force using correction even though config correction is not applied, but
* not for CUSTOM
* @return correction value
*/
float getCorrectedTempHum(MeasurementType type, int ch = 1, bool forceCorrection = false);
/**
* @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
* @param ch MeasurementType channel
* @param forceCorrection force using correction even though config correction is not applied, default to EPA
* @return float Corrected PM2.5 value
*/
float getCorrectedPM25(AirGradient &ag, Configuration &config, bool useAvg = false, int ch = 1);
float getCorrectedPM25(bool useAvg = false, int ch = 1, bool forceCorrection = false);
/**
* build json payload for every measurements
*/
String toString(bool localServer, AgFirmwareMode fwMode, int rssi, AirGradient &ag,
Configuration &config);
String toString(bool localServer, AgFirmwareMode fwMode, int rssi);
/**
* Set to true if want to debug every update value
*/
void setDebug(bool debug);
// TODO: update this to use setter
int bootCount;
int bootCount();
void setBootCount(int bootCount);
#ifndef ESP8266
void setResetReason(esp_reset_reason_t reason);
#endif
private:
Configuration &config;
AirGradient *ag;
// Some declared as an array (channel), because FW_MODE_O_1PPx has two PMS5003T
FloatValue _temperature[2];
FloatValue _humidity[2];
@ -171,7 +190,8 @@ private:
IntegerValue _pm_25_pc[2]; // particle count 2.5
IntegerValue _pm_5_pc[2]; // particle count 5.0
IntegerValue _pm_10_pc[2]; // particle count 10
int _bootCount;
int _resetReason;
bool _debug = false;
/**
@ -208,10 +228,9 @@ private:
*/
void validateChannel(int ch);
JSONVar buildOutdoor(bool localServer, AgFirmwareMode fwMode, AirGradient &ag,
Configuration &config);
JSONVar buildIndoor(bool localServer, AirGradient &ag, Configuration &config);
JSONVar buildPMS(AirGradient &ag, int ch, bool allCh, bool withTempHum, bool compensate);
JSONVar buildOutdoor(bool localServer, AgFirmwareMode fwMode);
JSONVar buildIndoor(bool localServer);
JSONVar buildPMS(int ch, bool allCh, bool withTempHum, bool compensate);
};
#endif /** _AG_VALUE_H_ */

View File

@ -81,16 +81,15 @@ bool WifiConnector::connect(void) {
// ssid = "AG-" + String(ESP.getChipId(), HEX);
WIFI()->setConfigPortalTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
WiFiManagerParameter postToAg("chbPostToAg",
"Prevent Connection to AirGradient Server", "T",
2, "type=\"checkbox\" ", WFM_LABEL_AFTER);
WIFI()->addParameter(&postToAg);
WiFiManagerParameter postToAgInfo(
WiFiManagerParameter disableCloud("chbPostToAg", "Prevent Connection to AirGradient Server", "T",
2, "type=\"checkbox\" ", WFM_LABEL_AFTER);
WIFI()->addParameter(&disableCloud);
WiFiManagerParameter disableCloudInfo(
"<p>Prevent connection to the AirGradient Server. Important: Only enable "
"it if you are sure you don't want to use any AirGradient cloud "
"features. As a result you will not receive automatic firmware updates "
"and your data will not reach the AirGradient dashboard.</p>");
WIFI()->addParameter(&postToAgInfo);
"features. As a result you will not receive automatic firmware updates, "
"configuration settings from cloud and the measure data will not reach the AirGradient dashboard.</p>");
WIFI()->addParameter(&disableCloudInfo);
WIFI()->autoConnect(ssid.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT);
@ -174,12 +173,11 @@ bool WifiConnector::connect(void) {
logInfo("WiFi Connected: " + WiFi.SSID() + " IP: " + localIpStr());
if (hasPortalConfig) {
String result = String(postToAg.getValue());
logInfo("Setting postToAirGradient set from " +
String(config.isPostDataToAirGradient() ? "True" : "False") +
String(" to ") + String(result != "T" ? "True" : "False") +
String(" successful"));
config.setPostToAirGradient(result != "T");
String result = String(disableCloud.getValue());
logInfo("Setting disableCloudConnection set from " +
String(config.isCloudConnectionDisabled() ? "True" : "False") + String(" to ") +
String(result == "T" ? "True" : "False") + String(" successful"));
config.setDisableCloudConnection(result == "T");
}
hasPortalConfig = false;
}

View File

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

View File

@ -95,26 +95,29 @@ enum ConfigurationControl {
};
enum PMCorrectionAlgorithm {
Unknown, // Unknown algorithm
None, // No PM correction
EPA_2021,
SLR_PMS5003_20220802,
SLR_PMS5003_20220803,
SLR_PMS5003_20220824,
SLR_PMS5003_20231030,
SLR_PMS5003_20231218,
SLR_PMS5003_20240104,
COR_ALGO_PM_UNKNOWN, // Unknown algorithm
COR_ALGO_PM_NONE, // No PM correction
COR_ALGO_PM_EPA_2021,
COR_ALGO_PM_SLR_CUSTOM,
};
// Don't change the order of the enum
enum TempHumCorrectionAlgorithm {
COR_ALGO_TEMP_HUM_UNKNOWN, // Unknown algorithm
COR_ALGO_TEMP_HUM_NONE, // No PM correction
COR_ALGO_TEMP_HUM_AG_PMS5003T_2024,
COR_ALGO_TEMP_HUM_SLR_CUSTOM
};
enum AgFirmwareMode {
FW_MODE_I_9PSL, /** ONE_INDOOR */
FW_MODE_O_1PST, /** PMS5003T, S8 and SGP41 */
FW_MODE_O_1PPT, /** PMS5003T_1, PMS5003T_2, SGP41 */
FW_MODE_O_1PP, /** PMS5003T_1, PMS5003T_2 */
FW_MODE_O_1PS, /** PMS5003T, S8 */
FW_MODE_O_1P, /** PMS5003T */
FW_MODE_I_42PS, /** DIY_PRO 4.2 */
FW_MODE_I_33PS, /** DIY_PRO 3.3 */
FW_MODE_I_9PSL, /** ONE_INDOOR */
FW_MODE_O_1PST, /** PMS5003T, S8 and SGP41 */
FW_MODE_O_1PPT, /** PMS5003T_1, PMS5003T_2, SGP41 */
FW_MODE_O_1PP, /** PMS5003T_1, PMS5003T_2 */
FW_MODE_O_1PS, /** PMS5003T, S8 */
FW_MODE_O_1P, /** PMS5003T */
FW_MODE_I_42PS, /** DIY_PRO 4.2 */
FW_MODE_I_33PS, /** DIY_PRO 3.3 */
FW_MODE_I_BASIC_40PS, /** DIY_BASIC 4.0 */
};
const char *AgFirmwareModeName(AgFirmwareMode mode);

171
src/OtaHandler.cpp Normal file
View File

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

43
src/OtaHandler.h Normal file
View File

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