mirror of
https://github.com/airgradienthq/arduino.git
synced 2025-06-27 16:50:58 +02:00
Compare commits
18 Commits
feat/local
...
3.1.20
Author | SHA1 | Date | |
---|---|---|---|
56809a412c | |||
6a83743e2a | |||
faaf051e39 | |||
5bc1821ef9 | |||
280ea5e997 | |||
38e792b88d | |||
aeee0cad01 | |||
401326d00d | |||
fb0dcad54d | |||
3556e4a96a | |||
283646a699 | |||
6312612ada | |||
c1f22674e2 | |||
40d38a75d8 | |||
4c165b31f5 | |||
2be91b3968 | |||
3ca2d1d208 | |||
aad12fc868 |
BIN
docs/epoch.png
BIN
docs/epoch.png
Binary file not shown.
Before Width: | Height: | Size: 221 KiB |
@ -1,56 +0,0 @@
|
||||
*This document to explain local storage mode - experimental*
|
||||
|
||||
## How it works?
|
||||
|
||||
1. Monitor directly goes to local storage mode
|
||||
2. On boot, monitor will attempt to connect to default wifi. And if connected, mdns and local server will be enabled, otherwise it will ignore and continues the measurements
|
||||
3. On display, when boot it will show the mode ("local storage mode") and wifi related scenario. After that, monitor will show the measurements dashboard
|
||||
4. Measurement records to the local storage every two minutes that saved on CSV file in SPIFFs partition
|
||||
5. Every successful writes, monitor will blink the most left led bar to *blue* twice, but if failed it will blink *red* twice. There are two possibilities for failed write, SPIFFs partition already full or out of heap memory when load the file.
|
||||
6. There are 2 endpoinds added for this mode, download measurements from local storage and reset measurement (delete old measurements file and create new one) with new timestamp. Timestamp here to set the monitor system time.
|
||||
|
||||
**Notes**
|
||||
|
||||
1. Default wifi
|
||||
- ssid ➝ `airgradient`
|
||||
- password ➝ `cleanair`
|
||||
2. Maximum measurements file is around 113kb. If assume each measurements is 60 bytes, with write schedule 2 minutes, SPIFFS will be full in around 5 days
|
||||
3. WiFi connection attempt on boot wait for 10s before considering timeout
|
||||
4. Tips. If monitor not connected to wifi on boot, no need to restart the monitor for reconnection, it will automatically connect to AP once it is available
|
||||
|
||||
### Local Storage Endpoinds
|
||||
|
||||
*Make sure monitor is connected to AP, and client also connect to it. And change the serial number on the url*
|
||||
|
||||
**Download measurements file**
|
||||
|
||||
To download measurements file from local storage, just directly access following url on the browser `http://airgradient_aaaaaaaa.local/storage`, and browser should automatically download the file.
|
||||
|
||||
**Reset measurements**
|
||||
|
||||
Execute below command in terminal
|
||||
|
||||
```sh
|
||||
curl -X PUT -H "Content-Type: text/plain" -d '1733431986' http://airgradient_aaaaaaa.local/storage/reset
|
||||
```
|
||||
|
||||
`1733431986` this data is the time that we want to set monitor system time to. Its in epoch time format and expecting UTC+0 timezone.
|
||||
|
||||
To get epoch time, access this url [https://www.unixtimestamp.com/](https://www.unixtimestamp.com/), and click copy button.
|
||||
|
||||

|
||||
|
||||
### Example measurements file content
|
||||
|
||||
```csv
|
||||
datetime,pm0.3 count,pm1,pm2.5,pm10,temp,rhum,co2,tvoc,nox
|
||||
05/12 21:10:59,869.67,11.17,20.33,21.83,26.69,72.93,417,40,1
|
||||
05/12 21:11:30,834.83,11.50,19.33,20.33,26.68,73.08,413,79,1
|
||||
05/12 21:12:01,829.67,10.33,19.33,22.00,26.64,73.09,412,90,1
|
||||
05/12 21:12:32,831.50,10.33,18.33,20.83,26.62,73.21,411,97,1
|
||||
05/12 21:13:02,887.50,12.00,20.33,21.67,26.59,73.33,412,95,1
|
||||
05/12 21:13:33,785.17,8.67,18.50,19.50,26.56,73.43,414,92,1
|
||||
05/12 21:14:04,827.50,10.50,18.50,19.50,26.54,73.43,415,98,1
|
||||
05/12 21:14:35,815.83,10.50,19.50,19.83,26.49,73.47,413,99,1
|
||||
```
|
||||
|
@ -331,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);
|
||||
@ -518,7 +518,8 @@ static void updatePm(void) {
|
||||
|
||||
static void sendDataToServer(void) {
|
||||
/** Increment bootcount when send measurements data is scheduled */
|
||||
measurements.bootCount++;
|
||||
int bootCount = measurements.bootCount() + 1;
|
||||
measurements.setBootCount(bootCount);
|
||||
|
||||
/** Ignore send data to server if postToAirGradient disabled */
|
||||
if (configuration.isPostDataToAirGradient() == false ||
|
||||
|
@ -388,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);
|
||||
@ -570,7 +570,8 @@ static void updatePm(void) {
|
||||
|
||||
static void sendDataToServer(void) {
|
||||
/** Increment bootcount when send measurements data is scheduled */
|
||||
measurements.bootCount++;
|
||||
int bootCount = measurements.bootCount() + 1;
|
||||
measurements.setBootCount(bootCount);
|
||||
|
||||
/** Ignore send data to server if postToAirGradient disabled */
|
||||
if (configuration.isPostDataToAirGradient() == false ||
|
||||
|
@ -411,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);
|
||||
@ -611,7 +611,8 @@ static void updatePm(void) {
|
||||
|
||||
static void sendDataToServer(void) {
|
||||
/** Increment bootcount when send measurements data is scheduled */
|
||||
measurements.bootCount++;
|
||||
int bootCount = measurements.bootCount() + 1;
|
||||
measurements.setBootCount(bootCount);
|
||||
|
||||
/** Ignore send data to server if postToAirGradient disabled */
|
||||
if (configuration.isPostDataToAirGradient() == false ||
|
||||
|
@ -9,16 +9,10 @@ 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(
|
||||
@ -44,13 +38,6 @@ 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());
|
||||
@ -81,174 +68,4 @@ void LocalServer::_GET_measure(void) {
|
||||
server.send(200, "application/json", toSend);
|
||||
}
|
||||
|
||||
void LocalServer::_GET_dashboard(void) {
|
||||
String timestamp = ag->getCurrentTime();
|
||||
server.send(200, "text/html", htmlDashboard(timestamp));
|
||||
}
|
||||
|
||||
void LocalServer::_GET_storage(void) {
|
||||
char *data = measure.getLocalStorage();
|
||||
if (data != nullptr) {
|
||||
String filename =
|
||||
"measurements-" + ag->deviceId().substring(8) + ".csv"; // measurements-fdsa.csv
|
||||
server.sendHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
|
||||
server.send_P(200, "text/plain", data);
|
||||
free(data);
|
||||
} else {
|
||||
server.send(204, "text/plain", "No data");
|
||||
}
|
||||
}
|
||||
|
||||
void LocalServer::_POST_storage(void) {
|
||||
String body;
|
||||
int statusCode = 200;
|
||||
|
||||
if (measure.resetLocalStorage()) {
|
||||
body = "Success reset storage";
|
||||
} else {
|
||||
body = "Failed reset local storage, unknown error";
|
||||
statusCode = 500;
|
||||
}
|
||||
body += ". Go to <a href='http://192.168.4.1/dashboard'>dashboard</a>.";
|
||||
|
||||
server.send(statusCode, "text/html", htmlResponse(body, false));
|
||||
}
|
||||
|
||||
void LocalServer::_POST_time(void) {
|
||||
String epochTime = server.arg(0);
|
||||
Serial.printf("Received epoch: %s \n", epochTime.c_str());
|
||||
if (epochTime.isEmpty()) {
|
||||
server.send(400, "text/plain", "Time query not provided");
|
||||
return;
|
||||
}
|
||||
|
||||
long _epochTime = epochTime.toInt();
|
||||
if (_epochTime == 0) {
|
||||
server.send(400, "text/plain", "Time format is not in epoch time");
|
||||
return;
|
||||
}
|
||||
|
||||
ag->setCurrentTime(_epochTime);
|
||||
|
||||
String body = "Success set new time. Go to <a href='http://192.168.4.1/dashboard'>dashboard</a>.";
|
||||
server.send(200, "text/html", htmlResponse(body, false));
|
||||
}
|
||||
|
||||
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }
|
||||
|
||||
String LocalServer::htmlDashboard(String timestamp) {
|
||||
String page = "";
|
||||
page += "<!DOCTYPE html>";
|
||||
page += "<html lang=\"en\">";
|
||||
page += "<head>";
|
||||
page += " <meta charset=\"UTF-8\">";
|
||||
page += " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">";
|
||||
page += " <title>AirGradient Local Storage Mode</title>";
|
||||
page += " <style>";
|
||||
page += " body {";
|
||||
page += " font-family: Arial, sans-serif;";
|
||||
page += " display: flex;";
|
||||
page += " flex-direction: column;";
|
||||
page += " align-items: center;";
|
||||
page += " margin-top: 50px;";
|
||||
page += " }";
|
||||
page += "";
|
||||
page += " button {";
|
||||
page += " display: block;";
|
||||
page += " margin: 10px 0;";
|
||||
page += " padding: 10px 20px;";
|
||||
page += " font-size: 16px;";
|
||||
page += " cursor: pointer;";
|
||||
page += " }";
|
||||
page += " .datetime-container {";
|
||||
page += " display: flex;";
|
||||
page += " align-items: center;";
|
||||
page += " margin: 10px 0;";
|
||||
page += " }";
|
||||
page += " .datetime-container input[type=\"datetime-local\"] {";
|
||||
page += " margin-left: 10px;";
|
||||
page += " padding: 5px;";
|
||||
page += " font-size: 16px;";
|
||||
page += " }";
|
||||
page += " button.reset-button {";
|
||||
page += " background-color: red;";
|
||||
page += " color: white;";
|
||||
page += " border: none;";
|
||||
page += " padding: 10px 20px;";
|
||||
page += " font-size: 16px;";
|
||||
page += " cursor: pointer;";
|
||||
page += " }";
|
||||
page += " .spacer {";
|
||||
page += " height: 50px;";
|
||||
page += " }";
|
||||
page += " </style>";
|
||||
page += "</head>";
|
||||
page += "<body>";
|
||||
page += " <h2>";
|
||||
page += " Device Time: ";
|
||||
page += timestamp;
|
||||
page += " </h2>";
|
||||
page += " <h2>";
|
||||
page += " Serial Number: ";
|
||||
page += ag->deviceId();
|
||||
page += " </h2>";
|
||||
page += " <form action=\"/storage/download\" method=\"GET\">";
|
||||
page += " <button type=\"submit\">Download Measurements</button>";
|
||||
page += " </form>";
|
||||
page += " <form id=\"timestampForm\" method=\"POST\" action=\"/timestamp\">";
|
||||
page += " <input type=\"datetime-local\" id=\"timestampInput\" required>";
|
||||
page += " <button type=\"submit\">Set Timestamp</button>";
|
||||
page += " <input type=\"hidden\" name=\"timestamp\" id=\"epochInput\">";
|
||||
page += " </form>";
|
||||
page += " <div class=\"spacer\"></div>";
|
||||
page += " <form action=\"/storage/reset\" method=\"POST\"";
|
||||
page += " onsubmit=\"return confirm('Are you sure you want to reset the measurements? "
|
||||
"This action will permanently delete the existing measurement files!');\">";
|
||||
page += " <button class=\"reset-button\" type=\"submit\">Reset Measurements</button>";
|
||||
page += " </form>";
|
||||
page += "</body>";
|
||||
page += "<script>";
|
||||
page += " document.querySelector('#timestampForm').onsubmit = function (event) {";
|
||||
page += " const datetimeInput = document.querySelector('#timestampInput').value;";
|
||||
page += " const localDate = new Date(datetimeInput);";
|
||||
page += " const epochTimeUTC = Math.floor(Date.UTC(";
|
||||
page += " localDate.getFullYear(),";
|
||||
page += " localDate.getMonth(),";
|
||||
page += " localDate.getDate(),";
|
||||
page += " localDate.getHours(),";
|
||||
page += " localDate.getMinutes()";
|
||||
page += " ) / 1000);";
|
||||
page += " document.querySelector('#epochInput').value = epochTimeUTC;";
|
||||
page += " return true;";
|
||||
page += " };";
|
||||
page += "</script>";
|
||||
page += "</html>";
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
String LocalServer::htmlResponse(String body, bool redirect) {
|
||||
String page = "";
|
||||
page += "<!DOCTYPE HTML>";
|
||||
page += "<html lang=\"en-US\">";
|
||||
page += " <head>";
|
||||
page += "<style>";
|
||||
page += "p { font-size: 22px; }";
|
||||
page += "</style>";
|
||||
page += " <meta charset=\"UTF-8\">";
|
||||
|
||||
if (redirect) {
|
||||
page += " <meta http-equiv=\"refresh\" content=\"0;url=/dashboard\">";
|
||||
}
|
||||
|
||||
page += " <title>Page Redirection</title>";
|
||||
page += " </head>";
|
||||
page += " <body>";
|
||||
page += " <p>";
|
||||
page += body;
|
||||
page += " </p>";
|
||||
page += " </body>";
|
||||
page += "</html>";
|
||||
|
||||
return page;
|
||||
}
|
@ -19,9 +19,6 @@ 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);
|
||||
@ -32,15 +29,10 @@ 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_ */
|
||||
|
@ -36,20 +36,21 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
||||
|
||||
*/
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include "AirGradient.h"
|
||||
#include "OtaHandler.h"
|
||||
#include "AgApiClient.h"
|
||||
#include "AgConfigure.h"
|
||||
#include "AgSchedule.h"
|
||||
#include "AgStateMachine.h"
|
||||
#include "AgWiFiConnector.h"
|
||||
#include "AirGradient.h"
|
||||
#include "EEPROM.h"
|
||||
#include "ESPmDNS.h"
|
||||
#include "LocalServer.h"
|
||||
#include "MqttClient.h"
|
||||
#include "OpenMetrics.h"
|
||||
#include "OtaHandler.h"
|
||||
#include "WebServer.h"
|
||||
#include "esp32c3/rom/rtc.h"
|
||||
#include <HardwareSerial.h>
|
||||
#include <WebServer.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
@ -93,7 +94,6 @@ static AgFirmwareMode fwMode = FW_MODE_I_9PSL;
|
||||
|
||||
static bool ledBarButtonTest = false;
|
||||
static String fwNewVersion;
|
||||
static bool isLocalServerInitialized = false;
|
||||
|
||||
static void boardInit(void);
|
||||
static void failedHandler(String msg);
|
||||
@ -112,37 +112,34 @@ static void wdgFeedUpdate(void);
|
||||
static void ledBarEnabledUpdate(void);
|
||||
static bool sgp41Init(void);
|
||||
static void firmwareCheckForUpdate(void);
|
||||
static void otaHandlerCallback(OtaState state, String mesasge);
|
||||
static void displayExecuteOta(OtaState state, String msg,
|
||||
int processing);
|
||||
static void otaHandlerCallback(OtaHandler::OtaState state, String mesasge);
|
||||
static void displayExecuteOta(OtaHandler::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 offlineStorage((2 * 60000), offlineStorageUpdate);
|
||||
// AgSchedule checkForUpdateSchedule(FIRMWARE_CHECK_FOR_UPDATE_MS, firmwareCheckForUpdate);
|
||||
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();
|
||||
|
||||
@ -176,34 +173,101 @@ void setup() {
|
||||
setMeasurementMaxPeriod();
|
||||
|
||||
// Comment below line to disable debug measurement readings
|
||||
measurements.setDebug(false);
|
||||
measurements.setDebug(true);
|
||||
|
||||
// Force to offline mode
|
||||
configuration.setOfflineMode(true);
|
||||
/** Connecting wifi */
|
||||
bool connectToWifi = false;
|
||||
if (ag->isOne()) {
|
||||
/** Show message confirm offline mode, should me perform if LED bar button
|
||||
* test pressed */
|
||||
if (ledBarButtonTest == false) {
|
||||
oledDisplay.setText(
|
||||
"Press now for",
|
||||
configuration.isOfflineMode() ? "online mode" : "offline mode", "");
|
||||
uint32_t startTime = millis();
|
||||
while (true) {
|
||||
if (ag->button.getState() == ag->button.BUTTON_PRESSED) {
|
||||
configuration.setOfflineMode(!configuration.isOfflineMode());
|
||||
|
||||
oledDisplay.setText(
|
||||
"Offline Mode",
|
||||
configuration.isOfflineMode() ? " = True" : " = False", "");
|
||||
delay(1000);
|
||||
break;
|
||||
}
|
||||
uint32_t periodMs = (uint32_t)(millis() - startTime);
|
||||
if (periodMs >= 3000) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
connectToWifi = !configuration.isOfflineMode();
|
||||
} else {
|
||||
configuration.setOfflineModeWithoutSave(true);
|
||||
}
|
||||
} else {
|
||||
connectToWifi = true;
|
||||
}
|
||||
|
||||
if (connectToWifi) {
|
||||
apiClient.begin();
|
||||
|
||||
if (wifiConnector.connect()) {
|
||||
if (wifiConnector.isConnected()) {
|
||||
mdnsInit();
|
||||
localServer.begin();
|
||||
initMqtt();
|
||||
sendDataToAg();
|
||||
|
||||
#ifdef ESP8266
|
||||
// ota not supported
|
||||
#else
|
||||
firmwareCheckForUpdate();
|
||||
checkForUpdateSchedule.update();
|
||||
#endif
|
||||
|
||||
apiClient.fetchServerConfiguration();
|
||||
configSchedule.update();
|
||||
if (apiClient.isFetchConfigureFailed()) {
|
||||
if (ag->isOne()) {
|
||||
if (apiClient.isNotAvailableOnDashboard()) {
|
||||
stateMachine.displaySetAddToDashBoard();
|
||||
stateMachine.displayHandle(
|
||||
AgStateMachineWiFiOkServerOkSensorConfigFailed);
|
||||
} else {
|
||||
stateMachine.displayClearAddToDashBoard();
|
||||
}
|
||||
}
|
||||
stateMachine.handleLeds(
|
||||
AgStateMachineWiFiOkServerOkSensorConfigFailed);
|
||||
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
||||
} else {
|
||||
ledBarEnabledUpdate();
|
||||
}
|
||||
} else {
|
||||
if (wifiConnector.isConfigurePorttalTimeout()) {
|
||||
oledDisplay.showRebooting();
|
||||
delay(2500);
|
||||
oledDisplay.setText("", "", "");
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Set offline mode without saving, cause wifi is not configured */
|
||||
if (wifiConnector.hasConfigurated() == false) {
|
||||
Serial.println("Set offline mode cause wifi is not configurated");
|
||||
configuration.setOfflineModeWithoutSave(true);
|
||||
}
|
||||
|
||||
/** Show display Warning up */
|
||||
if (ag->isOne()) {
|
||||
oledDisplay.setText("Warming Up", "Serial Number:", ag->deviceId().c_str());
|
||||
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
||||
|
||||
Serial.println("Display brightness: " + String(configuration.getDisplayBrightness()));
|
||||
oledDisplay.setBrightness(configuration.getDisplayBrightness());
|
||||
}
|
||||
|
||||
String deviceId = ag->deviceId();
|
||||
|
||||
// Connect to Wi-Fi network with SSID and password
|
||||
Serial.print("Setting AP (Access Point)…");
|
||||
// Remove the password parameter, if you want the AP (Access Point) to be open
|
||||
WiFi.softAP("ag_" + deviceId, "cleanair");
|
||||
IPAddress IP = WiFi.softAPIP();
|
||||
Serial.print("AP IP address: ");
|
||||
Serial.println(IP);
|
||||
Serial.printf("SSID: ag_%s\n", deviceId.c_str());
|
||||
|
||||
oledDisplay.setText("", "Offline Storage Mode", "");
|
||||
|
||||
delay(3000);
|
||||
// mdnsInit();
|
||||
localServer.begin();
|
||||
|
||||
// Update display and led bar after finishing setup to show dashboard
|
||||
updateDisplayAndLedBar();
|
||||
}
|
||||
@ -211,9 +275,8 @@ void setup() {
|
||||
void loop() {
|
||||
/** Handle schedule */
|
||||
dispLedSchedule.run();
|
||||
// configSchedule.run();
|
||||
// agApiPostSchedule.run();
|
||||
offlineStorage.run();
|
||||
configSchedule.run();
|
||||
agApiPostSchedule.run();
|
||||
|
||||
if (configuration.hasSensorS8) {
|
||||
co2Schedule.run();
|
||||
@ -249,8 +312,8 @@ void loop() {
|
||||
|
||||
watchdogFeedSchedule.run();
|
||||
|
||||
// /** Check for handle WiFi reconnect */
|
||||
// wifiConnector.handle();
|
||||
/** Check for handle WiFi reconnect */
|
||||
wifiConnector.handle();
|
||||
|
||||
/** factory reset handle */
|
||||
factoryConfigReset();
|
||||
@ -259,7 +322,7 @@ void loop() {
|
||||
configUpdateHandle();
|
||||
|
||||
/** Firmware check for update handle */
|
||||
// checkForUpdateSchedule.run();
|
||||
checkForUpdateSchedule.run();
|
||||
}
|
||||
|
||||
static void co2Update(void) {
|
||||
@ -377,7 +440,7 @@ static void factoryConfigReset(void) {
|
||||
WiFi.disconnect(true, true);
|
||||
|
||||
/** Reset local config */
|
||||
// configuration.reset();
|
||||
configuration.reset();
|
||||
|
||||
if (ag->isOne()) {
|
||||
oledDisplay.setText("Factory reset", "successful", "");
|
||||
@ -411,8 +474,6 @@ 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) {
|
||||
@ -457,29 +518,27 @@ static void firmwareCheckForUpdate(void) {
|
||||
Serial.println();
|
||||
}
|
||||
|
||||
static void otaHandlerCallback(OtaState state, String mesasge) {
|
||||
Serial.println("OTA message: " + mesasge);
|
||||
static void otaHandlerCallback(OtaHandler::OtaState state, String message) {
|
||||
Serial.println("OTA message: " + message);
|
||||
switch (state) {
|
||||
case OtaState::OTA_STATE_BEGIN:
|
||||
case OtaHandler::OTA_STATE_BEGIN:
|
||||
displayExecuteOta(state, fwNewVersion, 0);
|
||||
break;
|
||||
case OtaState::OTA_STATE_FAIL:
|
||||
case OtaHandler::OTA_STATE_FAIL:
|
||||
displayExecuteOta(state, "", 0);
|
||||
break;
|
||||
case OtaState::OTA_STATE_PROCESSING:
|
||||
displayExecuteOta(state, "", mesasge.toInt());
|
||||
break;
|
||||
case OtaState::OTA_STATE_SUCCESS:
|
||||
displayExecuteOta(state, "", mesasge.toInt());
|
||||
case OtaHandler::OTA_STATE_PROCESSING:
|
||||
case OtaHandler::OTA_STATE_SUCCESS:
|
||||
displayExecuteOta(state, "", message.toInt());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void displayExecuteOta(OtaState state, String msg, int processing) {
|
||||
static void displayExecuteOta(OtaHandler::OtaState state, String msg, int processing) {
|
||||
switch (state) {
|
||||
case OtaState::OTA_STATE_BEGIN: {
|
||||
case OtaHandler::OTA_STATE_BEGIN: {
|
||||
if (ag->isOne()) {
|
||||
oledDisplay.showFirmwareUpdateVersion(msg);
|
||||
} else {
|
||||
@ -488,7 +547,7 @@ static void displayExecuteOta(OtaState state, String msg, int processing) {
|
||||
delay(2500);
|
||||
break;
|
||||
}
|
||||
case OtaState::OTA_STATE_FAIL: {
|
||||
case OtaHandler::OTA_STATE_FAIL: {
|
||||
if (ag->isOne()) {
|
||||
oledDisplay.showFirmwareUpdateFailed();
|
||||
} else {
|
||||
@ -498,7 +557,7 @@ static void displayExecuteOta(OtaState state, String msg, int processing) {
|
||||
delay(2500);
|
||||
break;
|
||||
}
|
||||
case OtaState::OTA_STATE_SKIP: {
|
||||
case OtaHandler::OTA_STATE_SKIP: {
|
||||
if (ag->isOne()) {
|
||||
oledDisplay.showFirmwareUpdateSkipped();
|
||||
} else {
|
||||
@ -508,7 +567,7 @@ static void displayExecuteOta(OtaState state, String msg, int processing) {
|
||||
delay(2500);
|
||||
break;
|
||||
}
|
||||
case OtaState::OTA_STATE_UP_TO_DATE: {
|
||||
case OtaHandler::OTA_STATE_UP_TO_DATE: {
|
||||
if (ag->isOne()) {
|
||||
oledDisplay.showFirmwareUpdateUpToDate();
|
||||
} else {
|
||||
@ -518,7 +577,7 @@ static void displayExecuteOta(OtaState state, String msg, int processing) {
|
||||
delay(2500);
|
||||
break;
|
||||
}
|
||||
case OtaState::OTA_STATE_PROCESSING: {
|
||||
case OtaHandler::OTA_STATE_PROCESSING: {
|
||||
if (ag->isOne()) {
|
||||
oledDisplay.showFirmwareUpdateProgress(processing);
|
||||
} else {
|
||||
@ -527,7 +586,7 @@ static void displayExecuteOta(OtaState state, String msg, int processing) {
|
||||
|
||||
break;
|
||||
}
|
||||
case OtaState::OTA_STATE_SUCCESS: {
|
||||
case OtaHandler::OTA_STATE_SUCCESS: {
|
||||
int i = 6;
|
||||
while(i != 0) {
|
||||
i = i - 1;
|
||||
@ -577,7 +636,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);
|
||||
}
|
||||
@ -603,7 +662,6 @@ static void oneIndoorInit(void) {
|
||||
|
||||
/** Display init */
|
||||
oledDisplay.begin();
|
||||
oledDisplay.setBrightness(40);
|
||||
|
||||
/** Show boot display */
|
||||
Serial.println("Firmware Version: " + ag->getVersion());
|
||||
@ -907,7 +965,7 @@ static void updateDisplayAndLedBar(void) {
|
||||
if (configuration.isOfflineMode()) {
|
||||
// Ignore network related status when in offline mode
|
||||
stateMachine.displayHandle(AgStateMachineNormal);
|
||||
// stateMachine.handleLeds(AgStateMachineNormal);
|
||||
stateMachine.handleLeds(AgStateMachineNormal);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1079,7 +1137,8 @@ static void updatePm(void) {
|
||||
|
||||
static void sendDataToServer(void) {
|
||||
/** Increment bootcount when send measurements data is scheduled */
|
||||
measurements.bootCount++;
|
||||
int bootCount = measurements.bootCount() + 1;
|
||||
measurements.setBootCount(bootCount);
|
||||
|
||||
/** Ignore send data to server if postToAirGradient disabled */
|
||||
if (configuration.isPostDataToAirGradient() == false || configuration.isOfflineMode()) {
|
||||
@ -1093,6 +1152,9 @@ static void sendDataToServer(void) {
|
||||
"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) {
|
||||
@ -1163,12 +1225,3 @@ 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);
|
||||
}
|
@ -1,206 +0,0 @@
|
||||
#ifndef _OTA_HANDLER_H_
|
||||
#define _OTA_HANDLER_H_
|
||||
#include <Arduino.h>
|
||||
#include <esp_err.h>
|
||||
#include <esp_http_client.h>
|
||||
#include <esp_ota_ops.h>
|
||||
|
||||
#define OTA_BUF_SIZE 1024
|
||||
#define URL_BUF_SIZE 256
|
||||
|
||||
enum OtaUpdateOutcome {
|
||||
UPDATE_PERFORMED,
|
||||
ALREADY_UP_TO_DATE,
|
||||
UPDATE_FAILED,
|
||||
UDPATE_SKIPPED
|
||||
};
|
||||
|
||||
enum OtaState {
|
||||
OTA_STATE_BEGIN,
|
||||
OTA_STATE_FAIL,
|
||||
OTA_STATE_SKIP,
|
||||
OTA_STATE_UP_TO_DATE,
|
||||
OTA_STATE_PROCESSING,
|
||||
OTA_STATE_SUCCESS
|
||||
};
|
||||
|
||||
typedef void(*OtaHandlerCallback_t)(OtaState state,
|
||||
String message);
|
||||
|
||||
class OtaHandler {
|
||||
public:
|
||||
void updateFirmwareIfOutdated(String deviceId) {
|
||||
String url = "http://hw.airgradient.com/sensors/airgradient:" + deviceId +
|
||||
"/generic/os/firmware.bin";
|
||||
url += "?current_firmware=";
|
||||
url += GIT_VERSION;
|
||||
char urlAsChar[URL_BUF_SIZE];
|
||||
url.toCharArray(urlAsChar, URL_BUF_SIZE);
|
||||
Serial.printf("checking for new OTA update @ %s\n", urlAsChar);
|
||||
|
||||
esp_http_client_config_t config = {};
|
||||
config.url = urlAsChar;
|
||||
OtaUpdateOutcome ret = attemptToPerformOta(&config);
|
||||
Serial.println(ret);
|
||||
if (this->callback) {
|
||||
switch (ret) {
|
||||
case OtaUpdateOutcome::UPDATE_PERFORMED:
|
||||
this->callback(OtaState::OTA_STATE_SUCCESS, "");
|
||||
break;
|
||||
case OtaUpdateOutcome::UDPATE_SKIPPED:
|
||||
this->callback(OtaState::OTA_STATE_SKIP, "");
|
||||
break;
|
||||
case OtaUpdateOutcome::ALREADY_UP_TO_DATE:
|
||||
this->callback(OtaState::OTA_STATE_UP_TO_DATE, "");
|
||||
break;
|
||||
case OtaUpdateOutcome::UPDATE_FAILED:
|
||||
this->callback(OtaState::OTA_STATE_FAIL, "");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setHandlerCallback(OtaHandlerCallback_t callback) {
|
||||
this->callback = callback;
|
||||
}
|
||||
|
||||
private:
|
||||
OtaHandlerCallback_t callback;
|
||||
|
||||
OtaUpdateOutcome attemptToPerformOta(const esp_http_client_config_t *config) {
|
||||
esp_http_client_handle_t client = esp_http_client_init(config);
|
||||
if (client == NULL) {
|
||||
Serial.println("Failed to initialize HTTP connection");
|
||||
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||
}
|
||||
|
||||
esp_err_t err = esp_http_client_open(client, 0);
|
||||
if (err != ESP_OK) {
|
||||
esp_http_client_cleanup(client);
|
||||
Serial.printf("Failed to open HTTP connection: %s\n",
|
||||
esp_err_to_name(err));
|
||||
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||
}
|
||||
esp_http_client_fetch_headers(client);
|
||||
|
||||
int httpStatusCode = esp_http_client_get_status_code(client);
|
||||
if (httpStatusCode == 304) {
|
||||
Serial.println("Firmware is already up to date");
|
||||
cleanupHttp(client);
|
||||
return OtaUpdateOutcome::ALREADY_UP_TO_DATE;
|
||||
} else if (httpStatusCode != 200) {
|
||||
Serial.printf("Firmware update skipped, the server returned %d\n",
|
||||
httpStatusCode);
|
||||
cleanupHttp(client);
|
||||
return OtaUpdateOutcome::UDPATE_SKIPPED;
|
||||
}
|
||||
|
||||
esp_ota_handle_t update_handle = 0;
|
||||
const esp_partition_t *update_partition = NULL;
|
||||
Serial.println("Starting OTA update ...");
|
||||
update_partition = esp_ota_get_next_update_partition(NULL);
|
||||
if (update_partition == NULL) {
|
||||
Serial.println("Passive OTA partition not found");
|
||||
cleanupHttp(client);
|
||||
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||
}
|
||||
Serial.printf("Writing to partition subtype %d at offset 0x%x\n",
|
||||
update_partition->subtype, update_partition->address);
|
||||
|
||||
err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle);
|
||||
if (err != ESP_OK) {
|
||||
Serial.printf("esp_ota_begin failed, error=%d\n", err);
|
||||
cleanupHttp(client);
|
||||
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||
}
|
||||
|
||||
esp_err_t ota_write_err = ESP_OK;
|
||||
char *upgrade_data_buf = (char *)malloc(OTA_BUF_SIZE);
|
||||
if (!upgrade_data_buf) {
|
||||
Serial.println("Couldn't allocate memory for data buffer");
|
||||
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||
}
|
||||
|
||||
int binary_file_len = 0;
|
||||
int totalSize = esp_http_client_get_content_length(client);
|
||||
Serial.println("File size: " + String(totalSize) + String(" bytes"));
|
||||
|
||||
// Show display start update new firmware.
|
||||
if (this->callback) {
|
||||
this->callback(OtaState::OTA_STATE_BEGIN, "");
|
||||
}
|
||||
|
||||
// Download file and write new firmware to OTA partition
|
||||
uint32_t lastUpdate = millis();
|
||||
while (1) {
|
||||
int data_read =
|
||||
esp_http_client_read(client, upgrade_data_buf, OTA_BUF_SIZE);
|
||||
if (data_read == 0) {
|
||||
if (this->callback) {
|
||||
this->callback(OtaState::OTA_STATE_PROCESSING, String(100));
|
||||
}
|
||||
Serial.println("Connection closed, all data received");
|
||||
break;
|
||||
}
|
||||
if (data_read < 0) {
|
||||
Serial.println("Data read error");
|
||||
if (this->callback) {
|
||||
this->callback(OtaState::OTA_STATE_FAIL, "");
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (data_read > 0) {
|
||||
ota_write_err = esp_ota_write(
|
||||
update_handle, (const void *)upgrade_data_buf, data_read);
|
||||
if (ota_write_err != ESP_OK) {
|
||||
if (this->callback) {
|
||||
this->callback(OtaState::OTA_STATE_FAIL, "");
|
||||
}
|
||||
break;
|
||||
}
|
||||
binary_file_len += data_read;
|
||||
|
||||
int percent = (binary_file_len * 100) / totalSize;
|
||||
uint32_t ms = (uint32_t)(millis() - lastUpdate);
|
||||
if (ms >= 250) {
|
||||
// sm.executeOTA(StateMachine::OtaState::OTA_STATE_PROCESSING, "",
|
||||
// percent);
|
||||
if (this->callback) {
|
||||
this->callback(OtaState::OTA_STATE_PROCESSING,
|
||||
String(percent));
|
||||
}
|
||||
lastUpdate = millis();
|
||||
}
|
||||
}
|
||||
}
|
||||
free(upgrade_data_buf);
|
||||
cleanupHttp(client);
|
||||
Serial.printf("# of bytes written: %d\n", binary_file_len);
|
||||
|
||||
esp_err_t ota_end_err = esp_ota_end(update_handle);
|
||||
if (ota_write_err != ESP_OK) {
|
||||
Serial.printf("Error: esp_ota_write failed! err=0x%d\n", err);
|
||||
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||
} else if (ota_end_err != ESP_OK) {
|
||||
Serial.printf("Error: esp_ota_end failed! err=0x%d. Image is invalid",
|
||||
ota_end_err);
|
||||
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||
}
|
||||
|
||||
err = esp_ota_set_boot_partition(update_partition);
|
||||
if (err != ESP_OK) {
|
||||
Serial.printf("esp_ota_set_boot_partition failed! err=0x%d\n", err);
|
||||
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||
}
|
||||
return OtaUpdateOutcome::UPDATE_PERFORMED;
|
||||
}
|
||||
|
||||
void cleanupHttp(esp_http_client_handle_t client) {
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
@ -1,5 +1,5 @@
|
||||
name=AirGradient Air Quality Sensor
|
||||
version=3.1.13
|
||||
version=3.1.16
|
||||
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.
|
||||
|
@ -59,9 +59,20 @@ bool AgApiClient::fetchServerConfiguration(void) {
|
||||
#else
|
||||
HTTPClient client;
|
||||
client.setTimeout(timeoutMs);
|
||||
if (client.begin(uri) == false) {
|
||||
getConfigFailed = true;
|
||||
return false;
|
||||
if (apiRootChanged) {
|
||||
// If apiRoot is changed, assume not using https
|
||||
if (client.begin(uri) == false) {
|
||||
logError("Begin HTTPClient failed (GET)");
|
||||
getConfigFailed = true;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// By default, airgradient using https
|
||||
if (client.begin(uri, AG_SERVER_ROOT_CA) == false) {
|
||||
logError("Begin HTTPClient using tls failed (GET)");
|
||||
getConfigFailed = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -90,8 +101,6 @@ bool AgApiClient::fetchServerConfiguration(void) {
|
||||
String respContent = client.getString();
|
||||
client.end();
|
||||
|
||||
// logInfo("Get configuration: " + respContent);
|
||||
|
||||
/** Parse configuration and return result */
|
||||
return config.parse(respContent, false);
|
||||
}
|
||||
@ -115,22 +124,37 @@ bool AgApiClient::postToServer(String data) {
|
||||
}
|
||||
|
||||
String uri = apiRoot + "/sensors/airgradient:" + ag->deviceId() + "/measures";
|
||||
// logInfo("Post uri: " + uri);
|
||||
// logInfo("Post data: " + data);
|
||||
|
||||
WiFiClient wifiClient;
|
||||
#ifdef ESP8266
|
||||
HTTPClient client;
|
||||
client.setTimeout(timeoutMs);
|
||||
if (client.begin(wifiClient, uri.c_str()) == false) {
|
||||
logError("Init client failed");
|
||||
WiFiClient wifiClient;
|
||||
if (client.begin(wifiClient, uri) == false) {
|
||||
getConfigFailed = true;
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
HTTPClient client;
|
||||
client.setTimeout(timeoutMs);
|
||||
if (apiRootChanged) {
|
||||
// If apiRoot is changed, assume not using https
|
||||
if (client.begin(uri) == false) {
|
||||
logError("Begin HTTPClient failed (POST)");
|
||||
getConfigFailed = true;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// By default, airgradient using https
|
||||
if (client.begin(uri, AG_SERVER_ROOT_CA) == false) {
|
||||
logError("Begin HTTPClient using tls failed (POST)");
|
||||
getConfigFailed = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
client.addHeader("content-type", "application/json");
|
||||
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)) {
|
||||
@ -189,7 +213,10 @@ bool AgApiClient::sendPing(int rssi, int bootCount) {
|
||||
|
||||
String AgApiClient::getApiRoot() const { return apiRoot; }
|
||||
|
||||
void AgApiClient::setApiRoot(const String &apiRoot) { this->apiRoot = apiRoot; }
|
||||
void AgApiClient::setApiRoot(const String &apiRoot) {
|
||||
this->apiRootChanged = true;
|
||||
this->apiRoot = apiRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set http request timeout. (Default: 10s)
|
||||
|
@ -20,8 +20,14 @@ 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.
|
||||
|
@ -253,7 +253,7 @@ void Configuration::loadConfig(void) {
|
||||
}
|
||||
file.close();
|
||||
} else {
|
||||
// SPIFFS.format();
|
||||
SPIFFS.format();
|
||||
}
|
||||
#endif
|
||||
toConfig(buf);
|
||||
|
147
src/AgValue.cpp
147
src/AgValue.cpp
@ -2,7 +2,6 @@
|
||||
#include "AgConfigure.h"
|
||||
#include "AirGradient.h"
|
||||
#include "App/AppDef.h"
|
||||
#include "SPIFFS.h"
|
||||
|
||||
#define json_prop_pmFirmware "firmware"
|
||||
#define json_prop_pm01Ae "pm01"
|
||||
@ -28,6 +27,12 @@
|
||||
#define json_prop_noxRaw "noxRaw"
|
||||
#define json_prop_co2 "rco2"
|
||||
|
||||
Measurements::Measurements() {
|
||||
#ifndef ESP8266
|
||||
_resetReason = (int)ESP_RST_UNKNOWN;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Measurements::maxPeriod(MeasurementType type, int max) {
|
||||
switch (type) {
|
||||
case Temperature:
|
||||
@ -602,8 +607,8 @@ String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi,
|
||||
}
|
||||
}
|
||||
|
||||
root["boot"] = bootCount;
|
||||
root["bootCount"] = bootCount;
|
||||
root["boot"] = _bootCount;
|
||||
root["bootCount"] = _bootCount;
|
||||
root["wifi"] = rssi;
|
||||
|
||||
if (localServer) {
|
||||
@ -613,6 +618,11 @@ String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi,
|
||||
root["serialno"] = ag.deviceId();
|
||||
root["firmware"] = ag.getVersion();
|
||||
root["model"] = AgFirmwareModeName(fwMode);
|
||||
} else {
|
||||
#ifndef ESP8266
|
||||
root["resetReason"] = _resetReason;
|
||||
root["freeHeap"] = ESP.getFreeHeap();
|
||||
#endif
|
||||
}
|
||||
|
||||
String result = JSON.stringify(root);
|
||||
@ -1068,95 +1078,48 @@ JSONVar Measurements::buildPMS(AirGradient &ag, int ch, bool allCh, bool withTem
|
||||
|
||||
void Measurements::setDebug(bool debug) { _debug = debug; }
|
||||
|
||||
bool Measurements::resetLocalStorage() {
|
||||
if (!SPIFFS.remove(FILE_PATH)) {
|
||||
Serial.println("Failed reset local storage");
|
||||
return false;
|
||||
int Measurements::bootCount() { return _bootCount; }
|
||||
|
||||
void Measurements::setBootCount(int bootCount) { _bootCount = bootCount; }
|
||||
|
||||
#ifndef ESP8266
|
||||
void Measurements::setResetReason(esp_reset_reason_t reason) {
|
||||
switch (reason) {
|
||||
case ESP_RST_UNKNOWN:
|
||||
Serial.println("Reset reason: ESP_RST_UNKNOWN");
|
||||
break;
|
||||
case ESP_RST_POWERON:
|
||||
Serial.println("Reset reason: ESP_RST_POWERON");
|
||||
break;
|
||||
case ESP_RST_EXT:
|
||||
Serial.println("Reset reason: ESP_RST_EXT");
|
||||
break;
|
||||
case ESP_RST_SW:
|
||||
Serial.println("Reset reason: ESP_RST_SW");
|
||||
break;
|
||||
case ESP_RST_PANIC:
|
||||
Serial.println("Reset reason: ESP_RST_PANIC");
|
||||
break;
|
||||
case ESP_RST_INT_WDT:
|
||||
Serial.println("Reset reason: ESP_RST_INT_WDT");
|
||||
break;
|
||||
case ESP_RST_TASK_WDT:
|
||||
Serial.println("Reset reason: ESP_RST_TASK_WDT");
|
||||
break;
|
||||
case ESP_RST_WDT:
|
||||
Serial.println("Reset reason: ESP_RST_WDT");
|
||||
break;
|
||||
case ESP_RST_BROWNOUT:
|
||||
Serial.println("Reset reason: ESP_RST_BROWNOUT");
|
||||
break;
|
||||
case ESP_RST_SDIO:
|
||||
Serial.println("Reset reason: ESP_RST_SDIO");
|
||||
break;
|
||||
default:
|
||||
Serial.println("Reset reason: unknown");
|
||||
break;
|
||||
}
|
||||
|
||||
Serial.println("Success reset local storage");
|
||||
return true;
|
||||
_resetReason = (int)reason;
|
||||
}
|
||||
|
||||
bool Measurements::saveLocalStorage(AirGradient &ag, Configuration &config) {
|
||||
int spiffUsed = ((float)SPIFFS.usedBytes() / (float)SPIFFS.totalBytes()) * 100.0;
|
||||
Serial.printf("%d | %d\n", SPIFFS.totalBytes(), SPIFFS.usedBytes());
|
||||
Serial.printf("SPIFF used %d%%\n", spiffUsed);
|
||||
if (spiffUsed > 98) {
|
||||
Serial.println("SPIFF used already on maximum");
|
||||
return false;
|
||||
}
|
||||
|
||||
File file;
|
||||
if (!SPIFFS.exists(FILE_PATH)) {
|
||||
file = SPIFFS.open(FILE_PATH, FILE_APPEND, true);
|
||||
file.println(
|
||||
"datetime,pm0.3 count,pm2.5,temp,rhum,co2,tvoc,tvoc raw,nox,nox raw"); // csv header
|
||||
Serial.println("New measurements file created");
|
||||
} else {
|
||||
file = SPIFFS.open(FILE_PATH, FILE_APPEND, false);
|
||||
}
|
||||
|
||||
float pm25 = getCorrectedPM25(ag, config, true);
|
||||
|
||||
// Save new measurements
|
||||
char buff[100] = {0};
|
||||
sprintf(buff, "%s,%.2f,%.2f,%.2f,%.2f,%d,%d,%d,%d,%d\n\0", ag.getCurrentTime().c_str(),
|
||||
ag.round2(_pm_03_pc[0].update.avg), ag.round2(pm25),
|
||||
ag.round2(_temperature[0].update.avg), ag.round2(_humidity[0].update.avg),
|
||||
(int)round(_co2.update.avg), (int)round(_tvoc.update.avg),
|
||||
(int)round(_tvoc_raw.update.avg), (int)round(_nox.update.avg),
|
||||
(int)round(_nox_raw.update.avg));
|
||||
|
||||
size_t len = strlen(buff);
|
||||
if (file.write((const uint8_t *)buff, len) != len) {
|
||||
Serial.println("Write new measurements failed!");
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
file.close();
|
||||
Serial.println("Success save measurements to local storage");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
char *Measurements::getLocalStorage() {
|
||||
char *buf = nullptr;
|
||||
bool success = false;
|
||||
|
||||
if (!SPIFFS.exists(FILE_PATH)) {
|
||||
Serial.println("No measurements file exists yet");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
File file = SPIFFS.open(FILE_PATH);
|
||||
if (file && !file.isDirectory()) {
|
||||
// Allocate memory
|
||||
buf = new char[file.size() + 1];
|
||||
if (buf == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
memset(buf, 0, file.size() + 1);
|
||||
// Retrieve data from the file
|
||||
if (file.readBytes(buf, file.size()) != file.size()) {
|
||||
Serial.println("Reading measurements file: failed - size not match");
|
||||
} else {
|
||||
Serial.println("Reading measurements file: success");
|
||||
success = true;
|
||||
}
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
Serial.println("Reading measurements file failed");
|
||||
if (buf != nullptr) {
|
||||
delete buf;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// NOTE: Don't forget to free
|
||||
return buf;
|
||||
}
|
||||
#endif
|
@ -34,7 +34,7 @@ private:
|
||||
};
|
||||
|
||||
public:
|
||||
Measurements() {}
|
||||
Measurements();
|
||||
~Measurements() {}
|
||||
|
||||
// Enumeration for every AG measurements
|
||||
@ -142,17 +142,17 @@ public:
|
||||
String toString(bool localServer, AgFirmwareMode fwMode, int rssi, AirGradient &ag,
|
||||
Configuration &config);
|
||||
|
||||
bool resetLocalStorage();
|
||||
bool saveLocalStorage(AirGradient &ag, Configuration &config);
|
||||
char *getLocalStorage();
|
||||
|
||||
/**
|
||||
* Set to true if want to debug every update value
|
||||
*/
|
||||
void setDebug(bool debug);
|
||||
|
||||
// TODO: update this to use setter
|
||||
int bootCount;
|
||||
int bootCount();
|
||||
void setBootCount(int bootCount);
|
||||
|
||||
#ifndef ESP8266
|
||||
void setResetReason(esp_reset_reason_t reason);
|
||||
#endif
|
||||
|
||||
private:
|
||||
// Some declared as an array (channel), because FW_MODE_O_1PPx has two PMS5003T
|
||||
@ -175,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
|
||||
|
@ -85,25 +85,3 @@ String AirGradient::deviceId(void) {
|
||||
mac.toLowerCase();
|
||||
return mac;
|
||||
}
|
||||
|
||||
void AirGradient::setCurrentTime(long epochTime) {
|
||||
// set current day/time
|
||||
struct timeval tv;
|
||||
tv.tv_sec = epochTime; // - 1020; // 17 minutes // don't know why it always off by 17 minutes
|
||||
settimeofday(&tv, NULL);
|
||||
Serial.println(epochTime);
|
||||
Serial.printf("Set current time to %s\n", getCurrentTime().c_str());
|
||||
}
|
||||
|
||||
String AirGradient::getCurrentTime() {
|
||||
// Get time
|
||||
time_t now;
|
||||
char strftime_buf[64];
|
||||
struct tm timeinfo;
|
||||
time(&now);
|
||||
// Format
|
||||
localtime_r(&now, &timeinfo);
|
||||
strftime(strftime_buf, sizeof(strftime_buf), "%d/%m %H:%M:%S", &timeinfo);
|
||||
|
||||
return String(strftime_buf);
|
||||
}
|
@ -15,7 +15,46 @@
|
||||
#include "Main/utils.h"
|
||||
|
||||
#ifndef GIT_VERSION
|
||||
#define GIT_VERSION "3.1.13-snap"
|
||||
#define GIT_VERSION "3.1.16-snap"
|
||||
#endif
|
||||
|
||||
#ifndef ESP8266
|
||||
// Airgradient server root ca certificate
|
||||
const char *const AG_SERVER_ROOT_CA =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIF4jCCA8oCCQD7MgvcaVWxkTANBgkqhkiG9w0BAQsFADCBsjELMAkGA1UEBhMC\n"
|
||||
"VEgxEzARBgNVBAgMCkNoaWFuZyBNYWkxEDAOBgNVBAcMB01hZSBSaW0xGTAXBgNV\n"
|
||||
"BAoMEEFpckdyYWRpZW50IEx0ZC4xFDASBgNVBAsMC1NlbnNvciBMYWJzMSgwJgYD\n"
|
||||
"VQQDDB9BaXJHcmFkaWVudCBTZW5zb3IgTGFicyBSb290IENBMSEwHwYJKoZIhvcN\n"
|
||||
"AQkBFhJjYUBhaXJncmFkaWVudC5jb20wHhcNMjEwOTE3MTE0NDE3WhcNNDEwOTEy\n"
|
||||
"MTE0NDE3WjCBsjELMAkGA1UEBhMCVEgxEzARBgNVBAgMCkNoaWFuZyBNYWkxEDAO\n"
|
||||
"BgNVBAcMB01hZSBSaW0xGTAXBgNVBAoMEEFpckdyYWRpZW50IEx0ZC4xFDASBgNV\n"
|
||||
"BAsMC1NlbnNvciBMYWJzMSgwJgYDVQQDDB9BaXJHcmFkaWVudCBTZW5zb3IgTGFi\n"
|
||||
"cyBSb290IENBMSEwHwYJKoZIhvcNAQkBFhJjYUBhaXJncmFkaWVudC5jb20wggIi\n"
|
||||
"MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC6XkVQ4O9d5GcUjPYRgF/uaY6O\n"
|
||||
"5ry1xCGvotxkEeKkBk99lB1oNUUfNsP5bwuDci4XKfY9Ro6/jmkfHSVcPAwUnjAt\n"
|
||||
"BcHqZtA/cMXykaynf9yXPxPQN7XLu/Rk32RIfb90sIGS318xgNziCYvzWZmlxpxc\n"
|
||||
"3gUcAgGtamlgZ6wD3yOHVo8B9aFNvmP16QwkUm8fKDHunJG+iX2Bxa4ka5FJovhG\n"
|
||||
"TnUwtso6Vrn0JaWF9qWcPZE0JZMjFW8PYRriyJmHwr/nAXfPPKphD1oRO+oA7/jq\n"
|
||||
"dYkrJw6+OHfFXnPB1xkeh4OPBzcCZHT5XWNfwBYazYpjcJa9ngGFSmg8lX1ac23C\n"
|
||||
"zea1XJmSrPwbZbWxoQznnf7Y78mRjruYKgSP8rf74KYvBe/HGPL5NQyXQ3l6kwmu\n"
|
||||
"CCUqfcC0wCWEtWESxwSdFE2qQii8CZ12kQExzvR2PrOIyKQYSdkGx9/RBZtAVPXP\n"
|
||||
"hmLuRBQYHrF5Cxf1oIbBK8OMoNVgBm6ftt15t9Sq9dH5Aup2YR6WEJkVaYkYzZzK\n"
|
||||
"X7M+SQcdbXp+hAO8PFpABJxkaDAO2kiB5Ov7pDYPAcmNFqnJT48AY0TZJeVeCa5W\n"
|
||||
"sIv3lPvB/XcFjP0+aZxxNSEEwpGPUYgvKUYUUmb0NammlYQwZHKaShPEmZ3UZ0bp\n"
|
||||
"VNt4p6374nzO376sSwIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQB/LfBPgTx7xKQB\n"
|
||||
"JNMUhah17AFAn050NiviGJOHdPQely6u3DmJGg+ijEVlPWO1FEW3it+LOuNP5zOu\n"
|
||||
"bhq8paTYIxPxtALIxw5ksykX9woDuX3H6FF9mPdQIbL7ft+3ZtZ4FWPui9dUtaPe\n"
|
||||
"ZBmDFDi4U29nhWZK68JSp5QkWjfaYLV/vtag7120eVyGEPFZ0UAuTUNqpw+stOt9\n"
|
||||
"gJ2ZxNx13xJ8ZnLK7qz1crPe8/8IVAdxbVLoY7JaWPLc//+VF+ceKicy8+4gV7zN\n"
|
||||
"Gnq2IyM+CHFz8VYMLbW+3eVp4iJjTa72vae116kozboEIUVN9rgLqIKyVqQXiuoN\n"
|
||||
"g3xY+yfncPB2+H/+lfyy6mepPIfgksd3+KeNxFADSc5EVY2JKEdorRodnAh7a8K6\n"
|
||||
"WjTYgq+GjWXU2uQW2SyPt6Tu33OT8nBnu3NB80eT8WXgdVCkgsuyCuLvNRf1Xmze\n"
|
||||
"igvurpU6JmQ1GlLgLJo8omJHTh1zIbkR9injPYne2v9ciHCoP6+LDEqe+rOsvPCB\n"
|
||||
"C/o/iZ4svmYX4fWGuU7GgqZE8hhrC3+GdOTf2ADC752cYCZxBidXGtkrGNoHQKmQ\n"
|
||||
"KCOMFBxZIvWteB3tUo3BKYz1D2CvKWz1wV4moc5JHkOgS+jqxhvOkQ/vfQBQ1pUY\n"
|
||||
"TMui9BSwU7B1G2XjdLbfF3Dc67zaSg==\n"
|
||||
"-----END CERTIFICATE-----\n";
|
||||
#endif
|
||||
|
||||
/**
|
||||
@ -173,9 +212,6 @@ public:
|
||||
*/
|
||||
String deviceId(void);
|
||||
|
||||
void setCurrentTime(long epochTime);
|
||||
String getCurrentTime();
|
||||
|
||||
private:
|
||||
BoardType boardType;
|
||||
};
|
||||
|
171
src/OtaHandler.cpp
Normal file
171
src/OtaHandler.cpp
Normal file
@ -0,0 +1,171 @@
|
||||
#include "OtaHandler.h"
|
||||
|
||||
#ifndef ESP8266 // Only for esp32 based mcu
|
||||
|
||||
#include "AirGradient.h"
|
||||
|
||||
void OtaHandler::setHandlerCallback(OtaHandlerCallback_t callback) { _callback = callback; }
|
||||
|
||||
void OtaHandler::updateFirmwareIfOutdated(String deviceId) {
|
||||
String url =
|
||||
"https://hw.airgradient.com/sensors/airgradient:" + deviceId + "/generic/os/firmware.bin";
|
||||
url += "?current_firmware=";
|
||||
url += GIT_VERSION;
|
||||
char urlAsChar[URL_BUF_SIZE];
|
||||
url.toCharArray(urlAsChar, URL_BUF_SIZE);
|
||||
Serial.printf("checking for new OTA update @ %s\n", urlAsChar);
|
||||
|
||||
esp_http_client_config_t config = {};
|
||||
config.url = urlAsChar;
|
||||
config.cert_pem = AG_SERVER_ROOT_CA;
|
||||
OtaUpdateOutcome ret = attemptToPerformOta(&config);
|
||||
Serial.println(ret);
|
||||
if (_callback) {
|
||||
switch (ret) {
|
||||
case OtaUpdateOutcome::UPDATE_PERFORMED:
|
||||
_callback(OtaState::OTA_STATE_SUCCESS, "");
|
||||
break;
|
||||
case OtaUpdateOutcome::UPDATE_SKIPPED:
|
||||
_callback(OtaState::OTA_STATE_SKIP, "");
|
||||
break;
|
||||
case OtaUpdateOutcome::ALREADY_UP_TO_DATE:
|
||||
_callback(OtaState::OTA_STATE_UP_TO_DATE, "");
|
||||
break;
|
||||
case OtaUpdateOutcome::UPDATE_FAILED:
|
||||
_callback(OtaState::OTA_STATE_FAIL, "");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OtaHandler::OtaUpdateOutcome
|
||||
OtaHandler::attemptToPerformOta(const esp_http_client_config_t *config) {
|
||||
esp_http_client_handle_t client = esp_http_client_init(config);
|
||||
if (client == NULL) {
|
||||
Serial.println("Failed to initialize HTTP connection");
|
||||
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||
}
|
||||
|
||||
esp_err_t err = esp_http_client_open(client, 0);
|
||||
if (err != ESP_OK) {
|
||||
esp_http_client_cleanup(client);
|
||||
Serial.printf("Failed to open HTTP connection: %s\n", esp_err_to_name(err));
|
||||
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||
}
|
||||
esp_http_client_fetch_headers(client);
|
||||
|
||||
int httpStatusCode = esp_http_client_get_status_code(client);
|
||||
if (httpStatusCode == 304) {
|
||||
Serial.println("Firmware is already up to date");
|
||||
cleanupHttp(client);
|
||||
return OtaUpdateOutcome::ALREADY_UP_TO_DATE;
|
||||
} else if (httpStatusCode != 200) {
|
||||
Serial.printf("Firmware update skipped, the server returned %d\n", httpStatusCode);
|
||||
cleanupHttp(client);
|
||||
return OtaUpdateOutcome::UPDATE_SKIPPED;
|
||||
}
|
||||
|
||||
esp_ota_handle_t update_handle = 0;
|
||||
const esp_partition_t *update_partition = NULL;
|
||||
Serial.println("Starting OTA update ...");
|
||||
update_partition = esp_ota_get_next_update_partition(NULL);
|
||||
if (update_partition == NULL) {
|
||||
Serial.println("Passive OTA partition not found");
|
||||
cleanupHttp(client);
|
||||
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||
}
|
||||
Serial.printf("Writing to partition subtype %d at offset 0x%x\n", update_partition->subtype,
|
||||
update_partition->address);
|
||||
|
||||
err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle);
|
||||
if (err != ESP_OK) {
|
||||
Serial.printf("esp_ota_begin failed, error=%d\n", err);
|
||||
cleanupHttp(client);
|
||||
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||
}
|
||||
|
||||
esp_err_t ota_write_err = ESP_OK;
|
||||
char *upgrade_data_buf = (char *)malloc(OTA_BUF_SIZE);
|
||||
if (!upgrade_data_buf) {
|
||||
Serial.println("Couldn't allocate memory for data buffer");
|
||||
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||
}
|
||||
|
||||
int binary_file_len = 0;
|
||||
int totalSize = esp_http_client_get_content_length(client);
|
||||
Serial.println("File size: " + String(totalSize) + String(" bytes"));
|
||||
|
||||
// Show display start update new firmware.
|
||||
if (_callback) {
|
||||
_callback(OtaState::OTA_STATE_BEGIN, "");
|
||||
}
|
||||
|
||||
// Download file and write new firmware to OTA partition
|
||||
uint32_t lastUpdate = millis();
|
||||
while (1) {
|
||||
int data_read = esp_http_client_read(client, upgrade_data_buf, OTA_BUF_SIZE);
|
||||
if (data_read == 0) {
|
||||
if (_callback) {
|
||||
_callback(OtaState::OTA_STATE_PROCESSING, String(100));
|
||||
}
|
||||
Serial.println("Connection closed, all data received");
|
||||
break;
|
||||
}
|
||||
if (data_read < 0) {
|
||||
Serial.println("Data read error");
|
||||
if (_callback) {
|
||||
_callback(OtaState::OTA_STATE_FAIL, "");
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (data_read > 0) {
|
||||
ota_write_err = esp_ota_write(update_handle, (const void *)upgrade_data_buf, data_read);
|
||||
if (ota_write_err != ESP_OK) {
|
||||
if (_callback) {
|
||||
_callback(OtaState::OTA_STATE_FAIL, "");
|
||||
}
|
||||
break;
|
||||
}
|
||||
binary_file_len += data_read;
|
||||
|
||||
int percent = (binary_file_len * 100) / totalSize;
|
||||
uint32_t ms = (uint32_t)(millis() - lastUpdate);
|
||||
if (ms >= 250) {
|
||||
// sm.executeOTA(StateMachine::OtaState::OTA_STATE_PROCESSING, "",
|
||||
// percent);
|
||||
if (_callback) {
|
||||
_callback(OtaState::OTA_STATE_PROCESSING, String(percent));
|
||||
}
|
||||
lastUpdate = millis();
|
||||
}
|
||||
}
|
||||
}
|
||||
free(upgrade_data_buf);
|
||||
cleanupHttp(client);
|
||||
Serial.printf("# of bytes written: %d\n", binary_file_len);
|
||||
|
||||
esp_err_t ota_end_err = esp_ota_end(update_handle);
|
||||
if (ota_write_err != ESP_OK) {
|
||||
Serial.printf("Error: esp_ota_write failed! err=0x%d\n", err);
|
||||
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||
} else if (ota_end_err != ESP_OK) {
|
||||
Serial.printf("Error: esp_ota_end failed! err=0x%d. Image is invalid", ota_end_err);
|
||||
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||
}
|
||||
|
||||
err = esp_ota_set_boot_partition(update_partition);
|
||||
if (err != ESP_OK) {
|
||||
Serial.printf("esp_ota_set_boot_partition failed! err=0x%d\n", err);
|
||||
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||
}
|
||||
return OtaUpdateOutcome::UPDATE_PERFORMED;
|
||||
}
|
||||
|
||||
void OtaHandler::cleanupHttp(esp_http_client_handle_t client) {
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
}
|
||||
|
||||
#endif
|
43
src/OtaHandler.h
Normal file
43
src/OtaHandler.h
Normal file
@ -0,0 +1,43 @@
|
||||
#ifndef OTA_HANDLER_H
|
||||
#define OTA_HANDLER_H
|
||||
#ifndef ESP8266 // Only for esp32 based mcu
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <esp_err.h>
|
||||
#include <esp_http_client.h>
|
||||
#include <esp_ota_ops.h>
|
||||
|
||||
#define OTA_BUF_SIZE 1024
|
||||
#define URL_BUF_SIZE 256
|
||||
|
||||
class OtaHandler {
|
||||
public:
|
||||
enum OtaState {
|
||||
OTA_STATE_BEGIN,
|
||||
OTA_STATE_FAIL,
|
||||
OTA_STATE_SKIP,
|
||||
OTA_STATE_UP_TO_DATE,
|
||||
OTA_STATE_PROCESSING,
|
||||
OTA_STATE_SUCCESS
|
||||
};
|
||||
|
||||
typedef void (*OtaHandlerCallback_t)(OtaState state, String message);
|
||||
void setHandlerCallback(OtaHandlerCallback_t callback);
|
||||
void updateFirmwareIfOutdated(String deviceId);
|
||||
|
||||
private:
|
||||
OtaHandlerCallback_t _callback;
|
||||
|
||||
enum OtaUpdateOutcome {
|
||||
UPDATE_PERFORMED = 0,
|
||||
ALREADY_UP_TO_DATE,
|
||||
UPDATE_FAILED,
|
||||
UPDATE_SKIPPED
|
||||
}; // Internal use
|
||||
|
||||
OtaUpdateOutcome attemptToPerformOta(const esp_http_client_config_t *config);
|
||||
void cleanupHttp(esp_http_client_handle_t client);
|
||||
};
|
||||
|
||||
#endif // ESP8266
|
||||
#endif // OTA_HANDLER_H
|
Reference in New Issue
Block a user