mirror of
https://github.com/wled/WLED.git
synced 2025-11-10 19:40:48 +00:00
Compare commits
91 Commits
copilot/ad
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aaad22acc6 | ||
|
|
d04c9dc90a | ||
|
|
6fe7fb677c | ||
|
|
9db6a44809 | ||
|
|
7aedf77d83 | ||
|
|
50d33c5bf4 | ||
|
|
c7c379f962 | ||
|
|
af8c851cc6 | ||
|
|
88466c7d1f | ||
|
|
a36638ee6d | ||
|
|
ff93a48926 | ||
|
|
9474c29946 | ||
|
|
8097c7c86d | ||
|
|
abfe91d47b | ||
|
|
a4109c7ea8 | ||
|
|
34445dbe0f | ||
|
|
c5631b8fe3 | ||
|
|
4cddd3face | ||
|
|
5250a0fe2c | ||
|
|
95611f19c0 | ||
|
|
b98ee3e7b6 | ||
|
|
90d4dd79de | ||
|
|
00904d8862 | ||
|
|
2e7b6b79bf | ||
|
|
104d2ae7e8 | ||
|
|
f1242bfb7a | ||
|
|
aef5e9691c | ||
|
|
3d9012b43a | ||
|
|
3c5df5ae66 | ||
|
|
520f1f884b | ||
|
|
8fc33fd7b1 | ||
|
|
9e0f7ec4e9 | ||
|
|
00ca694eea | ||
|
|
c91a39f55c | ||
|
|
ffc7b66c20 | ||
|
|
94bea4405a | ||
|
|
2963c1b761 | ||
|
|
013ecfb189 | ||
|
|
670f74d589 | ||
|
|
d475d21a79 | ||
|
|
91349234a0 | ||
|
|
601bb6f0ca | ||
|
|
07e26d31f4 | ||
|
|
151acb249e | ||
|
|
25d5295d5d | ||
|
|
474a995845 | ||
|
|
f0f12e77ad | ||
|
|
46125773d9 | ||
|
|
ec61a35042 | ||
|
|
1afd72cb83 | ||
|
|
c623b82698 | ||
|
|
acd415c522 | ||
|
|
5fb37130f8 | ||
|
|
8e00e7175c | ||
|
|
0f06535932 | ||
|
|
46ff43889b | ||
|
|
7e1992fc5c | ||
|
|
0391488cef | ||
|
|
eb80fdf733 | ||
|
|
4973fd5a39 | ||
|
|
1dd338c5e0 | ||
|
|
186c4a7724 | ||
|
|
f0182eb1b2 | ||
|
|
2acf731baf | ||
|
|
e2b8f91417 | ||
|
|
5f33c69dd0 | ||
|
|
76bb3f7d77 | ||
|
|
da1d53c3b0 | ||
|
|
f4b98c43de | ||
|
|
62c78fc5ac | ||
|
|
9c4cf78a52 | ||
|
|
80c97076ae | ||
|
|
c3f394489f | ||
|
|
ce172df91a | ||
|
|
91baa34071 | ||
|
|
1da2692c34 | ||
|
|
d538736411 | ||
|
|
b268aea0ab | ||
|
|
a04d70293d | ||
|
|
c66d67dd19 | ||
|
|
0c22163fd9 | ||
|
|
50c0f41508 | ||
|
|
5ca10f35d1 | ||
|
|
a073bf32e4 | ||
|
|
d79b02379e | ||
|
|
f5f3fc338f | ||
|
|
042ed39464 | ||
|
|
c3e18905c1 | ||
|
|
a18a661c73 | ||
|
|
93908e758f | ||
|
|
30fbf55b9a |
@@ -2,6 +2,7 @@ Import('env')
|
||||
import os
|
||||
import shutil
|
||||
import gzip
|
||||
import json
|
||||
|
||||
OUTPUT_DIR = "build_output{}".format(os.path.sep)
|
||||
#OUTPUT_DIR = os.path.join("build_output")
|
||||
@@ -22,7 +23,8 @@ def create_release(source):
|
||||
release_name_def = _get_cpp_define_value(env, "WLED_RELEASE_NAME")
|
||||
if release_name_def:
|
||||
release_name = release_name_def.replace("\\\"", "")
|
||||
version = _get_cpp_define_value(env, "WLED_VERSION")
|
||||
with open("package.json", "r") as package:
|
||||
version = json.load(package)["version"]
|
||||
release_file = os.path.join(OUTPUT_DIR, "release", f"WLED_{version}_{release_name}.bin")
|
||||
release_gz_file = release_file + ".gz"
|
||||
print(f"Copying {source} to {release_file}")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
Import('env')
|
||||
import subprocess
|
||||
import json
|
||||
import re
|
||||
|
||||
def get_github_repo():
|
||||
@@ -42,7 +43,7 @@ def get_github_repo():
|
||||
|
||||
# Check if it's a GitHub URL
|
||||
if 'github.com' not in remote_url.lower():
|
||||
return 'unknown'
|
||||
return None
|
||||
|
||||
# Parse GitHub URL patterns:
|
||||
# https://github.com/owner/repo.git
|
||||
@@ -63,17 +64,53 @@ def get_github_repo():
|
||||
if ssh_match:
|
||||
return ssh_match.group(1)
|
||||
|
||||
return 'unknown'
|
||||
return None
|
||||
|
||||
except FileNotFoundError:
|
||||
# Git CLI is not installed or not in PATH
|
||||
return 'unknown'
|
||||
return None
|
||||
except subprocess.CalledProcessError:
|
||||
# Git command failed (e.g., not a git repo, no remote, etc.)
|
||||
return 'unknown'
|
||||
return None
|
||||
except Exception:
|
||||
# Any other unexpected error
|
||||
return 'unknown'
|
||||
return None
|
||||
|
||||
repo = get_github_repo()
|
||||
env.Append(BUILD_FLAGS=[f'-DWLED_REPO=\\"{repo}\\"'])
|
||||
# WLED version is managed by package.json; this is picked up in several places
|
||||
# - It's integrated in to the UI code
|
||||
# - Here, for wled_metadata.cpp
|
||||
# - The output_bins script
|
||||
# We always take it from package.json to ensure consistency
|
||||
with open("package.json", "r") as package:
|
||||
WLED_VERSION = json.load(package)["version"]
|
||||
|
||||
def has_def(cppdefs, name):
|
||||
""" Returns true if a given name is set in a CPPDEFINES collection """
|
||||
for f in cppdefs:
|
||||
if isinstance(f, tuple):
|
||||
f = f[0]
|
||||
if f == name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def add_wled_metadata_flags(env, node):
|
||||
cdefs = env["CPPDEFINES"].copy()
|
||||
|
||||
if not has_def(cdefs, "WLED_REPO"):
|
||||
repo = get_github_repo()
|
||||
if repo:
|
||||
cdefs.append(("WLED_REPO", f"\\\"{repo}\\\""))
|
||||
|
||||
cdefs.append(("WLED_VERSION", WLED_VERSION))
|
||||
|
||||
# This transforms the node in to a Builder; it cannot be modified again
|
||||
return env.Object(
|
||||
node,
|
||||
CPPDEFINES=cdefs
|
||||
)
|
||||
|
||||
env.AddBuildMiddleware(
|
||||
add_wled_metadata_flags,
|
||||
"*/wled_metadata.cpp"
|
||||
)
|
||||
@@ -1,8 +0,0 @@
|
||||
Import('env')
|
||||
import json
|
||||
|
||||
PACKAGE_FILE = "package.json"
|
||||
|
||||
with open(PACKAGE_FILE, "r") as package:
|
||||
version = json.load(package)["version"]
|
||||
env.Append(BUILD_FLAGS=[f"-DWLED_VERSION={version}"])
|
||||
@@ -10,7 +10,25 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# CI/release binaries
|
||||
default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_160, esp01_1m_full_160, nodemcuv2_compat, esp8266_2m_compat, esp01_1m_full_compat, esp32dev, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_16MB_opi, esp32s3dev_8MB_opi, esp32s3_4M_qspi, esp32_wrover, usermods
|
||||
default_envs = nodemcuv2
|
||||
esp8266_2m
|
||||
esp01_1m_full
|
||||
nodemcuv2_160
|
||||
esp8266_2m_160
|
||||
esp01_1m_full_160
|
||||
nodemcuv2_compat
|
||||
esp8266_2m_compat
|
||||
esp01_1m_full_compat
|
||||
esp32dev
|
||||
esp32dev_debug
|
||||
esp32_eth
|
||||
esp32_wrover
|
||||
lolin_s2_mini
|
||||
esp32c3dev
|
||||
esp32s3dev_16MB_opi
|
||||
esp32s3dev_8MB_opi
|
||||
esp32s3_4M_qspi
|
||||
usermods
|
||||
|
||||
src_dir = ./wled00
|
||||
data_dir = ./wled00/data
|
||||
@@ -110,8 +128,7 @@ ldscript_4m1m = eagle.flash.4m1m.ld
|
||||
|
||||
[scripts_defaults]
|
||||
extra_scripts =
|
||||
pre:pio-scripts/set_version.py
|
||||
pre:pio-scripts/set_repo.py
|
||||
pre:pio-scripts/set_metadata.py
|
||||
post:pio-scripts/output_bins.py
|
||||
post:pio-scripts/strip-floats.py
|
||||
pre:pio-scripts/user_config_copy.py
|
||||
@@ -431,13 +448,20 @@ board = esp32dev
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
build_unflags = ${common.build_unflags}
|
||||
custom_usermods = audioreactive
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_V4\" #-D WLED_DISABLE_BROWNOUT_DET
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32\" #-D WLED_DISABLE_BROWNOUT_DET
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.flash_mode = dio
|
||||
|
||||
[env:esp32dev_debug]
|
||||
extends = env:esp32dev
|
||||
upload_speed = 921600
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags}
|
||||
-D WLED_DEBUG
|
||||
-D WLED_RELEASE_NAME=\"ESP32_DEBUG\"
|
||||
|
||||
[env:esp32dev_8M]
|
||||
board = esp32dev
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
|
||||
@@ -541,12 +541,15 @@ build_flags = ${common.build_flags}
|
||||
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
|
||||
-D WLED_DEBUG_BUS
|
||||
; -D WLED_DEBUG
|
||||
-D SR_DMTYPE=-1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
|
||||
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#3.0.11
|
||||
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.flash_mode = dio
|
||||
custom_usermods = audioreactive
|
||||
|
||||
[env:esp32dev_hub75_forum_pinout]
|
||||
extends = env:esp32dev_hub75
|
||||
@@ -555,10 +558,10 @@ build_flags = ${common.build_flags}
|
||||
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
|
||||
-D ESP32_FORUM_PINOUT ;; enable for SmartMatrix default pins
|
||||
-D WLED_DEBUG_BUS
|
||||
-D SR_DMTYPE=-1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
|
||||
; -D WLED_DEBUG
|
||||
|
||||
|
||||
|
||||
[env:adafruit_matrixportal_esp32s3]
|
||||
; ESP32-S3 processor, 8 MB flash, 2 MB of PSRAM, dedicated driver pins for HUB75
|
||||
board = adafruit_matrixportal_esp32s3
|
||||
@@ -575,6 +578,7 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=
|
||||
-D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips
|
||||
-D ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3
|
||||
-D WLED_DEBUG_BUS
|
||||
-D SR_DMTYPE=-1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
|
||||
|
||||
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
@@ -584,6 +588,7 @@ board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
monitor_filters = esp32_exception_decoder
|
||||
custom_usermods = audioreactive
|
||||
|
||||
[env:esp32S3_PSRAM_HUB75]
|
||||
;; MOONHUB HUB75 adapter board
|
||||
@@ -601,6 +606,8 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=
|
||||
-D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips
|
||||
-D MOONHUB_S3_PINOUT ;; HUB75 pinout
|
||||
-D WLED_DEBUG_BUS
|
||||
-D LEDPIN=14 -D BTNPIN=0 -D RLYPIN=15 -D IRPIN=-1 -D AUDIOPIN=-1 ;; defaults that avoid pin conflicts with HUB75
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=10 -D I2S_CKPIN=11 -D I2S_WSPIN=12 -D MCLK_PIN=-1 ;; I2S mic
|
||||
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix
|
||||
@@ -609,3 +616,4 @@ board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
monitor_filters = esp32_exception_decoder
|
||||
custom_usermods = audioreactive
|
||||
@@ -26,7 +26,7 @@ const packageJson = require("../package.json");
|
||||
// Export functions for testing
|
||||
module.exports = { isFileNewerThan, isAnyFileInFolderNewerThan };
|
||||
|
||||
const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h", "wled00/html_pxmagic.h", "wled00/html_settings.h", "wled00/html_other.h"]
|
||||
const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h", "wled00/html_edit.h", "wled00/html_pxmagic.h", "wled00/html_settings.h", "wled00/html_other.h"]
|
||||
|
||||
// \x1b[34m is blue, \x1b[36m is cyan, \x1b[0m is reset
|
||||
const wledBanner = `
|
||||
@@ -246,6 +246,21 @@ 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/pxmagic/pxmagic.htm", "wled00/html_pxmagic.h", 'pxmagic');
|
||||
//writeHtmlGzipped("wled00/data/edit.htm", "wled00/html_edit.h", 'edit');
|
||||
|
||||
|
||||
writeChunks(
|
||||
"wled00/data",
|
||||
[
|
||||
{
|
||||
file: "edit.htm",
|
||||
name: "PAGE_edit",
|
||||
method: "gzip",
|
||||
filter: "html-minify"
|
||||
}
|
||||
],
|
||||
"wled00/html_edit.h"
|
||||
);
|
||||
|
||||
writeChunks(
|
||||
"wled00/data/cpal",
|
||||
@@ -388,12 +403,6 @@ const char PAGE_dmxmap[] PROGMEM = R"=====()=====";
|
||||
name: "PAGE_update",
|
||||
method: "gzip",
|
||||
filter: "html-minify",
|
||||
mangle: (str) =>
|
||||
str
|
||||
.replace(
|
||||
/function GetV().*\<\/script\>/gms,
|
||||
"</script><script src=\"/settings/s.js?p=9\"></script>"
|
||||
)
|
||||
},
|
||||
{
|
||||
file: "welcome.htm",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
// note: matrix may be comprised of multiple panels each with different orientation
|
||||
// but ledmap takes care of that. ledmap is constructed upon initialization
|
||||
// so matrix should disable regular ledmap processing
|
||||
// WARNING: effect drawing has to be suspended (strip.suspend()) or must be called from loop() context
|
||||
void WS2812FX::setUpMatrix() {
|
||||
#ifndef WLED_DISABLE_2D
|
||||
// isMatrix is set in cfg.cpp or set.cpp
|
||||
@@ -45,12 +46,12 @@ void WS2812FX::setUpMatrix() {
|
||||
return;
|
||||
}
|
||||
|
||||
suspend();
|
||||
waitForIt();
|
||||
|
||||
customMappingSize = 0; // prevent use of mapping if anything goes wrong
|
||||
|
||||
d_free(customMappingTable);
|
||||
// Segment::maxWidth and Segment::maxHeight are set according to panel layout
|
||||
// and the product will include at least all leds in matrix
|
||||
// if actual LEDs are more, getLengthTotal() will return correct number of LEDs
|
||||
customMappingTable = static_cast<uint16_t*>(d_malloc(sizeof(uint16_t)*getLengthTotal())); // prefer to not use SPI RAM
|
||||
|
||||
if (customMappingTable) {
|
||||
@@ -113,7 +114,6 @@ void WS2812FX::setUpMatrix() {
|
||||
|
||||
// delete gap array as we no longer need it
|
||||
p_free(gapTable);
|
||||
resume();
|
||||
|
||||
#ifdef WLED_DEBUG
|
||||
DEBUG_PRINT(F("Matrix ledmap:"));
|
||||
|
||||
@@ -1681,9 +1681,13 @@ void WS2812FX::setTransitionMode(bool t) {
|
||||
resume();
|
||||
}
|
||||
|
||||
// wait until frame is over (service() has finished or time for 1 frame has passed; yield() crashes on 8266)
|
||||
// wait until frame is over (service() has finished or time for 2 frames have passed; yield() crashes on 8266)
|
||||
// the latter may, in rare circumstances, lead to incorrectly assuming strip is done servicing but will not block
|
||||
// other processing "indefinitely"
|
||||
// rare circumstances are: setting FPS to high number (i.e. 120) and have very slow effect that will need more
|
||||
// time than 2 * _frametime (1000/FPS) to draw content
|
||||
void WS2812FX::waitForIt() {
|
||||
unsigned long maxWait = millis() + getFrameTime() + 100; // TODO: this needs a proper fix for timeout!
|
||||
unsigned long maxWait = millis() + 2*getFrameTime() + 100; // TODO: this needs a proper fix for timeout! see #4779
|
||||
while (isServicing() && maxWait > millis()) delay(1);
|
||||
#ifdef WLED_DEBUG
|
||||
if (millis() >= maxWait) DEBUG_PRINTLN(F("Waited for strip to finish servicing."));
|
||||
@@ -1810,7 +1814,11 @@ Segment& WS2812FX::getSegment(unsigned id) {
|
||||
return _segments[id >= _segments.size() ? getMainSegmentId() : id]; // vectors
|
||||
}
|
||||
|
||||
// WARNING: resetSegments(), makeAutoSegments() and fixInvalidSegments() must not be called while
|
||||
// strip is being serviced (strip.service()), you must call suspend prior if changing segments outside
|
||||
// loop() context
|
||||
void WS2812FX::resetSegments() {
|
||||
if (isServicing()) return;
|
||||
_segments.clear(); // destructs all Segment as part of clearing
|
||||
_segments.emplace_back(0, isMatrix ? Segment::maxWidth : _length, 0, isMatrix ? Segment::maxHeight : 1);
|
||||
_segments.shrink_to_fit(); // just in case ...
|
||||
@@ -1818,6 +1826,7 @@ void WS2812FX::resetSegments() {
|
||||
}
|
||||
|
||||
void WS2812FX::makeAutoSegments(bool forceReset) {
|
||||
if (isServicing()) return;
|
||||
if (autoSegments) { //make one segment per bus
|
||||
unsigned segStarts[MAX_NUM_SEGMENTS] = {0};
|
||||
unsigned segStops [MAX_NUM_SEGMENTS] = {0};
|
||||
@@ -1889,6 +1898,7 @@ void WS2812FX::makeAutoSegments(bool forceReset) {
|
||||
}
|
||||
|
||||
void WS2812FX::fixInvalidSegments() {
|
||||
if (isServicing()) return;
|
||||
//make sure no segment is longer than total (sanity check)
|
||||
for (size_t i = getSegmentsNum()-1; i > 0; i--) {
|
||||
if (isMatrix) {
|
||||
@@ -1951,6 +1961,7 @@ void WS2812FX::printSize() {
|
||||
|
||||
// load custom mapping table from JSON file (called from finalizeInit() or deserializeState())
|
||||
// if this is a matrix set-up and default ledmap.json file does not exist, create mapping table using setUpMatrix() from panel information
|
||||
// WARNING: effect drawing has to be suspended (strip.suspend()) or must be called from loop() context
|
||||
bool WS2812FX::deserializeMap(unsigned n) {
|
||||
char fileName[32];
|
||||
strcpy_P(fileName, PSTR("/ledmap"));
|
||||
@@ -1980,9 +1991,6 @@ bool WS2812FX::deserializeMap(unsigned n) {
|
||||
} else
|
||||
DEBUG_PRINTF_P(PSTR("Reading LED map from %s\n"), fileName);
|
||||
|
||||
suspend();
|
||||
waitForIt();
|
||||
|
||||
JsonObject root = pDoc->as<JsonObject>();
|
||||
// if we are loading default ledmap (at boot) set matrix width and height from the ledmap (compatible with WLED MM ledmaps)
|
||||
if (n == 0 && (!root[F("width")].isNull() || !root[F("height")].isNull())) {
|
||||
@@ -2040,8 +2048,6 @@ bool WS2812FX::deserializeMap(unsigned n) {
|
||||
DEBUG_PRINTLN(F("ERROR LED map allocation error."));
|
||||
}
|
||||
|
||||
resume();
|
||||
|
||||
releaseJSONBufferLock();
|
||||
return (customMappingSize > 0);
|
||||
}
|
||||
|
||||
1057
wled00/data/edit.htm
1057
wled00/data/edit.htm
File diff suppressed because it is too large
Load Diff
@@ -672,7 +672,6 @@ function parseInfo(i) {
|
||||
//syncTglRecv = i.str;
|
||||
maxSeg = i.leds.maxseg;
|
||||
pmt = i.fs.pmt;
|
||||
if (pcMode && !i.wifi.ap) gId('edit').classList.remove("hide"); else gId('edit').classList.add("hide");
|
||||
gId('buttonNodes').style.display = lastinfo.ndc > 0 ? null:"none";
|
||||
// do we have a matrix set-up
|
||||
mw = i.leds.matrix ? i.leds.matrix.w : 0;
|
||||
@@ -3151,7 +3150,6 @@ function togglePcMode(fromB = false)
|
||||
if (!fromB && ((wW < 1024 && lastw < 1024) || (wW >= 1024 && lastw >= 1024))) return; // no change in size and called from size()
|
||||
if (pcMode) openTab(0, true);
|
||||
gId('buttonPcm').className = (pcMode) ? "active":"";
|
||||
if (pcMode && !ap) gId('edit').classList.remove("hide"); else gId('edit').classList.add("hide");
|
||||
gId('bot').style.height = (pcMode && !cfg.comp.pcmbot) ? "0":"auto";
|
||||
sCol('--bh', gId('bot').clientHeight + "px");
|
||||
_C.style.width = (pcMode || simplifiedUI)?'100%':'400%';
|
||||
|
||||
@@ -34,12 +34,7 @@
|
||||
function SetVal(){switch(parseInt(d.Sf.EP.value)){case 5568: d.Sf.DI.value = 5568; break; case 6454: d.Sf.DI.value = 6454; break; case 4048: d.Sf.DI.value = 4048; break; }; SP();FC();}
|
||||
function S(){
|
||||
getLoc();
|
||||
loadJS(getURL('/settings/s.js?p=4'), false, ()=>{
|
||||
d.um_p = [];
|
||||
d.rsvd = [];
|
||||
d.ro_gpio = [];
|
||||
d.max_gpio = 50;
|
||||
}, ()=>{SetVal();pinDropdowns();}); // If we set async false, file is loaded and executed, then next statement is processed
|
||||
loadJS(getURL('/settings/s.js?p=4'), false, undefined, ()=>{SetVal();}); // If we set async false, file is loaded and executed, then next statement is processed
|
||||
if (loc) d.Sf.action = getURL('/settings/sync');
|
||||
}
|
||||
function getURL(path) {
|
||||
@@ -47,88 +42,6 @@
|
||||
}
|
||||
function hideDMXInput(){gId("dmxInput").style.display="none";}
|
||||
function hideNoDMXInput(){gId("dmxInputOff").style.display="none";}
|
||||
function pinDropdowns() {
|
||||
let fields = ["IDMR","IDMT","IDME"]; // DMX input pins
|
||||
for (let i of d.Sf.elements) {
|
||||
if (i.type === "number" && fields.includes(i.name)) { //select all pin select elements
|
||||
let v = parseInt(i.value);
|
||||
let sel = addDropdown(i.name);
|
||||
for (var j = -1; j < d.max_gpio; j++) {
|
||||
if (d.rsvd.includes(j)) continue;
|
||||
let foundPin = d.um_p.indexOf(j);
|
||||
let txt = (j === -1) ? "unused" : `${j}`;
|
||||
if (foundPin >= 0 && j !== v) txt += ` used`; // already reserved pin
|
||||
if (d.ro_gpio.includes(j)) txt += " (R/O)";
|
||||
let opt = addOption(sel, txt, j);
|
||||
if (j === v) opt.selected = true; // this is "our" pin
|
||||
else if (d.um_p.includes(j)) opt.disabled = true; // someone else's pin
|
||||
}
|
||||
}
|
||||
}
|
||||
// update select options
|
||||
d.Sf.querySelectorAll("select.pin").forEach((e)=>{pinUpd(e);});
|
||||
}
|
||||
function pinUpd(e) {
|
||||
// update changed select options across DMX pins
|
||||
let oldV = parseInt(e.dataset.val);
|
||||
e.dataset.val = e.value;
|
||||
let txt = e.name;
|
||||
let selects = d.Sf.querySelectorAll("select.pin");
|
||||
for (let sel of selects) {
|
||||
if (sel == e) continue;
|
||||
Array.from(sel.options).forEach((i)=>{
|
||||
if (!(i.value==oldV || i.value==e.value)) return;
|
||||
if (i.value == -1) {
|
||||
i.text = "unused";
|
||||
return;
|
||||
}
|
||||
i.text = i.value;
|
||||
if (i.value==oldV) {
|
||||
i.disabled = false;
|
||||
}
|
||||
if (i.value==e.value) {
|
||||
i.disabled = true;
|
||||
i.text += ` ${txt}`;
|
||||
}
|
||||
if (d.ro_gpio.includes(parseInt(i.value))) i.text += " (R/O)";
|
||||
});
|
||||
}
|
||||
}
|
||||
function addDropdown(field) {
|
||||
let sel = cE('select');
|
||||
sel.classList.add("pin");
|
||||
let inp = d.getElementsByName(field)[0];
|
||||
if (inp && inp.tagName === "INPUT" && (inp.type === "text" || inp.type === "number")) {
|
||||
let v = inp.value;
|
||||
let n = inp.name;
|
||||
// copy the existing input element's attributes to the new select element
|
||||
for (var i = 0; i < inp.attributes.length; ++ i) {
|
||||
var att = inp.attributes[i];
|
||||
// type and value don't apply, so skip them
|
||||
if (att.name != 'type' && att.name != 'value' && att.name != 'class' && att.name != 'style') {
|
||||
sel.setAttribute(att.name, att.value);
|
||||
}
|
||||
}
|
||||
sel.setAttribute("data-val", v);
|
||||
sel.setAttribute("onchange", "pinUpd(this)");
|
||||
// finally, replace the old input element with the new select element
|
||||
inp.parentElement.replaceChild(sel, inp);
|
||||
return sel;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function addOption(sel,txt,val) {
|
||||
if (sel===null) return; // select object missing
|
||||
let opt = cE("option");
|
||||
opt.value = val;
|
||||
opt.text = txt;
|
||||
sel.appendChild(opt);
|
||||
for (let i=0; i<sel.childNodes.length; i++) {
|
||||
let c = sel.childNodes[i];
|
||||
if (c.value == sel.dataset.val) sel.selectedIndex = i;
|
||||
}
|
||||
return opt;
|
||||
}
|
||||
</script>
|
||||
<style>@import url("style.css");</style>
|
||||
</head>
|
||||
|
||||
@@ -17,26 +17,65 @@
|
||||
}
|
||||
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 => {
|
||||
document.querySelector('.installed-version').textContent = `${data.brand} ${data.ver} (${data.vid})`;
|
||||
document.querySelector('.release-name').textContent = data.release;
|
||||
// TODO - assemble update URL
|
||||
// TODO - can this be done at build time?
|
||||
if (data.arch == "esp8266") {
|
||||
toggle('rev');
|
||||
}
|
||||
const isESP32 = data.arch && (data.arch.toLowerCase() === 'esp32' || data.arch.toLowerCase() === 'esp32-s2');
|
||||
if (isESP32) {
|
||||
gId('bootloader-section').style.display = 'block';
|
||||
if (data.bootloaderSHA256) {
|
||||
gId('bootloader-hash').innerText = 'Current bootloader SHA256: ' + data.bootloaderSHA256;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Could not fetch device info:', error);
|
||||
// Fallback to compiled-in value if API call fails
|
||||
document.querySelector('.installed-version').textContent = 'Unknown';
|
||||
document.querySelector('.release-name').textContent = 'Unknown';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
@import url("style.css");
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body onload="GetV()">
|
||||
<body onload="GetV();">
|
||||
<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>
|
||||
Installed version: <span class="sip installed-version">Loading...</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="hidden" name="skipValidation" value="" id="sV">
|
||||
<input type='file' name='update' required><br> <!--should have accept='.bin', but it prevents file upload from android app-->
|
||||
<input type='checkbox' onchange="sV.value=checked?1:''" 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>
|
||||
<button type="button" onclick="B()">Back</button>
|
||||
</form>
|
||||
<div id="bootloader-section" style="display:none;">
|
||||
<hr class="sml">
|
||||
<h2>ESP32 Bootloader Update</h2>
|
||||
<div id="bootloader-hash" class="sip" style="margin-bottom:8px;"></div>
|
||||
<form method='POST' action='./updatebootloader' id='bootupd' enctype='multipart/form-data' onsubmit="toggle('bootupd')">
|
||||
<b>Warning:</b> Only upload verified ESP32 bootloader files!<br>
|
||||
<input type='file' name='update' required><br>
|
||||
<button type="submit">Update Bootloader</button>
|
||||
</form>
|
||||
</div>
|
||||
<div id="Noupd" class="hide"><b>Updating...</b><br>Please do not close or refresh the page :)</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -55,8 +55,8 @@ static dmx_config_t createConfig()
|
||||
config.software_version_id = VERSION;
|
||||
strcpy(config.device_label, "WLED_MM");
|
||||
|
||||
const std::string versionString = "WLED_V" + std::to_string(VERSION);
|
||||
strncpy(config.software_version_label, versionString.c_str(), 32);
|
||||
const std::string dmxWledVersionString = "WLED_V" + std::to_string(VERSION);
|
||||
strncpy(config.software_version_label, dmxWledVersionString.c_str(), 32);
|
||||
config.software_version_label[32] = '\0'; // zero termination in case versionString string was longer than 32 chars
|
||||
|
||||
config.personalities[0].description = "SINGLE_RGB";
|
||||
|
||||
@@ -422,7 +422,7 @@ void prepareArtnetPollReply(ArtPollReply *reply) {
|
||||
|
||||
reply->reply_port = ARTNET_DEFAULT_PORT;
|
||||
|
||||
char * numberEnd = versionString;
|
||||
char * numberEnd = (char*) versionString; // strtol promises not to try to edit this.
|
||||
reply->reply_version_h = (uint8_t)strtol(numberEnd, &numberEnd, 10);
|
||||
numberEnd++;
|
||||
reply->reply_version_l = (uint8_t)strtol(numberEnd, &numberEnd, 10);
|
||||
|
||||
@@ -541,7 +541,6 @@ void handleSerial();
|
||||
void updateBaudRate(uint32_t rate);
|
||||
|
||||
//wled_server.cpp
|
||||
void createEditHandler(bool enable);
|
||||
void initServer();
|
||||
void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& headl, const String& subl="", byte optionT=255);
|
||||
void serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t error);
|
||||
|
||||
@@ -51,6 +51,9 @@ namespace {
|
||||
if (a.custom1 != b.custom1) d |= SEG_DIFFERS_FX;
|
||||
if (a.custom2 != b.custom2) d |= SEG_DIFFERS_FX;
|
||||
if (a.custom3 != b.custom3) d |= SEG_DIFFERS_FX;
|
||||
if (a.check1 != b.check1) d |= SEG_DIFFERS_FX;
|
||||
if (a.check2 != b.check2) d |= SEG_DIFFERS_FX;
|
||||
if (a.check3 != b.check3) d |= SEG_DIFFERS_FX;
|
||||
if (a.startY != b.startY) d |= SEG_DIFFERS_BOUNDS;
|
||||
if (a.stopY != b.stopY) d |= SEG_DIFFERS_BOUNDS;
|
||||
|
||||
@@ -817,6 +820,9 @@ void serializeInfo(JsonObject root)
|
||||
root[F("resetReason1")] = (int)rtc_get_reset_reason(1);
|
||||
#endif
|
||||
root[F("lwip")] = 0; //deprecated
|
||||
#ifndef WLED_DISABLE_OTA
|
||||
root[F("bootloaderSHA256")] = getBootloaderSHA256Hex();
|
||||
#endif
|
||||
#else
|
||||
root[F("arch")] = "esp8266";
|
||||
root[F("core")] = ESP.getCoreVersion();
|
||||
|
||||
728
wled00/ota_update.cpp
Normal file
728
wled00/ota_update.cpp
Normal file
@@ -0,0 +1,728 @@
|
||||
#include "ota_update.h"
|
||||
#include "wled.h"
|
||||
|
||||
#ifdef ESP32
|
||||
#include <esp_app_format.h>
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_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
|
||||
const size_t BOOTLOADER_OFFSET = 0x1000;
|
||||
#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
|
||||
|
||||
|
||||
/**
|
||||
* 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();
|
||||
backupConfig(); // backup current config in case the update ends badly
|
||||
strip.resetSegments(); // free as much memory as you can
|
||||
context->needsRestart = true;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
|
||||
// Cache for bootloader SHA256 digest as hex string
|
||||
static String bootloaderSHA256HexCache = "";
|
||||
|
||||
// Calculate and cache the bootloader SHA256 digest as hex string
|
||||
void calculateBootloaderSHA256() {
|
||||
if (!bootloaderSHA256HexCache.isEmpty()) return;
|
||||
|
||||
// Bootloader is at fixed offset 0x1000 (4KB) and is typically 32KB
|
||||
const uint32_t bootloaderSize = 0x8000; // 32KB, typical bootloader size
|
||||
|
||||
// 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 < bootloaderSize; offset += chunkSize) {
|
||||
size_t readSize = min((size_t)(bootloaderSize - offset), chunkSize);
|
||||
if (esp_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) {
|
||||
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 = String(hex);
|
||||
}
|
||||
|
||||
// Get bootloader SHA256 as hex string
|
||||
String getBootloaderSHA256Hex() {
|
||||
calculateBootloaderSHA256();
|
||||
return bootloaderSHA256HexCache;
|
||||
}
|
||||
|
||||
// Invalidate cached bootloader SHA256 (call after bootloader update)
|
||||
void invalidateBootloaderSHA256Cache() {
|
||||
bootloaderSHA256HexCache = "";
|
||||
}
|
||||
|
||||
// Verify complete buffered bootloader using ESP-IDF validation approach
|
||||
// This matches the key validation steps from esp_image_verify() in ESP-IDF
|
||||
// Returns the actual bootloader data pointer and length via the buffer and len parameters
|
||||
bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String* bootloaderErrorMsg) {
|
||||
size_t availableLen = len;
|
||||
if (!bootloaderErrorMsg) {
|
||||
DEBUG_PRINTLN(F("bootloaderErrorMsg is null"));
|
||||
return false;
|
||||
}
|
||||
// ESP32 image header structure (based on esp_image_format.h)
|
||||
// Offset 0: magic (0xE9)
|
||||
// Offset 1: segment_count
|
||||
// Offset 2: spi_mode
|
||||
// Offset 3: spi_speed (4 bits) + spi_size (4 bits)
|
||||
// Offset 4-7: entry_addr (uint32_t)
|
||||
// Offset 8: wp_pin
|
||||
// Offset 9-11: spi_pin_drv[3]
|
||||
// Offset 12-13: chip_id (uint16_t, little-endian)
|
||||
// Offset 14: min_chip_rev
|
||||
// Offset 15-22: reserved[8]
|
||||
// Offset 23: hash_appended
|
||||
|
||||
const size_t MIN_IMAGE_HEADER_SIZE = 24;
|
||||
|
||||
// 1. Validate minimum size for header
|
||||
if (len < MIN_IMAGE_HEADER_SIZE) {
|
||||
*bootloaderErrorMsg = "Bootloader too small - invalid header";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the bootloader starts at offset 0x1000 (common in partition table dumps)
|
||||
// This happens when someone uploads a complete flash dump instead of just the bootloader
|
||||
if (len > BOOTLOADER_OFFSET + MIN_IMAGE_HEADER_SIZE &&
|
||||
buffer[BOOTLOADER_OFFSET] == 0xE9 &&
|
||||
buffer[0] != 0xE9) {
|
||||
DEBUG_PRINTF_P(PSTR("Bootloader magic byte detected at offset 0x%04X - adjusting buffer\n"), BOOTLOADER_OFFSET);
|
||||
// Adjust buffer pointer to start at the actual bootloader
|
||||
buffer = buffer + BOOTLOADER_OFFSET;
|
||||
len = len - BOOTLOADER_OFFSET;
|
||||
|
||||
// Re-validate size after adjustment
|
||||
if (len < MIN_IMAGE_HEADER_SIZE) {
|
||||
*bootloaderErrorMsg = "Bootloader at offset 0x1000 too small - invalid header";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Magic byte check (matches esp_image_verify step 1)
|
||||
if (buffer[0] != 0xE9) {
|
||||
*bootloaderErrorMsg = "Invalid bootloader magic byte (expected 0xE9, got 0x" + String(buffer[0], HEX) + ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. Segment count validation (matches esp_image_verify step 2)
|
||||
uint8_t segmentCount = buffer[1];
|
||||
if (segmentCount == 0 || segmentCount > 16) {
|
||||
*bootloaderErrorMsg = "Invalid segment count: " + String(segmentCount);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. SPI mode validation (basic sanity check)
|
||||
uint8_t spiMode = buffer[2];
|
||||
if (spiMode > 3) { // Valid modes are 0-3 (QIO, QOUT, DIO, DOUT)
|
||||
*bootloaderErrorMsg = "Invalid SPI mode: " + String(spiMode);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. Chip ID validation (matches esp_image_verify step 3)
|
||||
uint16_t chipId = buffer[12] | (buffer[13] << 8); // Little-endian
|
||||
|
||||
// Known ESP32 chip IDs from ESP-IDF:
|
||||
// 0x0000 = ESP32
|
||||
// 0x0002 = ESP32-S2
|
||||
// 0x0005 = ESP32-C3
|
||||
// 0x0009 = ESP32-S3
|
||||
// 0x000C = ESP32-C2
|
||||
// 0x000D = ESP32-C6
|
||||
// 0x0010 = ESP32-H2
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32)
|
||||
if (chipId != 0x0000) {
|
||||
*bootloaderErrorMsg = "Chip ID mismatch - expected ESP32 (0x0000), got 0x" + String(chipId, HEX);
|
||||
return false;
|
||||
}
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
if (chipId != 0x0002) {
|
||||
*bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-S2 (0x0002), got 0x" + String(chipId, HEX);
|
||||
return false;
|
||||
}
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
if (chipId != 0x0005) {
|
||||
*bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-C3 (0x0005), got 0x" + String(chipId, HEX);
|
||||
return false;
|
||||
}
|
||||
*bootloaderErrorMsg = "ESP32-C3 update not supported yet";
|
||||
return false;
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
if (chipId != 0x0009) {
|
||||
*bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-S3 (0x0009), got 0x" + String(chipId, HEX);
|
||||
return false;
|
||||
}
|
||||
*bootloaderErrorMsg = "ESP32-S3 update not supported yet";
|
||||
return false;
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
if (chipId != 0x000D) {
|
||||
*bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-C6 (0x000D), got 0x" + String(chipId, HEX);
|
||||
return false;
|
||||
}
|
||||
*bootloaderErrorMsg = "ESP32-C6 update not supported yet";
|
||||
return false;
|
||||
#else
|
||||
// Generic validation - chip ID should be valid
|
||||
if (chipId > 0x00FF) {
|
||||
*bootloaderErrorMsg = "Invalid chip ID: 0x" + String(chipId, HEX);
|
||||
return false;
|
||||
}
|
||||
*bootloaderErrorMsg = "Unknown ESP32 target - bootloader update not supported";
|
||||
return false;
|
||||
#endif
|
||||
|
||||
// 6. Entry point validation (should be in valid memory range)
|
||||
uint32_t entryAddr = buffer[4] | (buffer[5] << 8) | (buffer[6] << 16) | (buffer[7] << 24);
|
||||
// ESP32 bootloader entry points are typically in IRAM range (0x40000000 - 0x40400000)
|
||||
// or ROM range (0x40000000 and above)
|
||||
if (entryAddr < 0x40000000 || entryAddr > 0x50000000) {
|
||||
*bootloaderErrorMsg = "Invalid entry address: 0x" + String(entryAddr, HEX);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 7. Basic segment structure validation
|
||||
// Each segment has a header: load_addr (4 bytes) + data_len (4 bytes)
|
||||
size_t offset = MIN_IMAGE_HEADER_SIZE;
|
||||
size_t actualBootloaderSize = MIN_IMAGE_HEADER_SIZE;
|
||||
|
||||
for (uint8_t i = 0; i < segmentCount && offset + 8 <= len; i++) {
|
||||
uint32_t segmentSize = buffer[offset + 4] | (buffer[offset + 5] << 8) |
|
||||
(buffer[offset + 6] << 16) | (buffer[offset + 7] << 24);
|
||||
|
||||
// Segment size sanity check
|
||||
// ESP32 classic bootloader segments can be larger, C3 are smaller
|
||||
if (segmentSize > 0x20000) { // 128KB max per segment (very generous)
|
||||
*bootloaderErrorMsg = "Segment " + String(i) + " too large: " + String(segmentSize) + " bytes";
|
||||
return false;
|
||||
}
|
||||
|
||||
offset += 8 + segmentSize; // Skip segment header and data
|
||||
}
|
||||
|
||||
actualBootloaderSize = offset;
|
||||
|
||||
// 8. Check for appended SHA256 hash (byte 23 in header)
|
||||
// If hash_appended != 0, there's a 32-byte SHA256 hash after the segments
|
||||
uint8_t hashAppended = buffer[23];
|
||||
if (hashAppended != 0) {
|
||||
actualBootloaderSize += 32;
|
||||
if (actualBootloaderSize > availableLen) {
|
||||
*bootloaderErrorMsg = "Bootloader missing SHA256 trailer";
|
||||
return false;
|
||||
}
|
||||
DEBUG_PRINTF_P(PSTR("Bootloader has appended SHA256 hash\n"));
|
||||
}
|
||||
|
||||
// 9. The image may also have a 1-byte checksum after segments/hash
|
||||
// Check if there's at least one more byte available
|
||||
if (actualBootloaderSize + 1 <= availableLen) {
|
||||
// There's likely a checksum byte
|
||||
actualBootloaderSize += 1;
|
||||
} else if (actualBootloaderSize > availableLen) {
|
||||
*bootloaderErrorMsg = "Bootloader truncated before checksum";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 10. Align to 16 bytes (ESP32 requirement for flash writes)
|
||||
// The bootloader image must be 16-byte aligned
|
||||
if (actualBootloaderSize % 16 != 0) {
|
||||
size_t alignedSize = ((actualBootloaderSize + 15) / 16) * 16;
|
||||
// Make sure we don't exceed available data
|
||||
if (alignedSize <= len) {
|
||||
actualBootloaderSize = alignedSize;
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("Bootloader validation: %d segments, actual size %d bytes (buffer size %d bytes, hash_appended=%d)\n"),
|
||||
segmentCount, actualBootloaderSize, len, hashAppended);
|
||||
|
||||
// 11. Verify we have enough data for all segments + hash + checksum
|
||||
if (actualBootloaderSize > availableLen) {
|
||||
*bootloaderErrorMsg = "Bootloader truncated - expected at least " + String(actualBootloaderSize) + " bytes, have " + String(availableLen) + " bytes";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (offset > availableLen) {
|
||||
*bootloaderErrorMsg = "Bootloader truncated - expected at least " + String(offset) + " bytes, have " + String(len) + " bytes";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update len to reflect actual bootloader size (including hash and checksum, with alignment)
|
||||
// This is critical - we must write the complete image including checksums
|
||||
len = actualBootloaderSize;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bootloader OTA context structure
|
||||
struct BootloaderUpdateContext {
|
||||
// State flags
|
||||
bool replySent = false;
|
||||
bool uploadComplete = false;
|
||||
String errorMessage;
|
||||
|
||||
// Buffer to hold bootloader data
|
||||
uint8_t* buffer = nullptr;
|
||||
size_t bytesBuffered = 0;
|
||||
const uint32_t bootloaderOffset = 0x1000;
|
||||
const uint32_t maxBootloaderSize = 0x10000; // 64KB buffer size
|
||||
};
|
||||
|
||||
// Cleanup bootloader OTA context
|
||||
static void endBootloaderOTA(AsyncWebServerRequest *request) {
|
||||
BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);
|
||||
request->_tempObject = nullptr;
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("EndBootloaderOTA %x --> %x\n"), (uintptr_t)request, (uintptr_t)context);
|
||||
if (context) {
|
||||
if (context->buffer) {
|
||||
free(context->buffer);
|
||||
context->buffer = nullptr;
|
||||
}
|
||||
|
||||
// If update failed, restore system state
|
||||
if (!context->uploadComplete || !context->errorMessage.isEmpty()) {
|
||||
strip.resume();
|
||||
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||
WLED::instance().enableWatchdog();
|
||||
#endif
|
||||
}
|
||||
|
||||
delete context;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize bootloader OTA context
|
||||
bool initBootloaderOTA(AsyncWebServerRequest *request) {
|
||||
if (request->_tempObject) {
|
||||
return true; // Already initialized
|
||||
}
|
||||
|
||||
BootloaderUpdateContext* context = new BootloaderUpdateContext();
|
||||
if (!context) {
|
||||
DEBUG_PRINTLN(F("Failed to allocate bootloader OTA context"));
|
||||
return false;
|
||||
}
|
||||
|
||||
request->_tempObject = context;
|
||||
request->onDisconnect([=]() { endBootloaderOTA(request); }); // ensures cleanup on disconnect
|
||||
|
||||
DEBUG_PRINTLN(F("Bootloader Update Start - initializing buffer"));
|
||||
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||
WLED::instance().disableWatchdog();
|
||||
#endif
|
||||
lastEditTime = millis(); // make sure PIN does not lock during update
|
||||
strip.suspend();
|
||||
strip.resetSegments();
|
||||
|
||||
// Check available heap before attempting allocation
|
||||
size_t freeHeap = getFreeHeapSize();
|
||||
DEBUG_PRINTF_P(PSTR("Free heap before bootloader buffer allocation: %d bytes (need %d bytes)\n"), freeHeap, context->maxBootloaderSize);
|
||||
|
||||
context->buffer = (uint8_t*)malloc(context->maxBootloaderSize);
|
||||
if (!context->buffer) {
|
||||
size_t freeHeapNow = getFreeHeapSize();
|
||||
DEBUG_PRINTF_P(PSTR("Failed to allocate %d byte bootloader buffer! Free heap: %d bytes\n"), context->maxBootloaderSize, freeHeapNow);
|
||||
context->errorMessage = "Out of memory! Free heap: " + String(freeHeapNow) + " bytes, need: " + String(context->maxBootloaderSize) + " bytes";
|
||||
strip.resume();
|
||||
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||
WLED::instance().enableWatchdog();
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
context->bytesBuffered = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Set bootloader OTA replied flag
|
||||
void setBootloaderOTAReplied(AsyncWebServerRequest *request) {
|
||||
BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);
|
||||
if (context) {
|
||||
context->replySent = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Get bootloader OTA result
|
||||
std::pair<bool, String> getBootloaderOTAResult(AsyncWebServerRequest *request) {
|
||||
BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);
|
||||
|
||||
if (!context) {
|
||||
return std::make_pair(true, String(F("Internal error: No bootloader OTA context")));
|
||||
}
|
||||
|
||||
bool needsReply = !context->replySent;
|
||||
String errorMsg = context->errorMessage;
|
||||
|
||||
// If upload was successful, return empty string and trigger reboot
|
||||
if (context->uploadComplete && errorMsg.isEmpty()) {
|
||||
doReboot = true;
|
||||
endBootloaderOTA(request);
|
||||
return std::make_pair(needsReply, String());
|
||||
}
|
||||
|
||||
// If there was an error, return it
|
||||
if (!errorMsg.isEmpty()) {
|
||||
endBootloaderOTA(request);
|
||||
return std::make_pair(needsReply, errorMsg);
|
||||
}
|
||||
|
||||
// Should never happen
|
||||
return std::make_pair(true, String(F("Internal software failure")));
|
||||
}
|
||||
|
||||
// Handle bootloader OTA data
|
||||
void handleBootloaderOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal) {
|
||||
BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);
|
||||
|
||||
if (!context) {
|
||||
DEBUG_PRINTLN(F("No bootloader OTA context - ignoring data"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context->errorMessage.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Buffer the incoming data
|
||||
if (context->buffer && context->bytesBuffered + len <= context->maxBootloaderSize) {
|
||||
memcpy(context->buffer + context->bytesBuffered, data, len);
|
||||
context->bytesBuffered += len;
|
||||
DEBUG_PRINTF_P(PSTR("Bootloader buffer progress: %d / %d bytes\n"), context->bytesBuffered, context->maxBootloaderSize);
|
||||
} else if (!context->buffer) {
|
||||
DEBUG_PRINTLN(F("Bootloader buffer not allocated!"));
|
||||
context->errorMessage = "Internal error: Bootloader buffer not allocated";
|
||||
return;
|
||||
} else {
|
||||
size_t totalSize = context->bytesBuffered + len;
|
||||
DEBUG_PRINTLN(F("Bootloader size exceeds maximum!"));
|
||||
context->errorMessage = "Bootloader file too large: " + String(totalSize) + " bytes (max: " + String(context->maxBootloaderSize) + " bytes)";
|
||||
return;
|
||||
}
|
||||
|
||||
// Only write to flash when upload is complete
|
||||
if (isFinal) {
|
||||
DEBUG_PRINTLN(F("Bootloader Upload Complete - validating and flashing"));
|
||||
|
||||
if (context->buffer && context->bytesBuffered > 0) {
|
||||
// Prepare pointers for verification (may be adjusted if bootloader at offset)
|
||||
const uint8_t* bootloaderData = context->buffer;
|
||||
size_t bootloaderSize = context->bytesBuffered;
|
||||
|
||||
// Verify the complete bootloader image before flashing
|
||||
// Note: verifyBootloaderImage may adjust bootloaderData pointer and bootloaderSize
|
||||
// for validation purposes only
|
||||
if (!verifyBootloaderImage(bootloaderData, bootloaderSize, &context->errorMessage)) {
|
||||
DEBUG_PRINTLN(F("Bootloader validation failed!"));
|
||||
// Error message already set by verifyBootloaderImage
|
||||
} else {
|
||||
// Calculate offset to write to flash
|
||||
// If bootloaderData was adjusted (partition table detected), we need to skip it in flash too
|
||||
size_t flashOffset = context->bootloaderOffset;
|
||||
const uint8_t* dataToWrite = context->buffer;
|
||||
size_t bytesToWrite = context->bytesBuffered;
|
||||
|
||||
// If validation adjusted the pointer, it means we have a partition table at the start
|
||||
// In this case, we should skip writing the partition table and write bootloader at 0x1000
|
||||
if (bootloaderData != context->buffer) {
|
||||
// bootloaderData was adjusted - skip partition table in our data
|
||||
size_t partitionTableSize = bootloaderData - context->buffer;
|
||||
dataToWrite = bootloaderData;
|
||||
bytesToWrite = bootloaderSize;
|
||||
DEBUG_PRINTF_P(PSTR("Skipping %d bytes of partition table data\n"), partitionTableSize);
|
||||
}
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("Bootloader validation passed - writing %d bytes to flash at 0x%04X\n"),
|
||||
bytesToWrite, flashOffset);
|
||||
|
||||
// Calculate erase size (must be multiple of 4KB)
|
||||
size_t eraseSize = ((bytesToWrite + 0xFFF) / 0x1000) * 0x1000;
|
||||
if (eraseSize > context->maxBootloaderSize) {
|
||||
eraseSize = context->maxBootloaderSize;
|
||||
}
|
||||
|
||||
// Erase bootloader region
|
||||
DEBUG_PRINTF_P(PSTR("Erasing %d bytes at 0x%04X...\n"), eraseSize, flashOffset);
|
||||
esp_err_t err = esp_flash_erase_region(NULL, flashOffset, eraseSize);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_PRINTF_P(PSTR("Bootloader erase error: %d\n"), err);
|
||||
context->errorMessage = "Flash erase failed (error code: " + String(err) + ")";
|
||||
} else {
|
||||
// Write the validated bootloader data to flash
|
||||
err = esp_flash_write(NULL, dataToWrite, flashOffset, bytesToWrite);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_PRINTF_P(PSTR("Bootloader flash write error: %d\n"), err);
|
||||
context->errorMessage = "Flash write failed (error code: " + String(err) + ")";
|
||||
} else {
|
||||
DEBUG_PRINTF_P(PSTR("Bootloader Update Success - %d bytes written to 0x%04X\n"),
|
||||
bytesToWrite, flashOffset);
|
||||
// Invalidate cached bootloader hash
|
||||
invalidateBootloaderSHA256Cache();
|
||||
context->uploadComplete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (context->bytesBuffered == 0) {
|
||||
context->errorMessage = "No bootloader data received";
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
114
wled00/ota_update.h
Normal file
114
wled00/ota_update.h
Normal file
@@ -0,0 +1,114 @@
|
||||
// WLED OTA update interface
|
||||
|
||||
#include <Arduino.h>
|
||||
#ifdef ESP8266
|
||||
#include <Updater.h>
|
||||
#else
|
||||
#include <Update.h>
|
||||
#endif
|
||||
|
||||
#pragma once
|
||||
|
||||
// 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);
|
||||
|
||||
#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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
void invalidateBootloaderSHA256Cache();
|
||||
|
||||
/**
|
||||
* Verify complete buffered bootloader using ESP-IDF validation approach
|
||||
* This matches the key validation steps from esp_image_verify() in ESP-IDF
|
||||
* @param buffer Reference to pointer to bootloader binary data (will be adjusted if offset detected)
|
||||
* @param len Reference to length of bootloader data (will be adjusted to actual size)
|
||||
* @param bootloaderErrorMsg Pointer to String to store error message (must not be null)
|
||||
* @return true if validation passed, false otherwise
|
||||
*/
|
||||
bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String* bootloaderErrorMsg);
|
||||
|
||||
/**
|
||||
* Create a bootloader OTA context object on an AsyncWebServerRequest
|
||||
* @param request Pointer to web request object
|
||||
* @return true if allocation was successful, false if not
|
||||
*/
|
||||
bool initBootloaderOTA(AsyncWebServerRequest *request);
|
||||
|
||||
/**
|
||||
* Indicate to the bootloader OTA subsystem that a reply has already been generated
|
||||
* @param request Pointer to web request object
|
||||
*/
|
||||
void setBootloaderOTAReplied(AsyncWebServerRequest *request);
|
||||
|
||||
/**
|
||||
* Retrieve the bootloader 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> getBootloaderOTAResult(AsyncWebServerRequest *request);
|
||||
|
||||
/**
|
||||
* Process a block of bootloader OTA data. This is a passthrough of an ArUploadHandlerFunction.
|
||||
* Requires that initBootloaderOTA 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
|
||||
*/
|
||||
void handleBootloaderOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal);
|
||||
#endif
|
||||
|
||||
@@ -613,7 +613,6 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
|
||||
#ifndef WLED_DISABLE_OTA
|
||||
aOtaEnabled = request->hasArg(F("AO"));
|
||||
#endif
|
||||
//createEditHandler(correctPIN && !otaLock);
|
||||
otaSameSubnet = request->hasArg(F("SU"));
|
||||
}
|
||||
}
|
||||
@@ -815,8 +814,13 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
|
||||
}
|
||||
}
|
||||
strip.panel.shrink_to_fit(); // release unused memory
|
||||
// we are changing matrix/ledmap geometry which *will* affect existing segments
|
||||
// since we are not in loop() context we must make sure that effects are not running. credit @blazonchek for properly fixing #4911
|
||||
strip.suspend();
|
||||
strip.waitForIt();
|
||||
strip.deserializeMap(); // (re)load default ledmap (will also setUpMatrix() if ledmap does not exist)
|
||||
strip.makeAutoSegments(true); // force re-creation of segments
|
||||
strip.resume();
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -369,7 +369,6 @@ void checkSettingsPIN(const char* pin) {
|
||||
if (!correctPIN && millis() - lastEditTime < PIN_RETRY_COOLDOWN) return; // guard against PIN brute force
|
||||
bool correctBefore = correctPIN;
|
||||
correctPIN = (strlen(settingsPIN) == 0 || strncmp(settingsPIN, pin, 4) == 0);
|
||||
if (correctBefore != correctPIN) createEditHandler(correctPIN);
|
||||
lastEditTime = millis();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#define WLED_DEFINE_GLOBAL_VARS //only in one source file, wled.cpp!
|
||||
#include "wled.h"
|
||||
#include "wled_ethernet.h"
|
||||
#include "ota_update.h"
|
||||
#ifdef WLED_ENABLE_AOTA
|
||||
#define NO_OTA_PORT
|
||||
#include <ArduinoOTA.h>
|
||||
@@ -11,6 +12,10 @@
|
||||
#include "soc/rtc_cntl_reg.h"
|
||||
#endif
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include <esp_ota_ops.h>
|
||||
#endif
|
||||
|
||||
extern "C" void usePWMFixedNMI();
|
||||
|
||||
/*
|
||||
@@ -166,16 +171,15 @@ void WLED::loop()
|
||||
// 15min PIN time-out
|
||||
if (strlen(settingsPIN)>0 && correctPIN && millis() - lastEditTime > PIN_TIMEOUT) {
|
||||
correctPIN = false;
|
||||
createEditHandler(false);
|
||||
}
|
||||
|
||||
// reconnect WiFi to clear stale allocations if heap gets too low
|
||||
if (millis() - heapTime > 15000) {
|
||||
uint32_t heap = getFreeHeapSize();
|
||||
if (heap < MIN_HEAP_SIZE && lastHeap < MIN_HEAP_SIZE) {
|
||||
DEBUG_PRINTF_P(PSTR("Heap too low! %u\n"), heap);
|
||||
forceReconnect = true;
|
||||
DEBUG_PRINTF_P(PSTR("Heap too low! %u\n"), heap);
|
||||
strip.resetSegments(); // remove all but one segments from memory
|
||||
if (!Update.isRunning()) forceReconnect = true;
|
||||
} else if (heap < MIN_HEAP_SIZE) {
|
||||
DEBUG_PRINTLN(F("Heap low, purging segments."));
|
||||
strip.purgeSegments();
|
||||
@@ -361,6 +365,26 @@ void WLED::setup()
|
||||
DEBUG_PRINTF_P(PSTR("---WLED %s %u INIT---\n"), versionString, VERSION);
|
||||
DEBUG_PRINTLN();
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
// Mark OTA app as valid IMMEDIATELY to prevent automatic rollback by ESP-IDF bootloader
|
||||
// This MUST happen as early as possible, before any potential crashes
|
||||
// Critical for OTA updates from 0.15 to 0.16+ on ESP32-C3 and other ESP32 variants
|
||||
const esp_partition_t* running = esp_ota_get_running_partition();
|
||||
esp_ota_img_states_t ota_state;
|
||||
if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) {
|
||||
if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) {
|
||||
DEBUG_PRINTLN(F("*** OTA UPDATE DETECTED - Marking app as valid to prevent rollback ***"));
|
||||
if (esp_ota_mark_app_valid_cancel_rollback() == ESP_OK) {
|
||||
DEBUG_PRINTLN(F("OTA app marked valid successfully"));
|
||||
} else {
|
||||
DEBUG_PRINTLN(F("WARNING: Failed to mark OTA app as valid!"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check IDF version for compatibility warnings
|
||||
const char* idf_ver = esp_get_idf_version();
|
||||
DEBUG_PRINTF_P(PSTR("ESP-IDF version: %s\n"), idf_ver);
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("esp32 %s\n"), ESP.getSdkVersion());
|
||||
#if defined(ESP_ARDUINO_VERSION)
|
||||
DEBUG_PRINTF_P(PSTR("arduino-esp32 v%d.%d.%d\n"), int(ESP_ARDUINO_VERSION_MAJOR), int(ESP_ARDUINO_VERSION_MINOR), int(ESP_ARDUINO_VERSION_PATCH)); // available since v2.0.0
|
||||
|
||||
@@ -189,11 +189,15 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument<PSRAM_Allocator>;
|
||||
#include "FastLED.h"
|
||||
#include "const.h"
|
||||
#include "fcn_declare.h"
|
||||
#ifndef WLED_DISABLE_OTA
|
||||
#include "ota_update.h"
|
||||
#endif
|
||||
#include "NodeStruct.h"
|
||||
#include "pin_manager.h"
|
||||
#include "colors.h"
|
||||
#include "bus_manager.h"
|
||||
#include "FX.h"
|
||||
#include "wled_metadata.h"
|
||||
|
||||
#ifndef CLIENT_SSID
|
||||
#define CLIENT_SSID DEFAULT_CLIENT_SSID
|
||||
@@ -270,20 +274,6 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument<PSRAM_Allocator>;
|
||||
#define STRINGIFY(X) #X
|
||||
#define TOSTRING(X) STRINGIFY(X)
|
||||
|
||||
#ifndef WLED_VERSION
|
||||
#define WLED_VERSION dev
|
||||
#endif
|
||||
#ifndef WLED_RELEASE_NAME
|
||||
#define WLED_RELEASE_NAME "Custom"
|
||||
#endif
|
||||
#ifndef WLED_REPO
|
||||
#define WLED_REPO "unknown"
|
||||
#endif
|
||||
|
||||
// Global Variable definitions
|
||||
WLED_GLOBAL char versionString[] _INIT(TOSTRING(WLED_VERSION));
|
||||
WLED_GLOBAL char releaseString[] _INIT(WLED_RELEASE_NAME); // must include the quotes when defining, e.g -D WLED_RELEASE_NAME=\"ESP32_MULTI_USREMODS\"
|
||||
WLED_GLOBAL char repoString[] _INIT(WLED_REPO);
|
||||
#define WLED_CODENAME "Niji"
|
||||
|
||||
// AP and OTA default passwords (for maximum security change them!)
|
||||
|
||||
164
wled00/wled_metadata.cpp
Normal file
164
wled00/wled_metadata.cpp
Normal file
@@ -0,0 +1,164 @@
|
||||
#include "ota_update.h"
|
||||
#include "wled.h"
|
||||
#include "wled_metadata.h"
|
||||
|
||||
#ifndef WLED_VERSION
|
||||
#warning WLED_VERSION was not set - using default value of 'dev'
|
||||
#define WLED_VERSION dev
|
||||
#endif
|
||||
#ifndef WLED_RELEASE_NAME
|
||||
#warning WLED_RELEASE_NAME was not set - using default value of 'Custom'
|
||||
#define WLED_RELEASE_NAME "Custom"
|
||||
#endif
|
||||
#ifndef WLED_REPO
|
||||
// No warning for this one: integrators are not always on GitHub
|
||||
#define WLED_REPO "unknown"
|
||||
#endif
|
||||
|
||||
constexpr uint32_t WLED_CUSTOM_DESC_MAGIC = 0x57535453; // "WSTS" (WLED System Tag Structure)
|
||||
constexpr uint32_t WLED_CUSTOM_DESC_VERSION = 1;
|
||||
|
||||
// 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");
|
||||
|
||||
|
||||
/**
|
||||
* DJB2 hash function (C++11 compatible constexpr)
|
||||
* Used for compile-time hash computation to validate structure contents
|
||||
* Recursive for compile time: not usable at runtime due to stack depth
|
||||
*
|
||||
* Note that this only works on strings; there is no way to produce a compile-time
|
||||
* hash of a struct in C++11 without explicitly listing all the struct members.
|
||||
* So for now, we hash only the release name. This suffices for a "did you find
|
||||
* valid structure" check.
|
||||
*
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// GLOBAL VARIABLES
|
||||
// ------------------------------------
|
||||
// Structure instantiation for this build
|
||||
const wled_metadata_t __attribute__((section(BUILD_METADATA_SECTION))) WLED_BUILD_DESCRIPTION = {
|
||||
WLED_CUSTOM_DESC_MAGIC, // magic
|
||||
WLED_CUSTOM_DESC_VERSION, // version
|
||||
TOSTRING(WLED_VERSION),
|
||||
WLED_RELEASE_NAME, // release_name
|
||||
std::integral_constant<uint32_t, djb2_hash_constexpr(WLED_RELEASE_NAME)>::value, // hash - computed at compile time; integral_constant enforces this
|
||||
};
|
||||
|
||||
static const char repoString_s[] PROGMEM = WLED_REPO;
|
||||
const __FlashStringHelper* repoString = FPSTR(repoString_s);
|
||||
|
||||
static const char productString_s[] PROGMEM = WLED_PRODUCT_NAME;
|
||||
const __FlashStringHelper* productString = FPSTR(productString_s);
|
||||
|
||||
static const char brandString_s [] PROGMEM = WLED_BRAND;
|
||||
const __FlashStringHelper* brandString = FPSTR(brandString_s);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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 findWledMetadata(const uint8_t* binaryData, size_t dataSize, wled_metadata_t* extractedDesc) {
|
||||
if (!binaryData || !extractedDesc || dataSize < sizeof(wled_metadata_t)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t offset = 0; offset <= dataSize - sizeof(wled_metadata_t); offset++) {
|
||||
if ((binaryData[offset]) == static_cast<char>(WLED_CUSTOM_DESC_MAGIC)) {
|
||||
// First byte matched; check next in an alignment-safe way
|
||||
uint32_t data_magic;
|
||||
memcpy(&data_magic, binaryData + offset, sizeof(data_magic));
|
||||
|
||||
// Check for magic number
|
||||
if (data_magic == WLED_CUSTOM_DESC_MAGIC) {
|
||||
wled_metadata_t candidate;
|
||||
memcpy(&candidate, binaryData + offset, sizeof(candidate));
|
||||
|
||||
// Found potential match, validate version
|
||||
if (candidate.desc_version != WLED_CUSTOM_DESC_VERSION) {
|
||||
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but version mismatch: %u\n"),
|
||||
offset, candidate.desc_version);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate hash using runtime function
|
||||
uint32_t expected_hash = djb2_hash_runtime(candidate.release_name);
|
||||
if (candidate.hash != expected_hash) {
|
||||
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but hash mismatch\n"), offset);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Valid structure found - copy entire structure
|
||||
*extractedDesc = candidate;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 wled_metadata_t& firmwareDescription, char* errorMessage, size_t errorMessageLen) {
|
||||
// Clear error message
|
||||
if (errorMessage && errorMessageLen > 0) {
|
||||
errorMessage[0] = '\0';
|
||||
}
|
||||
|
||||
// Validate compatibility using extracted release name
|
||||
// We make a stack copy so we can print it safely
|
||||
char safeFirmwareRelease[WLED_RELEASE_NAME_MAX_LEN];
|
||||
strncpy(safeFirmwareRelease, firmwareDescription.release_name, WLED_RELEASE_NAME_MAX_LEN - 1);
|
||||
safeFirmwareRelease[WLED_RELEASE_NAME_MAX_LEN - 1] = '\0';
|
||||
|
||||
if (strlen(safeFirmwareRelease) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strncmp_P(safeFirmwareRelease, releaseString, WLED_RELEASE_NAME_MAX_LEN) != 0) {
|
||||
if (errorMessage && errorMessageLen > 0) {
|
||||
snprintf_P(errorMessage, errorMessageLen, PSTR("Firmware compatibility mismatch: current='%s', uploaded='%s'."),
|
||||
releaseString, safeFirmwareRelease);
|
||||
errorMessage[errorMessageLen - 1] = '\0'; // Ensure null termination
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: additional checks go here
|
||||
|
||||
return true;
|
||||
}
|
||||
61
wled00/wled_metadata.h
Normal file
61
wled00/wled_metadata.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
WLED build metadata
|
||||
|
||||
Manages and exports information about the current WLED build.
|
||||
*/
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string.h>
|
||||
#include <WString.h>
|
||||
|
||||
#define WLED_VERSION_MAX_LEN 48
|
||||
#define WLED_RELEASE_NAME_MAX_LEN 48
|
||||
|
||||
/**
|
||||
* WLED Custom Description Structure
|
||||
* This structure is embedded in platform-specific sections at an approximately
|
||||
* fixed offset in ESP32/ESP8266 binaries, where it can be found and validated
|
||||
* by the OTA process.
|
||||
*/
|
||||
typedef struct {
|
||||
uint32_t magic; // Magic number to identify WLED custom description
|
||||
uint32_t desc_version; // Structure version for future compatibility
|
||||
char wled_version[WLED_VERSION_MAX_LEN];
|
||||
char release_name[WLED_RELEASE_NAME_MAX_LEN]; // Release name (null-terminated)
|
||||
uint32_t hash; // Structure sanity check
|
||||
} __attribute__((packed)) wled_metadata_t;
|
||||
|
||||
|
||||
// Global build description
|
||||
extern const wled_metadata_t WLED_BUILD_DESCRIPTION;
|
||||
|
||||
// Convenient metdata pointers
|
||||
#define versionString (WLED_BUILD_DESCRIPTION.wled_version) // Build version, WLED_VERSION
|
||||
#define releaseString (WLED_BUILD_DESCRIPTION.release_name) // Release name, WLED_RELEASE_NAME
|
||||
extern const __FlashStringHelper* repoString; // Github repository (if available)
|
||||
extern const __FlashStringHelper* productString; // Product, WLED_PRODUCT_NAME -- deprecated, use WLED_RELEASE_NAME
|
||||
extern const __FlashStringHelper* brandString ; // Brand
|
||||
|
||||
|
||||
// Metadata analysis functions
|
||||
|
||||
/**
|
||||
* Extract WLED custom description structure from binary data
|
||||
* @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 findWledMetadata(const uint8_t* binaryData, size_t dataSize, wled_metadata_t* extractedDesc);
|
||||
|
||||
/**
|
||||
* Check if OTA should be allowed based on release compatibility
|
||||
* @param firmwareDescription Pointer to firmware description
|
||||
* @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 wled_metadata_t& firmwareDescription, char* errorMessage, size_t errorMessageLen);
|
||||
@@ -1,11 +1,7 @@
|
||||
#include "wled.h"
|
||||
|
||||
#ifndef WLED_DISABLE_OTA
|
||||
#ifdef ESP8266
|
||||
#include <Updater.h>
|
||||
#else
|
||||
#include <Update.h>
|
||||
#endif
|
||||
#include "ota_update.h"
|
||||
#endif
|
||||
#include "html_ui.h"
|
||||
#include "html_settings.h"
|
||||
@@ -17,6 +13,8 @@
|
||||
#include "html_pxmagic.h"
|
||||
#endif
|
||||
#include "html_cpal.h"
|
||||
#include "html_edit.h"
|
||||
|
||||
|
||||
// define flash strings once (saves flash memory)
|
||||
static const char s_redirecting[] PROGMEM = "Redirecting...";
|
||||
@@ -26,8 +24,16 @@ static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN co
|
||||
static const char s_rebooting [] PROGMEM = "Rebooting now...";
|
||||
static const char s_notimplemented[] PROGMEM = "Not implemented";
|
||||
static const char s_accessdenied[] PROGMEM = "Access Denied";
|
||||
static const char s_not_found[] PROGMEM = "Not found";
|
||||
static const char s_wsec[] PROGMEM = "wsec.json";
|
||||
static const char s_func[] PROGMEM = "func";
|
||||
static const char s_path[] PROGMEM = "path";
|
||||
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";
|
||||
static const char _common_js[] PROGMEM = "/common.js";
|
||||
|
||||
|
||||
//Is this an IP?
|
||||
static bool isIp(const String &str) {
|
||||
for (size_t i = 0; i < str.length(); i++) {
|
||||
@@ -71,9 +77,9 @@ static void setStaticContentCacheHeaders(AsyncWebServerResponse *response, int c
|
||||
#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"), F("no-cache"));
|
||||
response->addHeader(FPSTR(s_cache_control), F("no-cache"));
|
||||
#else
|
||||
response->addHeader(F("Cache-Control"), F("no-store,max-age=0")); // prevent caching if debug build
|
||||
response->addHeader(FPSTR(s_cache_control), F("no-store,max-age=0")); // prevent caching if debug build
|
||||
#endif
|
||||
char etag[32];
|
||||
generateEtag(etag, eTagSuffix);
|
||||
@@ -176,6 +182,7 @@ static String msgProcessor(const String& var)
|
||||
return String();
|
||||
}
|
||||
|
||||
|
||||
static void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool isFinal) {
|
||||
if (!correctPIN) {
|
||||
if (isFinal) request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg));
|
||||
@@ -198,7 +205,7 @@ static void handleUpload(AsyncWebServerRequest *request, const String& filename,
|
||||
request->_tempFile.close();
|
||||
if (filename.indexOf(F("cfg.json")) >= 0) { // check for filename with or without slash
|
||||
doReboot = true;
|
||||
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Configuration restore successful.\nRebooting..."));
|
||||
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Config restore ok.\nRebooting..."));
|
||||
} else {
|
||||
if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) loadCustomPalettes();
|
||||
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File Uploaded!"));
|
||||
@@ -207,25 +214,94 @@ static void handleUpload(AsyncWebServerRequest *request, const String& filename,
|
||||
}
|
||||
}
|
||||
|
||||
void createEditHandler(bool enable) {
|
||||
static const char _edit_htm[] PROGMEM = "/edit.htm";
|
||||
|
||||
void createEditHandler() {
|
||||
if (editHandler != nullptr) server.removeHandler(editHandler);
|
||||
if (enable) {
|
||||
#ifdef WLED_ENABLE_FS_EDITOR
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
editHandler = &server.addHandler(new SPIFFSEditor(WLED_FS));//http_username,http_password));
|
||||
#else
|
||||
editHandler = &server.addHandler(new SPIFFSEditor("","",WLED_FS));//http_username,http_password));
|
||||
#endif
|
||||
#else
|
||||
editHandler = &server.on(F("/edit"), HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
serveMessage(request, 501, FPSTR(s_notimplemented), F("The FS editor is disabled in this build."), 254);
|
||||
});
|
||||
#endif
|
||||
} else {
|
||||
editHandler = &server.on(F("/edit"), HTTP_ANY, [](AsyncWebServerRequest *request){
|
||||
|
||||
editHandler = &server.on(F("/edit"), static_cast<WebRequestMethod>(HTTP_GET), [](AsyncWebServerRequest *request) {
|
||||
// PIN check for GET/DELETE, for POST it is done in handleUpload()
|
||||
if (!correctPIN) {
|
||||
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const String& func = request->arg(FPSTR(s_func));
|
||||
|
||||
if(func.length() == 0) {
|
||||
// default: serve the editor page
|
||||
handleStaticContent(request, FPSTR(_edit_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_edit, PAGE_edit_length);
|
||||
return;
|
||||
}
|
||||
|
||||
if (func == "list") {
|
||||
bool first = true;
|
||||
AsyncResponseStream* response = request->beginResponseStream(FPSTR(CONTENT_TYPE_JSON));
|
||||
response->addHeader(FPSTR(s_cache_control), FPSTR(s_no_store));
|
||||
response->addHeader(FPSTR(s_expires), F("0"));
|
||||
response->write('[');
|
||||
|
||||
File rootdir = WLED_FS.open("/", "r");
|
||||
File rootfile = rootdir.openNextFile();
|
||||
while (rootfile) {
|
||||
String name = rootfile.name();
|
||||
if (name.indexOf(FPSTR(s_wsec)) >= 0) {
|
||||
rootfile = rootdir.openNextFile(); // skip wsec.json
|
||||
continue;
|
||||
}
|
||||
if (!first) response->write(',');
|
||||
first = false;
|
||||
response->printf_P(PSTR("{\"name\":\"%s\",\"type\":\"file\",\"size\":%u}"), name.c_str(), rootfile.size());
|
||||
rootfile = rootdir.openNextFile();
|
||||
}
|
||||
rootfile.close();
|
||||
rootdir.close();
|
||||
response->write(']');
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
String path = request->arg(FPSTR(s_path)); // remaining functions expect a path
|
||||
|
||||
if (path.length() == 0) {
|
||||
request->send(400, FPSTR(CONTENT_TYPE_PLAIN), F("Missing path"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.charAt(0) != '/') {
|
||||
path = '/' + path; // prepend slash if missing
|
||||
}
|
||||
|
||||
if (!WLED_FS.exists(path)) {
|
||||
request->send(404, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_not_found));
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.indexOf(FPSTR(s_wsec)) >= 0) {
|
||||
request->send(403, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_accessdenied)); // skip wsec.json
|
||||
return;
|
||||
}
|
||||
|
||||
if (func == "edit") {
|
||||
request->send(WLED_FS, path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (func == "download") {
|
||||
request->send(WLED_FS, path, String(), true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (func == "delete") {
|
||||
if (!WLED_FS.remove(path))
|
||||
request->send(500, FPSTR(CONTENT_TYPE_PLAIN), F("Delete failed"));
|
||||
else
|
||||
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File deleted"));
|
||||
return;
|
||||
}
|
||||
|
||||
// unrecognized func
|
||||
request->send(400, FPSTR(CONTENT_TYPE_PLAIN), F("Invalid function"));
|
||||
});
|
||||
}
|
||||
|
||||
static bool captivePortal(AsyncWebServerRequest *request)
|
||||
@@ -391,7 +467,7 @@ void initServer()
|
||||
size_t len, bool isFinal) {handleUpload(request, filename, index, data, len, isFinal);}
|
||||
);
|
||||
|
||||
createEditHandler(correctPIN);
|
||||
createEditHandler(); // initialize "/edit" handler, access is protected by "correctPIN"
|
||||
|
||||
static const char _update[] PROGMEM = "/update";
|
||||
#ifndef WLED_DISABLE_OTA
|
||||
@@ -404,59 +480,47 @@ void initServer()
|
||||
});
|
||||
|
||||
server.on(_update, HTTP_POST, [](AsyncWebServerRequest *request){
|
||||
if (!correctPIN) {
|
||||
serveSettings(request, true); // handle PIN page POST request
|
||||
return;
|
||||
}
|
||||
if (otaLock) {
|
||||
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254);
|
||||
return;
|
||||
}
|
||||
if (Update.hasError()) {
|
||||
serveMessage(request, 500, F("Update failed!"), F("Please check your file and retry!"), 254);
|
||||
if (request->_tempObject) {
|
||||
auto ota_result = getOTAResult(request);
|
||||
if (ota_result.first) {
|
||||
if (ota_result.second.length() > 0) {
|
||||
serveMessage(request, 500, F("Update failed!"), ota_result.second, 254);
|
||||
} else {
|
||||
serveMessage(request, 200, F("Update successful!"), FPSTR(s_rebooting), 131);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
serveMessage(request, 200, F("Update successful!"), FPSTR(s_rebooting), 131);
|
||||
#ifndef ESP8266
|
||||
bootloopCheckOTA(); // let the bootloop-checker know there was an OTA update
|
||||
#endif
|
||||
doReboot = true;
|
||||
// No context structure - something's gone horribly wrong
|
||||
serveMessage(request, 500, F("Update failed!"), F("Internal server fault"), 254);
|
||||
}
|
||||
},[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){
|
||||
IPAddress client = request->client()->remoteIP();
|
||||
if (((otaSameSubnet && !inSameSubnet(client)) && !strlen(settingsPIN)) || (!otaSameSubnet && !inLocalSubnet(client))) {
|
||||
DEBUG_PRINTLN(F("Attempted OTA update from different/non-local subnet!"));
|
||||
request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_accessdenied));
|
||||
return;
|
||||
}
|
||||
if (!correctPIN || otaLock) return;
|
||||
if(!index){
|
||||
DEBUG_PRINTLN(F("OTA Update Start"));
|
||||
#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
|
||||
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);
|
||||
}
|
||||
if(!Update.hasError()) Update.write(data, len);
|
||||
if(isFinal){
|
||||
if(Update.end(true)){
|
||||
DEBUG_PRINTLN(F("Update Success"));
|
||||
} else {
|
||||
DEBUG_PRINTLN(F("Update Failed"));
|
||||
strip.resume();
|
||||
UsermodManager::onUpdateBegin(false); // notify usermods that update has failed (some may require task init)
|
||||
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||
WLED::instance().enableWatchdog();
|
||||
#endif
|
||||
if (index == 0) {
|
||||
// Allocate the context structure
|
||||
if (!initOTA(request)) {
|
||||
return; // Error will be dealt with after upload in response handler, above
|
||||
}
|
||||
|
||||
// Privilege checks
|
||||
IPAddress client = request->client()->remoteIP();
|
||||
if (((otaSameSubnet && !inSameSubnet(client)) && !strlen(settingsPIN)) || (!otaSameSubnet && !inLocalSubnet(client))) {
|
||||
DEBUG_PRINTLN(F("Attempted OTA update from different/non-local subnet!"));
|
||||
serveMessage(request, 401, FPSTR(s_accessdenied), F("Client is not on local subnet."), 254);
|
||||
setOTAReplied(request);
|
||||
return;
|
||||
}
|
||||
if (!correctPIN) {
|
||||
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
|
||||
setOTAReplied(request);
|
||||
return;
|
||||
};
|
||||
if (otaLock) {
|
||||
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254);
|
||||
setOTAReplied(request);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
handleOTAData(request, index, data, len, isFinal);
|
||||
});
|
||||
#else
|
||||
const auto notSupported = [](AsyncWebServerRequest *request){
|
||||
@@ -466,6 +530,53 @@ void initServer()
|
||||
server.on(_update, HTTP_POST, notSupported, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){});
|
||||
#endif
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
|
||||
// ESP32 bootloader update endpoint
|
||||
server.on(F("/updatebootloader"), HTTP_POST, [](AsyncWebServerRequest *request){
|
||||
if (request->_tempObject) {
|
||||
auto bootloader_result = getBootloaderOTAResult(request);
|
||||
if (bootloader_result.first) {
|
||||
if (bootloader_result.second.length() > 0) {
|
||||
serveMessage(request, 500, F("Bootloader update failed!"), bootloader_result.second, 254);
|
||||
} else {
|
||||
serveMessage(request, 200, F("Bootloader updated successfully!"), FPSTR(s_rebooting), 131);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No context structure - something's gone horribly wrong
|
||||
serveMessage(request, 500, F("Bootloader update failed!"), F("Internal server fault"), 254);
|
||||
}
|
||||
},[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){
|
||||
if (index == 0) {
|
||||
// Privilege checks
|
||||
IPAddress client = request->client()->remoteIP();
|
||||
if (((otaSameSubnet && !inSameSubnet(client)) && !strlen(settingsPIN)) || (!otaSameSubnet && !inLocalSubnet(client))) {
|
||||
DEBUG_PRINTLN(F("Attempted bootloader update from different/non-local subnet!"));
|
||||
serveMessage(request, 401, FPSTR(s_accessdenied), F("Client is not on local subnet."), 254);
|
||||
setBootloaderOTAReplied(request);
|
||||
return;
|
||||
}
|
||||
if (!correctPIN) {
|
||||
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
|
||||
setBootloaderOTAReplied(request);
|
||||
return;
|
||||
}
|
||||
if (otaLock) {
|
||||
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254);
|
||||
setBootloaderOTAReplied(request);
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate the context structure
|
||||
if (!initBootloaderOTA(request)) {
|
||||
return; // Error will be dealt with after upload in response handler, above
|
||||
}
|
||||
}
|
||||
|
||||
handleBootloaderOTAData(request, index, data, len, isFinal);
|
||||
});
|
||||
#endif
|
||||
|
||||
#ifdef WLED_ENABLE_DMX
|
||||
server.on(F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
request->send_P(200, FPSTR(CONTENT_TYPE_HTML), PAGE_dmxmap, dmxProcessor);
|
||||
@@ -569,8 +680,8 @@ void serveSettingsJS(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
AsyncResponseStream *response = request->beginResponseStream(FPSTR(CONTENT_TYPE_JAVASCRIPT));
|
||||
response->addHeader(F("Cache-Control"), F("no-store"));
|
||||
response->addHeader(F("Expires"), F("0"));
|
||||
response->addHeader(FPSTR(s_cache_control), FPSTR(s_no_store));
|
||||
response->addHeader(FPSTR(s_expires), F("0"));
|
||||
|
||||
response->print(F("function GetV(){var d=document;"));
|
||||
getSettingsJS(subPage, *response);
|
||||
@@ -694,7 +805,6 @@ void serveSettings(AsyncWebServerRequest* request, bool post) {
|
||||
#endif
|
||||
case SUBPAGE_LOCK : {
|
||||
correctPIN = !strlen(settingsPIN); // lock if a pin is set
|
||||
createEditHandler(correctPIN);
|
||||
serveMessage(request, 200, strlen(settingsPIN) > 0 ? PSTR("Settings locked") : PSTR("No PIN set"), FPSTR(s_redirecting), 1);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -671,16 +671,6 @@ void getSettingsJS(byte subPage, Print& settingsScript)
|
||||
UsermodManager::appendConfigData(settingsScript);
|
||||
}
|
||||
|
||||
if (subPage == SUBPAGE_UPDATE) // update
|
||||
{
|
||||
char tmp_buf[128];
|
||||
fillWLEDVersion(tmp_buf,sizeof(tmp_buf));
|
||||
printSetClassElementHTML(settingsScript,PSTR("sip"),0,tmp_buf);
|
||||
#ifndef ARDUINO_ARCH_ESP32
|
||||
settingsScript.print(F("toggle('rev');")); // hide revert button on ESP8266
|
||||
#endif
|
||||
}
|
||||
|
||||
if (subPage == SUBPAGE_2D) // 2D matrices
|
||||
{
|
||||
printSetFormValue(settingsScript,PSTR("SOMP"),strip.isMatrix);
|
||||
|
||||
Reference in New Issue
Block a user