Initial Commit
This commit is contained in:
462
main/ota_server.c
Normal file
462
main/ota_server.c
Normal file
@ -0,0 +1,462 @@
|
||||
#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));
|
||||
}
|
||||
Reference in New Issue
Block a user