diff --git a/examples/OneOpenAir/OneOpenAir.ino b/examples/OneOpenAir/OneOpenAir.ino index 240cfaf..52a4190 100644 --- a/examples/OneOpenAir/OneOpenAir.ino +++ b/examples/OneOpenAir/OneOpenAir.ino @@ -82,7 +82,7 @@ static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine, configuration); static OpenMetrics openMetrics(measurements, configuration, wifiConnector, apiClient); -static OtaHandler otaHandler; +static OtaHandler otaHandler(stateMachine, configuration); static LocalServer localServer(Serial, openMetrics, measurements, configuration, wifiConnector); @@ -157,6 +157,7 @@ void setup() { apiClient.setAirGradient(ag); openMetrics.setAirGradient(ag); localServer.setAirGraident(ag); + otaHandler.setAirGradient(ag); /** Connecting wifi */ bool connectToWifi = false; @@ -184,7 +185,7 @@ void setup() { #ifdef ESP8266 // ota not supported #else - otaHandler.updateFirmwareIfOutdated(ag->deviceId()); + // otaHandler.updateFirmwareIfOutdated(ag->deviceId()); #endif apiClient.fetchServerConfiguration(); @@ -733,6 +734,11 @@ static void configUpdateHandle() { String(configuration.getDisplayBrightness())); } + String newVer = configuration.newFirmwareVersion(); + if (newVer.length()) { + otaHandler.updateFirmwareIfOutdated(newVer); + } + appDispHandler(); appLedHandler(); } diff --git a/examples/OneOpenAir/OtaHandler.h b/examples/OneOpenAir/OtaHandler.h index 067d5bf..8775503 100644 --- a/examples/OneOpenAir/OtaHandler.h +++ b/examples/OneOpenAir/OtaHandler.h @@ -5,8 +5,11 @@ #include #include #include +#include "AgConfigure.h" +#include "AgStateMachine.h" +#include "AirGradient.h" -#define OTA_BUF_SIZE 512 +#define OTA_BUF_SIZE 1024 #define URL_BUF_SIZE 256 enum OtaUpdateOutcome { @@ -18,10 +21,23 @@ enum OtaUpdateOutcome { class OtaHandler { 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 + - "/generic/os/firmware.bin"; + void updateFirmwareIfOutdated(String newVersion) { + 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 += GIT_VERSION; char urlAsChar[URL_BUF_SIZE]; @@ -30,16 +46,31 @@ public: esp_http_client_config_t config = {}; 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); if (ret == OtaUpdateOutcome::UPDATE_PERFORMED) { 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(); } } 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); if (client == NULL) { Serial.println("Failed to initialize HTTP connection"); @@ -94,6 +125,14 @@ private: } 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) { int data_read = esp_http_client_read(client, upgrade_data_buf, OTA_BUF_SIZE); @@ -103,16 +142,25 @@ private: } if (data_read < 0) { Serial.println("Data read error"); + sm.executeOTA(StateMachine::OtaState::OTA_STATE_FAIL, "", 0); 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) { + sm.executeOTA(StateMachine::OtaState::OTA_STATE_FAIL, "", 0); break; } 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); diff --git a/src/AgConfigure.cpp b/src/AgConfigure.cpp index 7027895..2c3cdad 100644 --- a/src/AgConfigure.cpp +++ b/src/AgConfigure.cpp @@ -6,6 +6,7 @@ #else #include "EEPROM.h" #endif +#include #define EEPROM_CONFIG_SIZE 512 #define CONFIG_FILE_NAME "/cfg.bin" @@ -163,6 +164,7 @@ void Configuration::defaultConfig(void) { config.temperatureUnit = 'c'; config.ledBarBrightness = 100; config.displayBrightness = 100; + config.lastOta = 0; saveConfig(); } @@ -171,7 +173,10 @@ void Configuration::defaultConfig(void) { * @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 @@ -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) { udpated = true; saveConfig(); @@ -938,3 +955,52 @@ bool Configuration::isDisplayBrightnessChanged(void) { displayBrightnessChanged = false; 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; +} diff --git a/src/AgConfigure.h b/src/AgConfigure.h index 6759ba6..bb07626 100644 --- a/src/AgConfigure.h +++ b/src/AgConfigure.h @@ -28,6 +28,7 @@ private: int tvocLearningOffset; int noxLearningOffset; char temperatureUnit; // 'f' or 'c' + time_t lastOta; uint32_t _check; }; @@ -40,6 +41,7 @@ private: bool _tvocLearningOffsetChanged; bool ledBarBrightnessChanged = false; bool displayBrightnessChanged = false; + String otaNewFirmwareVersion; AirGradient* ag; @@ -97,6 +99,9 @@ public: int getLedBarBrightness(void); bool isDisplayBrightnessChanged(void); int getDisplayBrightness(void); + int getLastOta(void); + void updateLastOta(void); + String newFirmwareVersion(void); }; #endif /** _AG_CONFIG_H_ */ diff --git a/src/AgOledDisplay.cpp b/src/AgOledDisplay.cpp index 8a033bb..3e20c54 100644 --- a/src/AgOledDisplay.cpp +++ b/src/AgOledDisplay.cpp @@ -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 * @@ -314,3 +324,42 @@ void OledDisplay::showWiFiQrCode(String content, String label) { void OledDisplay::setBrightness(int percent) { 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()); +} diff --git a/src/AgOledDisplay.h b/src/AgOledDisplay.h index 69fb398..4a5044b 100644 --- a/src/AgOledDisplay.h +++ b/src/AgOledDisplay.h @@ -16,6 +16,9 @@ private: Measurements &value; void showTempHum(bool hasStatus); + void setCentralText(int y, String text); + void setCentralText(int y, const char *text); + public: OledDisplay(Configuration &config, Measurements &value, Stream &log); @@ -33,6 +36,10 @@ public: void showDashboard(const char *status); void showWiFiQrCode(String content, String label); void setBrightness(int percent); + void showNewFirmwareVersion(String version); + void showNewFirmwareUpdating(String percent); + void showNewFirmwareSuccess(String count); + void showNewFirmwareFailed(void); }; #endif /** _AG_OLED_DISPLAY_H_ */ diff --git a/src/AgStateMachine.cpp b/src/AgStateMachine.cpp index 5efb6f7..57635fe 100644 --- a/src/AgStateMachine.cpp +++ b/src/AgStateMachine.cpp @@ -759,3 +759,47 @@ void StateMachine::executeCo2Calibration(void) { void StateMachine::executeLedBarTest(void) { 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; + } +} diff --git a/src/AgStateMachine.h b/src/AgStateMachine.h index 7299bb3..a783810 100644 --- a/src/AgStateMachine.h +++ b/src/AgStateMachine.h @@ -49,6 +49,14 @@ public: AgStateMachineState getLedState(void); void executeCo2Calibration(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_ */ diff --git a/src/AgWiFiConnector.cpp b/src/AgWiFiConnector.cpp index 0e7f291..8ac9495 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -1,5 +1,6 @@ #include "AgWiFiConnector.h" #include "Libraries/WiFiManager/WiFiManager.h" +#include #define WIFI_CONNECT_COUNTDOWN_MAX 180 #define WIFI_HOTSPOT_PASSWORD_DEFAULT "cleanair" @@ -158,6 +159,11 @@ bool WifiConnector::connect(void) { config.setPostToAirGradient(result != "T"); } 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 _wifiProcess(); diff --git a/src/App/AppDef.h b/src/App/AppDef.h index 7009f92..2cc8ee8 100644 --- a/src/App/AppDef.h +++ b/src/App/AppDef.h @@ -60,6 +60,9 @@ enum AgStateMachineState { /* LED bar testing */ AgStateMachineLedBarTest, + /** OTA perform, show display status */ + AgStateMachineOtaPerform, + /** LED: Show working state. * Display: Show dashboard */ AgStateMachineNormal,