cargo: add local OTA firmware upload

This commit is contained in:
2026-06-22 22:30:55 -06:00
parent e0b56f42e9
commit a18ed0aeca
3 changed files with 103 additions and 1 deletions
@@ -3,6 +3,7 @@
#include <Preferences.h>
#include <esp_wifi.h>
#include <esp_system.h>
#include <Update.h>
#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(
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Cargo ESP OTA</title>
<style>
body{background:#0b0f14;color:#eef4ff;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Arial,sans-serif;margin:0}
.wrap{max-width:620px;margin:0 auto;padding:22px}
.card{background:#141b24;border:1px solid #223044;border-radius:18px;padding:18px}
h1{margin-top:0}
input,button{width:100%;box-sizing:border-box;margin-top:12px}
input{border:1px solid #223044;border-radius:12px;padding:12px;background:#0f151d;color:#eef4ff}
button{border:0;border-radius:12px;padding:13px;background:#1d324f;color:#eef4ff;font-weight:800}
.warn{color:#ffd166;font-size:14px;line-height:1.4}
.small{color:#7f8da3;font-size:13px}
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<h1>Cargo ESP OTA</h1>
<p class="warn">Upload only firmware built for the Cargo ESP. The controller will reboot after a successful update.</p>
<form method="POST" action="/api/v1/system/ota" enctype="multipart/form-data">
<input type="file" name="firmware" accept=".bin" required>
<button type="submit">Upload Firmware</button>
</form>
<p class="small">Tip: Arduino IDE can create this with Sketch → Export Compiled Binary.</p>
</div>
</div>
</body>
</html>
)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<bool>() ? "OTA update complete. Rebooting." : "OTA update failed.";
String output;
serializeJson(doc, output);
if (doc["ok"].as<bool>()) {
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);
@@ -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"