Implement regular OTA update attempt / indicate OTA processing on display

This commit is contained in:
Phat Nguyen
2024-05-02 10:19:49 +07:00
parent 0acb7d470d
commit 4493156739
10 changed files with 252 additions and 10 deletions

View File

@ -82,7 +82,7 @@ static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine,
configuration); configuration);
static OpenMetrics openMetrics(measurements, configuration, wifiConnector, static OpenMetrics openMetrics(measurements, configuration, wifiConnector,
apiClient); apiClient);
static OtaHandler otaHandler; static OtaHandler otaHandler(stateMachine, configuration);
static LocalServer localServer(Serial, openMetrics, measurements, configuration, static LocalServer localServer(Serial, openMetrics, measurements, configuration,
wifiConnector); wifiConnector);
@ -157,6 +157,7 @@ void setup() {
apiClient.setAirGradient(ag); apiClient.setAirGradient(ag);
openMetrics.setAirGradient(ag); openMetrics.setAirGradient(ag);
localServer.setAirGraident(ag); localServer.setAirGraident(ag);
otaHandler.setAirGradient(ag);
/** Connecting wifi */ /** Connecting wifi */
bool connectToWifi = false; bool connectToWifi = false;
@ -184,7 +185,7 @@ void setup() {
#ifdef ESP8266 #ifdef ESP8266
// ota not supported // ota not supported
#else #else
otaHandler.updateFirmwareIfOutdated(ag->deviceId()); // otaHandler.updateFirmwareIfOutdated(ag->deviceId());
#endif #endif
apiClient.fetchServerConfiguration(); apiClient.fetchServerConfiguration();
@ -733,6 +734,11 @@ static void configUpdateHandle() {
String(configuration.getDisplayBrightness())); String(configuration.getDisplayBrightness()));
} }
String newVer = configuration.newFirmwareVersion();
if (newVer.length()) {
otaHandler.updateFirmwareIfOutdated(newVer);
}
appDispHandler(); appDispHandler();
appLedHandler(); appLedHandler();
} }

View File

