ota: add ota update capabilities

This commit is contained in:
2026-03-12 10:59:35 +01:00
parent b3485fcb10
commit ee3ec3ac4f
10 changed files with 1118 additions and 3 deletions

View File

@@ -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/info` | Get system information |
| GET | `/api/system/status` | Get BMC status | | 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 ## Web Interface
Access the web interface by navigating to `http://<ip-address>/` in your browser. Access the web interface by navigating to `http://<ip-address>/` in your browser.
@@ -144,6 +152,7 @@ Features:
- Real-time GPIO monitoring - Real-time GPIO monitoring
- Serial console with WebSocket support - Serial console with WebSocket support
- System information display - System information display
- OTA firmware update
## Example Usage ## Example Usage
@@ -175,11 +184,30 @@ ws.onmessage = (event) => console.log(event.data);
ws.send('command\n'); 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 ## Project Structure
``` ```
esp32-bmc/ esp32-bmc/
├── CMakeLists.txt # Project CMake configuration ├── CMakeLists.txt # Project CMake configuration
├── partitions.csv # Partition table for OTA
├── sdkconfig.defaults # Default SDK configuration ├── sdkconfig.defaults # Default SDK configuration
├── .clang-format # Code formatter configuration ├── .clang-format # Code formatter configuration
├── main/ ├── main/
@@ -190,6 +218,7 @@ esp32-bmc/
│ ├── uart_handler.c/h # UART serial communication │ ├── uart_handler.c/h # UART serial communication
│ ├── ethernet_manager.c/h # W5500 Ethernet setup │ ├── ethernet_manager.c/h # W5500 Ethernet setup
│ ├── web_server.c/h # HTTP server and routes │ ├── web_server.c/h # HTTP server and routes
│ ├── ota_handler.c/h # OTA update handling
│ └── html/ │ └── html/
│ └── index.html # Web interface │ └── index.html # Web interface
└── README.md └── README.md

View File

