462 lines
15 KiB
C
462 lines
15 KiB
C
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <sys/socket.h>
|
|
#include <netdb.h>
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include "esp_system.h"
|
|
#include "esp_log.h"
|
|
#include "esp_ota_ops.h"
|
|
#include "esp_http_server.h"
|
|
#include "esp_flash_partitions.h"
|
|
#include "esp_partition.h"
|
|
#include "nvs.h"
|
|
#include "nvs_flash.h"
|
|
#include "ota_server.h"
|
|
|
|
// Define MIN macro if not already defined
|
|
#ifndef MIN
|
|
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
|
#endif
|
|
|
|
static const char *TAG = "OTA_SERVER";
|
|
|
|
// HTML page for OTA update
|
|
static const char *ota_html =
|
|
"<!DOCTYPE html>"
|
|
"<html>"
|
|
"<head>"
|
|
"<title>ESP32 OTA Update</title>"
|
|
"<meta name='viewport' content='width=device-width, initial-scale=1'>"
|
|
"<style>"
|
|
"body { font-family: Arial, sans-serif; margin: 40px; background-color: #f0f0f0; }"
|
|
".container { max-width: 600px; margin: 0 auto; background-color: white; padding: 20px; border-radius: 10px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }"
|
|
"h1 { color: #333; text-align: center; }"
|
|
".info { background-color: #e7f3ff; border-left: 4px solid #2196F3; padding: 10px; margin-bottom: 20px; }"
|
|
".upload-area { border: 2px dashed #ccc; border-radius: 5px; padding: 30px; text-align: center; margin: 20px 0; }"
|
|
".upload-area.dragover { background-color: #e7f3ff; border-color: #2196F3; }"
|
|
"input[type='file'] { display: none; }"
|
|
".btn { background-color: #4CAF50; color: white; padding: 12px 24px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }"
|
|
".btn:hover { background-color: #45a049; }"
|
|
".btn:disabled { background-color: #cccccc; cursor: not-allowed; }"
|
|
".progress { width: 100%; background-color: #f0f0f0; border-radius: 4px; margin-top: 20px; display: none; }"
|
|
".progress-bar { width: 0%; height: 30px; background-color: #4CAF50; border-radius: 4px; text-align: center; line-height: 30px; color: white; transition: width 0.3s; }"
|
|
".status { margin-top: 20px; padding: 10px; border-radius: 4px; display: none; }"
|
|
".status.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }"
|
|
".status.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }"
|
|
".file-info { margin-top: 10px; font-style: italic; color: #666; }"
|
|
"</style>"
|
|
"</head>"
|
|
"<body>"
|
|
"<div class='container'>"
|
|
"<h1>ESP32 OTA Update</h1>"
|
|
"<div class='info'>"
|
|
"<strong>Current Version:</strong> <span id='version'>%s</span><br>"
|
|
"<strong>Free Space:</strong> <span id='free-space'>%u KB</span>"
|
|
"</div>"
|
|
"<div class='upload-area' id='upload-area'>"
|
|
"<p>Drag and drop firmware file here or click to select</p>"
|
|
"<input type='file' id='file' accept='.bin'>"
|
|
"<button class='btn' onclick='document.getElementById(\"file\").click()'>Select File</button>"
|
|
"<div class='file-info' id='file-info'></div>"
|
|
"</div>"
|
|
"<button class='btn' id='upload-btn' onclick='uploadFirmware()' disabled>Upload Firmware</button>"
|
|
"<div class='progress' id='progress'>"
|
|
"<div class='progress-bar' id='progress-bar'>0%%</div>"
|
|
"</div>"
|
|
"<div class='status' id='status'></div>"
|
|
"</div>"
|
|
"<script>"
|
|
"console.log('OTA page loaded');"
|
|
"let selectedFile = null;"
|
|
"const uploadArea = document.getElementById('upload-area');"
|
|
"const fileInput = document.getElementById('file');"
|
|
"const uploadBtn = document.getElementById('upload-btn');"
|
|
"const progressDiv = document.getElementById('progress');"
|
|
"const progressBar = document.getElementById('progress-bar');"
|
|
"const statusDiv = document.getElementById('status');"
|
|
"const fileInfo = document.getElementById('file-info');"
|
|
""
|
|
"// File input change handler"
|
|
"fileInput.addEventListener('change', function(e) {"
|
|
" console.log('File input changed', e.target.files);"
|
|
" if (e.target.files.length > 0) {"
|
|
" handleFile(e.target.files[0]);"
|
|
" }"
|
|
"});"
|
|
""
|
|
"// Drag and drop handlers"
|
|
"uploadArea.addEventListener('dragover', function(e) {"
|
|
" e.preventDefault();"
|
|
" uploadArea.classList.add('dragover');"
|
|
"});"
|
|
""
|
|
"uploadArea.addEventListener('dragleave', function() {"
|
|
" uploadArea.classList.remove('dragover');"
|
|
"});"
|
|
""
|
|
"uploadArea.addEventListener('drop', function(e) {"
|
|
" e.preventDefault();"
|
|
" uploadArea.classList.remove('dragover');"
|
|
" const files = e.dataTransfer.files;"
|
|
" console.log('Files dropped:', files);"
|
|
" if (files.length > 0) {"
|
|
" handleFile(files[0]);"
|
|
" }"
|
|
"});"
|
|
""
|
|
"function handleFile(file) {"
|
|
" console.log('Handling file:', file.name, file.size);"
|
|
" if (file.name.toLowerCase().endsWith('.bin')) {"
|
|
" selectedFile = file;"
|
|
" fileInfo.textContent = 'Selected: ' + file.name + ' (' + (file.size/1024).toFixed(2) + ' KB)';"
|
|
" uploadBtn.disabled = false;"
|
|
" uploadBtn.textContent = 'Upload ' + file.name;"
|
|
" } else {"
|
|
" alert('Please select a .bin file');"
|
|
" selectedFile = null;"
|
|
" fileInfo.textContent = '';"
|
|
" uploadBtn.disabled = true;"
|
|
" uploadBtn.textContent = 'Upload Firmware';"
|
|
" }"
|
|
"}"
|
|
""
|
|
"function showStatus(message, type) {"
|
|
" statusDiv.textContent = message;"
|
|
" statusDiv.className = 'status ' + type;"
|
|
" statusDiv.style.display = 'block';"
|
|
"}"
|
|
""
|
|
"function uploadFirmware() {"
|
|
" if (!selectedFile) {"
|
|
" console.error('No file selected');"
|
|
" return;"
|
|
" }"
|
|
" "
|
|
" console.log('Starting upload...');"
|
|
" const xhr = new XMLHttpRequest();"
|
|
" uploadBtn.disabled = true;"
|
|
" progressDiv.style.display = 'block';"
|
|
" statusDiv.style.display = 'none';"
|
|
" "
|
|
" xhr.upload.addEventListener('progress', function(e) {"
|
|
" if (e.lengthComputable) {"
|
|
" const percent = Math.round((e.loaded / e.total) * 100);"
|
|
" progressBar.style.width = percent + '%%';"
|
|
" progressBar.textContent = percent + '%%';"
|
|
" console.log('Upload progress:', percent);"
|
|
" }"
|
|
" });"
|
|
" "
|
|
" xhr.addEventListener('load', function() {"
|
|
" console.log('Upload complete, status:', xhr.status);"
|
|
" if (xhr.status === 200) {"
|
|
" showStatus('Firmware uploaded successfully! Device will restart...', 'success');"
|
|
" setTimeout(function() { location.reload(); }, 5000);"
|
|
" } else {"
|
|
" showStatus('Upload failed: ' + xhr.responseText, 'error');"
|
|
" uploadBtn.disabled = false;"
|
|
" }"
|
|
" });"
|
|
" "
|
|
" xhr.addEventListener('error', function() {"
|
|
" console.error('Upload error');"
|
|
" showStatus('Upload error occurred', 'error');"
|
|
" uploadBtn.disabled = false;"
|
|
" });"
|
|
" "
|
|
" xhr.open('POST', '/update');"
|
|
" xhr.send(selectedFile);"
|
|
"}"
|
|
"</script>"
|
|
"</body>"
|
|
"</html>";
|
|
|
|
// Server handle
|
|
static httpd_handle_t s_server = NULL;
|
|
|
|
// OTA state
|
|
static ota_state_t s_ota_state = OTA_STATE_IDLE;
|
|
|
|
// Progress callback
|
|
static ota_progress_callback_t s_progress_callback = NULL;
|
|
|
|
// Version string
|
|
static char s_version[32] = "1.0.0";
|
|
|
|
// OTA handle
|
|
static esp_ota_handle_t s_ota_handle = 0;
|
|
static const esp_partition_t *s_update_partition = NULL;
|
|
static int s_binary_file_length = 0;
|
|
static bool s_ota_ongoing = false;
|
|
|
|
static esp_err_t index_handler(httpd_req_t *req)
|
|
{
|
|
const esp_partition_t *running = esp_ota_get_running_partition();
|
|
uint32_t free_space = 0;
|
|
|
|
// Calculate free OTA space
|
|
esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, NULL);
|
|
while (it != NULL) {
|
|
const esp_partition_t *p = esp_partition_get(it);
|
|
if (p != running && p->subtype >= ESP_PARTITION_SUBTYPE_APP_OTA_0 &&
|
|
p->subtype <= ESP_PARTITION_SUBTYPE_APP_OTA_15) {
|
|
free_space = p->size;
|
|
break;
|
|
}
|
|
it = esp_partition_next(it);
|
|
}
|
|
esp_partition_iterator_release(it);
|
|
|
|
// Allocate buffer for response
|
|
size_t response_size = strlen(ota_html) + 64;
|
|
char *response = malloc(response_size);
|
|
if (!response) {
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Memory allocation failed");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
snprintf(response, response_size, ota_html, s_version, free_space / 1024);
|
|
|
|
httpd_resp_set_type(req, "text/html");
|
|
httpd_resp_send(req, response, strlen(response));
|
|
|
|
free(response);
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t update_handler(httpd_req_t *req)
|
|
{
|
|
char buf[OTA_BUFFER_SIZE];
|
|
int received;
|
|
int remaining = req->content_len;
|
|
int total_received = 0;
|
|
esp_err_t err = ESP_OK;
|
|
|
|
if (s_ota_ongoing) {
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA already in progress");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Starting OTA update, file size: %d", req->content_len);
|
|
|
|
s_update_partition = esp_ota_get_next_update_partition(NULL);
|
|
if (s_update_partition == NULL) {
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "No OTA partition available");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Writing to partition subtype %d at offset 0x%x",
|
|
s_update_partition->subtype, s_update_partition->address);
|
|
|
|
err = esp_ota_begin(s_update_partition, req->content_len, &s_ota_handle);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "esp_ota_begin failed: %s", esp_err_to_name(err));
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to begin OTA");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
s_ota_ongoing = true;
|
|
s_ota_state = OTA_STATE_UPDATING;
|
|
s_binary_file_length = req->content_len;
|
|
|
|
while (remaining > 0) {
|
|
received = httpd_req_recv(req, buf, MIN(remaining, OTA_BUFFER_SIZE));
|
|
if (received <= 0) {
|
|
if (received == HTTPD_SOCK_ERR_TIMEOUT) {
|
|
continue;
|
|
}
|
|
ESP_LOGE(TAG, "File reception failed");
|
|
esp_ota_abort(s_ota_handle);
|
|
s_ota_ongoing = false;
|
|
s_ota_state = OTA_STATE_ERROR;
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to receive file");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
err = esp_ota_write(s_ota_handle, (const void *)buf, received);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "OTA write failed: %s", esp_err_to_name(err));
|
|
esp_ota_abort(s_ota_handle);
|
|
s_ota_ongoing = false;
|
|
s_ota_state = OTA_STATE_ERROR;
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to write OTA data");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
total_received += received;
|
|
remaining -= received;
|
|
|
|
// Report progress
|
|
if (s_progress_callback && s_binary_file_length > 0) {
|
|
int percent = (total_received * 100) / s_binary_file_length;
|
|
s_progress_callback(percent);
|
|
}
|
|
|
|
ESP_LOGD(TAG, "Written %d bytes, %d remaining", total_received, remaining);
|
|
}
|
|
|
|
err = esp_ota_end(s_ota_handle);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "esp_ota_end failed: %s", esp_err_to_name(err));
|
|
s_ota_ongoing = false;
|
|
s_ota_state = OTA_STATE_ERROR;
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA end failed");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
err = esp_ota_set_boot_partition(s_update_partition);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "esp_ota_set_boot_partition failed: %s", esp_err_to_name(err));
|
|
s_ota_ongoing = false;
|
|
s_ota_state = OTA_STATE_ERROR;
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to set boot partition");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
s_ota_ongoing = false;
|
|
s_ota_state = OTA_STATE_SUCCESS;
|
|
|
|
httpd_resp_sendstr(req, "OTA update successful. Restarting...");
|
|
|
|
ESP_LOGI(TAG, "OTA update successful. Restarting in 1 second...");
|
|
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
|
esp_restart();
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t ota_server_init(void)
|
|
{
|
|
// Check and print current partition info
|
|
const esp_partition_t *running = esp_ota_get_running_partition();
|
|
ESP_LOGI(TAG, "Running partition: %s", running->label);
|
|
|
|
// Mark current app as valid (for rollback support)
|
|
esp_ota_mark_app_valid_cancel_rollback();
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t test_handler(httpd_req_t *req)
|
|
{
|
|
const char* resp = "<html><body><h1>ESP32 OTA Server Test</h1><p>Server is working!</p><a href='/'>Go to OTA Page</a></body></html>";
|
|
httpd_resp_set_type(req, "text/html");
|
|
httpd_resp_send(req, resp, strlen(resp));
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t simple_handler(httpd_req_t *req)
|
|
{
|
|
const char* simple_html =
|
|
"<!DOCTYPE html>"
|
|
"<html><head><title>ESP32 OTA Simple</title></head>"
|
|
"<body>"
|
|
"<h1>ESP32 OTA Update - Simple Version</h1>"
|
|
"<p>Version: %s</p>"
|
|
"<form>"
|
|
"<input type='file' id='fw' accept='.bin'><br><br>"
|
|
"<button type='button' onclick='doUpload()'>Upload</button>"
|
|
"</form>"
|
|
"<div id='msg'></div>"
|
|
"<script>"
|
|
"function doUpload(){"
|
|
"var f=document.getElementById('fw').files[0];"
|
|
"if(!f){alert('Select file first');return;}"
|
|
"var x=new XMLHttpRequest();"
|
|
"document.getElementById('msg').innerHTML='Uploading...';"
|
|
"x.onload=function(){document.getElementById('msg').innerHTML=x.status==200?'Success!':'Error';};"
|
|
"x.open('POST','/update');"
|
|
"x.send(f);"
|
|
"}"
|
|
"</script>"
|
|
"</body></html>";
|
|
|
|
char response[1024];
|
|
snprintf(response, sizeof(response), simple_html, s_version);
|
|
|
|
httpd_resp_set_type(req, "text/html");
|
|
httpd_resp_send(req, response, strlen(response));
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t ota_server_start(void)
|
|
{
|
|
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
|
config.server_port = OTA_SERVER_PORT;
|
|
config.recv_wait_timeout = OTA_RECV_TIMEOUT;
|
|
config.max_uri_handlers = 8; // Increase handler limit
|
|
|
|
ESP_LOGI(TAG, "Starting OTA server on port %d", config.server_port);
|
|
|
|
if (httpd_start(&s_server, &config) != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to start HTTP server");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
// Register URI handlers
|
|
httpd_uri_t index_uri = {
|
|
.uri = "/",
|
|
.method = HTTP_GET,
|
|
.handler = index_handler,
|
|
.user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(s_server, &index_uri);
|
|
|
|
httpd_uri_t update_uri = {
|
|
.uri = "/update",
|
|
.method = HTTP_POST,
|
|
.handler = update_handler,
|
|
.user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(s_server, &update_uri);
|
|
|
|
httpd_uri_t test_uri = {
|
|
.uri = "/test",
|
|
.method = HTTP_GET,
|
|
.handler = test_handler,
|
|
.user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(s_server, &test_uri);
|
|
|
|
httpd_uri_t simple_uri = {
|
|
.uri = "/simple",
|
|
.method = HTTP_GET,
|
|
.handler = simple_handler,
|
|
.user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(s_server, &simple_uri);
|
|
|
|
ESP_LOGI(TAG, "OTA server started");
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t ota_server_stop(void)
|
|
{
|
|
if (s_server) {
|
|
httpd_stop(s_server);
|
|
s_server = NULL;
|
|
ESP_LOGI(TAG, "OTA server stopped");
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
ota_state_t ota_server_get_state(void)
|
|
{
|
|
return s_ota_state;
|
|
}
|
|
|
|
void ota_server_register_progress_callback(ota_progress_callback_t callback)
|
|
{
|
|
s_progress_callback = callback;
|
|
}
|
|
|
|
const char* ota_server_get_version(void)
|
|
{
|
|
return s_version;
|
|
}
|
|
|
|
void ota_server_set_version(const char* version)
|
|
{
|
|
strlcpy(s_version, version, sizeof(s_version));
|
|
} |