@ -5,8 +5,11 @@
#include <esp_err.h> #include <esp_err.h>
#include <esp_http_client.h> #include <esp_http_client.h>
#include <esp_ota_ops.h> #include <esp_ota_ops.h>
#include "AgConfigure.h"
#include "AgStateMachine.h"
#include "AirGradient.h"
#define OTA_BUF_SIZE 512 #define OTA_BUF_SIZE 1024
#define URL_BUF_SIZE 256 #define URL_BUF_SIZE 256
enum OtaUpdateOutcome { enum OtaUpdateOutcome {
@ -18,10 +21,23 @@ enum OtaUpdateOutcome {
class OtaHandler { class OtaHandler {
public: public:
void updateFirmwareIfOutdated(String deviceId) { OtaHandler(StateMachine &sm, Configuration &config)
: sm(sm), config(config) {}
void setAirGradient(AirGradient *ag) { this->ag = ag; };
String url = "http://hw.airgradient.com/sensors/airgradient:" + deviceId + void updateFirmwareIfOutdated(String newVersion) {
"/generic/os/firmware.bin"; int lastOta = config.getLastOta();
// Retry OTA after last udpate 24h
if (lastOta != 0 && lastOta < (60 * 60 * 24)) {
Serial.println("Ignore OTA cause last update is " + String(lastOta) +
String("sec"));
Serial.println("Retry again after 24h");
return;
}
String url =
"http://hw.airgradient.com/sensors/airgradient:" + ag->deviceId() +
"/generic/os/firmware.bin";
url += "?current_firmware="; url += "?current_firmware=";
url += GIT_VERSION; url += GIT_VERSION;
char urlAsChar[URL_BUF_SIZE]; char urlAsChar[URL_BUF_SIZE];
@ -30,16 +46,31 @@ public:
esp_http_client_config_t config = {}; esp_http_client_config_t config = {};
config.url = urlAsChar; config.url = urlAsChar;
esp_err_t ret = attemptToPerformOta(&config); OtaUpdateOutcome ret = attemptToPerformOta(&config, newVersion);
// Update last OTA time whatever result.
this->config.updateLastOta();
Serial.println(ret); Serial.println(ret);
if (ret == OtaUpdateOutcome::UPDATE_PERFORMED) { if (ret == OtaUpdateOutcome::UPDATE_PERFORMED) {
Serial.println("OTA update performed, restarting ..."); Serial.println("OTA update performed, restarting ...");
int i = 6;
while (i != 0) {
i = i - 1;
sm.executeOTA(StateMachine::OtaState::OTA_STATE_SUCCESS, "", i);
delay(1000);
}
esp_restart(); esp_restart();
} }
} }
private: private:
OtaUpdateOutcome attemptToPerformOta(const esp_http_client_config_t *config) { AirGradient *ag;
StateMachine &sm;
Configuration &config;
OtaUpdateOutcome attemptToPerformOta(const esp_http_client_config_t *config,
String newVersion) {
esp_http_client_handle_t client = esp_http_client_init(config); esp_http_client_handle_t client = esp_http_client_init(config);
if (client == NULL) { if (client == NULL) {
Serial.println("Failed to initialize HTTP connection"); Serial.println("Failed to initialize HTTP connection");
@ -94,6 +125,14 @@ private:
} }
int binary_file_len = 0; 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.
sm.executeOTA(StateMachine::OtaState::OTA_STATE_BEGIN, newVersion, 0);
// Download file and write new firmware to OTA partition
uint32_t lastUpdate = millis();
while (1) { while (1) {
int data_read = int data_read =
esp_http_client_read(client, upgrade_data_buf, OTA_BUF_SIZE); esp_http_client_read(client, upgrade_data_buf, OTA_BUF_SIZE);
@ -103,16 +142,25 @@ private:
} }
if (data_read < 0) { if (data_read < 0) {
Serial.println("Data read error"); Serial.println("Data read error");
sm.executeOTA(StateMachine::OtaState::OTA_STATE_FAIL, "", 0);
break; break;
} }
if (data_read > 0) { if (data_read > 0) {
ota_write_err = esp_ota_write( ota_write_err = esp_ota_write(
update_handle, (const void *)upgrade_data_buf, data_read); update_handle, (const void *)upgrade_data_buf, data_read);
if (ota_write_err != ESP_OK) { if (ota_write_err != ESP_OK) {
sm.executeOTA(StateMachine::OtaState::OTA_STATE_FAIL, "", 0);
break; break;
} }
binary_file_len += data_read; binary_file_len += data_read;
// Serial.printf("Written image length %d\n", binary_file_len);
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);
lastUpdate = millis();
}
} }
} }
free(upgrade_data_buf); free(upgrade_data_buf);

View File

@ -6,6 +6,7 @@
#else #else
#include "EEPROM.h" #include "EEPROM.h"
#endif #endif
#include <time.h>
#define EEPROM_CONFIG_SIZE 512 #define EEPROM_CONFIG_SIZE 512
#define CONFIG_FILE_NAME "/cfg.bin" #define CONFIG_FILE_NAME "/cfg.bin"
@ -163,6 +164,7 @@ void Configuration::defaultConfig(void) {
config.temperatureUnit = 'c'; config.temperatureUnit = 'c';
config.ledBarBrightness = 100; config.ledBarBrightness = 100;
config.displayBrightness = 100; config.displayBrightness = 100;
config.lastOta = 0;
saveConfig(); saveConfig();
} }
@ -171,7 +173,10 @@ void Configuration::defaultConfig(void) {
* @brief Show configuration as JSON string message over log * @brief Show configuration as JSON string message over log
* *
*/ */
void Configuration::printConfig(void) { logInfo(toString().c_str()); } void Configuration::printConfig(void) {
logInfo(toString().c_str());
logInfo("Last OTA time: " + String(config.lastOta));
}
/** /**
* @brief Construct a new Ag Configure:: Ag Configure object * @brief Construct a new Ag Configure:: Ag Configure object
@ -638,6 +643,18 @@ bool Configuration::parse(String data, bool isLocal) {
} }
} }
if (JSON.typeof_(root["targetFirmware"]) == "string") {
String newVer = root["targetFirmware"];
String curVer = String(GIT_VERSION);
if (curVer != newVer) {
logInfo("Detected new firwmare version: " + newVer);
otaNewFirmwareVersion = newVer;
udpated = true;
} else {
otaNewFirmwareVersion = String("");
}
}
if (changed) { if (changed) {
udpated = true; udpated = true;
saveConfig(); saveConfig();
@ -938,3 +955,52 @@ bool Configuration::isDisplayBrightnessChanged(void) {
displayBrightnessChanged = false; displayBrightnessChanged = false;
return changed; return changed;
} }
/**
* @brief Get number of sec from last OTA
*
* @return int < 0 is invalid, 0 mean there is no OTA trigger.
*/
int Configuration::getLastOta(void) {
struct tm timeInfo;
if (getLocalTime(&timeInfo) == false) {
logError("Get localtime failed");
return -1;
}
int curYear = timeInfo.tm_year + 1900;
if (curYear < 2024) {
logError("Current year " + String(curYear) + String(" invalid"));
return -1;
}
time_t curTime = mktime(&timeInfo);
logInfo("Last ota time: " + String(config.lastOta));
if (config.lastOta == 0) {
return 0;
}
int sec = curTime - config.lastOta;
logInfo("Last ota secconds: " + String(sec));
return sec;
}
void Configuration::updateLastOta(void) {
struct tm timeInfo;
if (getLocalTime(&timeInfo) == false) {
logError("updateLastOta: Get localtime failed");
return;
}
int curYear = timeInfo.tm_year + 1900;
if (curYear < 2024) {
logError("updateLastOta: lolcal time invalid");
return;
}
config.lastOta = mktime(&timeInfo);
logInfo("Last OTA: " + String(config.lastOta));
saveConfig();
}
String Configuration::newFirmwareVersion(void) {
String newFw = otaNewFirmwareVersion;
otaNewFirmwareVersion = String("");
return newFw;
}

View File

@ -28,6 +28,7 @@ private:
int tvocLearningOffset; int tvocLearningOffset;
int noxLearningOffset; int noxLearningOffset;
char temperatureUnit; // 'f' or 'c' char temperatureUnit; // 'f' or 'c'
time_t lastOta;
uint32_t _check; uint32_t _check;
}; };
@ -40,6 +41,7 @@ private:
bool _tvocLearningOffsetChanged; bool _tvocLearningOffsetChanged;
bool ledBarBrightnessChanged = false; bool ledBarBrightnessChanged = false;
bool displayBrightnessChanged = false; bool displayBrightnessChanged = false;
String otaNewFirmwareVersion;
AirGradient* ag; AirGradient* ag;
@ -97,6 +99,9 @@ public:
int getLedBarBrightness(void); int getLedBarBrightness(void);
bool isDisplayBrightnessChanged(void); bool isDisplayBrightnessChanged(void);
int getDisplayBrightness(void); int getDisplayBrightness(void);
int getLastOta(void);
void updateLastOta(void);
String newFirmwareVersion(void);
}; };
#endif /** _AG_CONFIG_H_ */ #endif /** _AG_CONFIG_H_ */

