diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e593034..48394ed 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +### Added +- Cargo ESP local OTA firmware upload page at `/ota`. +- Cargo ESP OTA API endpoint at `POST /api/v1/system/ota`. + +## Unreleased + ### Fixed - Cargo ESP starts AP and Web UI/API before attempting STA WiFi reconnects. - STA reconnect attempts now service the web server during short connection windows so AP mode remains usable when no saved WiFi is available. diff --git a/firmware/esp32/overland-controller/overland-controller.ino b/firmware/esp32/overland-controller/overland-controller.ino index f8af457..6ae6039 100644 --- a/firmware/esp32/overland-controller/overland-controller.ino +++ b/firmware/esp32/overland-controller/overland-controller.ino @@ -3,6 +3,7 @@ #include #include #include +#include #define OLED_ENABLED 0 @@ -1648,6 +1649,98 @@ void sendSimpleJsonError(int status, const char* error, const char* detail) { } + +const char OTA_HTML[] PROGMEM = R"rawliteral( + + + + +Cargo ESP OTA + + + +
+
+

Cargo ESP OTA

+

Upload only firmware built for the Cargo ESP. The controller will reboot after a successful update.

+
+ + +
+

Tip: Arduino IDE can create this with Sketch → Export Compiled Binary.

+
+
+ + +)rawliteral"; + +static bool otaUploadHadError = false; + +void handleOtaPage() { + server.send_P(200, "text/html", OTA_HTML); +} + +void handleOtaUploadDone() { + DynamicJsonDocument doc(512); + doc["ok"] = !otaUploadHadError && !Update.hasError(); + doc["firmware_name"] = FIRMWARE_NAME; + doc["message"] = doc["ok"].as() ? "OTA update complete. Rebooting." : "OTA update failed."; + + String output; + serializeJson(doc, output); + + if (doc["ok"].as()) { + server.send(200, "application/json", output); + delay(500); + ESP.restart(); + } else { + server.send(500, "application/json", output); + } +} + +void handleOtaUploadStream() { + HTTPUpload& upload = server.upload(); + + if (upload.status == UPLOAD_FILE_START) { + otaUploadHadError = false; + Serial.print("OTA: upload start: "); + Serial.println(upload.filename); + + if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { + otaUploadHadError = true; + Update.printError(Serial); + } + } else if (upload.status == UPLOAD_FILE_WRITE) { + if (!otaUploadHadError && Update.write(upload.buf, upload.currentSize) != upload.currentSize) { + otaUploadHadError = true; + Update.printError(Serial); + } + } else if (upload.status == UPLOAD_FILE_END) { + if (!otaUploadHadError && Update.end(true)) { + Serial.print("OTA: success, bytes="); + Serial.println(upload.totalSize); + } else { + otaUploadHadError = true; + Update.printError(Serial); + } + } else if (upload.status == UPLOAD_FILE_ABORTED) { + otaUploadHadError = true; + Update.abort(); + Serial.println("OTA: upload aborted"); + } +} + + void handleResetApConfig() { resetApConfigToDefaults(); restartAccessPoint(); @@ -3406,6 +3499,7 @@ void handleCapabilities() { endpoints.add("POST /api/v1/bms/setup/exit"); endpoints.add("POST /api/v1/bms/scan"); endpoints.add("POST /api/v1/bms/select"); + endpoints.add("POST /api/v1/system/ota"); JsonObject limits = doc.createNestedObject("limits"); limits["relay_count"] = MAX_RELAYS; @@ -4016,6 +4110,8 @@ void setup() { server.on("/", HTTP_GET, []() { server.send_P(200, "text/html", INDEX_HTML); }); + server.on("/ota", HTTP_GET, handleOtaPage); + server.on(API_V1("/system/ota"), HTTP_POST, handleOtaUploadDone, handleOtaUploadStream); server.on(API_V1("/status"), handleStatus); server.on(API_V1("/health"), HTTP_GET, handleHealth); diff --git a/firmware/esp32/overland-controller/system_status.h b/firmware/esp32/overland-controller/system_status.h index 916b9db..45c3bb0 100644 --- a/firmware/esp32/overland-controller/system_status.h +++ b/firmware/esp32/overland-controller/system_status.h @@ -1,4 +1,4 @@ #pragma once -#define FIRMWARE_VERSION "0.5.0" +#define FIRMWARE_VERSION "0.5.1-cargo-ota" #define FIRMWARE_NAME "overland-controller"