ota: add ota update capabilities
This commit is contained in:
29
README.md
29
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/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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
350
main/ota_handler.c
Normal 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
80
main/ota_handler.h
Normal 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
|
||||||
@@ -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
8
partitions.csv
Normal 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,
|
||||||
|
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user