mirror of
https://github.com/wled/WLED.git
synced 2025-11-10 11:38:58 +00:00
Compare commits
17 Commits
coderabbit
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
196f5798a1 | ||
|
|
614984257c | ||
|
|
6676705428 | ||
|
|
71ad0d99aa | ||
|
|
70e8be6b4d | ||
|
|
f706c6cbed | ||
|
|
18dfc7081c | ||
|
|
5c0c84eb6b | ||
|
|
691c058ae8 | ||
|
|
7d550baf94 | ||
|
|
42ff73ffe7 | ||
|
|
2d8edfcb24 | ||
|
|
fb077ecadc | ||
|
|
caf3d900fd | ||
|
|
e920d2e101 | ||
|
|
54746c9730 | ||
|
|
8225a2a07c |
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,3 +1,2 @@
|
||||
github: [Aircoookie,blazoncek,DedeHai,lost-hope,willmmiles]
|
||||
github: [Aircoookie,blazoncek]
|
||||
custom: ['https://paypal.me/Aircoookie','https://paypal.me/blazoncek']
|
||||
thanks_dev: u/gh/netmindz
|
||||
|
||||
@@ -10,12 +10,10 @@
|
||||
|
||||
</p>
|
||||
|
||||
# Welcome to WLED! ✨
|
||||
# Welcome to my project WLED! ✨
|
||||
|
||||
A fast and feature-rich implementation of an ESP32 and ESP8266 webserver to control NeoPixel (WS2812B, WS2811, SK6812) LEDs or also SPI based chipsets like the WS2801 and APA102!
|
||||
|
||||
Originally created by [Aircoookie](https://github.com/Aircoookie)
|
||||
|
||||
## ⚙️ Features
|
||||
- WS2812FX library with more than 100 special effects
|
||||
- FastLED noise effects and 50 palettes
|
||||
@@ -34,7 +32,7 @@ Originally created by [Aircoookie](https://github.com/Aircoookie)
|
||||
- Filesystem-based config for easier backup of presets and settings
|
||||
|
||||
## 💡 Supported light control interfaces
|
||||
- WLED app for [Android](https://play.google.com/store/apps/details?id=ca.cgagnier.wlednativeandroid) and [iOS](https://apps.apple.com/gb/app/wled-native/id6446207239)
|
||||
- WLED app for [Android](https://play.google.com/store/apps/details?id=com.aircoookie.WLED) and [iOS](https://apps.apple.com/us/app/wled/id1475695033)
|
||||
- JSON and HTTP request APIs
|
||||
- MQTT
|
||||
- E1.31, Art-Net, DDP and TPM2.net
|
||||
|
||||
@@ -126,20 +126,6 @@ async function minify(str, type = "plain") {
|
||||
throw new Error("Unknown filter: " + type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline-depends, minifies, gzip-compresses an HTML source and writes a C header array.
|
||||
*
|
||||
* Reads the HTML at sourceFile, inlines referenced resources, replaces repo/version placeholders,
|
||||
* minifies the HTML, compresses it with gzip, converts the compressed bytes to a C-style hex array,
|
||||
* and writes a header file to resultFile that defines:
|
||||
* - const uint16_t PAGE_<page>_length = <length>;
|
||||
* - const uint8_t PAGE_<page>[] PROGMEM = { ... };
|
||||
*
|
||||
* @param {string} sourceFile - Path to the source HTML file to inline and compress.
|
||||
* @param {string} resultFile - Path where the generated C header file will be written.
|
||||
* @param {string} page - Identifier used to name the generated symbols (e.g., "index", "pixart").
|
||||
* @throws {Error} If inlining the HTML fails (propagates the inline error).
|
||||
*/
|
||||
async function writeHtmlGzipped(sourceFile, resultFile, page) {
|
||||
console.info("Reading " + sourceFile);
|
||||
inline.html({
|
||||
@@ -157,7 +143,7 @@ async function writeHtmlGzipped(sourceFile, resultFile, page) {
|
||||
console.info("Minified and compressed " + sourceFile + " from " + originalLength + " to " + result.length + " bytes");
|
||||
const array = hexdump(result);
|
||||
let src = singleHeader;
|
||||
src += `const uint16_t PAGE_${page}_length = ${result.length};\n`;
|
||||
src += `const uint16_t PAGE_${page}_L = ${result.length};\n`;
|
||||
src += `const uint8_t PAGE_${page}[] PROGMEM = {\n${array}\n};\n\n`;
|
||||
console.info("Writing " + resultFile);
|
||||
fs.writeFileSync(resultFile, src);
|
||||
@@ -258,22 +244,9 @@ if (isAlreadyBuilt("wled00/data") && process.argv[2] !== '--force' && process.ar
|
||||
|
||||
writeHtmlGzipped("wled00/data/index.htm", "wled00/html_ui.h", 'index');
|
||||
writeHtmlGzipped("wled00/data/pixart/pixart.htm", "wled00/html_pixart.h", 'pixart');
|
||||
//writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal');
|
||||
writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal');
|
||||
writeHtmlGzipped("wled00/data/pxmagic/pxmagic.htm", "wled00/html_pxmagic.h", 'pxmagic');
|
||||
|
||||
writeChunks(
|
||||
"wled00/data/cpal",
|
||||
[
|
||||
{
|
||||
file: "cpal.htm",
|
||||
name: "PAGE_cpal",
|
||||
method: "gzip",
|
||||
filter: "html-minify"
|
||||
}
|
||||
],
|
||||
"wled00/html_cpal.h"
|
||||
);
|
||||
|
||||
writeChunks(
|
||||
"wled00/data",
|
||||
[
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
#include "wled.h"
|
||||
#include "FXparticleSystem.h" // TODO: better define the required function (mem service) in FX.h?
|
||||
#include "palettes.h"
|
||||
|
||||
/*
|
||||
Custom per-LED mapping has moved!
|
||||
@@ -199,15 +200,12 @@ void Segment::deallocateData() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clear pending runtime state when a segment is marked for reset.
|
||||
*
|
||||
* If this segment's reset flag is set and the segment is active, zeroes the
|
||||
* segment's runtime data buffer (to avoid heap fragmentation), clears the
|
||||
* pixel buffer, resets timing/step/call/aux counters, clears the reset flag,
|
||||
* and, when GIF playback support is enabled, ends any ongoing image playback.
|
||||
*
|
||||
* Note: this routine is safe to call only when no effect mode is currently
|
||||
* running for the segment — effect code may access the data/pixel buffers. */
|
||||
* If reset of this segment was requested, clears runtime
|
||||
* settings of this segment.
|
||||
* Must not be called while an effect mode function is running
|
||||
* because it could access the data buffer and this method
|
||||
* may free that data buffer.
|
||||
*/
|
||||
void Segment::resetIfRequired() {
|
||||
if (!reset || !isActive()) return;
|
||||
//DEBUG_PRINTF_P(PSTR("-- Segment reset: %p\n"), this);
|
||||
@@ -220,31 +218,9 @@ void Segment::resetIfRequired() {
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Selects and loads a palette into the supplied CRGBPalette16.
|
||||
*
|
||||
* Loads a palette identified by the numeric index `pal` into `targetPalette`.
|
||||
* Supported palette sources (by index):
|
||||
* - 0: the effect/default palette (resolved from _default_palette)
|
||||
* - 1: runtime-random palette
|
||||
* - 2–5: palettes derived from this segment's color slots (primary/secondary/tertiary combinations)
|
||||
* - fastLED and built-in gradient palettes: mapped from the next contiguous index range
|
||||
* - custom palettes: addressed from the high end (255, 254, ...) and stored in customPalettes
|
||||
*
|
||||
* If `pal` is outside the valid range for built-in/gradient/fastled indices it will be treated as 0
|
||||
* (the default palette). When a custom palette index is selected it is loaded from customPalettes.
|
||||
*
|
||||
* @param targetPalette Palette object to populate (returned by reference).
|
||||
* @param pal Numeric palette index selecting the source and layout (see summary above).
|
||||
* @return CRGBPalette16& Reference to the populated `targetPalette`.
|
||||
*/
|
||||
CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
|
||||
// there is one randomy generated palette (1) followed by 4 palettes created from segment colors (2-5)
|
||||
// those are followed by 7 fastled palettes (6-12) and 59 gradient palettes (13-71)
|
||||
// then come the custom palettes (255,254,...) growing downwards from 255 (255 being 1st custom palette)
|
||||
// palette 0 is a varying palette depending on effect and may be replaced by segment's color if so
|
||||
// instructed in color_from_palette()
|
||||
if (pal > FIXED_PALETTE_COUNT && pal < 255-customPalettes.size()+1) pal = 0; // out of bounds palette
|
||||
if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0;
|
||||
if (pal > 245 && (customPalettes.size() == 0 || 255U-pal > customPalettes.size()-1)) pal = 0;
|
||||
//default palette. Differs depending on effect
|
||||
if (pal == 0) pal = _default_palette; // _default_palette is set in setMode()
|
||||
switch (pal) {
|
||||
@@ -280,13 +256,13 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
|
||||
}
|
||||
break;}
|
||||
default: //progmem palettes
|
||||
if (pal > 255 - customPalettes.size()) {
|
||||
if (pal>245) {
|
||||
targetPalette = customPalettes[255-pal]; // we checked bounds above
|
||||
} else if (pal < DYNAMIC_PALETTE_COUNT+FASTLED_PALETTE_COUNT+1) { // palette 6 - 12, fastled palettes
|
||||
targetPalette = *fastledPalettes[pal-DYNAMIC_PALETTE_COUNT-1];
|
||||
} else if (pal < 13) { // palette 6 - 12, fastled palettes
|
||||
targetPalette = *fastledPalettes[pal-6];
|
||||
} else {
|
||||
byte tcp[72];
|
||||
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal-(DYNAMIC_PALETTE_COUNT+FASTLED_PALETTE_COUNT)-1])), 72);
|
||||
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal-13])), 72);
|
||||
targetPalette.loadDynamicGradientPalette(tcp);
|
||||
}
|
||||
break;
|
||||
@@ -553,24 +529,6 @@ Segment &Segment::setOption(uint8_t n, bool val) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set the effect (mode) for this segment.
|
||||
*
|
||||
* Sets the segment's mode to the first non-reserved effect at or after the
|
||||
* provided index, optionally loading the effect's default parameters. If the
|
||||
* new mode differs from the current one, a transition is started (a segment
|
||||
* copy is created), the mode-specific defaults and palette are applied when
|
||||
* requested, and the segment is marked for reset and state broadcast.
|
||||
*
|
||||
* @param fx Index of the desired effect/mode. If this index points to a
|
||||
* reserved mode the next non-reserved mode is used. If the index is
|
||||
* out of range the solid mode (index 0) is selected.
|
||||
* @param loadDefaults When true, extract and apply the effect's default
|
||||
* parameters (speed, intensity, custom values, mapping
|
||||
* flags, sound simulation, mirror/reverse flags, etc.)
|
||||
* and set the palette default when present.
|
||||
* @return Segment& Reference to this segment (allows chaining).
|
||||
*/
|
||||
Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
|
||||
// skip reserved
|
||||
while (fx < strip.getModeCount() && strncmp_P("RSVD", strip.getModeData(fx), 4) == 0) fx++;
|
||||
@@ -607,21 +565,9 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set the segment's palette by index.
|
||||
*
|
||||
* Validates the supplied palette index and, if it differs from the current
|
||||
* palette, begins a palette transition and marks the segment's state as
|
||||
* changed so the new palette is propagated to clients/hardware.
|
||||
*
|
||||
* If the provided index is outside the range of built-in or custom palettes,
|
||||
* it is normalized to 0.
|
||||
*
|
||||
* @param pal Palette index (may be adjusted to a valid value).
|
||||
* @return Segment& Reference to this segment (for chaining).
|
||||
*/
|
||||
Segment &Segment::setPalette(uint8_t pal) {
|
||||
if (pal <= 255-customPalettes.size() && pal > FIXED_PALETTE_COUNT) pal = 0; // not built in palette or custom palette
|
||||
if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; // built in palettes
|
||||
if (pal > 245 && (customPalettes.size() == 0 || 255U-pal > customPalettes.size()-1)) pal = 0; // custom palettes
|
||||
if (pal != palette) {
|
||||
//DEBUG_PRINTF_P(PSTR("- Starting palette transition: %d\n"), pal);
|
||||
startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change (no need to copy segment)
|
||||
|
||||
@@ -248,29 +248,11 @@ CRGBPalette16 generateRandomPalette() // generate fully random palette
|
||||
CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Loads user-defined color palettes from filesystem into runtime storage.
|
||||
*
|
||||
* Scans for files named "/palette0.json", "/palette1.json", ... up to
|
||||
* WLED_MAX_CUSTOM_PALETTES and builds dynamic gradient palettes from any valid
|
||||
* JSON found. Existing in-memory custom palettes are cleared before loading.
|
||||
*
|
||||
* Supported JSON formats for the "palette" array:
|
||||
* - Pairs of [index, hexString] (e.g. [0, "FF0000", 128, "00FF00", ...]) where
|
||||
* each pair is an index (0–255) followed by an RRGGBB or RRGGBBWW hex color.
|
||||
* - Quads of [index, R, G, B] (e.g. [0, 255, 0, 0, 128, 0, 255, 0, ...]) where
|
||||
* each group of four values is an index (0–255) followed by red/green/blue bytes.
|
||||
*
|
||||
* For each palette file the function converts the supplied entries into a
|
||||
* temporary gradient table (supporting up to 18 color stops) and appends the
|
||||
* resulting CRGBPalette16 to customPalettes. The loader stops at the first
|
||||
* missing palette file.
|
||||
*/
|
||||
void loadCustomPalettes() {
|
||||
byte tcp[72]; //support gradient palettes with up to 18 entries
|
||||
CRGBPalette16 targetPalette;
|
||||
customPalettes.clear(); // start fresh
|
||||
for (int index = 0; index < WLED_MAX_CUSTOM_PALETTES; index++) {
|
||||
for (int index = 0; index<10; index++) {
|
||||
char fileName[32];
|
||||
sprintf_P(fileName, PSTR("/palette%d.json"), index);
|
||||
|
||||
|
||||
@@ -123,21 +123,7 @@ CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette);
|
||||
CRGBPalette16 generateRandomPalette();
|
||||
void loadCustomPalettes();
|
||||
extern std::vector<CRGBPalette16> customPalettes;
|
||||
/**
|
||||
* Get the total number of available palettes (built-in fixed palettes plus user-defined custom palettes).
|
||||
*
|
||||
* @return Total palette count (FIXED_PALETTE_COUNT + customPalettes.size()).
|
||||
*/
|
||||
inline size_t getPaletteCount() { return FIXED_PALETTE_COUNT + customPalettes.size(); }
|
||||
/**
|
||||
* Pack an RGBW byte array into a 32-bit color value.
|
||||
*
|
||||
* The input must point to at least four bytes in order: R, G, B, W.
|
||||
* Returns a uint32_t with layout 0xWWRRGGBB (white in the highest byte).
|
||||
*
|
||||
* @param rgbw Pointer to 4 bytes: {R, G, B, W}.
|
||||
* @return 32-bit packed color in 0xWWRRGGBB format.
|
||||
*/
|
||||
inline size_t getPaletteCount() { return 13 + GRADIENT_PALETTE_COUNT + customPalettes.size(); }
|
||||
inline uint32_t colorFromRgbw(byte* rgbw) { return uint32_t((byte(rgbw[3]) << 24) | (byte(rgbw[0]) << 16) | (byte(rgbw[1]) << 8) | (byte(rgbw[2]))); }
|
||||
void hsv2rgb(const CHSV32& hsv, uint32_t& rgb);
|
||||
void colorHStoRGB(uint16_t hue, byte sat, byte* rgb);
|
||||
@@ -155,8 +141,4 @@ void setRandomColor(byte* rgb);
|
||||
|
||||
[[gnu::hot, gnu::pure]] uint32_t color_fade(uint32_t c1, uint8_t amount, bool video = false);
|
||||
|
||||
// palettes
|
||||
extern const TProgmemRGBPalette16* const fastledPalettes[];
|
||||
extern const uint8_t* const gGradientPalettes[];
|
||||
#endif
|
||||
|
||||
|
||||
@@ -1025,23 +1025,6 @@ function redrawPalPrev()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a CSS background rule showing a horizontal gradient preview for a palette.
|
||||
*
|
||||
* Uses the global `palettesData` array for palette entries. Each palette entry may be:
|
||||
* - an array [posByte, r, g, b] where `posByte` is 0..255 mapped to 0..100%,
|
||||
* - the literal 'r' to insert a random RGB color, or
|
||||
* - a reference value whose second character is treated as a 1-based index into the DOM color-slot list (element with id "csl") and reads its `data-r`, `data-g`, `data-b` attributes.
|
||||
*
|
||||
* Special cases:
|
||||
* - If the palette contains a single color, the function duplicates it to produce a two-color gradient.
|
||||
* - If `palettesData` is not defined the function returns undefined.
|
||||
* - If the requested palette id is not found the function returns the string `'display: none'`.
|
||||
*
|
||||
* @param {number|string} id - Palette identifier (index or key) into `palettesData`.
|
||||
* @return {string|undefined} CSS declaration for a left-to-right linear-gradient (e.g. `"background: linear-gradient(to right, ...);"`),
|
||||
* `'display: none'` when the palette is missing, or `undefined` if `palettesData` is not available.
|
||||
*/
|
||||
function genPalPrevCss(id)
|
||||
{
|
||||
if (!palettesData) return;
|
||||
@@ -1059,7 +1042,8 @@ function genPalPrevCss(id)
|
||||
}
|
||||
|
||||
var gradient = [];
|
||||
paletteData.forEach((e,j) => {
|
||||
for (let j = 0; j < paletteData.length; j++) {
|
||||
const e = paletteData[j];
|
||||
let r, g, b;
|
||||
let index = false;
|
||||
if (Array.isArray(e)) {
|
||||
@@ -1081,8 +1065,9 @@ function genPalPrevCss(id)
|
||||
if (index === false) {
|
||||
index = Math.round(j / paletteData.length * 100);
|
||||
}
|
||||
|
||||
gradient.push(`rgb(${r},${g},${b}) ${index}%`);
|
||||
});
|
||||
}
|
||||
|
||||
return `background: linear-gradient(to right,${gradient.join()});`;
|
||||
}
|
||||
@@ -3101,29 +3086,15 @@ let iSlide = 0, x0 = null, scrollS = 0, locked = false;
|
||||
|
||||
function unify(e) { return e.changedTouches ? e.changedTouches[0] : e; }
|
||||
|
||||
/**
|
||||
* Return true if any class name in the provided list starts with "Iro".
|
||||
*
|
||||
* @param {Iterable<string>} classList - An iterable of class name strings (e.g., Element.classList or an array).
|
||||
* @returns {boolean} True when at least one class name begins with "Iro", otherwise false.
|
||||
*/
|
||||
function hasIroClass(classList)
|
||||
{
|
||||
let found = false;
|
||||
classList.forEach((e)=>{ if (e.startsWith('Iro')) found = true; });
|
||||
return found;
|
||||
for (var i = 0; i < classList.length; i++) {
|
||||
var element = classList[i];
|
||||
if (element.startsWith('Iro')) return true;
|
||||
}
|
||||
/**
|
||||
* Handle touch/drag start to lock page scrolling and initiate horizontal slide gestures.
|
||||
*
|
||||
* If the app is in PC mode or simplified UI, or the event target (or its parent) is marked
|
||||
* to skip sliding (has class `noslide` or contains iro-related classes), the function returns
|
||||
* without side effects. Otherwise it records the initial pointer X position and current
|
||||
* scrollTop into globals used by the gesture handler, sets the global `locked` flag, and
|
||||
* toggles the container's `smooth` class accordingly.
|
||||
*
|
||||
* @param {Event} e - Pointer/touch event from rangetouch (the originating target is inspected).
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
//required by rangetouch.js
|
||||
function lock(e)
|
||||
{
|
||||
if (pcMode || simplifiedUI) return;
|
||||
|
||||
@@ -17,7 +17,27 @@
|
||||
}
|
||||
window.open(getURL("/update?revert"),"_self");
|
||||
}
|
||||
function GetV() {/*injected values here*/}
|
||||
function GetV() {
|
||||
// Fetch device info via JSON API instead of compiling it in
|
||||
fetch('/json/info')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.release) {
|
||||
var releaseSpan = document.querySelector('.release-name');
|
||||
if (releaseSpan) {
|
||||
releaseSpan.textContent = data.release;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Could not fetch device info:', error);
|
||||
// Fallback to compiled-in value if API call fails
|
||||
var releaseSpan = document.querySelector('.release-name');
|
||||
if (releaseSpan && releaseSpan.textContent === 'Loading...') {
|
||||
releaseSpan.textContent = 'Unknown';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
@import url("style.css");
|
||||
@@ -28,10 +48,13 @@
|
||||
<h2>WLED Software Update</h2>
|
||||
<form method='POST' action='./update' id='upd' enctype='multipart/form-data' onsubmit="toggle('upd')">
|
||||
Installed version: <span class="sip">WLED ##VERSION##</span><br>
|
||||
Release: <span class="sip release-name">Loading...</span><br>
|
||||
Download the latest binary: <a href="https://github.com/wled-dev/WLED/releases" target="_blank"
|
||||
style="vertical-align: text-bottom; display: inline-flex;">
|
||||
<img src="https://img.shields.io/github/release/wled-dev/WLED.svg?style=flat-square"></a><br>
|
||||
<input type='file' name='update' required><br> <!--should have accept='.bin', but it prevents file upload from android app-->
|
||||
<input type='checkbox' name='skipValidation' id='skipValidation'>
|
||||
<label for='skipValidation'>Ignore firmware validation</label><br>
|
||||
<button type="submit">Update!</button><br>
|
||||
<hr class="sml">
|
||||
<button id="rev" type="button" onclick="cR()">Revert update</button><br>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "wled.h"
|
||||
|
||||
#include "palettes.h"
|
||||
|
||||
#define JSON_PATH_STATE 1
|
||||
#define JSON_PATH_INFO 2
|
||||
#define JSON_PATH_STATE_INFO 3
|
||||
@@ -687,20 +689,6 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Populate a JSON object with device and runtime information.
|
||||
*
|
||||
* Fills the provided JsonObject with system, hardware, network, and LED subsystem
|
||||
* metadata used by the JSON API (version, build, LED counts and capabilities,
|
||||
* palettes and modes counts, WiFi and filesystem stats, uptime, time, and usermod
|
||||
* additions).
|
||||
*
|
||||
* The function writes several nested objects and arrays (for example "leds",
|
||||
* "wifi", "fs", "maps") and a set of top-level fields consumed by clients.
|
||||
*
|
||||
* @param root JsonObject to populate. Must be a valid writable JSON object;
|
||||
* the function will create nested objects/arrays inside it.
|
||||
*/
|
||||
void serializeInfo(JsonObject root)
|
||||
{
|
||||
root[F("ver")] = versionString;
|
||||
@@ -783,7 +771,6 @@ void serializeInfo(JsonObject root)
|
||||
root[F("fxcount")] = strip.getModeCount();
|
||||
root[F("palcount")] = getPaletteCount();
|
||||
root[F("cpalcount")] = customPalettes.size(); //number of custom palettes
|
||||
root[F("cpalmax")] = WLED_MAX_CUSTOM_PALETTES; // maximum number of custom palettes
|
||||
|
||||
JsonArray ledmaps = root.createNestedArray(F("maps"));
|
||||
for (size_t i=0; i<WLED_MAX_LEDMAPS; i++) {
|
||||
|
||||
111
wled00/ota_release_check.cpp
Normal file
111
wled00/ota_release_check.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
#include "ota_release_check.h"
|
||||
#include "wled.h"
|
||||
|
||||
#ifdef ESP32
|
||||
#include <esp_app_format.h>
|
||||
#include <esp_ota_ops.h>
|
||||
#endif
|
||||
|
||||
bool extractWledCustomDesc(const uint8_t* binaryData, size_t dataSize, wled_custom_desc_t* extractedDesc) {
|
||||
if (!binaryData || !extractedDesc || dataSize < 64) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Search in first 8KB only. This range was chosen because:
|
||||
// - ESP32 .rodata.wled_desc sections appear early in the binary (typically within first 2-4KB)
|
||||
// - ESP8266 .ver_number sections also appear early (typically within first 1-2KB)
|
||||
// - 8KB provides ample coverage for metadata discovery while minimizing processing time
|
||||
// - Larger firmware files (>1MB) would take significantly longer to process with full search
|
||||
// - Analysis of typical WLED binary layouts shows metadata appears well within this range
|
||||
const size_t search_limit = min(dataSize, (size_t)8192);
|
||||
|
||||
for (size_t offset = 0; offset <= search_limit - sizeof(wled_custom_desc_t); offset++) {
|
||||
const wled_custom_desc_t* custom_desc = (const wled_custom_desc_t*)(binaryData + offset);
|
||||
|
||||
// Check for magic number
|
||||
if (custom_desc->magic == WLED_CUSTOM_DESC_MAGIC) {
|
||||
// Found potential match, validate version
|
||||
if (custom_desc->version != WLED_CUSTOM_DESC_VERSION) {
|
||||
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but version mismatch: %u\n"),
|
||||
offset, custom_desc->version);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate hash using runtime function
|
||||
uint32_t expected_hash = djb2_hash_runtime(custom_desc->release_name);
|
||||
if (custom_desc->crc32 != expected_hash) {
|
||||
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but hash mismatch\n"), offset);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Valid structure found - copy entire structure
|
||||
memcpy(extractedDesc, custom_desc, sizeof(wled_custom_desc_t));
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("Extracted WLED structure at offset %u: '%s'\n"),
|
||||
offset, extractedDesc->release_name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_PRINTLN(F("No WLED custom description found in binary"));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool validateReleaseCompatibility(const char* extractedRelease) {
|
||||
if (!extractedRelease) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure extractedRelease is properly null terminated (guard against fixed-length buffer issues)
|
||||
char safeRelease[WLED_RELEASE_NAME_MAX_LEN];
|
||||
strncpy(safeRelease, extractedRelease, WLED_RELEASE_NAME_MAX_LEN - 1);
|
||||
safeRelease[WLED_RELEASE_NAME_MAX_LEN - 1] = '\0';
|
||||
|
||||
if (strlen(safeRelease) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Simple string comparison - releases must match exactly
|
||||
bool match = strcmp(releaseString, safeRelease) == 0;
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("Release compatibility check: current='%s', uploaded='%s', match=%s\n"),
|
||||
releaseString, safeRelease, match ? "YES" : "NO");
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
bool shouldAllowOTA(const uint8_t* binaryData, size_t dataSize, char* errorMessage, size_t errorMessageLen) {
|
||||
// Clear error message
|
||||
if (errorMessage && errorMessageLen > 0) {
|
||||
errorMessage[0] = '\0';
|
||||
}
|
||||
|
||||
// Ensure our custom description structure is referenced (prevents optimization)
|
||||
const wled_custom_desc_t* local_desc = getWledCustomDesc();
|
||||
(void)local_desc; // Suppress unused variable warning
|
||||
|
||||
// Try to extract WLED structure directly from binary data
|
||||
wled_custom_desc_t extractedDesc;
|
||||
bool hasCustomDesc = extractWledCustomDesc(binaryData, dataSize, &extractedDesc);
|
||||
|
||||
if (!hasCustomDesc) {
|
||||
// No custom description - this could be a legacy binary
|
||||
if (errorMessage && errorMessageLen > 0) {
|
||||
strncpy_P(errorMessage, PSTR("This firmware file is missing compatibility metadata. Enable 'Ignore firmware validation' to proceed anyway."), errorMessageLen - 1);
|
||||
errorMessage[errorMessageLen - 1] = '\0';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate compatibility using extracted release name
|
||||
if (!validateReleaseCompatibility(extractedDesc.release_name)) {
|
||||
if (errorMessage && errorMessageLen > 0) {
|
||||
snprintf_P(errorMessage, errorMessageLen, PSTR("Firmware compatibility mismatch: current='%s', uploaded='%s'. Enable 'Ignore firmware validation' to proceed anyway."),
|
||||
releaseString, extractedDesc.release_name);
|
||||
errorMessage[errorMessageLen - 1] = '\0'; // Ensure null termination
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
90
wled00/ota_release_check.h
Normal file
90
wled00/ota_release_check.h
Normal file
@@ -0,0 +1,90 @@
|
||||
#ifndef WLED_OTA_RELEASE_CHECK_H
|
||||
#define WLED_OTA_RELEASE_CHECK_H
|
||||
|
||||
/*
|
||||
* OTA Release Compatibility Checking using ESP-IDF Custom Description Section
|
||||
* Functions to extract and validate release names from uploaded binary files using embedded metadata
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#ifdef ESP32
|
||||
#include <esp_app_format.h>
|
||||
#endif
|
||||
|
||||
#define WLED_CUSTOM_DESC_MAGIC 0x57535453 // "WSTS" (WLED System Tag Structure)
|
||||
#define WLED_CUSTOM_DESC_VERSION 1
|
||||
#define WLED_RELEASE_NAME_MAX_LEN 48
|
||||
|
||||
// Platform-specific metadata offset in binary file
|
||||
#ifdef ESP32
|
||||
#define METADATA_OFFSET 0 // ESP32: metadata appears at beginning
|
||||
#elif defined(ESP8266)
|
||||
#define METADATA_OFFSET 0x1000 // ESP8266: metadata appears at 4KB offset
|
||||
#endif
|
||||
|
||||
/**
|
||||
* DJB2 hash function (C++11 compatible constexpr)
|
||||
* Used for compile-time hash computation of release names
|
||||
*/
|
||||
constexpr uint32_t djb2_hash_constexpr(const char* str, uint32_t hash = 5381) {
|
||||
return (*str == '\0') ? hash : djb2_hash_constexpr(str + 1, ((hash << 5) + hash) + *str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime DJB2 hash function for validation
|
||||
*/
|
||||
inline uint32_t djb2_hash_runtime(const char* str) {
|
||||
uint32_t hash = 5381;
|
||||
while (*str) {
|
||||
hash = ((hash << 5) + hash) + *str++;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* WLED Custom Description Structure
|
||||
* This structure is embedded in platform-specific sections at a fixed offset
|
||||
* in ESP32/ESP8266 binaries, allowing extraction without modifying the binary format
|
||||
*/
|
||||
typedef struct {
|
||||
uint32_t magic; // Magic number to identify WLED custom description
|
||||
uint32_t version; // Structure version for future compatibility
|
||||
char release_name[WLED_RELEASE_NAME_MAX_LEN]; // Release name (null-terminated)
|
||||
uint32_t crc32; // CRC32 of the above fields for integrity check
|
||||
} __attribute__((packed)) wled_custom_desc_t;
|
||||
|
||||
/**
|
||||
* Extract WLED custom description structure from binary
|
||||
* @param binaryData Pointer to binary file data
|
||||
* @param dataSize Size of binary data in bytes
|
||||
* @param extractedDesc Buffer to store extracted custom description structure
|
||||
* @return true if structure was found and extracted, false otherwise
|
||||
*/
|
||||
bool extractWledCustomDesc(const uint8_t* binaryData, size_t dataSize, wled_custom_desc_t* extractedDesc);
|
||||
|
||||
/**
|
||||
* Validate if extracted release name matches current release
|
||||
* @param extractedRelease Release name from uploaded binary
|
||||
* @return true if releases match (OTA should proceed), false if they don't match
|
||||
*/
|
||||
bool validateReleaseCompatibility(const char* extractedRelease);
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
bool shouldAllowOTA(const uint8_t* binaryData, size_t dataSize, char* errorMessage, size_t errorMessageLen);
|
||||
|
||||
/**
|
||||
* Get pointer to the embedded custom description structure
|
||||
* This ensures the structure is referenced and not optimized out
|
||||
* @return pointer to the custom description structure
|
||||
*/
|
||||
const wled_custom_desc_t* getWledCustomDesc();
|
||||
|
||||
#endif // WLED_OTA_RELEASE_CHECK_H
|
||||
@@ -210,24 +210,7 @@ void releaseJSONBufferLock()
|
||||
|
||||
|
||||
// extracts effect mode (or palette) name from names serialized string
|
||||
/**
|
||||
* @brief Extracts the display name for a mode or palette into a caller-provided buffer.
|
||||
*
|
||||
* When src is JSON_mode_names or nullptr, the name is read from the built-in mode data
|
||||
* (strip.getModeData). When src is JSON_palette_names and the mode index refers to a
|
||||
* custom palette (mode > 255 - customPalettes.size()), a formatted "~ Custom N ~"
|
||||
* name is written. Otherwise, the function parses a PROGMEM JSON-like string pointed
|
||||
* to by src to locate the mode's quoted name (handles commas and quoted fields) and
|
||||
* stops if an SR-extension marker '@' is encountered for that mode.
|
||||
*
|
||||
* The function always NUL-terminates dest and will truncate the name to fit maxLen.
|
||||
*
|
||||
* @param mode Index of the mode or palette to extract.
|
||||
* @param src PROGMEM string source to parse, or JSON_mode_names / JSON_palette_names / nullptr.
|
||||
* @param dest Caller-provided buffer to receive the NUL-terminated name (must be large enough).
|
||||
* @param maxLen Maximum number of bytes to write into dest (including the terminating NUL).
|
||||
* @return uint8_t Length of the resulting string written into dest (excluding the terminating NUL).
|
||||
*/
|
||||
// caller must provide large enough buffer for name (including SR extensions)!
|
||||
uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLen)
|
||||
{
|
||||
if (src == JSON_mode_names || src == nullptr) {
|
||||
@@ -247,7 +230,7 @@ uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLe
|
||||
} else return 0;
|
||||
}
|
||||
|
||||
if (src == JSON_palette_names && mode > 255-customPalettes.size()) {
|
||||
if (src == JSON_palette_names && mode > (GRADIENT_PALETTE_COUNT + 13)) {
|
||||
snprintf_P(dest, maxLen, PSTR("~ Custom %d ~"), 255-mode);
|
||||
dest[maxLen-1] = '\0';
|
||||
return strlen(dest);
|
||||
|
||||
29
wled00/wled_custom_desc.cpp
Normal file
29
wled00/wled_custom_desc.cpp
Normal file
@@ -0,0 +1,29 @@
|
||||
#include "ota_release_check.h"
|
||||
#include "wled.h"
|
||||
|
||||
// Platform-specific section definition
|
||||
#ifdef ESP32
|
||||
#define WLED_CUSTOM_DESC_SECTION ".rodata.wled_desc"
|
||||
#elif defined(ESP8266)
|
||||
#define WLED_CUSTOM_DESC_SECTION ".ver_number"
|
||||
#endif
|
||||
|
||||
// Single structure definition for both platforms
|
||||
const wled_custom_desc_t __attribute__((section(WLED_CUSTOM_DESC_SECTION))) wled_custom_description = {
|
||||
WLED_CUSTOM_DESC_MAGIC, // magic
|
||||
WLED_CUSTOM_DESC_VERSION, // version
|
||||
WLED_RELEASE_NAME, // release_name
|
||||
djb2_hash_constexpr(WLED_RELEASE_NAME) // crc32 - computed at compile time
|
||||
};
|
||||
|
||||
// Compile-time validation that release name doesn't exceed maximum length
|
||||
static_assert(sizeof(WLED_RELEASE_NAME) <= WLED_RELEASE_NAME_MAX_LEN,
|
||||
"WLED_RELEASE_NAME exceeds maximum length of WLED_RELEASE_NAME_MAX_LEN characters");
|
||||
|
||||
// Single reference to ensure it's not optimized away
|
||||
const wled_custom_desc_t* __attribute__((used)) wled_custom_desc_ref = &wled_custom_description;
|
||||
|
||||
// Function to ensure the structure is referenced by code
|
||||
const wled_custom_desc_t* getWledCustomDesc() {
|
||||
return &wled_custom_description;
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
#else
|
||||
#include <Update.h>
|
||||
#endif
|
||||
#include "ota_release_check.h"
|
||||
#endif
|
||||
#include "html_ui.h"
|
||||
#include "html_settings.h"
|
||||
@@ -244,24 +245,6 @@ static bool captivePortal(AsyncWebServerRequest *request)
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize and configure the HTTP server routes and handlers.
|
||||
*
|
||||
* Registers CORS/default headers and all web endpoints used by the device web UI and API,
|
||||
* including static content routes, settings UI, JSON API (/json), file upload (/upload),
|
||||
* OTA update endpoints (/update), optional pages (DMX, PixArt, PxMagic, CPAL, live views),
|
||||
* WebSocket attachment, captive portal handling and a NotFound handler that routes API calls
|
||||
* or serves a 404 page. Also installs the filesystem editor route (or an Access Denied
|
||||
* stub) via createEditHandler and attaches an AsyncJsonWebHandler for JSON POSTs.
|
||||
*
|
||||
* Side effects:
|
||||
* - Adds default HTTP headers (CORS).
|
||||
* - Registers many server routes and their callbacks with global state handlers.
|
||||
* - May set flags such as doReboot and configNeedsWrite from request handlers.
|
||||
* - Enforces PIN/OTA lock and subnet restrictions inside sensitive endpoints (OTA, settings, cfg).
|
||||
*
|
||||
* This function does not return a value.
|
||||
*/
|
||||
void initServer()
|
||||
{
|
||||
//CORS compatiblity
|
||||
@@ -442,23 +425,176 @@ void initServer()
|
||||
return;
|
||||
}
|
||||
if (!correctPIN || otaLock) return;
|
||||
|
||||
// Static variables to track release check status across chunks
|
||||
static bool releaseCheckPassed = false;
|
||||
static bool releaseCheckPending = true;
|
||||
static size_t totalBytesReceived = 0;
|
||||
static uint8_t* metadataBuffer = nullptr;
|
||||
static size_t metadataBufferUsed = 0;
|
||||
|
||||
if(!index){
|
||||
DEBUG_PRINTLN(F("OTA Update Start"));
|
||||
|
||||
// Reset validation state for new update
|
||||
releaseCheckPassed = false;
|
||||
releaseCheckPending = true;
|
||||
totalBytesReceived = 0;
|
||||
|
||||
// Free any existing metadata buffer
|
||||
if (metadataBuffer) {
|
||||
free(metadataBuffer);
|
||||
metadataBuffer = nullptr;
|
||||
metadataBufferUsed = 0;
|
||||
}
|
||||
|
||||
// Check if user wants to skip validation
|
||||
bool skipValidation = request->hasParam("skipValidation", true);
|
||||
|
||||
// If user chose to skip validation, proceed without compatibility check
|
||||
if (skipValidation) {
|
||||
DEBUG_PRINTLN(F("OTA validation skipped by user"));
|
||||
releaseCheckPassed = true;
|
||||
releaseCheckPending = false;
|
||||
}
|
||||
|
||||
DEBUG_PRINTLN(F("Release check passed, starting OTA update"));
|
||||
|
||||
#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)
|
||||
lastEditTime = millis(); // make sure PIN does not lock during update
|
||||
|
||||
// Start the actual OTA update
|
||||
strip.suspend();
|
||||
backupConfig(); // backup current config in case the update ends badly
|
||||
strip.resetSegments(); // free as much memory as you can
|
||||
#ifdef ESP8266
|
||||
Update.runAsync(true);
|
||||
#endif
|
||||
Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
|
||||
|
||||
// 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)) {
|
||||
DEBUG_PRINTF_P(PSTR("OTA Failed to begin: %s\n"), Update.getErrorString().c_str());
|
||||
strip.resume();
|
||||
UsermodManager::onUpdateBegin(false);
|
||||
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||
WLED::instance().enableWatchdog();
|
||||
#endif
|
||||
#ifdef ESP32
|
||||
request->send(500, FPSTR(CONTENT_TYPE_PLAIN), String("Update.begin failed: ") + Update.errorString());
|
||||
#else
|
||||
request->send(500, FPSTR(CONTENT_TYPE_PLAIN), String("Update.begin failed: ") + Update.getErrorString());
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
if(!Update.hasError()) Update.write(data, len);
|
||||
}
|
||||
|
||||
// Track total bytes received across all chunks
|
||||
size_t chunkStartOffset = totalBytesReceived;
|
||||
totalBytesReceived += len;
|
||||
|
||||
// Perform validation if we haven't done it yet and we have reached the metadata offset
|
||||
if (releaseCheckPending && totalBytesReceived > METADATA_OFFSET) {
|
||||
|
||||
if (chunkStartOffset <= METADATA_OFFSET) {
|
||||
// Current chunk contains the metadata offset
|
||||
size_t offsetInChunk = METADATA_OFFSET - chunkStartOffset;
|
||||
size_t availableDataAfterOffset = len - offsetInChunk;
|
||||
|
||||
if (availableDataAfterOffset >= sizeof(wled_custom_desc_t)) {
|
||||
// We have enough data in this chunk to validate immediately
|
||||
char errorMessage[128];
|
||||
releaseCheckPassed = shouldAllowOTA(data + offsetInChunk, availableDataAfterOffset,
|
||||
errorMessage, sizeof(errorMessage));
|
||||
releaseCheckPending = false;
|
||||
|
||||
if (!releaseCheckPassed) {
|
||||
DEBUG_PRINTF_P(PSTR("OTA declined: %s\n"), errorMessage);
|
||||
request->send(400, FPSTR(CONTENT_TYPE_PLAIN), errorMessage);
|
||||
return;
|
||||
} else {
|
||||
DEBUG_PRINTLN(F("OTA allowed: Release compatibility check passed"));
|
||||
}
|
||||
|
||||
} else {
|
||||
// Not enough data in this chunk, buffer it for the next chunk
|
||||
metadataBuffer = (uint8_t*)malloc(len - offsetInChunk);
|
||||
if (!metadataBuffer) {
|
||||
DEBUG_PRINTLN(F("OTA failed: Could not allocate metadata buffer"));
|
||||
request->send(500, FPSTR(CONTENT_TYPE_PLAIN), F("Out of memory for validation"));
|
||||
return;
|
||||
}
|
||||
memcpy(metadataBuffer, data + offsetInChunk, len - offsetInChunk);
|
||||
metadataBufferUsed = len - offsetInChunk;
|
||||
DEBUG_PRINTF_P(PSTR("Buffered %u bytes of metadata for next chunk\n"), metadataBufferUsed);
|
||||
}
|
||||
|
||||
} else if (metadataBuffer && metadataBufferUsed > 0) {
|
||||
// We have buffered metadata from previous chunk, combine with current chunk
|
||||
size_t totalMetadataSize = metadataBufferUsed + len;
|
||||
|
||||
if (totalMetadataSize >= sizeof(wled_custom_desc_t)) {
|
||||
// Create combined buffer for validation
|
||||
uint8_t* combinedBuffer = (uint8_t*)malloc(totalMetadataSize);
|
||||
if (!combinedBuffer) {
|
||||
DEBUG_PRINTLN(F("OTA failed: Could not allocate combined buffer"));
|
||||
request->send(500, FPSTR(CONTENT_TYPE_PLAIN), F("Out of memory for validation"));
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(combinedBuffer, metadataBuffer, metadataBufferUsed);
|
||||
memcpy(combinedBuffer + metadataBufferUsed, data, len);
|
||||
|
||||
char errorMessage[128];
|
||||
releaseCheckPassed = shouldAllowOTA(combinedBuffer, totalMetadataSize,
|
||||
errorMessage, sizeof(errorMessage));
|
||||
releaseCheckPending = false;
|
||||
|
||||
// Clean up buffers
|
||||
free(combinedBuffer);
|
||||
free(metadataBuffer);
|
||||
metadataBuffer = nullptr;
|
||||
metadataBufferUsed = 0;
|
||||
|
||||
if (!releaseCheckPassed) {
|
||||
DEBUG_PRINTF_P(PSTR("OTA declined: %s\n"), errorMessage);
|
||||
request->send(400, FPSTR(CONTENT_TYPE_PLAIN), errorMessage);
|
||||
return;
|
||||
} else {
|
||||
DEBUG_PRINTLN(F("OTA allowed: Release compatibility check passed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write chunk data to OTA update (only if release check passed or still pending)
|
||||
if ((releaseCheckPassed || releaseCheckPending) && !Update.hasError()) {
|
||||
if (Update.write(data, len) != len) {
|
||||
DEBUG_PRINTF_P(PSTR("OTA write failed on chunk %zu: %s\n"), index, Update.getErrorString().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if(isFinal){
|
||||
DEBUG_PRINTLN(F("OTA Update End"));
|
||||
|
||||
// Clean up metadata buffer if still allocated
|
||||
if (metadataBuffer) {
|
||||
free(metadataBuffer);
|
||||
metadataBuffer = nullptr;
|
||||
metadataBufferUsed = 0;
|
||||
}
|
||||
|
||||
// Check if validation was still pending (shouldn't happen normally)
|
||||
if (releaseCheckPending) {
|
||||
DEBUG_PRINTLN(F("OTA failed: Validation never completed"));
|
||||
request->send(400, FPSTR(CONTENT_TYPE_PLAIN), F("Firmware validation incomplete"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (releaseCheckPassed) {
|
||||
if(Update.end(true)){
|
||||
DEBUG_PRINTLN(F("Update Success"));
|
||||
} else {
|
||||
@@ -470,6 +606,7 @@ void initServer()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
#else
|
||||
const auto notSupported = [](AsyncWebServerRequest *request){
|
||||
@@ -488,31 +625,29 @@ void initServer()
|
||||
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
if (captivePortal(request)) return;
|
||||
if (!showWelcomePage || request->hasArg(F("sliders"))) {
|
||||
handleStaticContent(request, F("/index.htm"), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_index, PAGE_index_length);
|
||||
handleStaticContent(request, F("/index.htm"), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_index, PAGE_index_L);
|
||||
} else {
|
||||
serveSettings(request);
|
||||
}
|
||||
});
|
||||
|
||||
#ifndef WLED_DISABLE_2D
|
||||
#ifdef WLED_ENABLE_PIXART
|
||||
static const char _pixart_htm[] PROGMEM = "/pixart.htm";
|
||||
server.on(_pixart_htm, HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
handleStaticContent(request, FPSTR(_pixart_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pixart, PAGE_pixart_length);
|
||||
handleStaticContent(request, FPSTR(_pixart_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pixart, PAGE_pixart_L);
|
||||
});
|
||||
#endif
|
||||
|
||||
#ifndef WLED_DISABLE_PXMAGIC
|
||||
static const char _pxmagic_htm[] PROGMEM = "/pxmagic.htm";
|
||||
server.on(_pxmagic_htm, HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
handleStaticContent(request, FPSTR(_pxmagic_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pxmagic, PAGE_pxmagic_length);
|
||||
handleStaticContent(request, FPSTR(_pxmagic_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pxmagic, PAGE_pxmagic_L);
|
||||
});
|
||||
#endif
|
||||
#endif
|
||||
|
||||
static const char _cpal_htm[] PROGMEM = "/cpal.htm";
|
||||
server.on(_cpal_htm, HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
handleStaticContent(request, FPSTR(_cpal_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_cpal, PAGE_cpal_length);
|
||||
handleStaticContent(request, FPSTR(_cpal_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_cpal, PAGE_cpal_L);
|
||||
});
|
||||
|
||||
#ifdef WLED_ENABLE_WEBSOCKETS
|
||||
|
||||
Reference in New Issue
Block a user