diff --git a/examples/BASIC/BASIC.ino b/examples/BASIC/BASIC.ino index 78da232..97105fb 100644 --- a/examples/BASIC/BASIC.ino +++ b/examples/BASIC/BASIC.ino @@ -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 || diff --git a/examples/BASIC/OpenMetrics.cpp b/examples/BASIC/OpenMetrics.cpp index d9ce05c..c8395c0 100644 --- a/examples/BASIC/OpenMetrics.cpp +++ b/examples/BASIC/OpenMetrics.cpp @@ -81,7 +81,8 @@ String OpenMetrics::getPayload(void) { if (config.hasSensorPMS1) { pm01 = measure.get(Measurements::PM01); - pm25 = measure.get(Measurements::PM25); + float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1); + pm25 = round(correctedPm); pm10 = measure.get(Measurements::PM10); pm03PCount = measure.get(Measurements::PM03_PC); } diff --git a/examples/DiyProIndoorV3_3/DiyProIndoorV3_3.ino b/examples/DiyProIndoorV3_3/DiyProIndoorV3_3.ino index 04430e3..bd19b86 100644 --- a/examples/DiyProIndoorV3_3/DiyProIndoorV3_3.ino +++ b/examples/DiyProIndoorV3_3/DiyProIndoorV3_3.ino @@ -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 || diff --git a/examples/DiyProIndoorV3_3/OpenMetrics.cpp b/examples/DiyProIndoorV3_3/OpenMetrics.cpp index 7f3dc84..8ec9d2f 100644 --- a/examples/DiyProIndoorV3_3/OpenMetrics.cpp +++ b/examples/DiyProIndoorV3_3/OpenMetrics.cpp @@ -81,7 +81,8 @@ String OpenMetrics::getPayload(void) { if (config.hasSensorPMS1) { pm01 = measure.get(Measurements::PM01); - pm25 = measure.get(Measurements::PM25); + float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1); + pm25 = round(correctedPm); pm10 = measure.get(Measurements::PM10); pm03PCount = measure.get(Measurements::PM03_PC); } diff --git a/examples/DiyProIndoorV4_2/DiyProIndoorV4_2.ino b/examples/DiyProIndoorV4_2/DiyProIndoorV4_2.ino index 8bfb466..f29c2cf 100644 --- a/examples/DiyProIndoorV4_2/DiyProIndoorV4_2.ino +++ b/examples/DiyProIndoorV4_2/DiyProIndoorV4_2.ino @@ -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 || diff --git a/examples/DiyProIndoorV4_2/OpenMetrics.cpp b/examples/DiyProIndoorV4_2/OpenMetrics.cpp index d9ce05c..c8395c0 100644 --- a/examples/DiyProIndoorV4_2/OpenMetrics.cpp +++ b/examples/DiyProIndoorV4_2/OpenMetrics.cpp @@ -81,7 +81,8 @@ String OpenMetrics::getPayload(void) { if (config.hasSensorPMS1) { pm01 = measure.get(Measurements::PM01); - pm25 = measure.get(Measurements::PM25); + float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1); + pm25 = round(correctedPm); pm10 = measure.get(Measurements::PM10); pm03PCount = measure.get(Measurements::PM03_PC); } diff --git a/examples/OneOpenAir/OneOpenAir.ino b/examples/OneOpenAir/OneOpenAir.ino index fce0feb..ee4b680 100644 --- a/examples/OneOpenAir/OneOpenAir.ino +++ b/examples/OneOpenAir/OneOpenAir.ino @@ -36,20 +36,21 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License */ -#include -#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 #include #include @@ -111,9 +112,8 @@ 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(); @@ -136,6 +136,10 @@ void setup() { /** Print device ID into log */ Serial.println("Serial nr: " + ag->deviceId()); + // Set reason why esp is reset + esp_reset_reason_t reason = esp_reset_reason(); + measurements.setResetReason(reason); + /** Initialize local configure */ configuration.begin(); @@ -514,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 { @@ -545,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 { @@ -555,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 { @@ -565,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 { @@ -575,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 { @@ -584,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; @@ -634,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); } @@ -1135,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()) { @@ -1149,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) { diff --git a/examples/OneOpenAir/OpenMetrics.cpp b/examples/OneOpenAir/OpenMetrics.cpp index d280a61..fa59444 100644 --- a/examples/OneOpenAir/OpenMetrics.cpp +++ b/examples/OneOpenAir/OpenMetrics.cpp @@ -81,7 +81,10 @@ String OpenMetrics::getPayload(void) { measure.getFloat(Measurements::Humidity, 2)) / 2.0f; pm01 = (measure.get(Measurements::PM01, 1) + measure.get(Measurements::PM01, 2)) / 2.0f; - pm25 = (measure.get(Measurements::PM25, 1) + measure.get(Measurements::PM25, 2)) / 2.0f; + float correctedPm25_1 = measure.getCorrectedPM25(*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; pm03PCount = (measure.get(Measurements::PM03_PC, 1) + measure.get(Measurements::PM03_PC, 2)) / 2.0f; @@ -94,7 +97,8 @@ String OpenMetrics::getPayload(void) { if (config.hasSensorPMS1) { pm01 = measure.get(Measurements::PM01); - pm25 = measure.get(Measurements::PM25); + float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1); + pm25 = round(correctedPm); pm10 = measure.get(Measurements::PM10); pm03PCount = measure.get(Measurements::PM03_PC); } @@ -103,7 +107,8 @@ String OpenMetrics::getPayload(void) { _temp = measure.getFloat(Measurements::Temperature, 1); _hum = measure.getFloat(Measurements::Humidity, 1); pm01 = measure.get(Measurements::PM01, 1); - pm25 = measure.get(Measurements::PM25, 1); + float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1); + pm25 = round(correctedPm); pm10 = measure.get(Measurements::PM10, 1); pm03PCount = measure.get(Measurements::PM03_PC, 1); } @@ -111,7 +116,8 @@ String OpenMetrics::getPayload(void) { _temp = measure.getFloat(Measurements::Temperature, 2); _hum = measure.getFloat(Measurements::Humidity, 2); pm01 = measure.get(Measurements::PM01, 2); - pm25 = measure.get(Measurements::PM25, 2); + float correctedPm = measure.getCorrectedPM25(*ag, config, false, 2); + pm25 = round(correctedPm); pm10 = measure.get(Measurements::PM10, 2); pm03PCount = measure.get(Measurements::PM03_PC, 2); } diff --git a/examples/OneOpenAir/OtaHandler.h b/examples/OneOpenAir/OtaHandler.h deleted file mode 100644 index a845f8b..0000000 --- a/examples/OneOpenAir/OtaHandler.h +++ /dev/null @@ -1,206 +0,0 @@ -#ifndef _OTA_HANDLER_H_ -#define _OTA_HANDLER_H_ -#include -#include -#include -#include - -#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 diff --git a/src/AgApiClient.cpp b/src/AgApiClient.cpp index 8a72a67..8de2596 100644 --- a/src/AgApiClient.cpp +++ b/src/AgApiClient.cpp @@ -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) diff --git a/src/AgApiClient.h b/src/AgApiClient.h index 7035323..9f39ceb 100644 --- a/src/AgApiClient.h +++ b/src/AgApiClient.h @@ -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. diff --git a/src/AgOledDisplay.cpp b/src/AgOledDisplay.cpp index d28ba40..0682038 100644 --- a/src/AgOledDisplay.cpp +++ b/src/AgOledDisplay.cpp @@ -394,7 +394,7 @@ void OledDisplay::showDashboard(const char *status) { if (config.isTemperatureUnitInF()) { snprintf(strBuf, sizeof(strBuf), "T:%0.1f F", utils::degreeC_To_F(temp)); } else { - snprintf(strBuf, sizeof(strBuf), "T:%0.f1 C", temp); + snprintf(strBuf, sizeof(strBuf), "T:%0.1f C", temp); } } else { if (config.isTemperatureUnitInF()) { diff --git a/src/AgValue.cpp b/src/AgValue.cpp index 0df5db8..a8463ce 100644 --- a/src/AgValue.cpp +++ b/src/AgValue.cpp @@ -27,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: @@ -189,7 +195,7 @@ bool Measurements::update(MeasurementType type, int val, int ch) { // Sanity check if measurement type is defined for integer data type or not if (temporary == nullptr) { - Serial.printf("%s is not defined for integer data type\n", measurementTypeStr(type)); + Serial.printf("%s is not defined for integer data type\n", measurementTypeStr(type).c_str()); // TODO: Just assert? return false; } @@ -228,7 +234,7 @@ bool Measurements::update(MeasurementType type, int val, int ch) { // Calculate average based on how many elements on the list temporary->update.avg = temporary->sumValues / (float)temporary->listValues.size(); if (_debug) { - Serial.printf("%s{%d}: %.2f\n", measurementTypeStr(type), ch, temporary->update.avg); + Serial.printf("%s{%d}: %.2f\n", measurementTypeStr(type).c_str(), ch, temporary->update.avg); } return true; @@ -260,7 +266,7 @@ bool Measurements::update(MeasurementType type, float val, int ch) { // Sanity check if measurement type is defined for float data type or not if (temporary == nullptr) { - Serial.printf("%s is not defined for float data type\n", measurementTypeStr(type)); + Serial.printf("%s is not defined for float data type\n", measurementTypeStr(type).c_str()); // TODO: Just assert? return false; } @@ -299,7 +305,7 @@ bool Measurements::update(MeasurementType type, float val, int ch) { // Calculate average based on how many elements on the list temporary->update.avg = temporary->sumValues / (float)temporary->listValues.size(); if (_debug) { - Serial.printf("%s{%d}: %.2f\n", measurementTypeStr(type), ch, temporary->update.avg); + Serial.printf("%s{%d}: %.2f\n", measurementTypeStr(type).c_str(), ch, temporary->update.avg); } return true; @@ -348,7 +354,7 @@ int Measurements::get(MeasurementType type, int ch) { // Sanity check if measurement type is defined for integer data type or not if (temporary == nullptr) { - Serial.printf("%s is not defined for integer data type\n", measurementTypeStr(type)); + Serial.printf("%s is not defined for integer data type\n", measurementTypeStr(type).c_str()); // TODO: Just assert? return false; } @@ -383,7 +389,7 @@ float Measurements::getFloat(MeasurementType type, int ch) { // Sanity check if measurement type is defined for float data type or not if (temporary == nullptr) { - Serial.printf("%s is not defined for float data type\n", measurementTypeStr(type)); + Serial.printf("%s is not defined for float data type\n", measurementTypeStr(type).c_str()); // TODO: Just assert? return false; } @@ -434,7 +440,7 @@ float Measurements::getAverage(MeasurementType type, int ch) { // Sanity check if measurement type is not defined if (measurementAverage == -1000) { - Serial.printf("ERROR! %s is not defined on get average value function\n", measurementTypeStr(type)); + Serial.printf("ERROR! %s is not defined on get average value function\n", measurementTypeStr(type).c_str()); delay(1000); assert(0); } @@ -601,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) { @@ -612,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); @@ -1065,4 +1076,50 @@ JSONVar Measurements::buildPMS(AirGradient &ag, int ch, bool allCh, bool withTem return pms; } -void Measurements::setDebug(bool debug) { _debug = debug; } \ No newline at end of file +void Measurements::setDebug(bool debug) { _debug = debug; } + +int Measurements::bootCount() { return _bootCount; } + +void Measurements::setBootCount(int bootCount) { _bootCount = bootCount; } + +#ifndef ESP8266 +void Measurements::setResetReason(esp_reset_reason_t reason) { + switch (reason) { + case ESP_RST_UNKNOWN: + Serial.println("Reset reason: ESP_RST_UNKNOWN"); + break; + case ESP_RST_POWERON: + Serial.println("Reset reason: ESP_RST_POWERON"); + break; + case ESP_RST_EXT: + Serial.println("Reset reason: ESP_RST_EXT"); + break; + case ESP_RST_SW: + Serial.println("Reset reason: ESP_RST_SW"); + break; + case ESP_RST_PANIC: + Serial.println("Reset reason: ESP_RST_PANIC"); + break; + case ESP_RST_INT_WDT: + Serial.println("Reset reason: ESP_RST_INT_WDT"); + break; + case ESP_RST_TASK_WDT: + Serial.println("Reset reason: ESP_RST_TASK_WDT"); + break; + case ESP_RST_WDT: + Serial.println("Reset reason: ESP_RST_WDT"); + break; + case ESP_RST_BROWNOUT: + Serial.println("Reset reason: ESP_RST_BROWNOUT"); + break; + case ESP_RST_SDIO: + Serial.println("Reset reason: ESP_RST_SDIO"); + break; + default: + Serial.println("Reset reason: unknown"); + break; + } + + _resetReason = (int)reason; +} +#endif \ No newline at end of file diff --git a/src/AgValue.h b/src/AgValue.h index f6ae46d..6cc7a05 100644 --- a/src/AgValue.h +++ b/src/AgValue.h @@ -34,7 +34,7 @@ private: }; public: - Measurements() {} + Measurements(); ~Measurements() {} // Enumeration for every AG measurements @@ -147,8 +147,12 @@ public: */ 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 @@ -171,7 +175,8 @@ private: IntegerValue _pm_25_pc[2]; // particle count 2.5 IntegerValue _pm_5_pc[2]; // particle count 5.0 IntegerValue _pm_10_pc[2]; // particle count 10 - + int _bootCount; + int _resetReason; bool _debug = false; /** diff --git a/src/AirGradient.h b/src/AirGradient.h index 426c89e..25b66b2 100644 --- a/src/AirGradient.h +++ b/src/AirGradient.h @@ -18,6 +18,45 @@ #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 + /** * @brief Class with define all the sensor has supported by Airgradient. Each * sensor usage must be init before use. diff --git a/src/OtaHandler.cpp b/src/OtaHandler.cpp new file mode 100644 index 0000000..c451581 --- /dev/null +++ b/src/OtaHandler.cpp @@ -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 \ No newline at end of file diff --git a/src/OtaHandler.h b/src/OtaHandler.h new file mode 100644 index 0000000..8863f9e --- /dev/null +++ b/src/OtaHandler.h @@ -0,0 +1,43 @@ +#ifndef OTA_HANDLER_H +#define OTA_HANDLER_H +#ifndef ESP8266 // Only for esp32 based mcu + +#include +#include +#include +#include + +#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 \ No newline at end of file