View File

@ -50,6 +50,16 @@ void OledDisplay::showTempHum(bool hasStatus) {
} }
} }
void OledDisplay::setCentralText(int y, String text) {
setCentralText(y, text.c_str());
}
void OledDisplay::setCentralText(int y, const char *text) {
int x = (DISP()->getWidth() - DISP()->getStrWidth(text)) / 2;
DISP()->drawStr(x, y, text);
}
/** /**
* @brief Construct a new Ag Oled Display:: Ag Oled Display object * @brief Construct a new Ag Oled Display:: Ag Oled Display object
* *
@ -314,3 +324,42 @@ void OledDisplay::showWiFiQrCode(String content, String label) {
void OledDisplay::setBrightness(int percent) { void OledDisplay::setBrightness(int percent) {
DISP()->setContrast((127 * percent) / 100); DISP()->setContrast((127 * percent) / 100);
} }
void OledDisplay::showNewFirmwareVersion(String version) {
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
setCentralText(20, "Firmware Update");
setCentralText(40, "New version");
setCentralText(60, version.c_str());
} while (DISP()->nextPage());
}
void OledDisplay::showNewFirmwareUpdating(String percent) {
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
setCentralText(20, "Firmware Update");
setCentralText(50, String("Updating... ") + percent + String("%"));
} while (DISP()->nextPage());
}
void OledDisplay::showNewFirmwareSuccess(String count) {
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
setCentralText(20, "Firmware Update");
setCentralText(40, "Success");
setCentralText(60, String("Rebooting... ") + count);
} while (DISP()->nextPage());
}
void OledDisplay::showNewFirmwareFailed(void) {
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
setCentralText(20, "Firmware Update");
setCentralText(40, "Failed");
setCentralText(60, String("Retry after 24h"));
} while (DISP()->nextPage());
}

View File

@ -16,6 +16,9 @@ private:
Measurements &value; Measurements &value;
void showTempHum(bool hasStatus); void showTempHum(bool hasStatus);
void setCentralText(int y, String text);
void setCentralText(int y, const char *text);
public: public:
OledDisplay(Configuration &config, Measurements &value, OledDisplay(Configuration &config, Measurements &value,
Stream &log); Stream &log);
@ -33,6 +36,10 @@ public:
void showDashboard(const char *status); void showDashboard(const char *status);
void showWiFiQrCode(String content, String label); void showWiFiQrCode(String content, String label);
void setBrightness(int percent); void setBrightness(int percent);
void showNewFirmwareVersion(String version);
void showNewFirmwareUpdating(String percent);
void showNewFirmwareSuccess(String count);
void showNewFirmwareFailed(void);
}; };
#endif /** _AG_OLED_DISPLAY_H_ */ #endif /** _AG_OLED_DISPLAY_H_ */

