diff --git a/README.md b/README.md index 616067b..7523c41 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,14 @@ GPIO pins can be configured in `main/config.h`. Modify the `bmc_gpio_defaults` a | GET | `/api/system/info` | Get system information | | GET | `/api/system/status` | Get BMC status | +### OTA Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/ota/status` | Get OTA status and running partition | +| POST | `/api/ota/update` | Start OTA update from URL | +| POST | `/api/ota/upload` | Upload firmware file directly | + ## Web Interface Access the web interface by navigating to `http:///` in your browser. @@ -144,6 +152,7 @@ Features: - Real-time GPIO monitoring - Serial console with WebSocket support - System information display +- OTA firmware update ## Example Usage @@ -175,11 +184,30 @@ ws.onmessage = (event) => console.log(event.data); ws.send('command\n'); ``` +### OTA Firmware Update + +```bash +# Check OTA status +curl http://192.168.1.100/api/ota/status + +# Start OTA update from URL +curl -X POST http://192.168.1.100/api/ota/update \ + -H "Content-Type: application/json" \ + -d '{"url": "http://server/firmware.bin"}' + +# Upload firmware file directly +curl -X POST http://192.168.1.100/api/ota/upload \ + -F "firmware=@firmware.bin" +``` + +The web interface also supports drag-and-drop firmware upload - simply drag a `.bin` file onto the upload zone. + ## Project Structure ``` esp32-bmc/ ├── CMakeLists.txt # Project CMake configuration +├── partitions.csv # Partition table for OTA ├── sdkconfig.defaults # Default SDK configuration ├── .clang-format # Code formatter configuration ├── main/ @@ -190,6 +218,7 @@ esp32-bmc/ │ ├── uart_handler.c/h # UART serial communication │ ├── ethernet_manager.c/h # W5500 Ethernet setup │ ├── web_server.c/h # HTTP server and routes +│ ├── ota_handler.c/h # OTA update handling │ └── html/ │ └── index.html # Web interface └── README.md diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 16702f2..5b7b156 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -3,9 +3,11 @@ idf_component_register(SRCS "main.c" "uart_handler.c" "ethernet_manager.c" "web_server.c" + "ota_handler.c" INCLUDE_DIRS "." REQUIRES esp_driver_gpio esp_driver_uart esp_driver_spi - esp_http_server esp_eth esp_netif lwip spi_flash nvs_flash log) + esp_http_server esp_eth esp_netif lwip spi_flash nvs_flash log + esp_https_ota esp_http_client app_update) # Embed HTML file target_add_binary_data(${COMPONENT_TARGET} "html/index.html" TEXT) diff --git a/main/config.h b/main/config.h index f811a20..a6c7a6d 100644 --- a/main/config.h +++ b/main/config.h @@ -39,7 +39,7 @@ // HTTP Server Configuration // ============================================================================ #define BMC_HTTP_PORT 80 -#define BMC_HTTP_MAX_CONN 4 +#define BMC_HTTP_MAX_CONN 17 #define BMC_HTTP_STACK_SIZE 8192 // ============================================================================ diff --git a/main/html/index.html b/main/html/index.html index 4f128a6..42986e1 100644 --- a/main/html/index.html +++ b/main/html/index.html @@ -356,6 +356,151 @@ } } + /* OTA Update Styles */ + .ota-container { + display: flex; + flex-direction: column; + gap: 15px; + } + + .ota-form { + display: flex; + flex-direction: column; + gap: 10px; + } + + .ota-form label { + font-weight: 600; + color: #555; + } + + .ota-form input { + padding: 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + } + + .ota-form input:focus { + outline: none; + border-color: #2196F3; + } + + .ota-progress { + display: flex; + align-items: center; + gap: 10px; + } + + .progress-bar { + flex: 1; + height: 20px; + background: #e0e0e0; + border-radius: 10px; + overflow: hidden; + } + + .progress-fill { + height: 100%; + background: linear-gradient(90deg, #4CAF50, #8BC34A); + border-radius: 10px; + transition: width 0.3s ease; + width: 0%; + } + + #ota-progress-text { + font-weight: 600; + min-width: 45px; + } + + .ota-info { + margin-bottom: 10px; + } + + /* OTA Upload Section */ + .ota-upload-section { + display: flex; + flex-direction: column; + gap: 10px; + } + + .drop-zone { + border: 2px dashed #ccc; + border-radius: 8px; + padding: 30px 20px; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; + background: #fafafa; + } + + .drop-zone:hover, .drop-zone.drag-over { + border-color: #2196F3; + background: #e3f2fd; + } + + .drop-zone.drag-over { + transform: scale(1.02); + } + + .drop-zone-icon { + font-size: 48px; + display: block; + margin-bottom: 10px; + } + + .drop-zone p { + margin: 0; + font-weight: 600; + color: #333; + } + + .drop-zone-hint { + font-size: 12px; + color: #666; + margin-top: 5px; + display: block; + } + + .file-info { + display: flex; + justify-content: space-between; + padding: 10px; + background: #e8f5e9; + border-radius: 4px; + font-size: 14px; + } + + .file-info #file-name { + font-weight: 600; + color: #2e7d32; + } + + .file-info #file-size { + color: #666; + } + + .ota-divider { + display: flex; + align-items: center; + text-align: center; + margin: 15px 0; + } + + .ota-divider::before, + .ota-divider::after { + content: ''; + flex: 1; + border-bottom: 1px solid #ddd; + } + + .ota-divider span { + padding: 0 15px; + color: #999; + font-size: 12px; + font-weight: 600; + } + /* Responsive */ @media (max-width: 600px) { h1 { @@ -438,6 +583,77 @@ +
+ +
+

