Merge pull request #286 from netmindz/add-report-version-feature-MM
* Upgrade reporting: One-time install/upgrade prompt with Report / Not Now / Never Ask and optional anonymous upgrade reporting. * renamed the "tpram" item to "tpsram" (upstream future compatibility) * Upgrade prompts with "WLED-MM" branding * etags cache-control feature backported from upstream. Important: it might be necessary to clear your browser cache once after updating (press <ctrl>+<F5> in MS Edge). This should solve spurious JavaScript "object not found" errors.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,3 +27,4 @@ compile_commands.json
|
||||
/wled00/Release
|
||||
/wled00/wled00.ino.cpp
|
||||
/wled00/html_*.h
|
||||
_codeql_detected_source_root
|
||||
|
||||
@@ -1264,8 +1264,11 @@ monitor_filters = esp32_exception_decoder
|
||||
extends = esp32_4MB_S_base
|
||||
build_flags = ${common.build_flags} ${esp32_legacy.build_flags} ${common_mm.build_flags_S} ${common_mm.build_flags_M} ;; we don't want common_mm.build_disable_sync_interfaces, so we cannot inherit from esp32_4MB_S_base
|
||||
build_unflags = ${esp32_4MB_S_base.build_unflags}
|
||||
${common_mm.animartrix_build_flags} ;; exceeds flash limits in V3 builds
|
||||
-DUSERMOD_ARTIFX ;; exceeds flash limits in V3 builds
|
||||
lib_deps = ${esp32_4MB_S_base.lib_deps} ${common_mm.lib_deps_M}
|
||||
|
||||
lib_ignore = ${esp32_4MB_S_base.lib_ignore}
|
||||
${common_mm.animartrix_lib_ignore}
|
||||
[esp32_4MB_XL_base]
|
||||
extends = esp32_4MB_M_base
|
||||
build_flags = ${esp32_4MB_M_base.build_flags} ${common_mm.build_flags_XL}
|
||||
@@ -2879,6 +2882,7 @@ board_build.partitions = ${esp32.extreme_partitions} ;; WLED extended for 16MB f
|
||||
[env:athom_music_esp32_4MB_M]
|
||||
extends = esp32_4MB_M_base
|
||||
build_unflags = ${esp32_legacy.build_unflags}
|
||||
${common_mm.animartrix_build_flags} ;; exceeds flash limits in V3 builds
|
||||
-D USERMOD_ARTIFX ;; disabled to save some program space in flash
|
||||
-D USERMOD_DALLASTEMPERATURE ;; disabled - flash space is too tight for this
|
||||
-D USERMOD_ROTARY_ENCODER_UI ;; see above
|
||||
|
||||
@@ -113,6 +113,11 @@ function filter(str, type) {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate build timestamp as UNIX timestamp (seconds since epoch)
|
||||
function generateBuildTime() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
function writeHtmlGzipped(sourceFile, resultFile, page) {
|
||||
console.info("Reading " + sourceFile);
|
||||
new inliner(sourceFile, function (error, html) {
|
||||
@@ -142,6 +147,12 @@ function writeHtmlGzipped(sourceFile, resultFile, page) {
|
||||
* to find out how to easily modify the web UI source!
|
||||
*/
|
||||
|
||||
// Automatically generated build time for cache busting (UNIX timestamp)
|
||||
#ifdef WEB_BUILD_TIME // avoid duplicate defintions
|
||||
#undef WEB_BUILD_TIME
|
||||
#endif
|
||||
#define WEB_BUILD_TIME ${generateBuildTime()}
|
||||
|
||||
// Autogenerated from ${sourceFile}, do not edit!!
|
||||
const uint16_t PAGE_${page}_L = ${result.length};
|
||||
const uint8_t PAGE_${page}[] PROGMEM = {
|
||||
|
||||
@@ -717,11 +717,11 @@ ${inforow("Uptime",getRuntimeStr(i.uptime))}
|
||||
${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB, " +Math.round(i.fs.u*100/i.fs.t) + "%")}
|
||||
${theap>0?inforow("Heap ☾",((i.totalheap-i.freeheap)/1000).toFixed(0)+"/"+theap.toFixed(0)+" kB",", "+Math.round((i.totalheap-i.freeheap)/(10*theap))+"%"):inforow("Free heap",heap," kB")} <!--WLEDMM different for 8266-->
|
||||
${i.minfreeheap?inforow("Max used heap ☾",((i.totalheap-i.minfreeheap)/1000).toFixed(0)+" kB",", "+Math.round((i.totalheap-i.minfreeheap)/(10*theap))+"%"):""}
|
||||
${i.psram?inforow("PSRAM ☾",((i.tpram-i.psram)/1024).toFixed(0)+"/"+(i.tpram/1024).toFixed(0)+" kB",", "+((i.tpram-i.psram)*100.0/i.tpram).toFixed(1)+"%"):""}
|
||||
${i.psusedram?inforow("Max used PSRAM ☾",((i.tpram-i.psusedram)/1024).toFixed(0)+" kB",", "+((i.tpram-i.psusedram)*100.0/i.tpram).toFixed(1)+"%"):""}
|
||||
${i.psram?inforow("PSRAM ☾",((i.tpsram-i.psram)/1024).toFixed(0)+"/"+(i.tpsram/1024).toFixed(0)+" kB",", "+((i.tpsram-i.psram)*100.0/i.tpsram).toFixed(1)+"%"):""}
|
||||
${i.psusedram?inforow("Max used PSRAM ☾",((i.tpsram-i.psusedram)/1024).toFixed(0)+" kB",", "+((i.tpsram-i.psusedram)*100.0/i.tpsram).toFixed(1)+"%"):""}
|
||||
${i.freestack?inforow("Free stack ☾",(i.freestack/1000).toFixed(3)," kB"):""} <!--WLEDMM-->
|
||||
<tr><td colspan=2><hr style="height:1px;border-width:0;color:SeaGreen;background-color:SeaGreen"></td></tr>
|
||||
${i.tpram?inforow("PSRAM " + (i.psrmode?"("+i.psrmode+" mode) ":"") + " ☾",(i.tpram/1024/1024).toFixed(0)," MB"):inforow("NO PSRAM found.", "")}
|
||||
${i.tpsram?inforow("PSRAM " + (i.psrmode?"("+i.psrmode+" mode) ":"") + " ☾",(i.tpsram/1024/1024).toFixed(0)," MB"):inforow("NO PSRAM found.", "")}
|
||||
${i.e32flash?inforow("Flash mode "+i.e32flashmode+i.e32flashtext + " ☾",i.e32flash+" MB, "+i.e32flashspeed," Mhz"):""}
|
||||
${i.e32model?inforow(i.e32model + " ☾",i.e32cores +" core(s),"," "+i.e32speed+" Mhz"):""}
|
||||
${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")}
|
||||
@@ -2229,6 +2229,7 @@ function requestJson(command=null)
|
||||
if (json.info) {
|
||||
let i = json.info;
|
||||
parseInfo(i);
|
||||
checkVersionUpgrade(i); // Check for version upgrade
|
||||
populatePalettes(i);
|
||||
if (isInfo) populateInfo(i);
|
||||
}
|
||||
@@ -3686,6 +3687,184 @@ function mergeDeep(target, ...sources)
|
||||
}
|
||||
return mergeDeep(target, ...sources);
|
||||
}
|
||||
// Version reporting feature
|
||||
var versionCheckDone = false;
|
||||
|
||||
function checkVersionUpgrade(info) {
|
||||
// Only check once per page load
|
||||
if (versionCheckDone) return;
|
||||
versionCheckDone = true;
|
||||
|
||||
// Fetch version-info.json using existing /edit endpoint
|
||||
fetch('/edit?edit=/version-info.json', {
|
||||
method: 'get'
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status === 404) {
|
||||
// File doesn't exist - first install, show install prompt
|
||||
showVersionUpgradePrompt(info, null, info.ver);
|
||||
return null;
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch version-info.json');
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(versionInfo => {
|
||||
if (!versionInfo) return; // 404 case already handled
|
||||
|
||||
// Check if user opted out
|
||||
if (versionInfo.neverAsk) return;
|
||||
|
||||
// Check if version has changed
|
||||
const currentVersion = info.ver;
|
||||
const storedVersion = versionInfo.version || '';
|
||||
|
||||
if (storedVersion && storedVersion !== currentVersion) {
|
||||
// Version has changed, show upgrade prompt
|
||||
showVersionUpgradePrompt(info, storedVersion, currentVersion);
|
||||
} else if (!storedVersion) {
|
||||
// Empty version in file, show install prompt
|
||||
showVersionUpgradePrompt(info, null, currentVersion);
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
console.log('Failed to load version-info.json', e);
|
||||
});
|
||||
}
|
||||
|
||||
function showVersionUpgradePrompt(info, oldVersion, newVersion) {
|
||||
// Determine if this is an install or upgrade
|
||||
const isInstall = !oldVersion;
|
||||
|
||||
// Create overlay and dialog
|
||||
const overlay = d.createElement('div');
|
||||
overlay.id = 'versionUpgradeOverlay';
|
||||
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;';
|
||||
|
||||
const dialog = d.createElement('div');
|
||||
dialog.style.cssText = 'background:var(--c-1);border-radius:10px;padding:25px;max-width:500px;margin:20px;box-shadow:0 4px 6px rgba(0,0,0,0.3);';
|
||||
|
||||
// Build contextual message based on install vs upgrade
|
||||
const title = isInstall
|
||||
? '🎉 Thank you for installing WLED-MM!'
|
||||
: '🎉 WLED-MM Upgrade Detected!';
|
||||
|
||||
const description = isInstall
|
||||
? `You are now running WLED-MM <strong>${newVersion}</strong>.`
|
||||
: `Your WLED-MM has been upgraded from <strong>${oldVersion}</strong> to <strong>${newVersion}</strong>.`;
|
||||
|
||||
const question = 'Would you like to help the WLED development team by reporting your installation? This helps us understand what hardware and versions are being used.'
|
||||
|
||||
dialog.innerHTML = `
|
||||
<h2 style="margin-top:0;color:var(--c-f);">${title}</h2>
|
||||
<p style="color:var(--c-f);">${description}</p>
|
||||
<p style="color:var(--c-f);">${question}</p>
|
||||
<div style="margin-top:20px;">
|
||||
<button id="versionReportYes" class="btn">Yes</button>
|
||||
<button id="versionReportNo" class="btn">Not Now</button>
|
||||
<button id="versionReportNever" class="btn">Never Ask</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
overlay.appendChild(dialog);
|
||||
d.body.appendChild(overlay);
|
||||
|
||||
// Add event listeners
|
||||
gId('versionReportYes').addEventListener('click', () => {
|
||||
reportUpgradeEvent(oldVersion, newVersion);
|
||||
d.body.removeChild(overlay);
|
||||
});
|
||||
|
||||
gId('versionReportNo').addEventListener('click', () => {
|
||||
// Don't update version, will ask again on next load
|
||||
d.body.removeChild(overlay);
|
||||
});
|
||||
|
||||
gId('versionReportNever').addEventListener('click', () => {
|
||||
updateVersionInfo(newVersion, true);
|
||||
d.body.removeChild(overlay);
|
||||
showToast('You will not be asked again.');
|
||||
});
|
||||
}
|
||||
|
||||
function reportUpgradeEvent(oldVersion, newVersion) {
|
||||
showToast('Reporting upgrade...');
|
||||
|
||||
// Fetch fresh data from /json/info endpoint as requested
|
||||
fetch('/json/info', {
|
||||
method: 'get'
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(infoData => {
|
||||
// Map to UpgradeEventRequest structure per OpenAPI spec
|
||||
// Required fields: deviceId, version, previousVersion, releaseName, chip, ledCount, isMatrix, bootloaderSHA256
|
||||
const upgradeData = {
|
||||
deviceId: infoData.deviceId, // Use anonymous unique device ID
|
||||
version: infoData.ver || '', // Current version string
|
||||
previousVersion: oldVersion || '', // Previous version from version-info.json
|
||||
releaseName: infoData.release || '', // Release name (e.g., "WLED 0.15.0")
|
||||
chip: infoData.arch || '', // Chip architecture (esp32, esp8266, etc)
|
||||
ledCount: infoData.leds ? infoData.leds.count : 0, // Number of LEDs
|
||||
isMatrix: !!(infoData.leds && infoData.leds.matrix), // Whether it's a 2D matrix setup
|
||||
bootloaderSHA256: infoData.bootloaderSHA256 || '', // Bootloader SHA256 hash - not yet availeable in WLEDMM
|
||||
brand: infoData.brand, // Device brand (always present)
|
||||
product: infoData.product, // Product name (always present)
|
||||
flashSize: infoData.flash // Flash size (always present)
|
||||
};
|
||||
// Add optional fields if available
|
||||
if (infoData.tpsram !== undefined) upgradeData.psramSize = Math.round(infoData.tpsram / (1024 * 1024)); // convert bytes to MB - tpsram is MM specific
|
||||
// Note: partitionSizes not currently available in /json/info endpoint
|
||||
// it is availeable in WLEDMM => infoData.t = total FS size in bytes
|
||||
|
||||
// Make AJAX call to postUpgradeEvent API
|
||||
return fetch('https://usage.wled.me/api/usage/upgrade', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(upgradeData)
|
||||
});
|
||||
})
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
showToast('Thank you for reporting!');
|
||||
updateVersionInfo(newVersion, false);
|
||||
} else {
|
||||
showToast('Report failed. Please try again later.', true);
|
||||
// Do NOT update version info on failure - user will be prompted again
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
console.log('Failed to report upgrade', e);
|
||||
showToast('Report failed. Please try again later.', true);
|
||||
// Do NOT update version info on error - user will be prompted again
|
||||
});
|
||||
}
|
||||
|
||||
function updateVersionInfo(version, neverAsk) {
|
||||
const versionInfo = {
|
||||
version: version,
|
||||
neverAsk: neverAsk
|
||||
};
|
||||
|
||||
// Create a Blob with JSON content and use /upload endpoint
|
||||
const blob = new Blob([JSON.stringify(versionInfo)], { type: 'application/json' });
|
||||
const formData = new FormData();
|
||||
formData.append('data', blob, 'version-info.json');
|
||||
|
||||
fetch('/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(res => res.text())
|
||||
.then(data => {
|
||||
console.log('Version info updated', data);
|
||||
})
|
||||
.catch(e => {
|
||||
console.log('Failed to update version-info.json', e);
|
||||
});
|
||||
}
|
||||
|
||||
size();
|
||||
_C.style.setProperty('--n', N);
|
||||
|
||||
@@ -528,8 +528,8 @@ ${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB (" +Math.round(i.fs.u*100/i.
|
||||
${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")}
|
||||
${theap>0?inforow("Total heap",theap," kB"):""}
|
||||
${i.minfreeheap?inforow("Max used heap",((i.totalheap-i.minfreeheap)/1000).toFixed(1)," kB"):""}
|
||||
${i.tpram?inforow("Total PSRAM",(i.tpram/1024).toFixed(1)," kB"):""}
|
||||
${i.psusedram?((i.tpram-i.psusedram)>16383?inforow("Max Used PSRAM",((i.tpram-i.psusedram)/1024).toFixed(1)," kB"):inforow("Max Used PSRAM",(i.tpram-i.psusedram)," B")):""}
|
||||
${i.tpsram?inforow("Total PSRAM",(i.tpsram/1024).toFixed(1)," kB"):""}
|
||||
${i.psusedram?((i.tpsram-i.psusedram)>16383?inforow("Max Used PSRAM",((i.tpsram-i.psusedram)/1024).toFixed(1)," kB"):inforow("Max Used PSRAM",(i.tpsram-i.psusedram)," B")):""}
|
||||
${i.e32model?inforow(i.e32model,i.e32cores +" core(s)"," "+i.e32speed+" Mhz"):""}
|
||||
${i.e32flash?inforow("Flash "+i.e32flash+" MB"+", mode "+i.e32flashmode+i.e32flashtext,i.e32flashspeed," Mhz"):""}
|
||||
<!-- WLEDMM end-->
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "wled.h"
|
||||
#include "ota_update.h"
|
||||
|
||||
#include "palettes.h"
|
||||
|
||||
@@ -938,7 +939,7 @@ void serializeInfo(JsonObject root)
|
||||
//root[F("cn")] = F(WLED_CODENAME); //WLEDMM removed
|
||||
root[F("release")] = FPSTR(releaseString);
|
||||
root[F("rel")] = FPSTR(releaseString); //WLEDMM to add bin name
|
||||
|
||||
//root[F("repo")] = repoString; // WLEDMM not availeable
|
||||
root[F("deviceId")] = getDeviceId();
|
||||
|
||||
JsonObject leds = root.createNestedObject("leds");
|
||||
@@ -1083,6 +1084,9 @@ void serializeInfo(JsonObject root)
|
||||
|
||||
root[F("lwip")] = 0; //deprecated
|
||||
root[F("totalheap")] = ESP.getHeapSize(); //WLEDMM
|
||||
#ifndef WLED_DISABLE_OTA
|
||||
root[F("bootloaderSHA256")] = getBootloaderSHA256Hex();
|
||||
#endif
|
||||
#else
|
||||
root[F("arch")] = "esp8266";
|
||||
root[F("core")] = ESP.getCoreVersion();
|
||||
@@ -1104,7 +1108,7 @@ void serializeInfo(JsonObject root)
|
||||
#endif
|
||||
#if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM)
|
||||
if (psramFound()) {
|
||||
root[F("tpram")] = ESP.getPsramSize(); //WLEDMM
|
||||
root[F("tpsram")] = ESP.getPsramSize(); //WLEDMM
|
||||
root[F("psram")] = ESP.getFreePsram();
|
||||
root[F("psusedram")] = ESP.getMinFreePsram();
|
||||
#if CONFIG_ESP32S3_SPIRAM_SUPPORT // WLEDMM -S3 has "qspi" or "opi" PSRAM mode
|
||||
@@ -1117,7 +1121,7 @@ void serializeInfo(JsonObject root)
|
||||
}
|
||||
#else
|
||||
// for testing
|
||||
// root[F("tpram")] = 4194304; //WLEDMM
|
||||
// root[F("tpsram")] = 4194304; //WLEDMM
|
||||
// root[F("psram")] = 4193000;
|
||||
// root[F("psusedram")] = 3083000;
|
||||
#endif
|
||||
|
||||
330
wled00/ota_update.cpp
Normal file
330
wled00/ota_update.cpp
Normal file
@@ -0,0 +1,330 @@
|
||||
#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) // WLEDMM we only want getBootloaderSHA256Hex()
|
||||
|
||||
#include "ota_update.h"
|
||||
#include "wled.h"
|
||||
|
||||
#ifdef ESP32
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_spi_flash.h>
|
||||
#include <mbedtls/sha256.h>
|
||||
#endif
|
||||
|
||||
// Platform-specific metadata locations
|
||||
#ifdef ESP32
|
||||
constexpr size_t METADATA_OFFSET = 256; // ESP32: metadata appears after Espressif metadata
|
||||
#define UPDATE_ERROR errorString
|
||||
|
||||
// Bootloader is at fixed offset 0x1000 (4KB), 0x0000 (0KB), or 0x2000 (8KB), and is typically 32KB
|
||||
// Bootloader offsets for different MCUs => see https://github.com/wled/WLED/issues/5064
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
constexpr size_t BOOTLOADER_OFFSET = 0x0000; // esp32-S3, esp32-C3 and (future support) esp32-c6
|
||||
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32P4) || defined(CONFIG_IDF_TARGET_ESP32C5)
|
||||
constexpr size_t BOOTLOADER_OFFSET = 0x2000; // (future support) esp32-P4 and esp32-C5
|
||||
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
|
||||
#else
|
||||
constexpr size_t BOOTLOADER_OFFSET = 0x1000; // esp32 and esp32-s2
|
||||
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
|
||||
#endif
|
||||
|
||||
#elif defined(ESP8266)
|
||||
constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4KB offset
|
||||
#define UPDATE_ERROR getErrorString
|
||||
#endif
|
||||
constexpr size_t METADATA_SEARCH_RANGE = 512; // bytes
|
||||
|
||||
#endif
|
||||
|
||||
#if 0 // WLEDMM not needed - we only want getBootloaderSHA256Hex();
|
||||
|
||||
/**
|
||||
* Check if OTA should be allowed based on release compatibility using custom description
|
||||
* @param binaryData Pointer to binary file data (not modified)
|
||||
* @param dataSize Size of binary data in bytes
|
||||
* @param errorMessage Buffer to store error message if validation fails
|
||||
* @param errorMessageLen Maximum length of error message buffer
|
||||
* @return true if OTA should proceed, false if it should be blocked
|
||||
*/
|
||||
|
||||
static bool validateOTA(const uint8_t* binaryData, size_t dataSize, char* errorMessage, size_t errorMessageLen) {
|
||||
// Clear error message
|
||||
if (errorMessage && errorMessageLen > 0) {
|
||||
errorMessage[0] = '\0';
|
||||
}
|
||||
|
||||
// Try to extract WLED structure directly from binary data
|
||||
wled_metadata_t extractedDesc;
|
||||
bool hasDesc = findWledMetadata(binaryData, dataSize, &extractedDesc);
|
||||
|
||||
if (hasDesc) {
|
||||
return shouldAllowOTA(extractedDesc, errorMessage, errorMessageLen);
|
||||
} else {
|
||||
// No custom description - this could be a legacy binary
|
||||
if (errorMessage && errorMessageLen > 0) {
|
||||
strncpy_P(errorMessage, PSTR("This firmware file is missing compatibility metadata."), errorMessageLen - 1);
|
||||
errorMessage[errorMessageLen - 1] = '\0';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
struct UpdateContext {
|
||||
// State flags
|
||||
// FUTURE: the flags could be replaced by a state machine
|
||||
bool replySent = false;
|
||||
bool needsRestart = false;
|
||||
bool updateStarted = false;
|
||||
bool uploadComplete = false;
|
||||
bool releaseCheckPassed = false;
|
||||
String errorMessage;
|
||||
|
||||
// Buffer to hold block data across posts, if needed
|
||||
std::vector<uint8_t> releaseMetadataBuffer;
|
||||
}
|
||||
|
||||
|
||||
static void endOTA(AsyncWebServerRequest *request) {
|
||||
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
|
||||
request->_tempObject = nullptr;
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("EndOTA %x --> %x (%d)\n"), (uintptr_t)request,(uintptr_t) context, context ? context->uploadComplete : 0);
|
||||
if (context) {
|
||||
if (context->updateStarted) { // We initialized the update
|
||||
// We use Update.end() because not all forms of Update() support an abort.
|
||||
// If the upload is incomplete, Update.end(false) should error out.
|
||||
if (Update.end(context->uploadComplete)) {
|
||||
// Update successful!
|
||||
#ifndef ESP8266
|
||||
bootloopCheckOTA(); // let the bootloop-checker know there was an OTA update
|
||||
#endif
|
||||
doReboot = true;
|
||||
context->needsRestart = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (context->needsRestart) {
|
||||
strip.resume();
|
||||
UsermodManager::onUpdateBegin(false);
|
||||
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||
WLED::instance().enableWatchdog();
|
||||
#endif
|
||||
}
|
||||
delete context;
|
||||
}
|
||||
}
|
||||
|
||||
static bool beginOTA(AsyncWebServerRequest *request, UpdateContext* context)
|
||||
{
|
||||
#ifdef ESP8266
|
||||
Update.runAsync(true);
|
||||
#endif
|
||||
|
||||
if (Update.isRunning()) {
|
||||
request->send(503);
|
||||
setOTAReplied(request);
|
||||
return false;
|
||||
}
|
||||
|
||||
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||
WLED::instance().disableWatchdog();
|
||||
#endif
|
||||
UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init)
|
||||
|
||||
strip.suspend();
|
||||
strip.resetSegments(); // free as much memory as you can
|
||||
context->needsRestart = true;
|
||||
backupConfig(); // backup current config in case the update ends badly
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("OTA Update Start, %x --> %x\n"), (uintptr_t)request,(uintptr_t) context);
|
||||
|
||||
auto skipValidationParam = request->getParam("skipValidation", true);
|
||||
if (skipValidationParam && (skipValidationParam->value() == "1")) {
|
||||
context->releaseCheckPassed = true;
|
||||
DEBUG_PRINTLN(F("OTA validation skipped by user"));
|
||||
}
|
||||
|
||||
// Begin update with the firmware size from content length
|
||||
size_t updateSize = request->contentLength() > 0 ? request->contentLength() : ((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
|
||||
if (!Update.begin(updateSize)) {
|
||||
context->errorMessage = Update.UPDATE_ERROR();
|
||||
DEBUG_PRINTF_P(PSTR("OTA Failed to begin: %s\n"), context->errorMessage.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
context->updateStarted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create an OTA context object on an AsyncWebServerRequest
|
||||
// Returns true if successful, false on failure.
|
||||
bool initOTA(AsyncWebServerRequest *request) {
|
||||
// Allocate update context
|
||||
UpdateContext* context = new (std::nothrow) UpdateContext {};
|
||||
if (context) {
|
||||
request->_tempObject = context;
|
||||
request->onDisconnect([=]() { endOTA(request); }); // ensures we restart on failure
|
||||
};
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("OTA Update init, %x --> %x\n"), (uintptr_t)request,(uintptr_t) context);
|
||||
return (context != nullptr);
|
||||
}
|
||||
|
||||
void setOTAReplied(AsyncWebServerRequest *request) {
|
||||
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
|
||||
if (!context) return;
|
||||
context->replySent = true;
|
||||
};
|
||||
|
||||
// Returns pointer to error message, or nullptr if OTA was successful.
|
||||
std::pair<bool, String> getOTAResult(AsyncWebServerRequest* request) {
|
||||
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
|
||||
if (!context) return { true, F("OTA context unexpectedly missing") };
|
||||
if (context->replySent) return { false, {} };
|
||||
if (context->errorMessage.length()) return { true, context->errorMessage };
|
||||
|
||||
if (context->updateStarted) {
|
||||
// Release the OTA context now.
|
||||
endOTA(request);
|
||||
if (Update.hasError()) {
|
||||
return { true, Update.UPDATE_ERROR() };
|
||||
} else {
|
||||
return { true, {} };
|
||||
}
|
||||
}
|
||||
|
||||
// Should never happen
|
||||
return { true, F("Internal software failure") };
|
||||
}
|
||||
|
||||
|
||||
|
||||
void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal)
|
||||
{
|
||||
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
|
||||
if (!context) return;
|
||||
|
||||
//DEBUG_PRINTF_P(PSTR("HandleOTAData: %d %d %d\n"), index, len, isFinal);
|
||||
|
||||
if (context->replySent || (context->errorMessage.length())) return;
|
||||
|
||||
if (index == 0) {
|
||||
if (!beginOTA(request, context)) return;
|
||||
}
|
||||
|
||||
// Perform validation if we haven't done it yet and we have reached the metadata offset
|
||||
if (!context->releaseCheckPassed && (index+len) > METADATA_OFFSET) {
|
||||
// Current chunk contains the metadata offset
|
||||
size_t availableDataAfterOffset = (index + len) - METADATA_OFFSET;
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("OTA metadata check: %d in buffer, %d received, %d available\n"), context->releaseMetadataBuffer.size(), len, availableDataAfterOffset);
|
||||
|
||||
if (availableDataAfterOffset >= METADATA_SEARCH_RANGE) {
|
||||
// We have enough data to validate, one way or another
|
||||
const uint8_t* search_data = data;
|
||||
size_t search_len = len;
|
||||
|
||||
// If we have saved data, use that instead
|
||||
if (context->releaseMetadataBuffer.size()) {
|
||||
// Add this data
|
||||
context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len);
|
||||
search_data = context->releaseMetadataBuffer.data();
|
||||
search_len = context->releaseMetadataBuffer.size();
|
||||
}
|
||||
|
||||
// Do the checking
|
||||
char errorMessage[128];
|
||||
bool OTA_ok = validateOTA(search_data, search_len, errorMessage, sizeof(errorMessage));
|
||||
|
||||
// Release buffer if there was one
|
||||
context->releaseMetadataBuffer = decltype(context->releaseMetadataBuffer){};
|
||||
|
||||
if (!OTA_ok) {
|
||||
DEBUG_PRINTF_P(PSTR("OTA declined: %s\n"), errorMessage);
|
||||
context->errorMessage = errorMessage;
|
||||
context->errorMessage += F(" Enable 'Ignore firmware validation' to proceed anyway.");
|
||||
return;
|
||||
} else {
|
||||
DEBUG_PRINTLN(F("OTA allowed: Release compatibility check passed"));
|
||||
context->releaseCheckPassed = true;
|
||||
}
|
||||
} else {
|
||||
// Store the data we just got for next pass
|
||||
context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if validation was still pending (shouldn't happen normally)
|
||||
// This is done before writing the last chunk, so endOTA can abort
|
||||
if (isFinal && !context->releaseCheckPassed) {
|
||||
DEBUG_PRINTLN(F("OTA failed: Validation never completed"));
|
||||
// Don't write the last chunk to the updater: this will trip an error later
|
||||
context->errorMessage = F("Release check data never arrived?");
|
||||
return;
|
||||
}
|
||||
|
||||
// Write chunk data to OTA update (only if release check passed or still pending)
|
||||
if (!Update.hasError()) {
|
||||
if (Update.write(data, len) != len) {
|
||||
DEBUG_PRINTF_P(PSTR("OTA write failed on chunk %zu: %s\n"), index, Update.UPDATE_ERROR());
|
||||
}
|
||||
}
|
||||
|
||||
if(isFinal) {
|
||||
DEBUG_PRINTLN(F("OTA Update End"));
|
||||
// Upload complete
|
||||
context->uploadComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
|
||||
static String bootloaderSHA256HexCache = "";
|
||||
|
||||
// Calculate and cache the bootloader SHA256 digest as hex string
|
||||
static void calculateBootloaderSHA256() {
|
||||
if (!bootloaderSHA256HexCache.isEmpty()) return;
|
||||
|
||||
// Calculate SHA256
|
||||
uint8_t sha256[32];
|
||||
mbedtls_sha256_context ctx;
|
||||
mbedtls_sha256_init(&ctx);
|
||||
mbedtls_sha256_starts(&ctx, 0); // 0 = SHA256 (not SHA224)
|
||||
|
||||
const size_t chunkSize = 256;
|
||||
uint8_t buffer[chunkSize];
|
||||
|
||||
for (uint32_t offset = 0; offset < BOOTLOADER_SIZE; offset += chunkSize) {
|
||||
size_t readSize = min((size_t)(BOOTLOADER_SIZE - offset), chunkSize);
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
|
||||
if (esp_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) { // use esp_flash_read for V4 framework (-S2, -S3, -C3)
|
||||
#else
|
||||
if (spi_flash_read(BOOTLOADER_OFFSET + offset, buffer, readSize) == ESP_OK) { // use spi_flash_read for old V3 framework (legacy esp32)
|
||||
#endif
|
||||
mbedtls_sha256_update(&ctx, buffer, readSize);
|
||||
}
|
||||
}
|
||||
|
||||
mbedtls_sha256_finish(&ctx, sha256);
|
||||
mbedtls_sha256_free(&ctx);
|
||||
|
||||
// Convert to hex string and cache it
|
||||
char hex[65];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
sprintf(hex + (i * 2), "%02x", sha256[i]);
|
||||
}
|
||||
hex[64] = '\0';
|
||||
bootloaderSHA256HexCache = hex;
|
||||
}
|
||||
|
||||
// Get bootloader SHA256 as hex string
|
||||
String getBootloaderSHA256Hex() {
|
||||
calculateBootloaderSHA256();
|
||||
return bootloaderSHA256HexCache;
|
||||
}
|
||||
|
||||
// Invalidate cached bootloader SHA256 (call after bootloader update)
|
||||
static void invalidateBootloaderSHA256Cache() {
|
||||
bootloaderSHA256HexCache = "";
|
||||
}
|
||||
#endif
|
||||
76
wled00/ota_update.h
Normal file
76
wled00/ota_update.h
Normal file
@@ -0,0 +1,76 @@
|
||||
// WLED OTA update interface
|
||||
|
||||
#include <Arduino.h>
|
||||
#ifdef ESP8266
|
||||
#include <Updater.h>
|
||||
#else
|
||||
#include <Update.h>
|
||||
#endif
|
||||
|
||||
#pragma once
|
||||
|
||||
#if 0 // WLEDMM not needed - we only want getBootloaderSHA256Hex();
|
||||
|
||||
// Platform-specific metadata locations
|
||||
#ifdef ESP32
|
||||
#define BUILD_METADATA_SECTION ".rodata_custom_desc"
|
||||
#elif defined(ESP8266)
|
||||
#define BUILD_METADATA_SECTION ".ver_number"
|
||||
#endif
|
||||
|
||||
|
||||
class AsyncWebServerRequest;
|
||||
|
||||
/**
|
||||
* Create an OTA context object on an AsyncWebServerRequest
|
||||
* @param request Pointer to web request object
|
||||
* @return true if allocation was successful, false if not
|
||||
*/
|
||||
bool initOTA(AsyncWebServerRequest *request);
|
||||
|
||||
/**
|
||||
* Indicate to the OTA subsystem that a reply has already been generated
|
||||
* @param request Pointer to web request object
|
||||
*/
|
||||
void setOTAReplied(AsyncWebServerRequest *request);
|
||||
|
||||
/**
|
||||
* Retrieve the OTA result.
|
||||
* @param request Pointer to web request object
|
||||
* @return bool indicating if a reply is necessary; string with error message if the update failed.
|
||||
*/
|
||||
std::pair<bool, String> getOTAResult(AsyncWebServerRequest *request);
|
||||
|
||||
/**
|
||||
* Process a block of OTA data. This is a passthrough of an ArUploadHandlerFunction.
|
||||
* Requires that initOTA be called on the handler object before any work will be done.
|
||||
* @param request Pointer to web request object
|
||||
* @param index Offset in to uploaded file
|
||||
* @param data New data bytes
|
||||
* @param len Length of new data bytes
|
||||
* @param isFinal Indicates that this is the last block
|
||||
* @return bool indicating if a reply is necessary; string with error message if the update failed.
|
||||
*/
|
||||
void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal);
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
|
||||
/**
|
||||
* Calculate and cache the bootloader SHA256 digest
|
||||
* Reads the bootloader from flash at offset 0x1000 and computes SHA256 hash
|
||||
*/
|
||||
static void calculateBootloaderSHA256();
|
||||
|
||||
/**
|
||||
* Get bootloader SHA256 as hex string
|
||||
* @return String containing 64-character hex representation of SHA256 hash
|
||||
*/
|
||||
String getBootloaderSHA256Hex();
|
||||
|
||||
/**
|
||||
* Invalidate cached bootloader SHA256 (call after bootloader update)
|
||||
* Forces recalculation on next call to calculateBootloaderSHA256 or getBootloaderSHA256Hex
|
||||
*/
|
||||
static void invalidateBootloaderSHA256Cache();
|
||||
#endif
|
||||
@@ -7,6 +7,7 @@
|
||||
#else
|
||||
#include "mbedtls/sha1.h" // for SHA1 on ESP32
|
||||
#include "esp_efuse.h"
|
||||
#include "esp_adc_cal.h"
|
||||
#endif
|
||||
|
||||
//helper to get int value at a position in string
|
||||
@@ -703,18 +704,17 @@ String computeSHA1(const String& input) {
|
||||
}
|
||||
|
||||
#ifdef ESP32
|
||||
#include "esp_adc_cal.h"
|
||||
String generateDeviceFingerprint() {
|
||||
uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint
|
||||
esp_chip_info_t chip_info;
|
||||
esp_chip_info(&chip_info);
|
||||
esp_efuse_mac_get_default((uint8_t*)fp);
|
||||
fp[1] ^= ESP.getFlashChipSize();
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 3)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 4)
|
||||
fp[0] ^= chip_info.full_revision | (chip_info.model << 16);
|
||||
#else
|
||||
#else
|
||||
fp[0] ^= chip_info.revision | (chip_info.model << 16);
|
||||
#endif
|
||||
#endif
|
||||
// mix in ADC calibration data:
|
||||
esp_adc_cal_characteristics_t ch;
|
||||
#if SOC_ADC_MAX_BITWIDTH == 13 // S2 has 13 bit ADC
|
||||
@@ -739,6 +739,7 @@ String generateDeviceFingerprint() {
|
||||
sprintf(fp_string, "%08X%08X", fp[1], fp[0]);
|
||||
return String(fp_string);
|
||||
}
|
||||
|
||||
#else // ESP8266
|
||||
String generateDeviceFingerprint() {
|
||||
uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint
|
||||
|
||||
@@ -11,18 +11,56 @@
|
||||
#endif
|
||||
#include "html_cpal.h"
|
||||
|
||||
/*
|
||||
* Integrated HTTP web server page declarations
|
||||
*/
|
||||
|
||||
bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest* request);
|
||||
void setStaticContentCacheHeaders(AsyncWebServerResponse *response);
|
||||
|
||||
// define flash strings once (saves flash memory)
|
||||
static const char s_redirecting[] PROGMEM = "Redirecting...";
|
||||
static const char s_content_enc[] PROGMEM = "Content-Encoding";
|
||||
static const char s_unlock_ota [] PROGMEM = "Please unlock OTA in security settings!";
|
||||
static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN code!";
|
||||
static const char s_cache_control[] PROGMEM = "Cache-Control";
|
||||
//static const char s_no_store[] PROGMEM = "no-store";
|
||||
//static const char s_expires[] PROGMEM = "Expires";
|
||||
|
||||
/*
|
||||
* Integrated HTTP web server page declarations
|
||||
*/
|
||||
|
||||
static void generateEtag(char *etag, uint16_t eTagSuffix) {
|
||||
sprintf_P(etag, PSTR("%u-%02x-%04x"), WEB_BUILD_TIME, cacheInvalidate, eTagSuffix);
|
||||
}
|
||||
|
||||
static void setStaticContentCacheHeaders(AsyncWebServerResponse *response, int code=200, uint16_t eTagSuffix = 0) {
|
||||
// Only send ETag for 200 (OK) responses
|
||||
if (code != 200) return;
|
||||
|
||||
// https://medium.com/@codebyamir/a-web-developers-guide-to-browser-caching-cc41f3b73e7c
|
||||
#ifndef WLED_DEBUG
|
||||
// this header name is misleading, "no-cache" will not disable cache,
|
||||
// it just revalidates on every load using the "If-None-Match" header with the last ETag value
|
||||
response->addHeader(FPSTR(s_cache_control), F("no-cache"));
|
||||
#else
|
||||
response->addHeader(FPSTR(s_cache_control), F("no-store,max-age=0")); // prevent caching if debug build
|
||||
#endif
|
||||
char etag[32] = {'\0'};
|
||||
generateEtag(etag, eTagSuffix);
|
||||
response->addHeader(F("ETag"), etag);
|
||||
}
|
||||
|
||||
static bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest *request, int code=200, uint16_t eTagSuffix = 0) {
|
||||
// Only send 304 (Not Modified) if response code is 200 (OK)
|
||||
if (code != 200) return false;
|
||||
|
||||
AsyncWebHeader *header = request->getHeader(F("If-None-Match"));
|
||||
char etag[32] = {'\0'};
|
||||
generateEtag(etag, eTagSuffix);
|
||||
if (header && header->value() == etag) {
|
||||
AsyncWebServerResponse *response = request->beginResponse(304);
|
||||
setStaticContentCacheHeaders(response, code, eTagSuffix);
|
||||
request->send(response);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
//Is this an IP?
|
||||
bool isIp(String str) {
|
||||
@@ -451,7 +489,7 @@ void initServer()
|
||||
AsyncWebServerResponse *response = request->beginResponse_P(404, "text/html", PAGE_404, PAGE_404_length);
|
||||
#endif
|
||||
response->addHeader(FPSTR(s_content_enc),"gzip");
|
||||
setStaticContentCacheHeaders(response);
|
||||
setStaticContentCacheHeaders(response, 404);
|
||||
request->send(response);
|
||||
//request->send_P(404, "text/html", PAGE_404);
|
||||
});
|
||||
@@ -467,31 +505,6 @@ void serveIndexOrWelcome(AsyncWebServerRequest *request)
|
||||
}
|
||||
}
|
||||
|
||||
bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest* request)
|
||||
{
|
||||
AsyncWebHeader* header = request->getHeader("If-None-Match");
|
||||
if (header && header->value() == String(VERSION)) {
|
||||
request->send(304);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void setStaticContentCacheHeaders(AsyncWebServerResponse *response)
|
||||
{
|
||||
char tmp[12];
|
||||
// https://medium.com/@codebyamir/a-web-developers-guide-to-browser-caching-cc41f3b73e7c
|
||||
#ifndef WLED_DEBUG
|
||||
//this header name is misleading, "no-cache" will not disable cache,
|
||||
//it just revalidates on every load using the "If-None-Match" header with the last ETag value
|
||||
response->addHeader(F("Cache-Control"),"no-cache");
|
||||
#else
|
||||
response->addHeader(F("Cache-Control"),"no-store,max-age=0"); // prevent caching if debug build
|
||||
#endif
|
||||
sprintf_P(tmp, PSTR("%8d-%02x"), VERSION, cacheInvalidate);
|
||||
response->addHeader(F("ETag"), tmp);
|
||||
}
|
||||
|
||||
void serveIndex(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (handleFileRead(request, "/index.htm")) return;
|
||||
@@ -606,7 +619,7 @@ void serveSettingsJS(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncWebServerResponse *response;
|
||||
response = request->beginResponse(200, "application/javascript", buf);
|
||||
response->addHeader(F("Cache-Control"),"no-store");
|
||||
response->addHeader(FPSTR(s_cache_control),F("no-store"));
|
||||
response->addHeader(F("Expires"),"0");
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user