Initial commit

This commit is contained in:
2025-07-17 17:47:13 -06:00
commit b1ba6821e1
13 changed files with 1731 additions and 0 deletions

462
main/ota_server.c Normal file
View 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));
}