View File

@ -759,3 +759,47 @@ void StateMachine::executeCo2Calibration(void) {
void StateMachine::executeLedBarTest(void) { void StateMachine::executeLedBarTest(void) {
handleLeds(AgStateMachineLedBarTest); handleLeds(AgStateMachineLedBarTest);
} }
void StateMachine::executeOTA(StateMachine::OtaState state, String msg,
int processing) {
switch (state) {
case OtaState::OTA_STATE_BEGIN: {
if (ag->isOne()) {
disp.showNewFirmwareVersion(msg);
} else {
logInfo("New firmware: " + msg);
}
delay(2500);
break;
}
case OtaState::OTA_STATE_FAIL: {
if (ag->isOne()) {
disp.showNewFirmwareFailed();
} else {
logError("Firmware update: failed");
}
delay(2500);
break;
}
case OtaState::OTA_STATE_PROCESSING: {
if (ag->isOne()) {
disp.showNewFirmwareUpdating(String(processing));
} else {
logInfo("Firmware update: " + String(processing) + String("%"));
}
break;
}
case OtaState::OTA_STATE_SUCCESS: {
if (ag->isOne()) {
disp.showNewFirmwareSuccess(String(processing));
} else {
logInfo("Rebooting... " + String(processing));
}
break;
}
default:
break;
}
}

View File

@ -49,6 +49,14 @@ public:
AgStateMachineState getLedState(void); AgStateMachineState getLedState(void);
void executeCo2Calibration(void); void executeCo2Calibration(void);
void executeLedBarTest(void); void executeLedBarTest(void);
enum OtaState {
OTA_STATE_BEGIN,
OTA_STATE_FAIL,
OTA_STATE_PROCESSING,
OTA_STATE_SUCCESS
};
void executeOTA(OtaState state, String msg, int processing);
}; };
#endif /** _AG_STATE_MACHINE_H_ */ #endif /** _AG_STATE_MACHINE_H_ */

View File

@ -1,5 +1,6 @@
#include "AgWiFiConnector.h" #include "AgWiFiConnector.h"
#include "Libraries/WiFiManager/WiFiManager.h" #include "Libraries/WiFiManager/WiFiManager.h"
#include <time.h>
#define WIFI_CONNECT_COUNTDOWN_MAX 180 #define WIFI_CONNECT_COUNTDOWN_MAX 180
#define WIFI_HOTSPOT_PASSWORD_DEFAULT "cleanair" #define WIFI_HOTSPOT_PASSWORD_DEFAULT "cleanair"
@ -158,6 +159,11 @@ bool WifiConnector::connect(void) {
config.setPostToAirGradient(result != "T"); config.setPostToAirGradient(result != "T");
} }
hasPortalConfig = false; hasPortalConfig = false;
/** Configure internet time */
const char *ntp_server = "pool.ntp.org";
configTime(0, 0, ntp_server);
logInfo("Set internet time server: " + String(ntp_server));
} }
#else #else
_wifiProcess(); _wifiProcess();

View File

@ -60,6 +60,9 @@ enum AgStateMachineState {
/* LED bar testing */ /* LED bar testing */
AgStateMachineLedBarTest, AgStateMachineLedBarTest,
/** OTA perform, show display status */
AgStateMachineOtaPerform,
/** LED: Show working state. /** LED: Show working state.
* Display: Show dashboard */ * Display: Show dashboard */
AgStateMachineNormal, AgStateMachineNormal,