diff --git a/build.ps1 b/build.ps1 index c9dd803..f6a6ccf 100644 --- a/build.ps1 +++ b/build.ps1 @@ -29,7 +29,7 @@ $toolPaths = @( $env:PATH = ($toolPaths -join ";") + ";" + $env:PATH # 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 "Working directory: $(Get-Location)" diff --git a/flash.ps1 b/flash.ps1 index 20b5795..a86425a 100644 --- a/flash.ps1 +++ b/flash.ps1 @@ -17,7 +17,7 @@ $toolPaths = @( ) $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" $idfpy = "$env:IDF_PATH\tools\idf.py" diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index a3cf568..f2d6b90 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -18,4 +18,5 @@ idf_component_register( tablemaker json mbedtls + app_update ) diff --git a/main/user_main.c b/main/user_main.c index 074b965..6f90932 100644 --- a/main/user_main.c +++ b/main/user_main.c @@ -30,6 +30,8 @@ #include "esp_crt_bundle.h" #include "cJSON.h" #include "mqtt_client.h" +#include "esp_ota_ops.h" +#include "esp_app_format.h" #include "video_broadcast.h" #include "3d.h" @@ -1275,6 +1277,11 @@ static const char *html_page = "" "" "
" +"
Firmware Update (OTA):
" +"
" +"
" +"
" +"
" "" "

> IMAGE_UPLOAD

" "
" @@ -1518,7 +1525,18 @@ static const char *html_page = "loadMqtt();loadHaConfig();loadRotation();loadTransition();marginsLoaded=false;updateStatus();" "}).catch(e=>{document.getElementById('backupStatus').innerText='Import failed: '+e;});};" "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);" ""; /** @@ -2814,6 +2832,131 @@ static esp_err_t settings_import_handler(httpd_req_t *req) 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 */ @@ -3032,6 +3175,21 @@ static void start_webserver(void) }; 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); } else { ESP_LOGE(TAG, "Failed to start HTTP server"); diff --git a/partitions.csv b/partitions.csv index d7ea0dd..0bedd15 100644 --- a/partitions.csv +++ b/partitions.csv @@ -1,5 +1,8 @@ # 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, -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,