🔄 OTA Update

+
+
+
+
Running Partition
+
-
+
+
+ + +
+
+
+ 📁 +

Drag & drop firmware file here

+ or click to browse +
+ +
+ + +
+ +
+ OR +
+ +
+ + + +
+ +
+
+ + +
+

📊 System Status

+
+
+
Free Heap
+
-
+
+
+
Uptime
+
-
+
+
+
WiFi Mode
+
Ethernet
+
+
+
IP Address
+
-
+
+
+
+
+
@@ -808,17 +1024,205 @@ } } + // OTA Update Functions + let otaPollInterval = null; + + async function updateOTAStatus() { + try { + const response = await fetch(`${API_BASE}/api/ota/status`); + const result = await response.json(); + + if (result.success && result.data) { + document.getElementById('ota-partition').textContent = result.data.partition || '-'; + + if (result.data.updating) { + document.getElementById('ota-progress-container').style.display = 'flex'; + const progress = result.data.progress || 0; + document.getElementById('ota-progress-bar').style.width = `${progress}%`; + document.getElementById('ota-progress-text').textContent = `${progress}%`; + document.getElementById('ota-btn').disabled = true; + document.getElementById('ota-btn').textContent = 'Updating...'; + } else { + document.getElementById('ota-progress-container').style.display = 'none'; + document.getElementById('ota-btn').disabled = false; + document.getElementById('ota-btn').textContent = 'Start Update'; + + if (otaPollInterval) { + clearInterval(otaPollInterval); + otaPollInterval = null; + } + } + } + } catch (error) { + console.error('Failed to get OTA status:', error); + } + } + + async function startOTAUpdate() { + const url = document.getElementById('ota-url').value.trim(); + + if (!url) { + showToast('Please enter a firmware URL', 'error'); + return; + } + + try { + const response = await fetch(`${API_BASE}/api/ota/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ url: url }) + }); + + const result = await response.json(); + + if (result.success) { + showToast('OTA update started', 'success'); + document.getElementById('ota-btn').disabled = true; + document.getElementById('ota-btn').textContent = 'Updating...'; + document.getElementById('ota-progress-container').style.display = 'flex'; + + // Start polling for progress + otaPollInterval = setInterval(updateOTAStatus, 1000); + } else { + showToast(result.error || 'Failed to start OTA update', 'error'); + } + } catch (error) { + console.error('OTA update error:', error); + showToast('Failed to start OTA update', 'error'); + } + } + + // File Upload Functions + let selectedFile = null; + + function formatFileSize(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(2) + ' MB'; + } + + function handleFileSelect(file) { + if (!file) return; + + if (!file.name.endsWith('.bin')) { + showToast('Please select a .bin firmware file', 'error'); + return; + } + + selectedFile = file; + document.getElementById('file-info').style.display = 'flex'; + document.getElementById('file-name').textContent = file.name; + document.getElementById('file-size').textContent = formatFileSize(file.size); + document.getElementById('ota-upload-btn').disabled = false; + } + + async function uploadFirmware() { + if (!selectedFile) { + showToast('Please select a firmware file first', 'error'); + return; + } + + const uploadBtn = document.getElementById('ota-upload-btn'); + uploadBtn.disabled = true; + uploadBtn.textContent = 'Uploading...'; + + document.getElementById('ota-progress-container').style.display = 'flex'; + document.getElementById('ota-progress-bar').style.width = '0%'; + document.getElementById('ota-progress-text').textContent = '0%'; + + try { + const formData = new FormData(); + formData.append('firmware', selectedFile); + + const xhr = new XMLHttpRequest(); + + xhr.upload.onprogress = (e) => { + if (e.lengthComputable) { + const percent = Math.round((e.loaded / e.total) * 100); + document.getElementById('ota-progress-bar').style.width = percent + '%'; + document.getElementById('ota-progress-text').textContent = percent + '%'; + } + }; + + xhr.onload = () => { + if (xhr.status === 200) { + showToast('Firmware uploaded successfully! Device will restart...', 'success'); + document.getElementById('ota-progress-bar').style.width = '100%'; + document.getElementById('ota-progress-text').textContent = '100%'; + } else { + let errorMsg = 'Upload failed'; + try { + const result = JSON.parse(xhr.responseText); + errorMsg = result.error || errorMsg; + } catch (e) {} + showToast(errorMsg, 'error'); + uploadBtn.disabled = false; + uploadBtn.textContent = 'Upload Firmware'; + } + }; + + xhr.onerror = () => { + showToast('Upload failed - network error', 'error'); + uploadBtn.disabled = false; + uploadBtn.textContent = 'Upload Firmware'; + }; + + xhr.open('POST', `${API_BASE}/api/ota/upload`); + xhr.send(formData); + + } catch (error) { + console.error('Upload error:', error); + showToast('Failed to upload firmware', 'error'); + uploadBtn.disabled = false; + uploadBtn.textContent = 'Upload Firmware'; + } + } + // Initialize document.addEventListener('DOMContentLoaded', () => { updateSystemInfo(); updateGPIOStates(); updatePowerStatus(); + updateOTAStatus(); connectWebSocket(); + // Setup drag and drop + const dropZone = document.getElementById('drop-zone'); + const fileInput = document.getElementById('ota-file-input'); + + dropZone.addEventListener('click', () => fileInput.click()); + + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('drag-over'); + }); + + dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('drag-over'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('drag-over'); + const files = e.dataTransfer.files; + if (files.length > 0) { + handleFileSelect(files[0]); + } + }); + + fileInput.addEventListener('change', (e) => { + if (e.target.files.length > 0) { + handleFileSelect(e.target.files[0]); + } + }); + // Periodic updates setInterval(updateSystemInfo, 5000); setInterval(updateGPIOStates, 2000); setInterval(updatePowerStatus, 2000); + setInterval(updateOTAStatus, 5000); }); diff --git a/main/main.c b/main/main.c index f9e4d1a..6b5b37d 100644 --- a/main/main.c +++ b/main/main.c @@ -8,6 +8,7 @@ #include "uart_handler.h" #include "ethernet_manager.h" #include "web_server.h" +#include "ota_handler.h" static const char *TAG = TAG_BMC; @@ -92,6 +93,14 @@ void app_main(void) { return; } + // Initialize OTA handler + ESP_LOGI(TAG, "Initializing OTA handler..."); + ret = ota_handler_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize OTA handler: %s", esp_err_to_name(ret)); + // Continue anyway - OTA is optional + } + // Print network information char ip[16], netmask[16], gateway[16], mac[18]; if (ethernet_get_network_info(ip, netmask, gateway) == ESP_OK) { diff --git a/main/ota_handler.c b/main/ota_handler.c new file mode 100644 index 0000000..fd2bc01 --- /dev/null +++ b/main/ota_handler.c @@ -0,0 +1,350 @@ +#include "ota_handler.h" +#include "config.h" +#include "esp_log.h" +#include "esp_ota_ops.h" +#include "esp_http_client.h" +#include "esp_https_ota.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include + +static const char *TAG = "OTA"; + +// OTA state +static bool ota_initialized = false; +static bool ota_updating = false; +static int ota_progress = 0; +static SemaphoreHandle_t ota_mutex = NULL; +static TaskHandle_t ota_task_handle = NULL; +static esp_ota_handle_t ota_upload_handle = 0; +static const esp_partition_t *ota_update_partition = NULL; +static size_t ota_bytes_written = 0; + +// OTA configuration +#define OTA_TIMEOUT_MS 30000 +#define OTA_BUFFER_SIZE 4096 + +// ============================================================================ +// OTA Update Task +// ============================================================================ + +typedef struct { + char url[256]; +} ota_update_params_t; + +static void ota_update_task(void *arg) { + ota_update_params_t *params = (ota_update_params_t *)arg; + + if (!params) { + ESP_LOGE(TAG, "Invalid OTA parameters"); + ota_updating = false; + vTaskDelete(NULL); + return; + } + + ESP_LOGI(TAG, "Starting OTA update from: %s", params->url); + + esp_http_client_config_t http_config = { + .url = params->url, + .timeout_ms = OTA_TIMEOUT_MS, + .buffer_size = OTA_BUFFER_SIZE, + .skip_cert_common_name_check = true, + }; + + esp_https_ota_config_t ota_config = { + .http_config = &http_config, + }; + + esp_https_ota_handle_t ota_handle = NULL; + esp_err_t ret = esp_https_ota_begin(&ota_config, &ota_handle); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "OTA begin failed: %s", esp_err_to_name(ret)); + free(params); + ota_updating = false; + vTaskDelete(NULL); + return; + } + + int last_progress = -1; + + while (1) { + ret = esp_https_ota_perform(ota_handle); + if (ret == ESP_ERR_HTTPS_OTA_IN_PROGRESS) { + // Update progress + int downloaded = esp_https_ota_get_image_len_read(ota_handle); + int total = esp_https_ota_get_image_size(ota_handle); + + if (total > 0) { + ota_progress = (downloaded * 100) / total; + if (ota_progress != last_progress) { + ESP_LOGI(TAG, "OTA progress: %d%% (%d/%d bytes)", ota_progress, downloaded, total); + last_progress = ota_progress; + } + } + } else if (ret == ESP_OK) { + // OTA complete + ESP_LOGI(TAG, "OTA download complete"); + break; + } else { + ESP_LOGE(TAG, "OTA perform failed: %s", esp_err_to_name(ret)); + esp_https_ota_abort(ota_handle); + free(params); + ota_updating = false; + ota_progress = 0; + vTaskDelete(NULL); + return; + } + } + + ret = esp_https_ota_finish(ota_handle); + if (ret == ESP_OK) { + ESP_LOGI(TAG, "OTA update successful, restarting..."); + free(params); + ota_progress = 100; + vTaskDelay(pdMS_TO_TICKS(1000)); + esp_restart(); + } else { + ESP_LOGE(TAG, "OTA finish failed: %s", esp_err_to_name(ret)); + free(params); + ota_updating = false; + ota_progress = 0; + } + + vTaskDelete(NULL); +} + +// ============================================================================ +// Public Functions +// ============================================================================ + +esp_err_t ota_handler_init(void) { + if (ota_initialized) { + ESP_LOGW(TAG, "OTA handler already initialized"); + return ESP_OK; + } + + ESP_LOGI(TAG, "Initializing OTA handler"); + + ota_mutex = xSemaphoreCreateMutex(); + if (!ota_mutex) { + ESP_LOGE(TAG, "Failed to create OTA mutex"); + return ESP_ERR_NO_MEM; + } + + ota_initialized = true; + ota_updating = false; + ota_progress = 0; + + // Print current partition info + const esp_partition_t *running = esp_ota_get_running_partition(); + if (running) { + ESP_LOGI(TAG, "Running from partition: %s (offset: 0x%lx)", running->label, running->address); + } + + return ESP_OK; +} + +esp_err_t ota_handler_deinit(void) { + if (!ota_initialized) { + return ESP_OK; + } + + ESP_LOGI(TAG, "Deinitializing OTA handler"); + + if (ota_mutex) { + vSemaphoreDelete(ota_mutex); + ota_mutex = NULL; + } + + ota_initialized = false; + return ESP_OK; +} + +const char *ota_get_running_partition(void) { + const esp_partition_t *running = esp_ota_get_running_partition(); + if (running) { + return running->label; + } + return "unknown"; +} + +bool ota_is_updating(void) { + return ota_updating; +} + +int ota_get_progress(void) { + if (ota_updating) { + return ota_progress; + } + return -1; +} + +// ============================================================================ +// OTA Start Function (called from web server) +// ============================================================================ + +esp_err_t ota_start_update(const char *url) { + if (!ota_initialized) { + ESP_LOGE(TAG, "OTA handler not initialized"); + return ESP_ERR_INVALID_STATE; + } + + if (ota_updating) { + ESP_LOGW(TAG, "OTA update already in progress"); + return ESP_ERR_INVALID_STATE; + } + + if (!url || strlen(url) == 0) { + ESP_LOGE(TAG, "Invalid URL"); + return ESP_ERR_INVALID_ARG; + } + + // Allocate parameters + ota_update_params_t *params = calloc(1, sizeof(ota_update_params_t)); + if (!params) { + ESP_LOGE(TAG, "Failed to allocate OTA params"); + return ESP_ERR_NO_MEM; + } + + strncpy(params->url, url, sizeof(params->url) - 1); + params->url[sizeof(params->url) - 1] = '\0'; + + ota_updating = true; + ota_progress = 0; + + // Create OTA task + BaseType_t ret = xTaskCreate(ota_update_task, "ota_update", 8192, params, 5, &ota_task_handle); + + if (ret != pdPASS) { + ESP_LOGE(TAG, "Failed to create OTA task"); + free(params); + ota_updating = false; + ota_progress = 0; + return ESP_ERR_NO_MEM; + } + + return ESP_OK; +} + +// ============================================================================ +// OTA Upload Functions (for direct file upload) +// ============================================================================ + +esp_err_t ota_start_upload(void) { + if (!ota_initialized) { + ESP_LOGE(TAG, "OTA handler not initialized"); + return ESP_ERR_INVALID_STATE; + } + + if (ota_updating) { + ESP_LOGW(TAG, "OTA update already in progress"); + return ESP_ERR_INVALID_STATE; + } + + // Get next update partition + ota_update_partition = esp_ota_get_next_update_partition(NULL); + if (!ota_update_partition) { + ESP_LOGE(TAG, "No OTA partition found"); + return ESP_ERR_NOT_FOUND; + } + + ESP_LOGI(TAG, "Starting OTA upload to partition: %s", ota_update_partition->label); + + // Begin OTA + esp_err_t ret = esp_ota_begin(ota_update_partition, OTA_SIZE_UNKNOWN, &ota_upload_handle); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_begin failed: %s", esp_err_to_name(ret)); + ota_update_partition = NULL; + return ret; + } + + ota_updating = true; + ota_progress = 0; + ota_bytes_written = 0; + + return ESP_OK; +} + +esp_err_t ota_write_data(const uint8_t *data, size_t len) { + if (!ota_updating || !ota_update_partition) { + ESP_LOGE(TAG, "OTA upload not started"); + return ESP_ERR_INVALID_STATE; + } + + esp_err_t ret = esp_ota_write(ota_upload_handle, data, len); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_write failed: %s", esp_err_to_name(ret)); + return ret; + } + + ota_bytes_written += len; + + // Update progress (we don't know total size, so show bytes written) + ESP_LOGD(TAG, "Written %zu bytes", ota_bytes_written); + + return ESP_OK; +} + +esp_err_t ota_finish_upload(void) { + if (!ota_updating || !ota_update_partition) { + ESP_LOGE(TAG, "OTA upload not started"); + return ESP_ERR_INVALID_STATE; + } + + ESP_LOGI(TAG, "Finishing OTA upload, total bytes: %zu", ota_bytes_written); + + esp_err_t ret = esp_ota_end(ota_upload_handle); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_end failed: %s", esp_err_to_name(ret)); + ota_updating = false; + ota_upload_handle = 0; + ota_update_partition = NULL; + ota_bytes_written = 0; + return ret; + } + + // Set boot partition + ret = esp_ota_set_boot_partition(ota_update_partition); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_set_boot_partition failed: %s", esp_err_to_name(ret)); + ota_updating = false; + ota_upload_handle = 0; + ota_update_partition = NULL; + ota_bytes_written = 0; + return ret; + } + + ESP_LOGI(TAG, "OTA upload complete, restarting..."); + ota_progress = 100; + ota_updating = false; + ota_upload_handle = 0; + ota_update_partition = NULL; + ota_bytes_written = 0; + + // Restart after short delay + vTaskDelay(pdMS_TO_TICKS(1000)); + esp_restart(); + + return ESP_OK; +} + +esp_err_t ota_abort_upload(void) { + if (!ota_updating) { + return ESP_OK; + } + + ESP_LOGI(TAG, "Aborting OTA upload"); + + if (ota_upload_handle) { + esp_ota_abort(ota_upload_handle); + ota_upload_handle = 0; + } + + ota_updating = false; + ota_progress = 0; + ota_update_partition = NULL; + ota_bytes_written = 0; + + return ESP_OK; +} diff --git a/main/ota_handler.h b/main/ota_handler.h new file mode 100644 index 0000000..48cb8cd --- /dev/null +++ b/main/ota_handler.h @@ -0,0 +1,80 @@ +#ifndef OTA_HANDLER_H +#define OTA_HANDLER_H + +#include +#include "esp_err.h" + +/** + * @brief Initialize OTA handler + * + * @return esp_err_t ESP_OK on success + */ +esp_err_t ota_handler_init(void); + +/** + * @brief Deinitialize OTA handler + * + * @return esp_err_t ESP_OK on success + */ +esp_err_t ota_handler_deinit(void); + +/** + * @brief Get current running partition label + * + * @return const char* Partition label string + */ +const char *ota_get_running_partition(void); + +/** + * @brief Check if OTA update is in progress + * + * @return true if update in progress + */ +bool ota_is_updating(void); + +/** + * @brief Get OTA update progress percentage + * + * @return int 0-100 percentage, -1 if not updating + */ +int ota_get_progress(void); + +/** + * @brief Start OTA update from URL + * + * @param url URL to download firmware from + * @return esp_err_t ESP_OK on success + */ +esp_err_t ota_start_update(const char *url); + +/** + * @brief Start OTA update from direct data (for file upload) + * + * @return esp_err_t ESP_OK on success + */ +esp_err_t ota_start_upload(void); + +/** + * @brief Write data to OTA partition + * + * @param data Data to write + * @param len Length of data + * @return esp_err_t ESP_OK on success + */ +esp_err_t ota_write_data(const uint8_t *data, size_t len); + +/** + * @brief Finish OTA upload and apply update + * + * @return esp_err_t ESP_OK on success + */ +esp_err_t ota_finish_upload(void); + +/** + * @brief Abort ongoing OTA upload + * + * @return esp_err_t ESP_OK on success + */ +esp_err_t ota_abort_upload(void); + +#endif // OTA_HANDLER_H diff --git a/main/web_server.c b/main/web_server.c index 90f8857..b29c966 100644 --- a/main/web_server.c +++ b/main/web_server.c @@ -3,6 +3,7 @@ #include "gpio_controller.h" #include "uart_handler.h" #include "ethernet_manager.h" +#include "ota_handler.h" #include "esp_log.h" #include "cJSON.h" #include @@ -504,6 +505,206 @@ static esp_err_t api_system_status_handler(httpd_req_t *req) { return send_json_success(req, status); } +// ============================================================================ +// OTA API Handlers +// ============================================================================ + +static esp_err_t api_ota_status_handler(httpd_req_t *req) { + cJSON *status = cJSON_CreateObject(); + + cJSON_AddStringToObject(status, "partition", ota_get_running_partition()); + cJSON_AddBoolToObject(status, "updating", ota_is_updating()); + cJSON_AddNumberToObject(status, "progress", ota_get_progress()); + + return send_json_success(req, status); +} + +static esp_err_t api_ota_update_handler(httpd_req_t *req) { + // Check if already updating + if (ota_is_updating()) { + return send_json_error(req, "OTA update already in progress"); + } + + // Parse JSON body + char buf[512]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) { + return send_json_error(req, "No data received"); + } + buf[ret] = '\0'; + + cJSON *json = cJSON_Parse(buf); + if (!json) { + return send_json_error(req, "Invalid JSON"); + } + + cJSON *url_item = cJSON_GetObjectItem(json, "url"); + if (!url_item || !cJSON_IsString(url_item)) { + cJSON_Delete(json); + return send_json_error(req, "Missing or invalid 'url' field"); + } + + const char *url = url_item->valuestring; + + // Start OTA update + esp_err_t err = ota_start_update(url); + cJSON_Delete(json); + + if (err != ESP_OK) { + return send_json_error(req, "Failed to start OTA update"); + } + + cJSON *response = cJSON_CreateObject(); + cJSON_AddStringToObject(response, "message", "OTA update started"); + return send_json_success(req, response); +} + +static esp_err_t api_ota_upload_handler(httpd_req_t *req) { + // Check if already updating + if (ota_is_updating()) { + ESP_LOGE(TAG_HTTP, "OTA update already in progress"); + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "application/json"); + httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"OTA update already in progress\"}"); + return ESP_OK; + } + + // Get content type to check for multipart + char content_type[128] = {0}; + esp_err_t hdr_ret = httpd_req_get_hdr_value_str(req, "Content-Type", content_type, sizeof(content_type)); + if (hdr_ret != ESP_OK || strstr(content_type, "multipart/form-data") == NULL) { + ESP_LOGE(TAG_HTTP, "Invalid content type for upload"); + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "application/json"); + httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"Expected multipart/form-data\"}"); + return ESP_OK; + } + + // Find boundary + char *boundary_start = strstr(content_type, "boundary="); + if (!boundary_start) { + ESP_LOGE(TAG_HTTP, "No boundary in content type"); + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "application/json"); + httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"No boundary found\"}"); + return ESP_OK; + } + boundary_start += 9; // Skip "boundary=" + + // Extract boundary string + char boundary[64] = {0}; + char *boundary_end = boundary_start; + while (*boundary_end && *boundary_end != '\r' && *boundary_end != '\n' && *boundary_end != ';') { + boundary_end++; + } + int boundary_len = boundary_end - boundary_start; + if (boundary_len >= (int)sizeof(boundary)) { + boundary_len = sizeof(boundary) - 1; + } + strncpy(boundary, boundary_start, boundary_len); + + // Start OTA upload + esp_err_t err = ota_start_upload(); + if (err != ESP_OK) { + ESP_LOGE(TAG_HTTP, "Failed to start OTA upload: %s", esp_err_to_name(err)); + httpd_resp_set_status(req, "500 Internal Server Error"); + httpd_resp_set_type(req, "application/json"); + httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"Failed to start OTA upload\"}"); + return ESP_OK; + } + + ESP_LOGI(TAG_HTTP, "OTA upload started, receiving firmware data..."); + + // Buffer for reading data + char recv_buf[1024]; + int total_received = 0; + bool found_header = false; + + while (true) { + int received = httpd_req_recv(req, recv_buf, sizeof(recv_buf)); + if (received < 0) { + ESP_LOGE(TAG_HTTP, "Error receiving data"); + ota_abort_upload(); + httpd_resp_set_status(req, "500 Internal Server Error"); + httpd_resp_set_type(req, "application/json"); + httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"Error receiving data\"}"); + return ESP_OK; + } + if (received == 0) { + break; // End of data + } + + total_received += received; + + // Parse multipart data - find file data start + if (!found_header) { + // Look for end of headers (double CRLF) + char *header_end = strstr(recv_buf, "\r\n\r\n"); + if (header_end) { + // Skip headers + int header_len = (header_end + 4) - recv_buf; + found_header = true; + + // Write remaining data after headers + int data_len = received - header_len; + if (data_len > 0) { + // Check if this is the end boundary + char *boundary_pos = strstr(header_end + 4, boundary); + if (boundary_pos) { + // This is the end, don't write boundary + data_len = (boundary_pos - 2) - (header_end + 4); // -2 for \r\n before boundary + if (data_len > 0) { + err = ota_write_data((uint8_t *)(header_end + 4), data_len); + } + } else { + err = ota_write_data((uint8_t *)(header_end + 4), data_len); + } + } + } + } else { + // Already in file data - check for boundary + char *boundary_pos = strstr(recv_buf, boundary); + if (boundary_pos) { + // Found end boundary - write data up to boundary (minus \r\n) + int data_len = boundary_pos - recv_buf - 2; // -2 for \r\n before boundary + if (data_len > 0) { + err = ota_write_data((uint8_t *)recv_buf, data_len); + } + break; // Done + } else { + // Write all data + err = ota_write_data((uint8_t *)recv_buf, received); + } + } + + if (err != ESP_OK) { + ESP_LOGE(TAG_HTTP, "Failed to write OTA data"); + ota_abort_upload(); + httpd_resp_set_status(req, "500 Internal Server Error"); + httpd_resp_set_type(req, "application/json"); + httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"Failed to write OTA data\"}"); + return ESP_OK; + } + } + + ESP_LOGI(TAG_HTTP, "OTA upload complete, total received: %d bytes", total_received); + + // Finish OTA upload (this will restart) + err = ota_finish_upload(); + if (err != ESP_OK) { + ESP_LOGE(TAG_HTTP, "Failed to finish OTA upload"); + httpd_resp_set_status(req, "500 Internal Server Error"); + httpd_resp_set_type(req, "application/json"); + httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"Failed to finish OTA upload\"}"); + return ESP_OK; + } + + // Send success (device will restart, client may not receive this) + httpd_resp_set_type(req, "application/json"); + httpd_resp_sendstr(req, "{\"success\":true,\"data\":{\"message\":\"OTA update complete, restarting...\"}}"); + return ESP_OK; +} + // ============================================================================ // URI Registration // ============================================================================ @@ -593,6 +794,24 @@ static const httpd_uri_t uri_api_system_status = { .handler = api_system_status_handler, }; +static const httpd_uri_t uri_api_ota_status = { + .uri = "/api/ota/status", + .method = HTTP_GET, + .handler = api_ota_status_handler, +}; + +static const httpd_uri_t uri_api_ota_update = { + .uri = "/api/ota/update", + .method = HTTP_POST, + .handler = api_ota_update_handler, +}; + +static const httpd_uri_t uri_api_ota_upload = { + .uri = "/api/ota/upload", + .method = HTTP_POST, + .handler = api_ota_upload_handler, +}; + // ============================================================================ // Public Functions // ============================================================================ @@ -639,6 +858,9 @@ esp_err_t web_server_start(void) { httpd_register_uri_handler(server, &uri_ws_serial); httpd_register_uri_handler(server, &uri_api_system_info); httpd_register_uri_handler(server, &uri_api_system_status); + httpd_register_uri_handler(server, &uri_api_ota_status); + httpd_register_uri_handler(server, &uri_api_ota_update); + httpd_register_uri_handler(server, &uri_api_ota_upload); // Register UART callback for WebSocket broadcast at startup esp_err_t cb_ret = uart_register_rx_callback(uart_to_ws_callback); diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..8a64fcd --- /dev/null +++ b/partitions.csv @@ -0,0 +1,8 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap +nvs, data, nvs, , 0x6000, +phy_init, data, phy, , 0x1000, +factory, app, factory, , 0x200000, +ota_0, app, ota_0, , 0x200000, +ota_1, app, ota_1, , 0x200000, +ota_data, data, ota, , 0x2000, diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 0316e7e..ded7a37 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -17,7 +17,7 @@ CONFIG_LWIP_LOCAL_HOSTNAME="vf2-bmc" CONFIG_LWIP_TCP_MSS=1436 CONFIG_LWIP_TCP_SND_BUF_DEFAULT=5744 CONFIG_LWIP_TCP_WND_DEFAULT=5744 -CONFIG_LWIP_MAX_SOCKETS=7 +CONFIG_LWIP_MAX_SOCKETS=20 CONFIG_LWIP_SO_REUSE=y CONFIG_LWIP_SO_RCVBUF=y @@ -33,3 +33,14 @@ CONFIG_LOG_DEFAULT_LEVEL_INFO=y # USB Serial JTAG console output (ESP32-S3) CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y CONFIG_ESP_CONSOLE_SECONDARY_NONE=y + +# OTA Update Configuration +CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=y +CONFIG_ESP_HTTPS_OTA_SKIP_COMMON_NAME_CHECK=y + +# Partition Table for OTA (two OTA partitions + factory) +CONFIG_PARTITION_TABLE_TWO_OTA=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" + +# Mark flash size as 4MB (needed for OTA) +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y