Compare commits

..

37 Commits

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

BIN
docs/epoch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

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,18 +154,15 @@ 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**
- `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.
#### Corrections
### 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.
The `corrections` object allows configuring PM2.5 correction algorithms and parameters locally. This affects both the display and local server response values.
Example correction configuration:
@ -179,29 +176,11 @@ 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 |
@ -235,23 +214,3 @@ 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

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

View File

@ -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(configuration);
static Measurements measurements;
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
@ -124,7 +124,6 @@ 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");
@ -150,12 +149,9 @@ void setup() {
initMqtt();
sendDataToAg();
if (configuration.getConfigurationControl() !=
ConfigurationControl::ConfigurationControlLocal) {
apiClient.fetchServerConfiguration();
}
apiClient.fetchServerConfiguration();
configSchedule.update();
if (apiClient.isFetchConfigurationFailed()) {
if (apiClient.isFetchConfigureFailed()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
@ -320,7 +316,7 @@ static void mqttHandle(void) {
}
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(), ag, configuration);
String topic = "airgradient/readings/" + ag.deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
@ -335,7 +331,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);
@ -419,14 +415,6 @@ 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();
}
@ -484,7 +472,7 @@ static void appDispHandler(void) {
if (configuration.isOfflineMode() == false) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigurationFailed()) {
} else if (apiClient.isFetchConfigureFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
@ -530,24 +518,19 @@ static void updatePm(void) {
static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */
int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
measurements.bootCount++;
if (configuration.isOfflineMode() || !configuration.isPostDataToAirGradient()) {
Serial.println("Skipping transmission of data to AG server. Either mode is offline "
"or post data to server disabled");
/** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false ||
configuration.isOfflineMode()) {
return;
}
if (wifiConnector.isConnected() == false) {
Serial.println("WiFi not connected, skipping data transmission to AG server");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(), ag, configuration);
if (apiClient.postToServer(syncData)) {
Serial.println();
Serial.println("Online mode and isPostToAirGradient = true");
Serial.println(
"Online mode and isPostToAirGradient = true: watchdog reset");
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());
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
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.isFetchConfigurationFailed() ? "0" : "1");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "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 rhumCompensated = utils::getInvalidHumidity();
int ahumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
@ -76,12 +76,12 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature);
_hum = measure.getFloat(Measurements::Humidity);
atmpCompensated = _temp;
rhumCompensated = _hum;
ahumCompensated = _hum;
}
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(false, 1);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
@ -191,12 +191,12 @@ String OpenMetrics::getPayload(void) {
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(rhumCompensated)) {
if (utils::isValidHumidity(ahumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the "
"AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(rhumCompensated));
add_metric_point("", String(ahumCompensated));
}
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(configuration);
static Measurements measurements;
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
@ -124,7 +124,6 @@ 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");
@ -150,12 +149,9 @@ void setup() {
initMqtt();
sendDataToAg();
if (configuration.getConfigurationControl() !=
ConfigurationControl::ConfigurationControlLocal) {
apiClient.fetchServerConfiguration();
}
apiClient.fetchServerConfiguration();
configSchedule.update();
if (apiClient.isFetchConfigurationFailed()) {
if (apiClient.isFetchConfigureFailed()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
@ -377,7 +373,7 @@ static void mqttHandle(void) {
}
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(), ag, configuration);
String topic = "airgradient/readings/" + ag.deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
@ -392,7 +388,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);
@ -471,14 +467,6 @@ 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();
}
@ -536,7 +524,7 @@ static void appDispHandler(void) {
if (configuration.isOfflineMode() == false) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigurationFailed()) {
} else if (apiClient.isFetchConfigureFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
@ -582,24 +570,19 @@ static void updatePm(void) {
static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */
int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
measurements.bootCount++;
if (configuration.isOfflineMode() || !configuration.isPostDataToAirGradient()) {
Serial.println("Skipping transmission of data to AG server. Either mode is offline "
"or post data to server disabled");
/** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false ||
configuration.isOfflineMode()) {
return;
}
if (wifiConnector.isConnected() == false) {
Serial.println("WiFi not connected, skipping data transmission to AG server");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(), ag, configuration);
if (apiClient.postToServer(syncData)) {
Serial.println();
Serial.println("Online mode and isPostToAirGradient = true");
Serial.println(
"Online mode and isPostToAirGradient = true: watchdog reset");
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());
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
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.isFetchConfigurationFailed() ? "0" : "1");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "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 rhumCompensated = utils::getInvalidHumidity();
int ahumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
@ -76,12 +76,12 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature);
_hum = measure.getFloat(Measurements::Humidity);
atmpCompensated = _temp;
rhumCompensated = _hum;
ahumCompensated = _hum;
}
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(false, 1);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
@ -192,12 +192,12 @@ String OpenMetrics::getPayload(void) {
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(rhumCompensated)) {
if (utils::isValidHumidity(ahumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the "
"AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(rhumCompensated));
add_metric_point("", String(ahumCompensated));
}
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(configuration);
static Measurements measurements;
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
@ -125,7 +125,6 @@ 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");
@ -177,12 +176,9 @@ void setup() {
initMqtt();
sendDataToAg();
if (configuration.getConfigurationControl() !=
ConfigurationControl::ConfigurationControlLocal) {
apiClient.fetchServerConfiguration();
}
apiClient.fetchServerConfiguration();
configSchedule.update();
if (apiClient.isFetchConfigurationFailed()) {
if (apiClient.isFetchConfigureFailed()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
@ -400,7 +396,7 @@ static void mqttHandle(void) {
}
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(), ag, configuration);
String topic = "airgradient/readings/" + ag.deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
@ -415,7 +411,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);
@ -511,14 +507,6 @@ 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();
}
@ -576,7 +564,7 @@ static void appDispHandler(void) {
if (configuration.isOfflineMode() == false) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigurationFailed()) {
} else if (apiClient.isFetchConfigureFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
@ -623,24 +611,19 @@ static void updatePm(void) {
static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */
int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
measurements.bootCount++;
if (configuration.isOfflineMode() || !configuration.isPostDataToAirGradient()) {
Serial.println("Skipping transmission of data to AG server. Either mode is offline "
"or post data to server disabled");
/** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false ||
configuration.isOfflineMode()) {
return;
}
if (wifiConnector.isConnected() == false) {
Serial.println("WiFi not connected, skipping data transmission to AG server");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(), ag, configuration);
if (apiClient.postToServer(syncData)) {
Serial.println();
Serial.println("Online mode and isPostToAirGradient = true");
Serial.println(
"Online mode and isPostToAirGradient = true: watchdog reset");
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());
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
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.isFetchConfigurationFailed() ? "0" : "1");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "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 rhumCompensated = utils::getInvalidHumidity();
int ahumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
@ -76,12 +76,12 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature);
_hum = measure.getFloat(Measurements::Humidity);
atmpCompensated = _temp;
rhumCompensated = _hum;
ahumCompensated = _hum;
}
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(false, 1);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
@ -191,12 +191,12 @@ String OpenMetrics::getPayload(void) {
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(rhumCompensated)) {
if (utils::isValidHumidity(ahumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the "
"AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(rhumCompensated));
add_metric_point("", String(ahumCompensated));
}
response += "# EOF\n";

View File

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

View File

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

View File

@ -36,21 +36,20 @@ 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>
@ -76,7 +75,7 @@ static MqttClient mqttClient(Serial);
static TaskHandle_t mqttTask = NULL;
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static Measurements measurements(configuration);
static Measurements measurements;
static AirGradient *ag;
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
@ -94,9 +93,9 @@ static AgFirmwareMode fwMode = FW_MODE_I_9PSL;
static bool ledBarButtonTest = false;
static String fwNewVersion;
static bool isLocalServerInitialized = false;
static void boardInit(void);
static void initializeNetwork(void);
static void failedHandler(String msg);
static void configurationUpdateSchedule(void);
static void updateDisplayAndLedBar(void);
@ -112,35 +111,38 @@ static void factoryConfigReset(void);
static void wdgFeedUpdate(void);
static void ledBarEnabledUpdate(void);
static bool sgp41Init(void);
static void checkForFirmwareUpdate(void);
static void otaHandlerCallback(OtaHandler::OtaState state, String mesasge);
static void displayExecuteOta(OtaHandler::OtaState state, String msg, int processing);
static void firmwareCheckForUpdate(void);
static void otaHandlerCallback(OtaState state, String mesasge);
static void displayExecuteOta(OtaState state, String msg,
int processing);
static int calculateMaxPeriod(int updateInterval);
static void setMeasurementMaxPeriod();
static void offlineStorageUpdate();
AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, updateDisplayAndLedBar);
AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL,
configurationUpdateSchedule);
AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
// AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL,
// configurationUpdateSchedule);
// AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update);
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, updatePm);
AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate);
AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc);
AgSchedule watchdogFeedSchedule(60000, wdgFeedUpdate);
AgSchedule checkForUpdateSchedule(FIRMWARE_CHECK_FOR_UPDATE_MS, checkForFirmwareUpdate);
AgSchedule offlineStorage((2 * 60000), offlineStorageUpdate);
// AgSchedule checkForUpdateSchedule(FIRMWARE_CHECK_FOR_UPDATE_MS, firmwareCheckForUpdate);
void setup() {
/** Serial for print debug message */
Serial.begin(115200);
delay(100); /** For bester show log */
// Set timezone to UTC
setenv("TZ", "UTC", 1);
tzset();
/** Print device ID into log */
Serial.println("Serial nr: " + ag->deviceId());
// Set reason why esp is reset
esp_reset_reason_t reason = esp_reset_reason();
measurements.setResetReason(reason);
/** Initialize local configure */
configuration.begin();
@ -165,7 +167,6 @@ 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");
@ -175,71 +176,44 @@ void setup() {
setMeasurementMaxPeriod();
// Comment below line to disable debug measurement readings
measurements.setDebug(true);
measurements.setDebug(false);
/** Connecting wifi */
bool connectToWifi = false;
if (ag->isOne()) {
/** Show message confirm offline mode, should me perform if LED bar button
* test pressed */
if (ledBarButtonTest == false) {
oledDisplay.setText(
"Press now for",
configuration.isOfflineMode() ? "online mode" : "offline mode", "");
uint32_t startTime = millis();
while (true) {
if (ag->button.getState() == ag->button.BUTTON_PRESSED) {
configuration.setOfflineMode(!configuration.isOfflineMode());
oledDisplay.setText(
"Offline Mode",
configuration.isOfflineMode() ? " = True" : " = False", "");
delay(1000);
break;
}
uint32_t periodMs = (uint32_t)(millis() - startTime);
if (periodMs >= 3000) {
break;
}
}
connectToWifi = !configuration.isOfflineMode();
} else {
configuration.setOfflineModeWithoutSave(true);
}
} else {
connectToWifi = true;
}
// Initialize networking configuration
if (connectToWifi) {
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");
configuration.setOfflineModeWithoutSave(true);
}
// Force to offline mode
configuration.setOfflineMode(true);
/** Show display Warning up */
if (ag->isOne()) {
oledDisplay.setText("Warming Up", "Serial Number:", ag->deviceId().c_str());
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
Serial.println("Display brightness: " + String(configuration.getDisplayBrightness()));
oledDisplay.setBrightness(configuration.getDisplayBrightness());
}
// Reset post schedulers to make sure measurements value already available
agApiPostSchedule.update();
String deviceId = ag->deviceId();
// Connect to Wi-Fi network with SSID and password
Serial.print("Setting AP (Access Point)…");
// Remove the password parameter, if you want the AP (Access Point) to be open
WiFi.softAP("ag_" + deviceId, "cleanair");
IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(IP);
Serial.printf("SSID: ag_%s\n", deviceId.c_str());
oledDisplay.setText("", "Offline Storage Mode", "");
delay(3000);
// mdnsInit();
localServer.begin();
// Update display and led bar after finishing setup to show dashboard
updateDisplayAndLedBar();
}
void loop() {
/** Run schedulers */
/** Handle schedule */
dispLedSchedule.run();
configSchedule.run();
agApiPostSchedule.run();
watchdogFeedSchedule.run();
// configSchedule.run();
// agApiPostSchedule.run();
offlineStorage.run();
if (configuration.hasSensorS8) {
co2Schedule.run();
@ -273,17 +247,19 @@ void loop() {
}
}
/** Check for handle WiFi reconnect */
wifiConnector.handle();
watchdogFeedSchedule.run();
// /** Check for handle WiFi reconnect */
// wifiConnector.handle();
/** factory reset handle */
factoryConfigReset();
/** check that local configuration changed then do some action */
/** check that local configura changed then do some action */
configUpdateHandle();
/** Firmware check for update handle */
checkForUpdateSchedule.run();
// checkForUpdateSchedule.run();
}
static void co2Update(void) {
@ -329,7 +305,8 @@ static void createMqttTask(void) {
/** Send data */
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
String payload =
measurements.toString(true, fwMode, wifiConnector.RSSI(), *ag, configuration);
String topic = "airgradient/readings/" + ag->deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(),
@ -400,7 +377,7 @@ static void factoryConfigReset(void) {
WiFi.disconnect(true, true);
/** Reset local config */
configuration.reset();
// configuration.reset();
if (ag->isOne()) {
oledDisplay.setText("Factory reset", "successful", "");
@ -434,6 +411,8 @@ static void factoryConfigReset(void) {
static void wdgFeedUpdate(void) {
ag->watchdog.reset();
Serial.println("External watchdog feed!");
/** Log current free heap size */
Serial.printf("Free heap: %u\n", ESP.getFreeHeap());
}
static void ledBarEnabledUpdate(void) {
@ -464,47 +443,43 @@ static bool sgp41Init(void) {
return false;
}
static void checkForFirmwareUpdate(void) {
static void firmwareCheckForUpdate(void) {
Serial.println();
Serial.print("checkForFirmwareUpdate: ");
Serial.println("firmwareCheckForUpdate:");
if (configuration.isOfflineMode() || configuration.isCloudConnectionDisabled()) {
Serial.println("mode is offline or cloud connection disabled, ignored");
return;
if (wifiConnector.isConnected()) {
Serial.println("firmwareCheckForUpdate: Perform");
otaHandler.setHandlerCallback(otaHandlerCallback);
otaHandler.updateFirmwareIfOutdated(ag->deviceId());
} else {
Serial.println("firmwareCheckForUpdate: Ignored");
}
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(OtaHandler::OtaState state, String message) {
Serial.println("OTA message: " + message);
static void otaHandlerCallback(OtaState state, String mesasge) {
Serial.println("OTA message: " + mesasge);
switch (state) {
case OtaHandler::OTA_STATE_BEGIN:
case OtaState::OTA_STATE_BEGIN:
displayExecuteOta(state, fwNewVersion, 0);
break;
case OtaHandler::OTA_STATE_FAIL:
case OtaState::OTA_STATE_FAIL:
displayExecuteOta(state, "", 0);
break;
case OtaHandler::OTA_STATE_PROCESSING:
case OtaHandler::OTA_STATE_SUCCESS:
displayExecuteOta(state, "", message.toInt());
case OtaState::OTA_STATE_PROCESSING:
displayExecuteOta(state, "", mesasge.toInt());
break;
case OtaState::OTA_STATE_SUCCESS:
displayExecuteOta(state, "", mesasge.toInt());
break;
default:
break;
}
}
static void displayExecuteOta(OtaHandler::OtaState state, String msg, int processing) {
static void displayExecuteOta(OtaState state, String msg, int processing) {
switch (state) {
case OtaHandler::OTA_STATE_BEGIN: {
case OtaState::OTA_STATE_BEGIN: {
if (ag->isOne()) {
oledDisplay.showFirmwareUpdateVersion(msg);
} else {
@ -513,7 +488,7 @@ static void displayExecuteOta(OtaHandler::OtaState state, String msg, int proces
delay(2500);
break;
}
case OtaHandler::OTA_STATE_FAIL: {
case OtaState::OTA_STATE_FAIL: {
if (ag->isOne()) {
oledDisplay.showFirmwareUpdateFailed();
} else {
@ -523,7 +498,7 @@ static void displayExecuteOta(OtaHandler::OtaState state, String msg, int proces
delay(2500);
break;
}
case OtaHandler::OTA_STATE_SKIP: {
case OtaState::OTA_STATE_SKIP: {
if (ag->isOne()) {
oledDisplay.showFirmwareUpdateSkipped();
} else {
@ -533,7 +508,7 @@ static void displayExecuteOta(OtaHandler::OtaState state, String msg, int proces
delay(2500);
break;
}
case OtaHandler::OTA_STATE_UP_TO_DATE: {
case OtaState::OTA_STATE_UP_TO_DATE: {
if (ag->isOne()) {
oledDisplay.showFirmwareUpdateUpToDate();
} else {
@ -543,7 +518,7 @@ static void displayExecuteOta(OtaHandler::OtaState state, String msg, int proces
delay(2500);
break;
}
case OtaHandler::OTA_STATE_PROCESSING: {
case OtaState::OTA_STATE_PROCESSING: {
if (ag->isOne()) {
oledDisplay.showFirmwareUpdateProgress(processing);
} else {
@ -552,7 +527,7 @@ static void displayExecuteOta(OtaHandler::OtaState state, String msg, int proces
break;
}
case OtaHandler::OTA_STATE_SUCCESS: {
case OtaState::OTA_STATE_SUCCESS: {
int i = 6;
while(i != 0) {
i = i - 1;
@ -602,7 +577,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);
}
@ -613,7 +588,7 @@ static void sendDataToAg() {
}
stateMachine.handleLeds(AgStateMachineWiFiOkServerConnectFailed);
}
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
stateMachine.handleLeds(AgStateMachineNormal);
}
@ -628,6 +603,7 @@ static void oneIndoorInit(void) {
/** Display init */
oledDisplay.begin();
oledDisplay.setBrightness(40);
/** Show boot display */
Serial.println("Firmware Version: " + ag->getVersion());
@ -837,82 +813,7 @@ 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();
}
@ -1006,25 +907,14 @@ static void updateDisplayAndLedBar(void) {
if (configuration.isOfflineMode()) {
// Ignore network related status when in offline mode
stateMachine.displayHandle(AgStateMachineNormal);
stateMachine.handleLeds(AgStateMachineNormal);
return;
}
if (wifiConnector.isConnected() == false) {
stateMachine.displayHandle(AgStateMachineWiFiLost);
stateMachine.handleLeds(AgStateMachineWiFiLost);
return;
}
if (configuration.isCloudConnectionDisabled()) {
// Ignore API related check since cloud is disabled
stateMachine.displayHandle(AgStateMachineNormal);
stateMachine.handleLeds(AgStateMachineNormal);
// stateMachine.handleLeds(AgStateMachineNormal);
return;
}
AgStateMachineState state = AgStateMachineNormal;
if (apiClient.isFetchConfigurationFailed()) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigureFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
@ -1189,30 +1079,20 @@ static void updatePm(void) {
static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */
int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
measurements.bootCount++;
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");
/** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false || configuration.isOfflineMode()) {
return;
}
if (wifiConnector.isConnected() == false) {
Serial.println("WiFi not connected, skipping data transmission to AG server");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(), *ag, configuration);
if (apiClient.postToServer(syncData)) {
Serial.println();
Serial.println("Online mode and isPostToAirGradient = true");
Serial.println(
"Online mode and isPostToAirGradient = true: watchdog reset");
Serial.println();
}
/** Log current free heap size */
Serial.printf("Free heap: %u\n", ESP.getFreeHeap());
}
static void tempHumUpdate(void) {
@ -1283,3 +1163,12 @@ int calculateMaxPeriod(int updateInterval) {
// 0.8 is 80% reduced interval for max period
return (SERVER_SYNC_INTERVAL - (SERVER_SYNC_INTERVAL * 0.8)) / updateInterval;
}
void offlineStorageUpdate() {
if (measurements.saveLocalStorage(*ag, configuration)) {
oledDisplay.setText("", "New Measurements", "");
} else {
oledDisplay.setText("Failed write", "Measurements", "");
}
delay(1200);
}

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.isFetchConfigurationFailed() ? "0" : "1");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "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 rhumCompensated = utils::getInvalidHumidity();
int ahumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
@ -81,8 +81,8 @@ String OpenMetrics::getPayload(void) {
measure.getFloat(Measurements::Humidity, 2)) /
2.0f;
pm01 = (measure.get(Measurements::PM01, 1) + measure.get(Measurements::PM01, 2)) / 2.0f;
float correctedPm25_1 = measure.getCorrectedPM25(false, 1);
float correctedPm25_2 = measure.getCorrectedPM25(false, 2);
float correctedPm25_1 = measure.getCorrectedPM25(*ag, config, false, 1);
float correctedPm25_2 = measure.getCorrectedPM25(*ag, config, false, 2);
float correctedPm25 = (correctedPm25_1 + correctedPm25_2) / 2.0f;
pm25 = round(correctedPm25);
pm10 = (measure.get(Measurements::PM10, 1) + measure.get(Measurements::PM10, 2)) / 2.0f;
@ -97,7 +97,7 @@ String OpenMetrics::getPayload(void) {
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(false, 1);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
@ -107,7 +107,7 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature, 1);
_hum = measure.getFloat(Measurements::Humidity, 1);
pm01 = measure.get(Measurements::PM01, 1);
float correctedPm = measure.getCorrectedPM25(false, 1);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10, 1);
pm03PCount = measure.get(Measurements::PM03_PC, 1);
@ -116,7 +116,7 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature, 2);
_hum = measure.getFloat(Measurements::Humidity, 2);
pm01 = measure.get(Measurements::PM01, 2);
float correctedPm = measure.getCorrectedPM25(false, 2);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 2);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10, 2);
pm03PCount = measure.get(Measurements::PM03_PC, 2);
@ -137,15 +137,11 @@ String OpenMetrics::getPayload(void) {
/** Get temperature and humidity compensated */
if (ag->isOne()) {
atmpCompensated = round(measure.getCorrectedTempHum(Measurements::Temperature));
rhumCompensated = round(measure.getCorrectedTempHum(Measurements::Humidity));
atmpCompensated = _temp;
ahumCompensated = _hum;
} else {
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);
atmpCompensated = ag->pms5003t_1.compensateTemp(_temp);
ahumCompensated = ag->pms5003t_1.compensateHum(_hum);
}
// Add measurements that valid to the metrics
@ -238,11 +234,11 @@ String OpenMetrics::getPayload(void) {
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(rhumCompensated)) {
if (utils::isValidHumidity(ahumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(rhumCompensated));
add_metric_point("", String(ahumCompensated));
}
response += "# EOF\n";

View File

@ -0,0 +1,206 @@
#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.2.0-alpha
version=3.1.13
author=AirGradient <support@airgradient.com>
maintainer=AirGradient <support@airgradient.com>
sentence=ESP32-C3 / ESP8266 library for air quality monitor measuring PM, CO2, Temperature, TVOC and Humidity with OLED display.

View File

@ -34,6 +34,17 @@ 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";
@ -47,22 +58,10 @@ bool AgApiClient::fetchServerConfiguration(void) {
}
#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 (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;
}
client.setTimeout(timeoutMs);
if (client.begin(uri) == false) {
getConfigFailed = true;
return false;
}
#endif
@ -91,6 +90,8 @@ bool AgApiClient::fetchServerConfiguration(void) {
String respContent = client.getString();
client.end();
// logInfo("Get configuration: " + respContent);
/** Parse configuration and return result */
return config.parse(respContent, false);
}
@ -104,39 +105,32 @@ bool AgApiClient::fetchServerConfiguration(void) {
* @return false Failure
*/
bool AgApiClient::postToServer(String data) {
String uri = apiRoot + "/sensors/airgradient:" + ag->deviceId() + "/measures";
#ifdef ESP8266
HTTPClient client;
WiFiClient wifiClient;
if (client.begin(wifiClient, uri) == false) {
getConfigFailed = true;
if (config.isPostDataToAirGradient() == false) {
logWarning("Ignore post data to server");
return true;
}
if (WiFi.isConnected() == false) {
return false;
}
#else
String uri = apiRoot + "/sensors/airgradient:" + ag->deviceId() + "/measures";
// logInfo("Post uri: " + uri);
// logInfo("Post data: " + data);
WiFiClient wifiClient;
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;
}
client.setTimeout(timeoutMs);
if (client.begin(wifiClient, uri.c_str()) == false) {
logError("Init client failed");
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)) {
@ -155,12 +149,7 @@ bool AgApiClient::postToServer(String data) {
* @return true Success
* @return false Failure
*/
bool AgApiClient::isFetchConfigurationFailed(void) { return getConfigFailed; }
/**
* @brief Reset status of get configuration from AirGradient cloud
*/
void AgApiClient::resetFetchConfigurationStatus(void) { getConfigFailed = false; }
bool AgApiClient::isFetchConfigureFailed(void) { return getConfigFailed; }
/**
* @brief Get failed status when post data to AirGradient cloud
@ -200,10 +189,7 @@ bool AgApiClient::sendPing(int rssi, int bootCount) {
String AgApiClient::getApiRoot() const { return apiRoot; }
void AgApiClient::setApiRoot(const String &apiRoot) {
this->apiRootChanged = true;
this->apiRoot = apiRoot;
}
void AgApiClient::setApiRoot(const String &apiRoot) { this->apiRoot = apiRoot; }
/**
* @brief Set http request timeout. (Default: 10s)

View File

@ -20,18 +20,12 @@ 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 = 15000; // Default set to 15s
uint16_t timeoutMs = 10000; // Default set to 10s
public:
AgApiClient(Stream &stream, Configuration &config);
@ -40,8 +34,7 @@ public:
void begin(void);
bool fetchServerConfiguration(void);
bool postToServer(String data);
bool isFetchConfigurationFailed(void);
void resetFetchConfigurationStatus(void);
bool isFetchConfigureFailed(void);
bool isPostToServerFailed(void);
bool isNotAvailableOnDashboard(void);
void setAirGradient(AirGradient *ag);

View File

@ -22,17 +22,15 @@ const char *LED_BAR_MODE_NAMES[] = {
};
const char *PM_CORRECTION_ALGORITHM_NAMES[] = {
[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",
[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",
};
#define JSON_PROP_NAME(name) jprop_##name
@ -49,7 +47,6 @@ 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);
@ -57,8 +54,6 @@ 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"
@ -71,7 +66,6 @@ JSON_PROP_DEF(rhum);
#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
@ -110,8 +104,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 = COR_ALGO_PM_SLR_CUSTOM + 1; // Get the actual size of the enum
PMCorrectionAlgorithm result = COR_ALGO_PM_UNKNOWN;;
const size_t enumSize = SLR_PMS5003_20240104 + 1; // Get the actual size of the enum
PMCorrectionAlgorithm result = PMCorrectionAlgorithm::Unknown;
// Loop through enum values
for (size_t enumVal = 0; enumVal < enumSize; enumVal++) {
@ -120,63 +114,42 @@ 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")) {
logInfo("corrections not found");
// TODO: need to response message?
Serial.println("corrections not found");
return false;
}
JSONVar corrections = json["corrections"];
if (!corrections.hasOwnProperty("pm02")) {
logWarning("pm02 not found");
Serial.println("pm02 not found");
return false;
}
JSONVar pm02 = corrections["pm02"];
if (!pm02.hasOwnProperty("correctionAlgorithm")) {
logWarning("pm02 correctionAlgorithm not found");
Serial.println("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 == COR_ALGO_PM_UNKNOWN) {
logWarning("Unknown algorithm");
if (algo == Unknown) {
logInfo("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 == COR_ALGO_PM_NONE || algo == COR_ALGO_PM_EPA_2021) {
if (algo == None || algo == 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;
@ -193,7 +166,7 @@ bool Configuration::updatePmCorrection(JSONVar &json) {
// Check if pm02 has slr object
if (!pm02.hasOwnProperty("slr")) {
logWarning("slr not found");
Serial.println("slr not found");
return false;
}
@ -202,7 +175,7 @@ bool Configuration::updatePmCorrection(JSONVar &json) {
// Validate required slr properties exist
if (!slr.hasOwnProperty("intercept") || !slr.hasOwnProperty("scalingFactor") ||
!slr.hasOwnProperty("useEpa2021")) {
logWarning("Missing required slr properties");
Serial.println("Missing required slr properties");
return false;
}
@ -232,87 +205,6 @@ 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)
*
@ -361,7 +253,7 @@ void Configuration::loadConfig(void) {
}
file.close();
} else {
SPIFFS.format();
// SPIFFS.format();
}
#endif
toConfig(buf);
@ -380,7 +272,6 @@ 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;
@ -398,8 +289,8 @@ void Configuration::defaultConfig(void) {
jconfig[jprop_offlineMode] = jprop_offlineMode_default;
jconfig[jprop_monitorDisplayCompensatedValues] = jprop_monitorDisplayCompensatedValues_default;
// PM2.5 default correction
pmCorrection.algorithm = COR_ALGO_PM_NONE;
// PM2.5 correction
pmCorrection.algorithm = None;
pmCorrection.changed = false;
pmCorrection.intercept = 0;
pmCorrection.scalingFactor = 1;
@ -901,21 +792,11 @@ bool Configuration::parse(String data, bool isLocal) {
}
}
// PM2.5 Corrections
// 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();
@ -1155,20 +1036,20 @@ void Configuration::toConfig(const char *buf) {
}
bool changed = false;
bool isConfigFieldInvalid = false;
bool isInvalid = false;
/** Validate country */
if (JSON.typeof_(jconfig[jprop_country]) != "string") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
String country = jconfig[jprop_country];
if (country.length() != 2) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_country] = jprop_country_default;
changed = true;
logInfo("toConfig: country changed");
@ -1176,17 +1057,17 @@ void Configuration::toConfig(const char *buf) {
/** validate: PM standard */
if (JSON.typeof_(jconfig[jprop_pmStandard]) != "string") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
String standard = jconfig[jprop_pmStandard];
if (standard != getPMStandardString(true) &&
standard != getPMStandardString(false)) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_pmStandard] = jprop_pmStandard_default;
changed = true;
logInfo("toConfig: pmStandard changed");
@ -1194,18 +1075,18 @@ void Configuration::toConfig(const char *buf) {
/** validate led bar mode */
if (JSON.typeof_(jconfig[jprop_ledBarMode]) != "string") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
String mode = jconfig[jprop_ledBarMode];
if (mode != getLedBarModeName(LedBarMode::LedBarModeCO2) &&
mode != getLedBarModeName(LedBarMode::LedBarModeOff) &&
mode != getLedBarModeName(LedBarMode::LedBarModePm)) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_ledBarMode] = jprop_ledBarMode_default;
changed = true;
logInfo("toConfig: ledBarMode changed");
@ -1213,11 +1094,11 @@ void Configuration::toConfig(const char *buf) {
/** validate abcday */
if (JSON.typeof_(jconfig[jprop_abcDays]) != "number") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_abcDays] = jprop_abcDays_default;
changed = true;
logInfo("toConfig: abcDays changed");
@ -1225,16 +1106,16 @@ void Configuration::toConfig(const char *buf) {
/** validate tvoc learning offset */
if (JSON.typeof_(jconfig[jprop_tvocLearningOffset]) != "number") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
int value = jconfig[jprop_tvocLearningOffset];
if (value < 0) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_tvocLearningOffset] = jprop_tvocLearningOffset_default;
changed = true;
logInfo("toConfig: tvocLearningOffset changed");
@ -1242,16 +1123,16 @@ void Configuration::toConfig(const char *buf) {
/** validate nox learning offset */
if (JSON.typeof_(jconfig[jprop_noxLearningOffset]) != "number") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
int value = jconfig[jprop_noxLearningOffset];
if (value < 0) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_noxLearningOffset] = jprop_noxLearningOffset_default;
changed = true;
logInfo("toConfig: noxLearningOffset changed");
@ -1259,11 +1140,11 @@ void Configuration::toConfig(const char *buf) {
/** validate mqtt broker */
if (JSON.typeof_(jconfig[jprop_mqttBrokerUrl]) != "string") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
if (isConfigFieldInvalid) {
if (isInvalid) {
changed = true;
jconfig[jprop_mqttBrokerUrl] = jprop_mqttBrokerUrl_default;
logInfo("toConfig: mqttBroker changed");
@ -1271,36 +1152,24 @@ void Configuration::toConfig(const char *buf) {
/** Validate temperature unit */
if (JSON.typeof_(jconfig[jprop_temperatureUnit]) != "string") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
String unit = jconfig[jprop_temperatureUnit];
if (unit != "c" && unit != "f") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
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") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
String ctrl = jconfig[jprop_configurationControl];
if (ctrl != String(CONFIGURATION_CONTROL_NAME
@ -1309,12 +1178,12 @@ void Configuration::toConfig(const char *buf) {
[ConfigurationControl::ConfigurationControlLocal]) &&
ctrl != String(CONFIGURATION_CONTROL_NAME
[ConfigurationControl::ConfigurationControlCloud])) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_configurationControl] =jprop_configurationControl_default;
changed = true;
logInfo("toConfig: configurationControl changed");
@ -1322,11 +1191,11 @@ void Configuration::toConfig(const char *buf) {
/** Validate post to airgradient cloud */
if (JSON.typeof_(jconfig[jprop_postDataToAirGradient]) != "boolean") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_postDataToAirGradient] = jprop_postDataToAirGradient_default;
changed = true;
logInfo("toConfig: postToAirGradient changed");
@ -1334,16 +1203,16 @@ void Configuration::toConfig(const char *buf) {
/** validate led bar brightness */
if (JSON.typeof_(jconfig[jprop_ledBarBrightness]) != "number") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
int value = jconfig[jprop_ledBarBrightness];
if (value < 0 || value > 100) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_ledBarBrightness] = jprop_ledBarBrightness_default;
changed = true;
logInfo("toConfig: ledBarBrightness changed");
@ -1351,16 +1220,16 @@ void Configuration::toConfig(const char *buf) {
/** Validate display brightness */
if (JSON.typeof_(jconfig[jprop_displayBrightness]) != "number") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
int value = jconfig[jprop_displayBrightness];
if (value < 0 || value > 100) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_displayBrightness] = jprop_displayBrightness_default;
changed = true;
logInfo("toConfig: displayBrightness changed");
@ -1379,31 +1248,15 @@ void Configuration::toConfig(const char *buf) {
jprop_monitorDisplayCompensatedValues_default;
}
// PM2.5 correction
/// Set default first before parsing local config
pmCorrection.algorithm = COR_ALGO_PM_NONE;
// Set default first before parsing local config
pmCorrection.algorithm = PMCorrectionAlgorithm::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();
}
@ -1481,17 +1334,6 @@ 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;
@ -1527,16 +1369,14 @@ bool Configuration::isPMCorrectionChanged(void) {
*/
bool Configuration::isPMCorrectionEnabled(void) {
PMCorrection pmCorrection = getPMCorrection();
if (pmCorrection.algorithm == COR_ALGO_PM_NONE ||
pmCorrection.algorithm == COR_ALGO_PM_UNKNOWN) {
if (pmCorrection.algorithm == PMCorrectionAlgorithm::None ||
pmCorrection.algorithm == PMCorrectionAlgorithm::Unknown) {
return false;
}
return true;
}
Configuration::PMCorrection Configuration::getPMCorrection(void) { return pmCorrection; }
Configuration::TempHumCorrection Configuration::getTempCorrection(void) { return tempCorrection; }
Configuration::TempHumCorrection Configuration::getHumCorrection(void) { return rhumCorrection; }
Configuration::PMCorrection Configuration::getPMCorrection(void) {
return pmCorrection;
}

View File

@ -17,13 +17,6 @@ public:
bool changed;
};
struct TempHumCorrection {
TempHumCorrectionAlgorithm algorithm;
float intercept;
float scalingFactor;
bool changed;
};
private:
bool co2CalibrationRequested;
bool ledBarTestRequested;
@ -37,17 +30,12 @@ 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);
@ -58,7 +46,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);
@ -106,15 +94,11 @@ 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.getCorrectedTempHum(Measurements::Temperature, 1);
float temp = value.getAverage(Measurements::Temperature);
if (utils::isValidTemperature(temp)) {
float t = 0.0f;
if (config.isTemperatureUnitInF()) {
@ -44,7 +44,7 @@ void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) {
DISP()->drawUTF8(1, 10, buf);
/** Show humidity */
int rhum = round(value.getCorrectedTempHum(Measurements::Humidity, 1));
int rhum = round(value.getAverage(Measurements::Humidity));
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(true));
pm25 = round(value.getCorrectedPM25(*ag, config, 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(true));
pm25 = round(value.getCorrectedPM25(*ag, config, true));
}
ag->display.setCursor(0, 12);
@ -389,7 +389,7 @@ void OledDisplay::showDashboard(const char *status) {
ag->display.setText(strBuf);
/** Set temperature and humidity */
float temp = value.getCorrectedTempHum(Measurements::Temperature, 1);
float temp = value.getAverage(Measurements::Temperature);
if (utils::isValidTemperature(temp)) {
if (config.isTemperatureUnitInF()) {
snprintf(strBuf, sizeof(strBuf), "T:%0.1f F", utils::degreeC_To_F(temp));
@ -407,7 +407,7 @@ void OledDisplay::showDashboard(const char *status) {
ag->display.setCursor(0, 24);
ag->display.setText(strBuf);
int rhum = round(value.getCorrectedTempHum(Measurements::Humidity, 1));
int rhum = round(value.getAverage(Measurements::Humidity));
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(true));
pm25Value = round(value.getCorrectedPM25(*ag, config, true));
}
if (pm25Value <= 5) {

File diff suppressed because it is too large Load Diff

View File

@ -34,11 +34,9 @@ private:
};
public:
Measurements(Configuration &config);
Measurements() {}
~Measurements() {}
void setAirGradient(AirGradient *ag);
// Enumeration for every AG measurements
enum MeasurementType {
Temperature,
@ -125,51 +123,38 @@ 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(bool useAvg = false, int ch = 1, bool forceCorrection = false);
float getCorrectedPM25(AirGradient &ag, Configuration &config, bool useAvg = false, int ch = 1);
/**
* build json payload for every measurements
*/
String toString(bool localServer, AgFirmwareMode fwMode, int rssi);
String toString(bool localServer, AgFirmwareMode fwMode, int rssi, AirGradient &ag,
Configuration &config);
bool resetLocalStorage();
bool saveLocalStorage(AirGradient &ag, Configuration &config);
char *getLocalStorage();
/**
* Set to true if want to debug every update value
*/
void setDebug(bool debug);
int bootCount();
void setBootCount(int bootCount);
#ifndef ESP8266
void setResetReason(esp_reset_reason_t reason);
#endif
// TODO: update this to use setter
int bootCount;
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];
@ -190,9 +175,9 @@ 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;
const char *FILE_PATH = "/measurements.csv"; // Local storage file path
/**
* @brief Get PMS5003 firmware version string
@ -228,9 +213,10 @@ private:
*/
void validateChannel(int ch);
JSONVar buildOutdoor(bool localServer, AgFirmwareMode fwMode);
JSONVar buildIndoor(bool localServer);
JSONVar buildPMS(int ch, bool allCh, bool withTempHum, bool compensate);
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);
};
#endif /** _AG_VALUE_H_ */

View File

@ -81,15 +81,16 @@ bool WifiConnector::connect(void) {
// ssid = "AG-" + String(ESP.getChipId(), HEX);
WIFI()->setConfigPortalTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
WiFiManagerParameter disableCloud("chbPostToAg", "Prevent Connection to AirGradient Server", "T",
2, "type=\"checkbox\" ", WFM_LABEL_AFTER);
WIFI()->addParameter(&disableCloud);
WiFiManagerParameter disableCloudInfo(
WiFiManagerParameter postToAg("chbPostToAg",
"Prevent Connection to AirGradient Server", "T",
2, "type=\"checkbox\" ", WFM_LABEL_AFTER);
WIFI()->addParameter(&postToAg);
WiFiManagerParameter postToAgInfo(
"<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, "
"configuration settings from cloud and the measure data will not reach the AirGradient dashboard.</p>");
WIFI()->addParameter(&disableCloudInfo);
"features. As a result you will not receive automatic firmware updates "
"and your data will not reach the AirGradient dashboard.</p>");
WIFI()->addParameter(&postToAgInfo);
WIFI()->autoConnect(ssid.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT);
@ -173,11 +174,12 @@ bool WifiConnector::connect(void) {
logInfo("WiFi Connected: " + WiFi.SSID() + " IP: " + localIpStr());
if (hasPortalConfig) {
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");
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");
}
hasPortalConfig = false;
}

View File

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

View File

@ -15,46 +15,7 @@
#include "Main/utils.h"
#ifndef GIT_VERSION
#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";
#define GIT_VERSION "3.1.13-snap"
#endif
/**
@ -212,6 +173,9 @@ public:
*/
String deviceId(void);
void setCurrentTime(long epochTime);
String getCurrentTime();
private:
BoardType boardType;
};

View File

@ -95,29 +95,26 @@ enum ConfigurationControl {
};
enum PMCorrectionAlgorithm {
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
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,
};
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);

View File

@ -1,171 +0,0 @@
#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

View File

@ -1,43 +0,0 @@
#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