This is an old revision of the document!
#include <Arduino.h> #include <WiFi.h> #include <AsyncTCP.h> #include <ESPAsyncWebServer.h> #include <ElegantOTA.h> #include <Adafruit_NeoPixel.h> #include <HTTPClient.h> #include <Update.h> #include <ArduinoJson.h> #include "mbedtls/sha256.h" const char* ssid = "TP-Link_2A64"; const char* password = "99481100"; // OTA basic auth (recommended) const char* ota_user = "admin"; const char* ota_pass = "change-me"; // Pull-OTA settings (host these on your local machine) const char* fw_version = "1.0.1"; 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 constexpr unsigned long OTA_CHECK_INTERVAL_MS = 10UL * 1000UL; unsigned long lastOtaCheckMs = 0; constexpr uint8_t NEOPIXEL_PIN = 3; constexpr uint16_t NEOPIXEL_COUNT = 1; Adafruit_NeoPixel statusPixel(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800); AsyncWebServer server(80); struct OtaManifest { String version; String url; String sha256; }; String toHex(const uint8_t* data, size_t len) { static const char* hex = "0123456789abcdef"; String out; out.reserve(len * 2); for (size_t i = 0; i < len; ++i) { out += hex[(data[i] >> 4) & 0x0F]; out += hex[data[i] & 0x0F]; } return out; } String urlEncode(const String& input) { String encoded; for (size_t i = 0; i < input.length(); i++) { char c = input[i]; if (isalnum(static_cast<unsigned char>(c)) || c == '-' || c == '_' || c == '.' || c == '~') { encoded += c; } else if (c == ' ') { encoded += "%20"; } else { char buf[4]; snprintf(buf, sizeof(buf), "%%%02X", static_cast<unsigned char>(c)); encoded += buf; } } return encoded; } void sendTelemetry(const char* status, const String& detail) { if (ota_telemetry_url == nullptr || strlen(ota_telemetry_url) == 0) { return; } HTTPClient http; String url = String(ota_telemetry_url) + "?status=" + status + "&version=" + fw_version + "&msg=" + urlEncode(detail.substring(0, 60)); // keep it short if (http.begin(url)) { http.GET(); // best-effort; ignore response http.end(); } } bool fetchManifest(OtaManifest& manifest) { HTTPClient http; if (!http.begin(ota_manifest_url)) { sendTelemetry("manifest_begin_fail", "http.begin failed"); return false; } const int code = http.GET(); if (code != HTTP_CODE_OK) { sendTelemetry("manifest_http_fail", "code " + String(code)); http.end(); return false; } JsonDocument doc; DeserializationError err = deserializeJson(doc, http.getString()); http.end(); if (err) { sendTelemetry("manifest_parse_fail", err.f_str()); return false; } manifest.version = doc["version"] | ""; manifest.url = doc["url"] | ""; manifest.sha256 = doc["sha256"] | ""; if (manifest.version.isEmpty() || manifest.url.isEmpty() || manifest.sha256.isEmpty()) { sendTelemetry("manifest_missing", "missing field"); return false; } return true; } bool downloadAndUpdate(const String& firmwareUrl, const String& expectedSha256) { WiFiClient client; HTTPClient http; if (!http.begin(client, firmwareUrl)) { sendTelemetry("fw_begin_fail", "http.begin"); return false; } const int code = http.GET(); if (code != HTTP_CODE_OK) { sendTelemetry("fw_http_fail", "code " + String(code)); http.end(); return false; } const int contentLength = http.getSize(); if (contentLength <= 0 && contentLength != -1) { sendTelemetry("fw_size_fail", "invalid length"); http.end(); return false; } if (!Update.begin(contentLength > 0 ? contentLength : UPDATE_SIZE_UNKNOWN)) { sendTelemetry("update_begin_fail", Update.errorString()); http.end(); return false; } mbedtls_sha256_context shaCtx; mbedtls_sha256_init(&shaCtx); mbedtls_sha256_starts(&shaCtx, 0); // 0 = SHA-256 WiFiClient* stream = http.getStreamPtr(); uint8_t buff[1024]; size_t written = 0; while (http.connected()) { const size_t avail = stream->available(); if (avail) { const size_t toRead = avail > sizeof(buff) ? sizeof(buff) : avail; const size_t read = stream->readBytes(buff, toRead); if (read == 0) { break; } if (Update.write(buff, read) != read) { sendTelemetry("update_write_fail", Update.errorString()); Update.abort(); mbedtls_sha256_free(&shaCtx); http.end(); return false; } mbedtls_sha256_update(&shaCtx, buff, read); written += read; } else { delay(1); if (contentLength > 0 && written >= static_cast<size_t>(contentLength)) { break; } } } uint8_t hashResult[32]; mbedtls_sha256_finish(&shaCtx, hashResult); mbedtls_sha256_free(&shaCtx); http.end(); const String computedHash = toHex(hashResult, sizeof(hashResult)); if (!computedHash.equalsIgnoreCase(expectedSha256)) { sendTelemetry("hash_mismatch", computedHash); Update.abort(); return false; } if (!Update.end(true)) { // true = even if size is smaller sendTelemetry("update_end_fail", Update.errorString()); return false; } if (!Update.isFinished()) { sendTelemetry("update_not_finished", "not finished"); return false; } sendTelemetry("update_ok", "restarting"); delay(100); ESP.restart(); return true; // not reached } void checkForOtaUpdate() { OtaManifest manifest; if (!fetchManifest(manifest)) { return; } if (manifest.version == fw_version) { return; // already up to date } sendTelemetry("update_found", "v" + manifest.version); downloadAndUpdate(manifest.url, manifest.sha256); } void setup() { Serial.begin(115200); delay(200); WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); Serial.print("Connecting to WiFi"); while (WiFi.status() != WL_CONNECTED) { delay(300); Serial.print("."); } Serial.println(); Serial.print("Connected. IP: "); Serial.println(WiFi.localIP()); // Simple landing page server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(200, "text/plain", "ESP32-C6 Sparrow OTA ready.\n" "Go to /update to upload new firmware."); }); statusPixel.begin(); statusPixel.setBrightness(40); statusPixel.show(); // initialize to off // Start ElegantOTA (async mode with ESPAsyncWebServer) ElegantOTA.begin(&server, ota_user, ota_pass); server.begin(); Serial.println("HTTP server started."); Serial.println("Open http://<device-ip>/update"); // First pull-based OTA check after boot checkForOtaUpdate(); } void loop() { ElegantOTA.loop(); static bool ledOn = false; static unsigned long lastToggle = 0; const unsigned long blinkIntervalMs = 500; const unsigned long now = millis(); if (now - lastToggle >= blinkIntervalMs) { lastToggle = now; ledOn = !ledOn; const uint32_t color = ledOn ? statusPixel.Color(255, 0, 0) : 0; statusPixel.setPixelColor(0, color); statusPixel.show(); } if (WiFi.isConnected() && now - lastOtaCheckMs >= OTA_CHECK_INTERVAL_MS) { lastOtaCheckMs = now; checkForOtaUpdate(); } }