@@ -3,9 +3,11 @@ idf_component_register(SRCS "main.c"
"uart_handler.c" "uart_handler.c"
"ethernet_manager.c" "ethernet_manager.c"
"web_server.c" "web_server.c"
"ota_handler.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
REQUIRES esp_driver_gpio esp_driver_uart esp_driver_spi 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 # Embed HTML file
target_add_binary_data(${COMPONENT_TARGET} "html/index.html" TEXT) target_add_binary_data(${COMPONENT_TARGET} "html/index.html" TEXT)

View File

@@ -39,7 +39,7 @@
// HTTP Server Configuration // HTTP Server Configuration
// ============================================================================ // ============================================================================
#define BMC_HTTP_PORT 80 #define BMC_HTTP_PORT 80
#define BMC_HTTP_MAX_CONN 4 #define BMC_HTTP_MAX_CONN 17
#define BMC_HTTP_STACK_SIZE 8192 #define BMC_HTTP_STACK_SIZE 8192
// ============================================================================ // ============================================================================

View File

@@ -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 */ /* Responsive */
@media (max-width: 600px) { @media (max-width: 600px) {
h1 { h1 {
@@ -438,6 +583,77 @@
</div> </div>
</div> </div>
<div class="grid">
<!-- OTA Update Card -->
<div class="card">
<h2><span class="icon">🔄</span> OTA Update</h2>
<div class="ota-container">
<div class="ota-info">
<div class="info-item">
<div class="info-label">Running Partition</div>
<div class="info-value" id="ota-partition">-</div>
</div>
</div>
<!-- File Upload Section -->
<div class="ota-upload-section">
<div class="drop-zone" id="drop-zone">
<div class="drop-zone-content">
<span class="drop-zone-icon">📁</span>
<p>Drag & drop firmware file here</p>
<span class="drop-zone-hint">or click to browse</span>
</div>
<input type="file" id="ota-file-input" accept=".bin" style="display: none;" />
</div>
<div class="file-info" id="file-info" style="display: none;">
<span id="file-name">-</span>
<span id="file-size">-</span>
</div>
<button class="btn btn-primary" id="ota-upload-btn" onclick="uploadFirmware()" disabled>Upload Firmware</button>
</div>
<div class="ota-divider">
<span>OR</span>
</div>
<div class="ota-form">
<label for="ota-url">Firmware URL:</label>
<input type="text" id="ota-url" placeholder="http://server/firmware.bin" />
<button class="btn btn-secondary" id="ota-btn" onclick="startOTAUpdate()">Download from URL</button>
</div>
<div class="ota-progress" id="ota-progress-container" style="display: none;">
<div class="progress-bar">
<div class="progress-fill" id="ota-progress-bar"></div>
</div>
<span id="ota-progress-text">0%</span>
</div>
</div>
</div>
<!-- System Info Card (Additional) -->
<div class="card">
<h2><span class="icon">📊</span> System Status</h2>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Free Heap</div>
<div class="info-value" id="free-heap">-</div>
</div>
<div class="info-item">
<div class="info-label">Uptime</div>
<div class="info-value" id="uptime">-</div>
</div>
<div class="info-item">
<div class="info-label">WiFi Mode</div>
<div class="info-value" id="wifi-mode">Ethernet</div>
</div>
<div class="info-item">
<div class="info-label">IP Address</div>
<div class="info-value" id="ip-address">-</div>
</div>
</div>
</div>
</div>
<div class="grid"> <div class="grid">
<!-- GPIO Monitor Card --> <!-- GPIO Monitor Card -->
<div class="card"> <div class="card">
@@ -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 // Initialize
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
updateSystemInfo(); updateSystemInfo();
updateGPIOStates(); updateGPIOStates();
updatePowerStatus(); updatePowerStatus();
updateOTAStatus();
connectWebSocket(); 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 // Periodic updates
setInterval(updateSystemInfo, 5000); setInterval(updateSystemInfo, 5000);
setInterval(updateGPIOStates, 2000); setInterval(updateGPIOStates, 2000);
setInterval(updatePowerStatus, 2000); setInterval(updatePowerStatus, 2000);
setInterval(updateOTAStatus, 5000);
}); });
</script> </script>
</body> </body>

View File

@@ -8,6 +8,7 @@
#include "uart_handler.h" #include "uart_handler.h"
#include "ethernet_manager.h" #include "ethernet_manager.h"
#include "web_server.h" #include "web_server.h"
#include "ota_handler.h"
static const char *TAG = TAG_BMC; static const char *TAG = TAG_BMC;
@@ -92,6 +93,14 @@ void app_main(void) {
return; 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 // Print network information
char ip[16], netmask[16], gateway[16], mac[18]; char ip[16], netmask[16], gateway[16], mac[18];
if (ethernet_get_network_info(ip, netmask, gateway) == ESP_OK) { if (ethernet_get_network_info(ip, netmask, gateway) == ESP_OK) {

350
main/ota_handler.c Normal file
View File

@@ -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 <string.h>
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;
}

80
main/ota_handler.h Normal file
View File

@@ -0,0 +1,80 @@
#ifndef OTA_HANDLER_H
#define OTA_HANDLER_H
#include <stdbool.h>
#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

View File

@@ -3,6 +3,7 @@
#include "gpio_controller.h" #include "gpio_controller.h"
#include "uart_handler.h" #include "uart_handler.h"
#include "ethernet_manager.h" #include "ethernet_manager.h"
#include "ota_handler.h"
#include "esp_log.h" #include "esp_log.h"
#include "cJSON.h" #include "cJSON.h"
#include <string.h> #include <string.h>
@@ -504,6 +505,206 @@ static esp_err_t api_system_status_handler(httpd_req_t *req) {
return send_json_success(req, status); 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 // URI Registration
// ============================================================================ // ============================================================================
@@ -593,6 +794,24 @@ static const httpd_uri_t uri_api_system_status = {
.handler = api_system_status_handler, .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 // 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_ws_serial);
httpd_register_uri_handler(server, &uri_api_system_info); 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_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 // Register UART callback for WebSocket broadcast at startup
esp_err_t cb_ret = uart_register_rx_callback(uart_to_ws_callback); esp_err_t cb_ret = uart_register_rx_callback(uart_to_ws_callback);

8
partitions.csv Normal file
View File

@@ -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,
1 # Name, Type, SubType, Offset, Size, Flags
2 # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
3 nvs, data, nvs, , 0x6000,
4 phy_init, data, phy, , 0x1000,
5 factory, app, factory, , 0x200000,
6 ota_0, app, ota_0, , 0x200000,
7 ota_1, app, ota_1, , 0x200000,
8 ota_data, data, ota, , 0x2000,

View File

@@ -17,7 +17,7 @@ CONFIG_LWIP_LOCAL_HOSTNAME="vf2-bmc"
CONFIG_LWIP_TCP_MSS=1436 CONFIG_LWIP_TCP_MSS=1436
CONFIG_LWIP_TCP_SND_BUF_DEFAULT=5744 CONFIG_LWIP_TCP_SND_BUF_DEFAULT=5744
CONFIG_LWIP_TCP_WND_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_REUSE=y
CONFIG_LWIP_SO_RCVBUF=y CONFIG_LWIP_SO_RCVBUF=y
@@ -33,3 +33,14 @@ CONFIG_LOG_DEFAULT_LEVEL_INFO=y
# USB Serial JTAG console output (ESP32-S3) # USB Serial JTAG console output (ESP32-S3)
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
CONFIG_ESP_CONSOLE_SECONDARY_NONE=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