Browse Source

Add OTA (Over-The-Air) firmware update support

- Add dual OTA partition layout (ota_0, ota_1 at 1.5MB each)
- Add HTTP POST /ota endpoint for firmware upload
- Add HTTP GET /ota/status for version info
- Add web UI for firmware file selection and upload
- Add app_update component dependency
- Update build scripts to use Esp32TV directory

After this update, firmware can be updated via WiFi at:
http://<device-ip>/ -> CALIBRATION -> Firmware Update (OTA)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
master
melancholytron 2 weeks ago
parent
commit
730bcc8243
  1. 2
      build.ps1
  2. 2
      flash.ps1
  3. 1
      main/CMakeLists.txt
  4. 160
      main/user_main.c
  5. 9
      partitions.csv

2
build.ps1

@ -29,7 +29,7 @@ $toolPaths = @(
$env:PATH = ($toolPaths -join ";") + ";" + $env:PATH $env:PATH = ($toolPaths -join ";") + ";" + $env:PATH
# Change to project directory # Change to project directory
Set-Location "C:\git\channel3\esp32_channel3"
Set-Location "C:\git\Esp32TV"
Write-Host "ESP-IDF Path: $env:IDF_PATH" Write-Host "ESP-IDF Path: $env:IDF_PATH"
Write-Host "Working directory: $(Get-Location)" Write-Host "Working directory: $(Get-Location)"

2
flash.ps1

@ -17,7 +17,7 @@ $toolPaths = @(
) )
$env:PATH = ($toolPaths -join ";") + ";" + $env:PATH $env:PATH = ($toolPaths -join ";") + ";" + $env:PATH
Set-Location "C:\git\channel3\esp32_channel3"
Set-Location "C:\git\Esp32TV"
$python = "C:\Espressif\python_env\idf5.5_py3.11_env\Scripts\python.exe" $python = "C:\Espressif\python_env\idf5.5_py3.11_env\Scripts\python.exe"
$idfpy = "$env:IDF_PATH\tools\idf.py" $idfpy = "$env:IDF_PATH\tools\idf.py"

1
main/CMakeLists.txt

@ -18,4 +18,5 @@ idf_component_register(
tablemaker tablemaker
json json
mbedtls mbedtls
app_update
) )

160
main/user_main.c

