Implement bootloader version checking for OTA updates

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-09-02 08:09:20 +00:00
parent 822660ed43
commit 6e4db40382
6 changed files with 202 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""
Simple script to add bootloader requirement metadata to WLED binary files.
This adds a metadata tag that the OTA handler can detect.
Usage: python add_bootloader_metadata.py <binary_file> <required_version>
Example: python add_bootloader_metadata.py firmware.bin 4
"""
import sys
import os
def add_bootloader_metadata(binary_file, required_version):
"""Add bootloader metadata to a binary file"""
if not os.path.exists(binary_file):
print(f"Error: File {binary_file} does not exist")
return False
# Validate version
try:
version = int(required_version)
if version < 1 or version > 9:
print("Error: Bootloader version must be between 1 and 9")
return False
except ValueError:
print("Error: Bootloader version must be a number")
return False
# Create metadata string
metadata = f"WLED_BOOTLOADER:{version}"
# Append metadata to file
try:
with open(binary_file, 'ab') as f:
f.write(metadata.encode('ascii'))
print(f"Successfully added bootloader v{version} requirement to {binary_file}")
return True
except Exception as e:
print(f"Error writing to file: {e}")
return False
def main():
if len(sys.argv) != 3:
print("Usage: python add_bootloader_metadata.py <binary_file> <required_version>")
print("Example: python add_bootloader_metadata.py firmware.bin 4")
sys.exit(1)
binary_file = sys.argv[1]
required_version = sys.argv[2]
if add_bootloader_metadata(binary_file, required_version):
sys.exit(0)
else:
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,54 @@
# Bootloader Metadata Tool
This tool adds bootloader version requirements to WLED firmware binaries to prevent incompatible OTA updates.
## Usage
```bash
python3 tools/add_bootloader_metadata.py <binary_file> <required_version>
```
Example:
```bash
python3 tools/add_bootloader_metadata.py firmware.bin 4
```
## Bootloader Versions
- **Version 2**: Legacy bootloader (ESP-IDF < 4.4)
- **Version 3**: Intermediate bootloader (ESP-IDF 4.4+)
- **Version 4**: Modern bootloader (ESP-IDF 5.0+) with rollback support
## How It Works
1. The script appends a metadata tag `WLED_BOOTLOADER:X` to the binary file
2. During OTA upload, WLED checks the first 512 bytes for this metadata
3. If found, WLED compares the required version with the current bootloader
4. The update is blocked if the current bootloader is incompatible
## Metadata Format
The metadata is a simple ASCII string: `WLED_BOOTLOADER:X` where X is the required bootloader version (1-9).
This approach was chosen over filename-based detection because users often rename firmware files.
## Integration with Build Process
To automatically add metadata during builds, add this to your platformio.ini:
```ini
[env:your_env]
extra_scripts = post:add_metadata.py
```
Create `add_metadata.py`:
```python
Import("env")
import subprocess
def add_metadata(source, target, env):
firmware_path = str(target[0])
subprocess.run(["python3", "tools/add_bootloader_metadata.py", firmware_path, "4"])
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", add_metadata)
```

View File

@@ -466,6 +466,10 @@ void handleBootLoop(); // detect and handle bootloops
#ifndef ESP8266
void bootloopCheckOTA(); // swap boot image if bootloop is detected instead of restoring config
#endif
#ifndef WLED_DISABLE_OTA
uint32_t getBootloaderVersion(); // get current bootloader version
bool isBootloaderCompatible(uint32_t required_version); // check bootloader compatibility
#endif
// RAII guard class for the JSON Buffer lock
// Modeled after std::lock_guard
class JSONBufferGuard {

View File

@@ -857,6 +857,54 @@ void handleBootLoop() {
ESP.restart(); // restart cleanly and don't wait for another crash
}
#ifndef WLED_DISABLE_OTA
#ifdef ESP32
// Get bootloader version/info for OTA compatibility checking
// Returns a simple version indicator that can be used to check compatibility
uint32_t getBootloaderVersion() {
static uint32_t cached_version = 0;
if (cached_version != 0) return cached_version;
// Try to detect bootloader capabilities
// For now, use a simple heuristic based on ESP-IDF features available
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
// ESP-IDF 5.0+ generally has newer bootloader
cached_version = 4; // Assume V4 bootloader for IDF 5.0+
#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
// ESP-IDF 4.4+ may have V3 or V4 bootloader
cached_version = 3; // Conservative assumption
#else
// Older ESP-IDF versions have older bootloaders
cached_version = 2;
#endif
// Check if we have rollback capability as indicator of newer bootloader
if (Update.canRollBack()) {
cached_version = 4; // Rollback capability suggests V4 bootloader
}
DEBUG_PRINTF_P(PSTR("Detected bootloader version: %d\n"), cached_version);
return cached_version;
}
// Check if current bootloader is compatible with given required version
bool isBootloaderCompatible(uint32_t required_version) {
uint32_t current_version = getBootloaderVersion();
bool compatible = current_version >= required_version;
DEBUG_PRINTF_P(PSTR("Bootloader compatibility check: current=%d, required=%d, compatible=%s\n"),
current_version, required_version, compatible ? "YES" : "NO");
return compatible;
}
#else
// ESP8266 compatibility functions - always assume compatible for now
uint32_t getBootloaderVersion() { return 1; }
bool isBootloaderCompatible(uint32_t required_version) { return true; }
#endif
#endif
/*
* Fixed point integer based Perlin noise functions by @dedehai
* Note: optimized for speed and to mimic fastled inoise functions, not for accuracy or best randomness

View File

@@ -106,6 +106,11 @@
#include <LittleFS.h>
#endif
#include "esp_task_wdt.h"
#ifndef WLED_DISABLE_OTA
#include "esp_ota_ops.h"
#include "esp_app_desc.h"
#endif
#ifndef WLED_DISABLE_ESPNOW
#include <esp_now.h>

View File

@@ -426,6 +426,40 @@ void initServer()
if (!correctPIN || otaLock) return;
if(!index){
DEBUG_PRINTLN(F("OTA Update Start"));
#ifndef WLED_DISABLE_OTA
// Check for bootloader compatibility metadata in first chunk
if (len >= 32) {
// Look for metadata header: "WLED_BOOTLOADER:X" where X is required version
const char* metadata_prefix = "WLED_BOOTLOADER:";
size_t prefix_len = strlen(metadata_prefix);
// Search for metadata in first 512 bytes or available data, whichever is smaller
size_t search_len = (len > 512) ? 512 : len;
for (size_t i = 0; i <= search_len - prefix_len - 1; i++) {
if (memcmp(data + i, metadata_prefix, prefix_len) == 0) {
// Found metadata header, extract required version
char version_char = data[i + prefix_len];
if (version_char >= '1' && version_char <= '9') {
uint32_t required_version = version_char - '0';
DEBUG_PRINTF_P(PSTR("OTA file requires bootloader v%d\n"), required_version);
if (!isBootloaderCompatible(required_version)) {
DEBUG_PRINTF_P(PSTR("Bootloader incompatible! Current: v%d, Required: v%d\n"),
getBootloaderVersion(), required_version);
request->send(400, FPSTR(CONTENT_TYPE_PLAIN),
F("Bootloader incompatible! Please update to a newer bootloader first."));
return;
}
DEBUG_PRINTLN(F("Bootloader compatibility check passed"));
break;
}
}
}
}
#endif
#if WLED_WATCHDOG_TIMEOUT > 0
WLED::instance().disableWatchdog();
#endif