This shows you the differences between two versions of the page.
|
iothings:laboratoare:2025_code:lab10_3 [2025/12/06 20:49] dan.tudose |
iothings:laboratoare:2025_code:lab10_3 [2025/12/06 21:23] (current) dan.tudose |
||
|---|---|---|---|
| Line 4: | Line 4: | ||
| #include <AsyncTCP.h> | #include <AsyncTCP.h> | ||
| #include <ESPAsyncWebServer.h> | #include <ESPAsyncWebServer.h> | ||
| - | #include <ElegantOTA.h> | ||
| #include <Adafruit_NeoPixel.h> | #include <Adafruit_NeoPixel.h> | ||
| #include <HTTPClient.h> | #include <HTTPClient.h> | ||
| #include <Update.h> | #include <Update.h> | ||
| #include <ArduinoJson.h> | #include <ArduinoJson.h> | ||
| + | #include <Preferences.h> | ||
| #include "mbedtls/sha256.h" | #include "mbedtls/sha256.h" | ||
| const char* ssid = "UPB-Guest"; | const char* ssid = "UPB-Guest"; | ||
| const char* password = ""; | const char* password = ""; | ||
| - | // OTA basic auth (recommended) | ||
| - | const char* ota_user = "admin"; | ||
| - | const char* ota_pass = "change-me"; | ||
| // Pull-OTA settings (host these on your local machine) | // Pull-OTA settings (host these on your local machine) | ||
| - | const char* fw_version = "1.0.1"; | + | const char* fw_version = "1.0.0"; |
| const char* ota_manifest_url = "http://192.168.0.104:8000/manifest.json"; // adjust to your LAN host | const char* ota_manifest_url = "http://192.168.0.104:8000/manifest.json"; // adjust to your LAN host | ||
| const char* ota_telemetry_url = "http://192.168.0.104:8000/telemetry"; // optional: logs update results | const char* ota_telemetry_url = "http://192.168.0.104:8000/telemetry"; // optional: logs update results | ||
| - | constexpr unsigned long OTA_CHECK_INTERVAL_MS = 10UL * 1000UL; | + | constexpr unsigned long OTA_CHECK_INTERVAL_MS = 30UL * 1000UL; |
| unsigned long lastOtaCheckMs = 0; | unsigned long lastOtaCheckMs = 0; | ||
| Line 29: | Line 26: | ||
| AsyncWebServer server(80); | AsyncWebServer server(80); | ||
| + | Preferences prefs; | ||
| + | |||
| + | String getPrefStringSafe(const char* key) { | ||
| + | if (!prefs.isKey(key)) { | ||
| + | return ""; | ||
| + | } | ||
| + | return prefs.getString(key, ""); | ||
| + | } | ||
| struct OtaManifest { | struct OtaManifest { | ||
| Line 35: | Line 40: | ||
| String sha256; | String sha256; | ||
| }; | }; | ||
| + | |||
| + | struct TelemetryState { | ||
| + | String lastResult; // success | failed | noop | "" | ||
| + | String lastError; // sha_mismatch | manifest_fail | ... | "" | ||
| + | String fromVersion; | ||
| + | String toVersion; | ||
| + | String message; // human-readable summary | ||
| + | } telemetry; | ||
| + | |||
| + | void loadTelemetryFromNvs() { | ||
| + | prefs.begin("ota", false); | ||
| + | telemetry.lastResult = getPrefStringSafe("result"); | ||
| + | telemetry.lastError = getPrefStringSafe("error"); | ||
| + | telemetry.fromVersion = getPrefStringSafe("from"); | ||
| + | telemetry.toVersion = getPrefStringSafe("to"); | ||
| + | telemetry.message = getPrefStringSafe("msg"); | ||
| + | } | ||
| + | |||
| + | void storeTelemetryToNvs() { | ||
| + | prefs.putString("result", telemetry.lastResult); | ||
| + | prefs.putString("error", telemetry.lastError); | ||
| + | prefs.putString("from", telemetry.fromVersion); | ||
| + | prefs.putString("to", telemetry.toVersion); | ||
| + | prefs.putString("msg", telemetry.message); | ||
| + | } | ||
| + | |||
| + | void recordTelemetry(const char* result, const char* error, const String& fromVer, const String& toVer, const String& msg) { | ||
| + | telemetry.lastResult = result ? result : ""; | ||
| + | telemetry.lastError = error ? error : ""; | ||
| + | telemetry.fromVersion = fromVer; | ||
| + | telemetry.toVersion = toVer; | ||
| + | telemetry.message = msg; | ||
| + | storeTelemetryToNvs(); | ||
| + | } | ||
| String toHex(const uint8_t* data, size_t len) { | String toHex(const uint8_t* data, size_t len) { | ||
| Line 84: | Line 123: | ||
| if (!http.begin(ota_manifest_url)) { | if (!http.begin(ota_manifest_url)) { | ||
| sendTelemetry("manifest_begin_fail", "http.begin failed"); | sendTelemetry("manifest_begin_fail", "http.begin failed"); | ||
| + | recordTelemetry("failed", "manifest_begin_fail", fw_version, "", "Manifest fetch failed"); | ||
| return false; | return false; | ||
| } | } | ||
| Line 90: | Line 130: | ||
| if (code != HTTP_CODE_OK) { | if (code != HTTP_CODE_OK) { | ||
| sendTelemetry("manifest_http_fail", "code " + String(code)); | sendTelemetry("manifest_http_fail", "code " + String(code)); | ||
| + | recordTelemetry("failed", "manifest_http_fail", fw_version, "", "HTTP " + String(code)); | ||
| http.end(); | http.end(); | ||
| return false; | return false; | ||
| } | } | ||
| - | JsonDocument doc; | + | StaticJsonDocument<512> doc; |
| DeserializationError err = deserializeJson(doc, http.getString()); | DeserializationError err = deserializeJson(doc, http.getString()); | ||
| http.end(); | http.end(); | ||
| if (err) { | if (err) { | ||
| sendTelemetry("manifest_parse_fail", err.f_str()); | sendTelemetry("manifest_parse_fail", err.f_str()); | ||
| + | recordTelemetry("failed", "manifest_parse_fail", fw_version, "", "Manifest parse failed"); | ||
| return false; | return false; | ||
| } | } | ||
| Line 108: | Line 150: | ||
| if (manifest.version.isEmpty() || manifest.url.isEmpty() || manifest.sha256.isEmpty()) { | if (manifest.version.isEmpty() || manifest.url.isEmpty() || manifest.sha256.isEmpty()) { | ||
| sendTelemetry("manifest_missing", "missing field"); | sendTelemetry("manifest_missing", "missing field"); | ||
| + | recordTelemetry("failed", "manifest_missing", fw_version, "", "Manifest missing field"); | ||
| return false; | return false; | ||
| } | } | ||
| Line 114: | Line 157: | ||
| } | } | ||
| - | bool downloadAndUpdate(const String& firmwareUrl, const String& expectedSha256) { | + | bool downloadAndUpdate(const String& firmwareUrl, const String& expectedSha256, const String& targetVersion) { |
| WiFiClient client; | WiFiClient client; | ||
| HTTPClient http; | HTTPClient http; | ||
| if (!http.begin(client, firmwareUrl)) { | if (!http.begin(client, firmwareUrl)) { | ||
| sendTelemetry("fw_begin_fail", "http.begin"); | sendTelemetry("fw_begin_fail", "http.begin"); | ||
| + | recordTelemetry("failed", "fw_begin_fail", fw_version, targetVersion, "Firmware HTTP begin failed"); | ||
| return false; | return false; | ||
| } | } | ||
| Line 125: | Line 169: | ||
| if (code != HTTP_CODE_OK) { | if (code != HTTP_CODE_OK) { | ||
| sendTelemetry("fw_http_fail", "code " + String(code)); | sendTelemetry("fw_http_fail", "code " + String(code)); | ||
| + | recordTelemetry("failed", "fw_http_fail", fw_version, targetVersion, "HTTP " + String(code)); | ||
| http.end(); | http.end(); | ||
| return false; | return false; | ||
| Line 132: | Line 177: | ||
| if (contentLength <= 0 && contentLength != -1) { | if (contentLength <= 0 && contentLength != -1) { | ||
| sendTelemetry("fw_size_fail", "invalid length"); | sendTelemetry("fw_size_fail", "invalid length"); | ||
| + | recordTelemetry("failed", "fw_size_fail", fw_version, targetVersion, "Invalid content length"); | ||
| http.end(); | http.end(); | ||
| return false; | return false; | ||
| Line 138: | Line 184: | ||
| if (!Update.begin(contentLength > 0 ? contentLength : UPDATE_SIZE_UNKNOWN)) { | if (!Update.begin(contentLength > 0 ? contentLength : UPDATE_SIZE_UNKNOWN)) { | ||
| sendTelemetry("update_begin_fail", Update.errorString()); | sendTelemetry("update_begin_fail", Update.errorString()); | ||
| + | recordTelemetry("failed", "update_begin_fail", fw_version, targetVersion, "Update.begin failed"); | ||
| http.end(); | http.end(); | ||
| return false; | return false; | ||
| Line 159: | Line 206: | ||
| if (Update.write(buff, read) != read) { | if (Update.write(buff, read) != read) { | ||
| sendTelemetry("update_write_fail", Update.errorString()); | sendTelemetry("update_write_fail", Update.errorString()); | ||
| + | recordTelemetry("failed", "update_write_fail", fw_version, targetVersion, "Write failed"); | ||
| Update.abort(); | Update.abort(); | ||
| mbedtls_sha256_free(&shaCtx); | mbedtls_sha256_free(&shaCtx); | ||
| Line 182: | Line 230: | ||
| if (!computedHash.equalsIgnoreCase(expectedSha256)) { | if (!computedHash.equalsIgnoreCase(expectedSha256)) { | ||
| sendTelemetry("hash_mismatch", computedHash); | sendTelemetry("hash_mismatch", computedHash); | ||
| + | recordTelemetry("failed", "sha_mismatch", fw_version, targetVersion, "SHA mismatch"); | ||
| Update.abort(); | Update.abort(); | ||
| return false; | return false; | ||
| Line 188: | Line 237: | ||
| if (!Update.end(true)) { // true = even if size is smaller | if (!Update.end(true)) { // true = even if size is smaller | ||
| sendTelemetry("update_end_fail", Update.errorString()); | sendTelemetry("update_end_fail", Update.errorString()); | ||
| + | recordTelemetry("failed", "update_end_fail", fw_version, targetVersion, "Update end failed"); | ||
| return false; | return false; | ||
| } | } | ||
| Line 193: | Line 243: | ||
| if (!Update.isFinished()) { | if (!Update.isFinished()) { | ||
| sendTelemetry("update_not_finished", "not finished"); | sendTelemetry("update_not_finished", "not finished"); | ||
| + | recordTelemetry("failed", "update_not_finished", fw_version, targetVersion, "Update not finished"); | ||
| return false; | return false; | ||
| } | } | ||
| sendTelemetry("update_ok", "restarting"); | sendTelemetry("update_ok", "restarting"); | ||
| + | recordTelemetry("success", "", fw_version, targetVersion, "Update applied; restarting"); | ||
| delay(100); | delay(100); | ||
| ESP.restart(); | ESP.restart(); | ||
| Line 209: | Line 261: | ||
| if (manifest.version == fw_version) { | if (manifest.version == fw_version) { | ||
| + | recordTelemetry("noop", "", fw_version, manifest.version, "No update needed."); | ||
| return; // already up to date | return; // already up to date | ||
| } | } | ||
| sendTelemetry("update_found", "v" + manifest.version); | sendTelemetry("update_found", "v" + manifest.version); | ||
| - | downloadAndUpdate(manifest.url, manifest.sha256); | + | downloadAndUpdate(manifest.url, manifest.sha256, manifest.version); |
| } | } | ||
| Line 219: | Line 272: | ||
| Serial.begin(115200); | Serial.begin(115200); | ||
| delay(200); | delay(200); | ||
| + | |||
| + | loadTelemetryFromNvs(); | ||
| WiFi.mode(WIFI_STA); | WiFi.mode(WIFI_STA); | ||
| Line 237: | Line 292: | ||
| request->send(200, "text/plain", | request->send(200, "text/plain", | ||
| "ESP32-C6 Sparrow OTA ready.\n" | "ESP32-C6 Sparrow OTA ready.\n" | ||
| - | "Go to /update to upload new firmware."); | + | "Updates pull from manifest server.\n" |
| + | "Go to /status for telemetry."); | ||
| + | }); | ||
| + | |||
| + | server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request) { | ||
| + | String payload; | ||
| + | payload.reserve(256); | ||
| + | payload += "{"; | ||
| + | payload += "\"device_version\":\"" + String(fw_version) + "\","; | ||
| + | payload += "\"last_result\":\"" + telemetry.lastResult + "\","; | ||
| + | payload += "\"last_error\":\"" + telemetry.lastError + "\","; | ||
| + | payload += "\"from_version\":\"" + telemetry.fromVersion + "\","; | ||
| + | payload += "\"to_version\":\"" + telemetry.toVersion + "\","; | ||
| + | payload += "\"message\":\"" + telemetry.message + "\""; | ||
| + | payload += "}"; | ||
| + | request->send(200, "application/json", payload); | ||
| }); | }); | ||
| Line 243: | Line 313: | ||
| statusPixel.setBrightness(40); | statusPixel.setBrightness(40); | ||
| statusPixel.show(); // initialize to off | statusPixel.show(); // initialize to off | ||
| - | |||
| - | // Start ElegantOTA (async mode with ESPAsyncWebServer) | ||
| - | ElegantOTA.begin(&server, ota_user, ota_pass); | ||
| server.begin(); | server.begin(); | ||
| Serial.println("HTTP server started."); | Serial.println("HTTP server started."); | ||
| - | Serial.println("Open http://<device-ip>/update"); | + | Serial.println("Open http://<device-ip>/status"); |
| // First pull-based OTA check after boot | // First pull-based OTA check after boot | ||
| Line 256: | Line 323: | ||
| void loop() { | void loop() { | ||
| - | ElegantOTA.loop(); | ||
| - | |||
| static bool ledOn = false; | static bool ledOn = false; | ||
| static unsigned long lastToggle = 0; | static unsigned long lastToggle = 0; | ||
| Line 275: | Line 340: | ||
| checkForOtaUpdate(); | checkForOtaUpdate(); | ||
| } | } | ||
| + | |||
| + | // Yield to keep the watchdog happy | ||
| + | delay(1); | ||
| } | } | ||
| </code> | </code> | ||