@ -30,6 +30,8 @@
#include "esp_crt_bundle.h" #include "esp_crt_bundle.h"
#include "cJSON.h" #include "cJSON.h"
#include "mqtt_client.h" #include "mqtt_client.h"
#include "esp_ota_ops.h"
#include "esp_app_format.h"
#include "video_broadcast.h" #include "video_broadcast.h"
#include "3d.h" #include "3d.h"
@ -1275,6 +1277,11 @@ static const char *html_page =
"<input type='file' id='importFile' accept='.json' style='width:120px'>" "<input type='file' id='importFile' accept='.json' style='width:120px'>"
"<button class='btn' onclick='importSettings()'>IMPORT</button></div>" "<button class='btn' onclick='importSettings()'>IMPORT</button></div>"
"<div id='backupStatus' class='msg'></div>" "<div id='backupStatus' class='msg'></div>"
"<div class='sep'><b>Firmware Update (OTA):</b></div>"
"<div class='flex'><input type='file' id='fwFile' accept='.bin' style='width:150px'>"
"<button class='btn' onclick='uploadFirmware()'>UPDATE</button></div>"
"<div id='fwVersion' class='msg' style='margin-top:4px'></div>"
"<div id='otaStatus' class='msg'></div>"
"</div></div>" "</div></div>"
"<div><h2>> IMAGE_UPLOAD</h2><div class='panel'>" "<div><h2>> IMAGE_UPLOAD</h2><div class='panel'>"
"<div class='flex'><input type='file' id='imgFile' accept='image/*' style='width:150px'>" "<div class='flex'><input type='file' id='imgFile' accept='image/*' style='width:150px'>"
@ -1518,7 +1525,18 @@ static const char *html_page =
"loadMqtt();loadHaConfig();loadRotation();loadTransition();marginsLoaded=false;updateStatus();" "loadMqtt();loadHaConfig();loadRotation();loadTransition();marginsLoaded=false;updateStatus();"
"}).catch(e=>{document.getElementById('backupStatus').innerText='Import failed: '+e;});};" "}).catch(e=>{document.getElementById('backupStatus').innerText='Import failed: '+e;});};"
"reader.readAsText(f);}" "reader.readAsText(f);}"
"updateStatus();loadMqtt();loadHaConfig();loadRotation();loadTransition();setInterval(updateStatus,2000);setInterval(loadHaConfig,10000);"
"function loadOtaStatus(){fetch('/ota/status').then(r=>r.json()).then(d=>{"
"document.getElementById('fwVersion').innerText='v'+d.version+' ('+d.running+') '+d.date;}).catch(e=>{});}"
"function uploadFirmware(){var f=document.getElementById('fwFile').files[0];"
"if(!f){alert('Select a .bin firmware file first');return;}"
"if(!f.name.endsWith('.bin')){alert('File must be a .bin firmware file');return;}"
"if(!confirm('Update firmware to '+f.name+'?\\nDevice will reboot after update.')){return;}"
"document.getElementById('otaStatus').innerText='Uploading firmware...';"
"fetch('/ota',{method:'POST',body:f,headers:{'Content-Type':'application/octet-stream'}}).then(r=>{"
"if(r.ok){document.getElementById('otaStatus').innerText='Update complete! Rebooting...';}"
"else{r.text().then(t=>{document.getElementById('otaStatus').innerText='Update failed: '+t;});}}"
").catch(e=>{document.getElementById('otaStatus').innerText='Upload failed: '+e;});}"
"updateStatus();loadMqtt();loadHaConfig();loadRotation();loadTransition();loadOtaStatus();setInterval(updateStatus,2000);setInterval(loadHaConfig,10000);"
"</script></body></html>"; "</script></body></html>";
/** /**
@ -2814,6 +2832,131 @@ static esp_err_t settings_import_handler(httpd_req_t *req)
return ESP_OK; return ESP_OK;
} }
/**
* @brief Handler for POST /ota - Over-The-Air firmware update
*/
static esp_err_t ota_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "OTA update started, firmware size: %d bytes", req->content_len);
// Get the next OTA partition
const esp_partition_t *update_partition = esp_ota_get_next_update_partition(NULL);
if (!update_partition) {
ESP_LOGE(TAG, "OTA: No update partition found");
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "No OTA partition");
return ESP_FAIL;
}
ESP_LOGI(TAG, "OTA: Writing to partition '%s' at offset 0x%lx",
update_partition->label, update_partition->address);
// Start OTA
esp_ota_handle_t ota_handle;
esp_err_t err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &ota_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "OTA begin failed: %s", esp_err_to_name(err));
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
return ESP_FAIL;
}
// Receive and write firmware in chunks
char *buf = malloc(4096);
if (!buf) {
esp_ota_abort(ota_handle);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory");
return ESP_FAIL;
}
int total_received = 0;
int remaining = req->content_len;
int last_progress = -1;
while (remaining > 0) {
int to_read = remaining > 4096 ? 4096 : remaining;
int received = httpd_req_recv(req, buf, to_read);
if (received <= 0) {
if (received == HTTPD_SOCK_ERR_TIMEOUT) {
continue;
}
ESP_LOGE(TAG, "OTA receive error at %d bytes", total_received);
free(buf);
esp_ota_abort(ota_handle);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Receive failed");
return ESP_FAIL;
}
err = esp_ota_write(ota_handle, buf, received);
if (err != ESP_OK) {
ESP_LOGE(TAG, "OTA write failed: %s", esp_err_to_name(err));
free(buf);
esp_ota_abort(ota_handle);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Flash write failed");
return ESP_FAIL;
}
total_received += received;
remaining -= received;
// Log progress every 10%
int progress = (total_received * 100) / req->content_len;
if (progress / 10 != last_progress / 10) {
ESP_LOGI(TAG, "OTA progress: %d%% (%d/%d bytes)", progress, total_received, req->content_len);
last_progress = progress;
}
}
free(buf);
// Finish OTA
err = esp_ota_end(ota_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "OTA end failed: %s", esp_err_to_name(err));
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA validation failed");
return ESP_FAIL;
}
// Set boot partition
err = esp_ota_set_boot_partition(update_partition);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Set boot partition failed: %s", esp_err_to_name(err));
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Set boot failed");
return ESP_FAIL;
}
ESP_LOGI(TAG, "OTA update successful! Rebooting...");
httpd_resp_send(req, "OTA update successful! Rebooting...", -1);
// Delay to allow response to be sent, then reboot
vTaskDelay(pdMS_TO_TICKS(1000));
esp_restart();
return ESP_OK;
}
/**
* @brief Handler for GET /ota/status - Return OTA partition info
*/
static esp_err_t ota_status_handler(httpd_req_t *req)
{
const esp_partition_t *running = esp_ota_get_running_partition();
const esp_partition_t *next = esp_ota_get_next_update_partition(NULL);
const esp_app_desc_t *app_desc = esp_app_get_description();
char response[512];
snprintf(response, sizeof(response),
"{\"running\":\"%s\",\"next\":\"%s\",\"version\":\"%s\",\"idf\":\"%s\",\"date\":\"%s\",\"time\":\"%s\"}",
running ? running->label : "unknown",
next ? next->label : "unknown",
app_desc->version,
app_desc->idf_ver,
app_desc->date,
app_desc->time);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, response, strlen(response));
return ESP_OK;
}
/** /**
* @brief Start the HTTP server * @brief Start the HTTP server
*/ */
@ -3032,6 +3175,21 @@ static void start_webserver(void)
}; };
httpd_register_uri_handler(http_server, &settings_import_uri); httpd_register_uri_handler(http_server, &settings_import_uri);
// OTA firmware update endpoints
httpd_uri_t ota_uri = {
.uri = "/ota",
.method = HTTP_POST,
.handler = ota_handler
};
httpd_register_uri_handler(http_server, &ota_uri);
httpd_uri_t ota_status_uri = {
.uri = "/ota/status",
.method = HTTP_GET,
.handler = ota_status_handler
};
httpd_register_uri_handler(http_server, &ota_status_uri);
ESP_LOGI(TAG, "HTTP server started on port %d", config.server_port); ESP_LOGI(TAG, "HTTP server started on port %d", config.server_port);
} else { } else {
ESP_LOGE(TAG, "Failed to start HTTP server"); ESP_LOGE(TAG, "Failed to start HTTP server");

9
partitions.csv

@ -1,5 +1,8 @@
# Name, Type, SubType, Offset, Size, Flags # Name, Type, SubType, Offset, Size, Flags
# Custom partition table with larger NVS for image storage
# OTA-enabled partition table for ESP32 Channel3
# 4MB flash layout with dual OTA partitions
nvs, data, nvs, 0x9000, 0x10000, nvs, data, nvs, 0x9000, 0x10000,
phy_init, data, phy, 0x19000, 0x1000,
factory, app, factory, 0x20000, 0x100000,
otadata, data, ota, 0x19000, 0x2000,
phy_init, data, phy, 0x1b000, 0x1000,
ota_0, app, ota_0, 0x20000, 0x180000,
ota_1, app, ota_1, 0x1a0000,0x180000,
Loading…
Cancel
Save