mirror of
https://github.com/wled/WLED.git
synced 2025-11-30 05:07:41 +00:00
Compare commits
172 Commits
coderabbit
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28d6e347ec | ||
|
|
1b1f5811de | ||
|
|
ff184c1f41 | ||
|
|
75a7ed132a | ||
|
|
fc25eb2c90 | ||
|
|
dc5732a5f5 | ||
|
|
a9811c2020 | ||
|
|
33411f0300 | ||
|
|
8bc434b614 | ||
|
|
ce6577ee35 | ||
|
|
579021f5fc | ||
|
|
17e91a7d2a | ||
|
|
61f5737df2 | ||
|
|
49a25af1f2 | ||
|
|
b6f3cb6394 | ||
|
|
571ab674c3 | ||
|
|
6b607fb545 | ||
|
|
e761418531 | ||
|
|
fca921ee82 | ||
|
|
fc7993f4a7 | ||
|
|
f12e3e03ac | ||
|
|
eb87fbf8e4 | ||
|
|
c534328cc5 | ||
|
|
28d8a1c25c | ||
|
|
d1c4de2499 | ||
|
|
1e081a7f0d | ||
|
|
730205ded5 | ||
|
|
90ca6ccf8b | ||
|
|
d7fd49cc4c | ||
|
|
eb03520aa9 | ||
|
|
d8e2ceecf7 | ||
|
|
7dfed581b7 | ||
|
|
49a1ae54cf | ||
|
|
5b3cc753e2 | ||
|
|
4615eb8258 | ||
|
|
9b787e13d1 | ||
|
|
3dbcd79b3c | ||
|
|
a1aac452de | ||
|
|
b90fbe6b1a | ||
|
|
1860258deb | ||
|
|
a2935b87c2 | ||
|
|
c1ce1d8aba | ||
|
|
54b7dfe04b | ||
|
|
4a33809d66 | ||
|
|
336e074b4a | ||
|
|
aaad450175 | ||
|
|
85b3c5d91b | ||
|
|
4db86ebf7f | ||
|
|
65c43b5224 | ||
|
|
c649ec1d8c | ||
|
|
271e9ac7b7 | ||
|
|
8348089b50 | ||
|
|
4f968861d6 | ||
|
|
66ffd65476 | ||
|
|
194829336f | ||
|
|
f1d708ca43 | ||
|
|
4f93661865 | ||
|
|
cd2dc437a3 | ||
|
|
f95dae1b1b | ||
|
|
6ae4b1fc38 | ||
|
|
fc776eeb16 | ||
|
|
79376bbc58 | ||
|
|
3b14c31e00 | ||
|
|
a666f07340 | ||
|
|
bd933ff230 | ||
|
|
7addae9c24 | ||
|
|
a73a2aaa33 | ||
|
|
a96e88043d | ||
|
|
29d2f7fc1b | ||
|
|
7aedf77d83 | ||
|
|
1324d49098 | ||
|
|
79a52a60ff | ||
|
|
6581dd6ff9 | ||
|
|
4659939547 | ||
|
|
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 | ||
|
|
790be35ab8 | ||
|
|
0eef321f88 | ||
|
|
69dfe6c8a1 | ||
|
|
80c97076ae | ||
|
|
c3f394489f | ||
|
|
ce172df91a | ||
|
|
91baa34071 | ||
|
|
0e043b2a1b | ||
|
|
01c84b0140 | ||
|
|
1da2692c34 | ||
|
|
d538736411 | ||
|
|
b268aea0ab | ||
|
|
a04d70293d | ||
|
|
c66d67dd19 | ||
|
|
0c22163fd9 | ||
|
|
50c0f41508 | ||
|
|
b60313e1f8 | ||
|
|
1fff61b726 | ||
|
|
c4850aed08 | ||
|
|
43eb2d6f47 | ||
|
|
0ec4488dd1 | ||
|
|
cc0230f83f | ||
|
|
65b91762fd | ||
|
|
5ca10f35d1 | ||
|
|
a073bf32e4 | ||
|
|
d79b02379e | ||
|
|
f5f3fc338f | ||
|
|
042ed39464 | ||
|
|
c3e18905c1 | ||
|
|
a18a661c73 | ||
|
|
93908e758f | ||
|
|
30fbf55b9a |
66
boards/adafruit_matrixportal_esp32s3.json
Normal file
66
boards/adafruit_matrixportal_esp32s3.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino":{
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "partitions-8MB-tinyuf2.csv"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1",
|
||||
"-DBOARD_HAS_PSRAM"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"hwids": [
|
||||
[
|
||||
"0x239A",
|
||||
"0x8125"
|
||||
],
|
||||
[
|
||||
"0x239A",
|
||||
"0x0125"
|
||||
],
|
||||
[
|
||||
"0x239A",
|
||||
"0x8126"
|
||||
]
|
||||
],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "adafruit_matrixportal_esp32s3"
|
||||
},
|
||||
"connectivity": [
|
||||
"bluetooth",
|
||||
"wifi"
|
||||
],
|
||||
"debug": {
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": [
|
||||
"arduino",
|
||||
"espidf"
|
||||
],
|
||||
"name": "Adafruit MatrixPortal ESP32-S3",
|
||||
"upload": {
|
||||
"arduino": {
|
||||
"flash_extra_images": [
|
||||
[
|
||||
"0x410000",
|
||||
"variants/adafruit_matrixportal_esp32s3/tinyuf2.bin"
|
||||
]
|
||||
]
|
||||
},
|
||||
"flash_size": "8MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 8388608,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"require_upload_port": true,
|
||||
"speed": 460800
|
||||
},
|
||||
"url": "https://www.adafruit.com/product/5778",
|
||||
"vendor": "Adafruit"
|
||||
}
|
||||
@@ -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,27 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# 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
|
||||
esp32c3dev_qio
|
||||
esp32S3_wroom2
|
||||
esp32s3dev_16MB_opi
|
||||
esp32s3dev_8MB_opi
|
||||
esp32s3_4M_qspi
|
||||
usermods
|
||||
|
||||
src_dir = ./wled00
|
||||
data_dir = ./wled00/data
|
||||
@@ -110,8 +130,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
|
||||
@@ -265,12 +284,14 @@ AR_lib_deps = ;; for pre-usermod-library platformio_override compatibility
|
||||
|
||||
[esp32_idf_V4]
|
||||
;; build environment for ESP32 using ESP-IDF 4.4.x / arduino-esp32 v2.0.5
|
||||
;; *** important: build flags from esp32_idf_V4 are inherited by _all_ esp32-based MCUs: esp32, esp32s2, esp32s3, esp32c3
|
||||
;;
|
||||
;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly.
|
||||
;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio.
|
||||
|
||||
;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them)
|
||||
platform = https://github.com/tasmota/platform-espressif32/releases/download/2023.06.02/platform-espressif32.zip ;; Tasmota Arduino Core 2.0.9 with IPv6 support, based on IDF 4.4.4
|
||||
platform_packages =
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = -g
|
||||
-Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one
|
||||
@@ -285,6 +306,7 @@ lib_deps =
|
||||
[esp32s2]
|
||||
;; generic definitions for all ESP32-S2 boards
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages = ${esp32_idf_V4.platform_packages}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = -g
|
||||
-DARDUINO_ARCH_ESP32
|
||||
@@ -303,6 +325,7 @@ board_build.partitions = ${esp32.default_partitions} ;; default partioning for
|
||||
[esp32c3]
|
||||
;; generic definitions for all ESP32-C3 boards
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages = ${esp32_idf_V4.platform_packages}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = -g
|
||||
-DARDUINO_ARCH_ESP32
|
||||
@@ -321,6 +344,7 @@ board_build.flash_mode = qio
|
||||
[esp32s3]
|
||||
;; generic definitions for all ESP32-S3 boards
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages = ${esp32_idf_V4.platform_packages}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = -g
|
||||
-DESP32
|
||||
@@ -429,21 +453,31 @@ custom_usermods = audioreactive
|
||||
[env:esp32dev]
|
||||
board = esp32dev
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages = ${esp32_idf_V4.platform_packages}
|
||||
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}
|
||||
platform_packages = ${esp32_idf_V4.platform_packages}
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_8M\" #-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.large_partitions}
|
||||
@@ -455,9 +489,11 @@ board_build.flash_mode = dio
|
||||
[env:esp32dev_16M]
|
||||
board = esp32dev
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages = ${esp32_idf_V4.platform_packages}
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_16M\" #-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.extreme_partitions}
|
||||
@@ -469,10 +505,12 @@ board_build.flash_mode = dio
|
||||
[env:esp32_eth]
|
||||
board = esp32-poe
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages = ${esp32_idf_V4.platform_packages}
|
||||
upload_speed = 921600
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_Ethernet\" -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
|
||||
; -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only
|
||||
lib_deps = ${esp32.lib_deps}
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
@@ -487,6 +525,7 @@ board_build.partitions = ${esp32.extended_partitions}
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_WROVER\"
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
|
||||
-DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue ;; Older ESP32 (rev.<3) need a PSRAM fix (increases static RAM used) https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html
|
||||
-D DATA_PINS=25
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
@@ -494,6 +533,7 @@ lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
[env:esp32c3dev]
|
||||
extends = esp32c3
|
||||
platform = ${esp32c3.platform}
|
||||
platform_packages = ${esp32c3.platform_packages}
|
||||
framework = arduino
|
||||
board = esp32-c3-devkitm-1
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
@@ -505,19 +545,26 @@ build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=
|
||||
upload_speed = 460800
|
||||
build_unflags = ${common.build_unflags}
|
||||
lib_deps = ${esp32c3.lib_deps}
|
||||
board_build.flash_mode = dio ; safe default, required for OTA updates to 0.16 from older version which used dio (must match the bootloader!)
|
||||
|
||||
[env:esp32c3dev_qio]
|
||||
extends = env:esp32c3dev
|
||||
build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-C3-QIO\"
|
||||
board_build.flash_mode = qio ; qio is faster and works on almost all boards (some boards may use dio to get 2 extra pins)
|
||||
|
||||
[env:esp32s3dev_16MB_opi]
|
||||
;; ESP32-S3 development board, with 16MB FLASH and >= 8MB PSRAM (memory_type: qio_opi)
|
||||
board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support
|
||||
board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB
|
||||
platform = ${esp32s3.platform}
|
||||
platform_packages = ${esp32s3.platform_packages}
|
||||
upload_speed = 921600
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_16MB_opi\"
|
||||
-D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
board_build.partitions = ${esp32.extreme_partitions}
|
||||
@@ -532,13 +579,14 @@ monitor_filters = esp32_exception_decoder
|
||||
board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support
|
||||
board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB
|
||||
platform = ${esp32s3.platform}
|
||||
platform_packages = ${esp32s3.platform_packages}
|
||||
upload_speed = 921600
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8MB_opi\"
|
||||
-D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
board_build.partitions = ${esp32.large_partitions}
|
||||
@@ -550,19 +598,20 @@ monitor_filters = esp32_exception_decoder
|
||||
;; For ESP32-S3 WROOM-2, a.k.a. ESP32-S3 DevKitC-1 v1.1
|
||||
;; with >= 16MB FLASH and >= 8MB PSRAM (memory_type: opi_opi)
|
||||
platform = ${esp32s3.platform}
|
||||
platform_packages = ${esp32s3.platform_packages}
|
||||
board = esp32s3camlcd ;; this is the only standard board with "opi_opi"
|
||||
board_build.arduino.memory_type = opi_opi
|
||||
upload_speed = 921600
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_WROOM-2\"
|
||||
-D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
|
||||
;; -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
;; -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
-D LEDPIN=38 -D DATA_PINS=38 ;; buildin WS2812b LED
|
||||
-D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1
|
||||
-D WLED_DEBUG
|
||||
;;-D WLED_DEBUG
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4 ;; I2S mic
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
|
||||
@@ -571,15 +620,33 @@ board_upload.flash_size = 16MB
|
||||
board_upload.maximum_size = 16777216
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:esp32S3_wroom2_32MB]
|
||||
;; For ESP32-S3 WROOM-2 with 32MB Flash, and >= 8MB PSRAM (memory_type: opi_opi)
|
||||
extends = env:esp32S3_wroom2
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_WROOM-2_32MB\"
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
|
||||
;; -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
-D LEDPIN=38 -D DATA_PINS=38 ;; buildin WS2812b LED
|
||||
-D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1
|
||||
;;-D WLED_DEBUG
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4 ;; I2S mic
|
||||
board_build.partitions = tools/WLED_ESP32_32MB.csv
|
||||
board_upload.flash_size = 32MB
|
||||
board_upload.maximum_size = 33554432
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:esp32s3_4M_qspi]
|
||||
;; ESP32-S3, with 4MB FLASH and <= 4MB PSRAM (memory_type: qio_qspi)
|
||||
board = lolin_s3_mini ;; -S3 mini, 4MB flash 2MB PSRAM
|
||||
platform = ${esp32s3.platform}
|
||||
platform_packages = ${esp32s3.platform_packages}
|
||||
upload_speed = 921600
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_4M_qspi\"
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
-DLOLIN_WIFI_FIX ; seems to work much better with this
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
@@ -591,6 +658,7 @@ monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:lolin_s2_mini]
|
||||
platform = ${esp32s2.platform}
|
||||
platform_packages = ${esp32s2.platform_packages}
|
||||
board = lolin_s2_mini
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.flash_mode = qio
|
||||
@@ -617,6 +685,7 @@ lib_deps = ${esp32s2.lib_deps}
|
||||
[env:usermods]
|
||||
board = esp32dev
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages = ${esp32_idf_V4.platform_packages}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_USERMODS\"
|
||||
-DTOUCH_CS=9
|
||||
|
||||
@@ -191,6 +191,22 @@ build_flags = ${common.build_flags} ${esp8266.build_flags}
|
||||
; -D HW_PIN_MISOSPI=9
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Optional: build flags for speed, instead of optimising for size.
|
||||
# Example of usage: see [env:esp32S3_PSRAM_HUB75]
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
[Speed_Flags]
|
||||
build_unflags = -Os ;; to disable standard optimization for small size
|
||||
build_flags =
|
||||
-O2 ;; optimize for speed
|
||||
-free -fipa-pta ;; very useful, too
|
||||
;;-fsingle-precision-constant ;; makes all floating point literals "float" (default is "double")
|
||||
;;-funsafe-math-optimizations ;; less dangerous than -ffast-math; still allows the compiler to exploit FMA and reciprocals (up to 10% faster on -S3)
|
||||
# Important: we need to explicitly switch off some "-O2" optimizations
|
||||
-fno-jump-tables -fno-tree-switch-conversion ;; needed - firmware may crash otherwise
|
||||
-freorder-blocks -Wwrite-strings -fstrict-volatile-bitfields ;; needed - recommended by espressif
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# PRE-CONFIGURED DEVELOPMENT BOARDS AND CONTROLLERS
|
||||
@@ -541,12 +557,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,19 +574,22 @@ 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
|
||||
;; adafruit recommends to use arduino-esp32 2.0.14
|
||||
;;platform = espressif32@ ~6.5.0
|
||||
;;platform_packages = platformio/framework-arduinoespressif32 @ 3.20014.231204 ;; arduino-esp32 2.0.14
|
||||
platform = ${esp32s3.platform}
|
||||
platform_packages =
|
||||
upload_speed = 921600
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_4M_qspi\"
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8M_qspi\"
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
-DLOLIN_WIFI_FIX ; seems to work much better with this
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
@@ -575,25 +597,30 @@ 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}
|
||||
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix
|
||||
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.partitions = ${esp32.large_partitions} ;; standard bootloader and 8MB Flash partitions
|
||||
;; board_build.partitions = tools/partitions-8MB_spiffs-tinyuf2.csv ;; supports adafruit UF2 bootloader
|
||||
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
|
||||
;; MOONHUB HUB75 adapter board (lilygo T7-S3 with 16MB flash and PSRAM)
|
||||
board = lilygo-t7-s3
|
||||
platform = ${esp32s3.platform}
|
||||
platform_packages =
|
||||
upload_speed = 921600
|
||||
build_unflags = ${common.build_unflags}
|
||||
${Speed_Flags.build_unflags} ;; optional: removes "-Os" so we can override with "-O2" in build_flags
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"esp32S3_16MB_PSRAM_HUB75\"
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
${Speed_Flags.build_flags} ;; optional: -O2 -> optimize for speed instead of size
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
-DLOLIN_WIFI_FIX ; seems to work much better with this
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
@@ -601,11 +628,15 @@ 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
|
||||
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
;;board_build.partitions = ${esp32.large_partitions} ;; for 8MB flash
|
||||
board_build.partitions = ${esp32.extreme_partitions} ;; for 16MB flash
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
monitor_filters = esp32_exception_decoder
|
||||
custom_usermods = audioreactive
|
||||
7
tools/WLED_ESP32_32MB.csv
Normal file
7
tools/WLED_ESP32_32MB.csv
Normal file
@@ -0,0 +1,7 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xe000, 0x2000,
|
||||
app0, app, ota_0, 0x10000, 0x300000,
|
||||
app1, app, ota_1, 0x310000,0x300000,
|
||||
spiffs, data, spiffs, 0x610000,0x19E0000,
|
||||
coredump, data, coredump,,64K
|
||||
|
@@ -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 = `
|
||||
@@ -38,6 +38,11 @@ const wledBanner = `
|
||||
\t\t\x1b[36m build script for web UI
|
||||
\x1b[0m`;
|
||||
|
||||
// Generate build timestamp as UNIX timestamp (seconds since epoch)
|
||||
function generateBuildTime() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
const singleHeader = `/*
|
||||
* Binary array for the Web UI.
|
||||
* gzip is used for smaller size and improved speeds.
|
||||
@@ -45,6 +50,9 @@ const singleHeader = `/*
|
||||
* Please see https://kno.wled.ge/advanced/custom-features/#changing-web-ui
|
||||
* to find out how to easily modify the web UI source!
|
||||
*/
|
||||
|
||||
// Automatically generated build time for cache busting (UNIX timestamp)
|
||||
#define WEB_BUILD_TIME ${generateBuildTime()}
|
||||
|
||||
`;
|
||||
|
||||
@@ -246,6 +254,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 +411,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",
|
||||
|
||||
10
tools/partitions-16MB_spiffs-tinyuf2.csv
Normal file
10
tools/partitions-16MB_spiffs-tinyuf2.csv
Normal file
@@ -0,0 +1,10 @@
|
||||
# ESP-IDF Partition Table
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
# bootloader.bin,, 0x1000, 32K
|
||||
# partition table,, 0x8000, 4K
|
||||
nvs, data, nvs, 0x9000, 20K,
|
||||
otadata, data, ota, 0xe000, 8K,
|
||||
ota_0, app, ota_0, 0x10000, 2048K,
|
||||
ota_1, app, ota_1, 0x210000, 2048K,
|
||||
uf2, app, factory,0x410000, 256K,
|
||||
spiffs, data, spiffs, 0x450000, 11968K,
|
||||
|
11
tools/partitions-4MB_spiffs-tinyuf2.csv
Normal file
11
tools/partitions-4MB_spiffs-tinyuf2.csv
Normal file
@@ -0,0 +1,11 @@
|
||||
# ESP-IDF Partition Table
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
# bootloader.bin,, 0x1000, 32K
|
||||
# partition table, 0x8000, 4K
|
||||
|
||||
nvs, data, nvs, 0x9000, 20K,
|
||||
otadata, data, ota, 0xe000, 8K,
|
||||
ota_0, 0, ota_0, 0x10000, 1408K,
|
||||
ota_1, 0, ota_1, 0x170000, 1408K,
|
||||
uf2, app, factory,0x2d0000, 256K,
|
||||
spiffs, data, spiffs, 0x310000, 960K,
|
||||
|
10
tools/partitions-8MB_spiffs-tinyuf2.csv
Normal file
10
tools/partitions-8MB_spiffs-tinyuf2.csv
Normal file
@@ -0,0 +1,10 @@
|
||||
# ESP-IDF Partition Table
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
# bootloader.bin,, 0x1000, 32K
|
||||
# partition table,, 0x8000, 4K
|
||||
nvs, data, nvs, 0x9000, 20K,
|
||||
otadata, data, ota, 0xe000, 8K,
|
||||
ota_0, app, ota_0, 0x10000, 2048K,
|
||||
ota_1, app, ota_1, 0x210000, 2048K,
|
||||
uf2, app, factory,0x410000, 256K,
|
||||
spiffs, data, spiffs, 0x450000, 3776K,
|
||||
|
@@ -313,11 +313,11 @@ class MyExampleUsermod : public Usermod {
|
||||
yield();
|
||||
// ignore certain button types as they may have other consequences
|
||||
if (!enabled
|
||||
|| buttonType[b] == BTN_TYPE_NONE
|
||||
|| buttonType[b] == BTN_TYPE_RESERVED
|
||||
|| buttonType[b] == BTN_TYPE_PIR_SENSOR
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|
||||
|| buttons[b].type == BTN_TYPE_NONE
|
||||
|| buttons[b].type == BTN_TYPE_RESERVED
|
||||
|| buttons[b].type == BTN_TYPE_PIR_SENSOR
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1227,7 +1227,6 @@ class AudioReactive : public Usermod {
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
// ADC over I2S is only possible on "classic" ESP32
|
||||
case 0:
|
||||
default:
|
||||
DEBUGSR_PRINTLN(F("AR: Analog Microphone (left channel only)."));
|
||||
audioSource = new I2SAdcSource(SAMPLE_RATE, BLOCK_SIZE);
|
||||
delay(100);
|
||||
@@ -1235,10 +1234,25 @@ class AudioReactive : public Usermod {
|
||||
if (audioSource) audioSource->initialize(audioPin);
|
||||
break;
|
||||
#endif
|
||||
|
||||
case 254: // dummy "network receive only" mode
|
||||
if (audioSource) delete audioSource; audioSource = nullptr;
|
||||
disableSoundProcessing = true;
|
||||
audioSyncEnabled = 2; // force udp sound receive mode
|
||||
enabled = true;
|
||||
break;
|
||||
|
||||
case 255: // 255 = -1 = no audio source
|
||||
// falls through to default
|
||||
default:
|
||||
if (audioSource) delete audioSource; audioSource = nullptr;
|
||||
disableSoundProcessing = true;
|
||||
enabled = false;
|
||||
break;
|
||||
}
|
||||
delay(250); // give microphone enough time to initialise
|
||||
|
||||
if (!audioSource) enabled = false; // audio failed to initialise
|
||||
if (!audioSource && (dmType != 254)) enabled = false;// audio failed to initialise
|
||||
#endif
|
||||
if (enabled) onUpdateBegin(false); // create FFT task, and initialize network
|
||||
|
||||
@@ -1530,7 +1544,7 @@ class AudioReactive : public Usermod {
|
||||
// better would be for AudioSource to implement getType()
|
||||
if (enabled
|
||||
&& dmType == 0 && audioPin>=0
|
||||
&& (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED)
|
||||
&& (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -562,11 +562,11 @@ void MultiRelay::loop() {
|
||||
bool MultiRelay::handleButton(uint8_t b) {
|
||||
yield();
|
||||
if (!enabled
|
||||
|| buttonType[b] == BTN_TYPE_NONE
|
||||
|| buttonType[b] == BTN_TYPE_RESERVED
|
||||
|| buttonType[b] == BTN_TYPE_PIR_SENSOR
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|
||||
|| buttons[b].type == BTN_TYPE_NONE
|
||||
|| buttons[b].type == BTN_TYPE_RESERVED
|
||||
|| buttons[b].type == BTN_TYPE_PIR_SENSOR
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -581,20 +581,20 @@ bool MultiRelay::handleButton(uint8_t b) {
|
||||
unsigned long now = millis();
|
||||
|
||||
//button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0)
|
||||
if (buttonType[b] == BTN_TYPE_SWITCH) {
|
||||
if (buttons[b].type == BTN_TYPE_SWITCH) {
|
||||
//handleSwitch(b);
|
||||
if (buttonPressedBefore[b] != isButtonPressed(b)) {
|
||||
buttonPressedTime[b] = now;
|
||||
buttonPressedBefore[b] = !buttonPressedBefore[b];
|
||||
if (buttons[b].pressedBefore != isButtonPressed(b)) {
|
||||
buttons[b].pressedTime = now;
|
||||
buttons[b].pressedBefore = !buttons[b].pressedBefore;
|
||||
}
|
||||
|
||||
if (buttonLongPressed[b] == buttonPressedBefore[b]) return handled;
|
||||
if (buttons[b].longPressed == buttons[b].pressedBefore) return handled;
|
||||
|
||||
if (now - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
|
||||
if (now - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
|
||||
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
|
||||
if (_relay[i].button == b) {
|
||||
switchRelay(i, buttonPressedBefore[b]);
|
||||
buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state
|
||||
switchRelay(i, buttons[b].pressedBefore);
|
||||
buttons[b].longPressed = buttons[b].pressedBefore; //save the last "long term" switch state
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -604,40 +604,40 @@ bool MultiRelay::handleButton(uint8_t b) {
|
||||
//momentary button logic
|
||||
if (isButtonPressed(b)) { //pressed
|
||||
|
||||
if (!buttonPressedBefore[b]) buttonPressedTime[b] = now;
|
||||
buttonPressedBefore[b] = true;
|
||||
if (!buttons[b].pressedBefore) buttons[b].pressedTime = now;
|
||||
buttons[b].pressedBefore = true;
|
||||
|
||||
if (now - buttonPressedTime[b] > 600) { //long press
|
||||
if (now - buttons[b].pressedTime > 600) { //long press
|
||||
//longPressAction(b); //not exposed
|
||||
//handled = false; //use if you want to pass to default behaviour
|
||||
buttonLongPressed[b] = true;
|
||||
buttons[b].longPressed = true;
|
||||
}
|
||||
|
||||
} else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released
|
||||
} else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released
|
||||
|
||||
long dur = now - buttonPressedTime[b];
|
||||
long dur = now - buttons[b].pressedTime;
|
||||
if (dur < WLED_DEBOUNCE_THRESHOLD) {
|
||||
buttonPressedBefore[b] = false;
|
||||
buttons[b].pressedBefore = false;
|
||||
return handled;
|
||||
} //too short "press", debounce
|
||||
bool doublePress = buttonWaitTime[b]; //did we have short press before?
|
||||
buttonWaitTime[b] = 0;
|
||||
bool doublePress = buttons[b].waitTime; //did we have short press before?
|
||||
buttons[b].waitTime = 0;
|
||||
|
||||
if (!buttonLongPressed[b]) { //short press
|
||||
if (!buttons[b].longPressed) { //short press
|
||||
// if this is second release within 350ms it is a double press (buttonWaitTime!=0)
|
||||
if (doublePress) {
|
||||
//doublePressAction(b); //not exposed
|
||||
//handled = false; //use if you want to pass to default behaviour
|
||||
} else {
|
||||
buttonWaitTime[b] = now;
|
||||
buttons[b].waitTime = now;
|
||||
}
|
||||
}
|
||||
buttonPressedBefore[b] = false;
|
||||
buttonLongPressed[b] = false;
|
||||
buttons[b].pressedBefore = false;
|
||||
buttons[b].longPressed = false;
|
||||
}
|
||||
// if 350ms elapsed since last press/release it is a short press
|
||||
if (buttonWaitTime[b] && now - buttonWaitTime[b] > 350 && !buttonPressedBefore[b]) {
|
||||
buttonWaitTime[b] = 0;
|
||||
if (buttons[b].waitTime && now - buttons[b].waitTime > 350 && !buttons[b].pressedBefore) {
|
||||
buttons[b].waitTime = 0;
|
||||
//shortPressAction(b); //not exposed
|
||||
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
|
||||
if (_relay[i].button == b) {
|
||||
|
||||
@@ -461,11 +461,11 @@ class PixelsDiceTrayUsermod : public Usermod {
|
||||
#if USING_TFT_DISPLAY
|
||||
bool handleButton(uint8_t b) override {
|
||||
if (!enabled || b > 1 // buttons 0,1 only
|
||||
|| buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_NONE ||
|
||||
buttonType[b] == BTN_TYPE_RESERVED ||
|
||||
buttonType[b] == BTN_TYPE_PIR_SENSOR ||
|
||||
buttonType[b] == BTN_TYPE_ANALOG ||
|
||||
buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|
||||
|| buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_NONE ||
|
||||
buttons[b].type == BTN_TYPE_RESERVED ||
|
||||
buttons[b].type == BTN_TYPE_PIR_SENSOR ||
|
||||
buttons[b].type == BTN_TYPE_ANALOG ||
|
||||
buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -476,43 +476,43 @@ class PixelsDiceTrayUsermod : public Usermod {
|
||||
static unsigned long buttonWaitTime[2] = {0};
|
||||
|
||||
//momentary button logic
|
||||
if (!buttonLongPressed[b] && isButtonPressed(b)) { //pressed
|
||||
if (!buttonPressedBefore[b]) {
|
||||
buttonPressedTime[b] = now;
|
||||
if (!buttons[b].longPressed && isButtonPressed(b)) { //pressed
|
||||
if (!buttons[b].pressedBefore) {
|
||||
buttons[b].pressedTime = now;
|
||||
}
|
||||
buttonPressedBefore[b] = true;
|
||||
buttons[b].pressedBefore = true;
|
||||
|
||||
if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press
|
||||
if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press
|
||||
menu_ctrl.HandleButton(ButtonType::LONG, b);
|
||||
buttonLongPressed[b] = true;
|
||||
buttons[b].longPressed = true;
|
||||
return true;
|
||||
}
|
||||
} else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released
|
||||
} else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released
|
||||
|
||||
long dur = now - buttonPressedTime[b];
|
||||
long dur = now - buttons[b].pressedTime;
|
||||
if (dur < WLED_DEBOUNCE_THRESHOLD) {
|
||||
buttonPressedBefore[b] = false;
|
||||
buttons[b].pressedBefore = false;
|
||||
return true;
|
||||
} //too short "press", debounce
|
||||
|
||||
bool doublePress = buttonWaitTime[b]; //did we have short press before?
|
||||
buttonWaitTime[b] = 0;
|
||||
bool doublePress = buttons[b].waitTime; //did we have short press before?
|
||||
buttons[b].waitTime = 0;
|
||||
|
||||
if (!buttonLongPressed[b]) { //short press
|
||||
if (!buttons[b].longPressed) { //short press
|
||||
// if this is second release within 350ms it is a double press (buttonWaitTime!=0)
|
||||
if (doublePress) {
|
||||
menu_ctrl.HandleButton(ButtonType::DOUBLE, b);
|
||||
} else {
|
||||
buttonWaitTime[b] = now;
|
||||
buttons[b].waitTime = now;
|
||||
}
|
||||
}
|
||||
buttonPressedBefore[b] = false;
|
||||
buttonLongPressed[b] = false;
|
||||
buttons[b].pressedBefore = false;
|
||||
buttons[b].longPressed = false;
|
||||
}
|
||||
// if 350ms elapsed since last press/release it is a short press
|
||||
if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS &&
|
||||
!buttonPressedBefore[b]) {
|
||||
buttonWaitTime[b] = 0;
|
||||
if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS &&
|
||||
!buttons[b].pressedBefore) {
|
||||
buttons[b].waitTime = 0;
|
||||
menu_ctrl.HandleButton(ButtonType::SINGLE, b);
|
||||
}
|
||||
|
||||
|
||||
@@ -749,12 +749,12 @@ bool FourLineDisplayUsermod::handleButton(uint8_t b) {
|
||||
yield();
|
||||
if (!enabled
|
||||
|| b // button 0 only
|
||||
|| buttonType[b] == BTN_TYPE_SWITCH
|
||||
|| buttonType[b] == BTN_TYPE_NONE
|
||||
|| buttonType[b] == BTN_TYPE_RESERVED
|
||||
|| buttonType[b] == BTN_TYPE_PIR_SENSOR
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|
||||
|| buttons[b].type == BTN_TYPE_SWITCH
|
||||
|| buttons[b].type == BTN_TYPE_NONE
|
||||
|| buttons[b].type == BTN_TYPE_RESERVED
|
||||
|| buttons[b].type == BTN_TYPE_PIR_SENSOR
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
494
wled00/FX.cpp
494
wled00/FX.cpp
@@ -15,14 +15,25 @@
|
||||
#include "fcn_declare.h"
|
||||
|
||||
#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D))
|
||||
#include "FXparticleSystem.h"
|
||||
#include "FXparticleSystem.h" // include particle system code only if at least one system is enabled
|
||||
#ifdef WLED_DISABLE_PARTICLESYSTEM2D
|
||||
#define WLED_PS_DONT_REPLACE_2D_FX
|
||||
#endif
|
||||
#ifdef WLED_DISABLE_PARTICLESYSTEM1D
|
||||
#define WLED_PS_DONT_REPLACE_1D_FX
|
||||
#endif
|
||||
#ifdef ESP8266
|
||||
#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) && !defined(WLED_DISABLE_PARTICLESYSTEM1D)
|
||||
#error ESP8266 does not support 1D and 2D particle systems simultaneously. Please disable one of them.
|
||||
#error ESP8266 does not support 1D and 2D particle systems simultaneously. Please disable one of them.
|
||||
#endif
|
||||
#endif
|
||||
#else
|
||||
#define WLED_PS_DONT_REPLACE_FX
|
||||
#define WLED_PS_DONT_REPLACE_1D_FX
|
||||
#define WLED_PS_DONT_REPLACE_2D_FX
|
||||
#endif
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
#define WLED_PS_DONT_REPLACE_1D_FX
|
||||
#define WLED_PS_DONT_REPLACE_2D_FX
|
||||
#endif
|
||||
|
||||
//////////////
|
||||
@@ -713,7 +724,7 @@ uint16_t dissolve(uint32_t color) {
|
||||
if (SEGENV.aux0) { //dissolve to primary/palette
|
||||
if (pixels[i] == SEGCOLOR(1)) {
|
||||
pixels[i] = color == SEGCOLOR(0) ? SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0) : color;
|
||||
break; //only spawn 1 new pixel per frame per 50 LEDs
|
||||
break; //only spawn 1 new pixel per frame
|
||||
}
|
||||
} else { //dissolve to secondary
|
||||
if (pixels[i] != SEGCOLOR(1)) {
|
||||
@@ -724,14 +735,27 @@ uint16_t dissolve(uint32_t color) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// fix for #4401
|
||||
for (unsigned i = 0; i < SEGLEN; i++) SEGMENT.setPixelColor(i, pixels[i]);
|
||||
unsigned incompletePixels = 0;
|
||||
for (unsigned i = 0; i < SEGLEN; i++) {
|
||||
SEGMENT.setPixelColor(i, pixels[i]); // fix for #4401
|
||||
if (SEGMENT.check2) {
|
||||
if (SEGENV.aux0) {
|
||||
if (pixels[i] == SEGCOLOR(1)) incompletePixels++;
|
||||
} else {
|
||||
if (pixels[i] != SEGCOLOR(1)) incompletePixels++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (SEGENV.step > (255 - SEGMENT.speed) + 15U) {
|
||||
SEGENV.aux0 = !SEGENV.aux0;
|
||||
SEGENV.step = 0;
|
||||
} else {
|
||||
SEGENV.step++;
|
||||
if (SEGMENT.check2) {
|
||||
if (incompletePixels == 0)
|
||||
SEGENV.step++; // only advance step once all pixels have changed
|
||||
} else
|
||||
SEGENV.step++;
|
||||
}
|
||||
|
||||
return FRAMETIME;
|
||||
@@ -744,7 +768,7 @@ uint16_t dissolve(uint32_t color) {
|
||||
uint16_t mode_dissolve(void) {
|
||||
return dissolve(SEGMENT.check1 ? SEGMENT.color_wheel(hw_random8()) : SEGCOLOR(0));
|
||||
}
|
||||
static const char _data_FX_MODE_DISSOLVE[] PROGMEM = "Dissolve@Repeat speed,Dissolve speed,,,,Random;!,!;!";
|
||||
static const char _data_FX_MODE_DISSOLVE[] PROGMEM = "Dissolve@Repeat speed,Dissolve speed,,,,Random,Complete;!,!;!";
|
||||
|
||||
|
||||
/*
|
||||
@@ -755,7 +779,6 @@ uint16_t mode_dissolve_random(void) {
|
||||
}
|
||||
static const char _data_FX_MODE_DISSOLVE_RANDOM[] PROGMEM = "Dissolve Rnd@Repeat speed,Dissolve speed;,!;!";
|
||||
|
||||
|
||||
/*
|
||||
* Blinks one LED at a time.
|
||||
* Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/
|
||||
@@ -777,7 +800,6 @@ uint16_t mode_sparkle(void) {
|
||||
}
|
||||
static const char _data_FX_MODE_SPARKLE[] PROGMEM = "Sparkle@!,,,,,,Overlay;!,!;!;;m12=0";
|
||||
|
||||
|
||||
/*
|
||||
* Lights all LEDs in the color. Flashes single col 1 pixels randomly. (List name: Sparkle Dark)
|
||||
* Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/
|
||||
@@ -1752,7 +1774,6 @@ uint16_t mode_tricolor_fade(void) {
|
||||
}
|
||||
static const char _data_FX_MODE_TRICOLOR_FADE[] PROGMEM = "Tri Fade@!;1,2,3;!";
|
||||
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
/*
|
||||
* Creates random comets
|
||||
* Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/MultiComet.h
|
||||
@@ -1791,7 +1812,6 @@ uint16_t mode_multi_comet(void) {
|
||||
}
|
||||
static const char _data_FX_MODE_MULTI_COMET[] PROGMEM = "Multi Comet@!,Fade;!,!;!;1";
|
||||
#undef MAX_COMETS
|
||||
#endif // WLED_PS_DONT_REPLACE_FX
|
||||
|
||||
/*
|
||||
* Running random pixels ("Stream 2")
|
||||
@@ -2118,7 +2138,7 @@ uint16_t mode_palette() {
|
||||
}
|
||||
static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Anamorphic;;!;12;ix=112,c1=0,o1=1,o2=0,o3=1";
|
||||
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
#if defined(WLED_PS_DONT_REPLACE_1D_FX) || defined(WLED_PS_DONT_REPLACE_2D_FX)
|
||||
// WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active
|
||||
// Fire2012 by Mark Kriegsman, July 2012
|
||||
// as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY
|
||||
@@ -2205,7 +2225,7 @@ uint16_t mode_fire_2012() {
|
||||
return FRAMETIME;
|
||||
}
|
||||
static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,2D Blur,Boost;;!;1;pal=35,sx=64,ix=160,m12=1,c2=128"; // bars
|
||||
#endif // WLED_PS_DONT_REPLACE_FX
|
||||
#endif // WLED_PS_DONT_REPLACE_x_FX
|
||||
|
||||
// colored stripes pulsing at a defined Beats-Per-Minute (BPM)
|
||||
uint16_t mode_bpm() {
|
||||
@@ -3056,7 +3076,7 @@ uint16_t mode_bouncing_balls(void) {
|
||||
}
|
||||
static const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = "Bouncing Balls@Gravity,# of balls,,,,,Overlay;!,!,!;!;1;m12=1"; //bar
|
||||
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
#ifdef WLED_PS_DONT_REPLACE_1D_FX
|
||||
/*
|
||||
* bouncing balls on a track track Effect modified from Aircoookie's bouncing balls
|
||||
* Courtesy of pjhatch (https://github.com/pjhatch)
|
||||
@@ -3156,7 +3176,7 @@ static uint16_t rolling_balls(void) {
|
||||
return FRAMETIME;
|
||||
}
|
||||
static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collide,Overlay,Trails;!,!,!;!;1;m12=1"; //bar
|
||||
#endif // WLED_PS_DONT_REPLACE_FX
|
||||
#endif // WLED_PS_DONT_REPLACE_1D_FX
|
||||
|
||||
/*
|
||||
* Sinelon stolen from FASTLED examples
|
||||
@@ -3213,7 +3233,6 @@ uint16_t mode_sinelon_rainbow(void) {
|
||||
}
|
||||
static const char _data_FX_MODE_SINELON_RAINBOW[] PROGMEM = "Sinelon Rainbow@!,Trail;,,!;!";
|
||||
|
||||
|
||||
// utility function that will add random glitter to SEGMENT
|
||||
void glitter_base(uint8_t intensity, uint32_t col = ULTRAWHITE) {
|
||||
if (intensity > hw_random8()) SEGMENT.setPixelColor(hw_random16(SEGLEN), col);
|
||||
@@ -3418,7 +3437,7 @@ uint16_t mode_candle_multi()
|
||||
}
|
||||
static const char _data_FX_MODE_CANDLE_MULTI[] PROGMEM = "Candle Multi@!,!;!,!;!;;sx=96,ix=224,pal=0";
|
||||
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
#ifdef WLED_PS_DONT_REPLACE_1D_FX
|
||||
/*
|
||||
/ Fireworks in starburst effect
|
||||
/ based on the video: https://www.reddit.com/r/arduino/comments/c3sd46/i_made_this_fireworks_effect_for_my_led_strips/
|
||||
@@ -3550,9 +3569,9 @@ uint16_t mode_starburst(void) {
|
||||
}
|
||||
#undef STARBURST_MAX_FRAG
|
||||
static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chance,Fragments,,,,,Overlay;,!;!;;pal=11,m12=0";
|
||||
#endif // WLED_PS_DONT_REPLACE_FX
|
||||
#endif // WLED_PS_DONT_REPLACE_1DFX
|
||||
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
#if defined(WLED_PS_DONT_REPLACE_1D_FX) || defined(WLED_PS_DONT_REPLACE_2D_FX)
|
||||
/*
|
||||
* Exploding fireworks effect
|
||||
* adapted from: http://www.anirama.com/1000leds/1d-fireworks/
|
||||
@@ -3690,7 +3709,7 @@ uint16_t mode_exploding_fireworks(void)
|
||||
}
|
||||
#undef MAX_SPARKS
|
||||
static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side;!,!;!;12;pal=11,ix=128";
|
||||
#endif // WLED_PS_DONT_REPLACE_FX
|
||||
#endif // WLED_PS_DONT_REPLACE_x_FX
|
||||
|
||||
/*
|
||||
* Drip Effect
|
||||
@@ -4338,7 +4357,7 @@ static const char _data_FX_MODE_CHUNCHUN[] PROGMEM = "Chunchun@!,Gap size;!,!;!"
|
||||
#define SPOT_MAX_COUNT 49 //Number of simultaneous waves
|
||||
#endif
|
||||
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
#ifdef WLED_PS_DONT_REPLACE_1D_FX
|
||||
//13 bytes
|
||||
typedef struct Spotlight {
|
||||
float speed;
|
||||
@@ -4472,7 +4491,7 @@ uint16_t mode_dancing_shadows(void)
|
||||
return FRAMETIME;
|
||||
}
|
||||
static const char _data_FX_MODE_DANCING_SHADOWS[] PROGMEM = "Dancing Shadows@!,# of shadows;!;!";
|
||||
#endif // WLED_PS_DONT_REPLACE_FX
|
||||
#endif // WLED_PS_DONT_REPLACE_1D_FX
|
||||
|
||||
/*
|
||||
Imitates a washing machine, rotating same waves forward, then pause, then backward.
|
||||
@@ -4509,7 +4528,7 @@ uint16_t mode_image(void) {
|
||||
// Serial.println(status);
|
||||
// }
|
||||
}
|
||||
static const char _data_FX_MODE_IMAGE[] PROGMEM = "Image@!,;;;12;sx=128";
|
||||
static const char _data_FX_MODE_IMAGE[] PROGMEM = "Image@!,Blur,;;;12;sx=128,ix=0";
|
||||
|
||||
/*
|
||||
Blends random colors across palette
|
||||
@@ -4669,7 +4688,8 @@ static const char _data_FX_MODE_TV_SIMULATOR[] PROGMEM = "TV Simulator@!,!;;!;01
|
||||
|
||||
|
||||
/*
|
||||
Aurora effect
|
||||
Aurora effect by @Mazen
|
||||
improved and converted to integer math by @dedehai
|
||||
*/
|
||||
|
||||
//CONFIG
|
||||
@@ -4681,140 +4701,138 @@ static const char _data_FX_MODE_TV_SIMULATOR[] PROGMEM = "TV Simulator@!,!;;!;01
|
||||
#define W_MAX_SPEED 6 //Higher number, higher speed
|
||||
#define W_WIDTH_FACTOR 6 //Higher number, smaller waves
|
||||
|
||||
//24 bytes
|
||||
// fixed-point math scaling
|
||||
#define AW_SHIFT 16
|
||||
#define AW_SCALE (1 << AW_SHIFT) // 65536 representing 1.0
|
||||
|
||||
// 32 bytes
|
||||
class AuroraWave {
|
||||
private:
|
||||
int32_t center; // scaled by AW_SCALE
|
||||
uint32_t ageFactor_cached; // cached age factor scaled by AW_SCALE
|
||||
uint16_t ttl;
|
||||
CRGB basecolor;
|
||||
float basealpha;
|
||||
uint16_t age;
|
||||
uint16_t width;
|
||||
float center;
|
||||
uint16_t basealpha; // scaled by AW_SCALE
|
||||
uint16_t speed_factor; // scaled by AW_SCALE
|
||||
int16_t wave_start; // wave start LED index
|
||||
int16_t wave_end; // wave end LED index
|
||||
bool goingleft;
|
||||
float speed_factor;
|
||||
bool alive = true;
|
||||
CRGBW basecolor;
|
||||
|
||||
public:
|
||||
void init(uint32_t segment_length, CRGB color) {
|
||||
void init(uint32_t segment_length, CRGBW color) {
|
||||
ttl = hw_random16(500, 1501);
|
||||
basecolor = color;
|
||||
basealpha = hw_random8(60, 101) / (float)100;
|
||||
basealpha = hw_random8(60, 100) * AW_SCALE / 100; // 0-99% note: if using 100% there is risk of integer overflow
|
||||
age = 0;
|
||||
width = hw_random16(segment_length / 20, segment_length / W_WIDTH_FACTOR); //half of width to make math easier
|
||||
if (!width) width = 1;
|
||||
center = hw_random8(101) / (float)100 * segment_length;
|
||||
goingleft = hw_random8(0, 2) == 0;
|
||||
speed_factor = (hw_random8(10, 31) / (float)100 * W_MAX_SPEED / 255);
|
||||
width = hw_random16(segment_length / 20, segment_length / W_WIDTH_FACTOR) + 1;
|
||||
center = (((uint32_t)hw_random8(101) << AW_SHIFT) / 100) * segment_length; // 0-100%
|
||||
goingleft = hw_random8() & 0x01; // 50/50 chance
|
||||
speed_factor = (((uint32_t)hw_random8(10, 31) * W_MAX_SPEED) << AW_SHIFT) / (100 * 255);
|
||||
alive = true;
|
||||
}
|
||||
|
||||
CRGB getColorForLED(int ledIndex) {
|
||||
if(ledIndex < center - width || ledIndex > center + width) return 0; //Position out of range of this wave
|
||||
|
||||
CRGB rgb;
|
||||
|
||||
//Offset of this led from center of wave
|
||||
//The further away from the center, the dimmer the LED
|
||||
float offset = ledIndex - center;
|
||||
if (offset < 0) offset = -offset;
|
||||
float offsetFactor = offset / width;
|
||||
|
||||
//The age of the wave determines it brightness.
|
||||
//At half its maximum age it will be the brightest.
|
||||
float ageFactor = 0.1;
|
||||
if((float)age / ttl < 0.5) {
|
||||
ageFactor = (float)age / (ttl / 2);
|
||||
void updateCachedValues() {
|
||||
uint32_t half_ttl = ttl >> 1;
|
||||
if (age < half_ttl) {
|
||||
ageFactor_cached = ((uint32_t)age << AW_SHIFT) / half_ttl;
|
||||
} else {
|
||||
ageFactor = (float)(ttl - age) / ((float)ttl * 0.5);
|
||||
ageFactor_cached = ((uint32_t)(ttl - age) << AW_SHIFT) / half_ttl;
|
||||
}
|
||||
if (ageFactor_cached >= AW_SCALE) ageFactor_cached = AW_SCALE - 1; // prevent overflow
|
||||
|
||||
//Calculate color based on above factors and basealpha value
|
||||
float factor = (1 - offsetFactor) * ageFactor * basealpha;
|
||||
rgb.r = basecolor.r * factor;
|
||||
rgb.g = basecolor.g * factor;
|
||||
rgb.b = basecolor.b * factor;
|
||||
uint32_t center_led = center >> AW_SHIFT;
|
||||
wave_start = (int16_t)center_led - (int16_t)width;
|
||||
wave_end = (int16_t)center_led + (int16_t)width;
|
||||
}
|
||||
|
||||
CRGBW getColorForLED(int ledIndex) {
|
||||
// linear brightness falloff from center to edge of wave
|
||||
if (ledIndex < wave_start || ledIndex > wave_end) return 0;
|
||||
int32_t ledIndex_scaled = (int32_t)ledIndex << AW_SHIFT;
|
||||
int32_t offset = ledIndex_scaled - center;
|
||||
if (offset < 0) offset = -offset;
|
||||
uint32_t offsetFactor = offset / width; // scaled by AW_SCALE
|
||||
if (offsetFactor > AW_SCALE) return 0; // outside of wave
|
||||
uint32_t brightness_factor = (AW_SCALE - offsetFactor);
|
||||
brightness_factor = (brightness_factor * ageFactor_cached) >> AW_SHIFT;
|
||||
brightness_factor = (brightness_factor * basealpha) >> AW_SHIFT;
|
||||
|
||||
CRGBW rgb;
|
||||
rgb.r = (basecolor.r * brightness_factor) >> AW_SHIFT;
|
||||
rgb.g = (basecolor.g * brightness_factor) >> AW_SHIFT;
|
||||
rgb.b = (basecolor.b * brightness_factor) >> AW_SHIFT;
|
||||
rgb.w = (basecolor.w * brightness_factor) >> AW_SHIFT;
|
||||
|
||||
return rgb;
|
||||
};
|
||||
|
||||
//Change position and age of wave
|
||||
//Determine if its sill "alive"
|
||||
//Determine if its still "alive"
|
||||
void update(uint32_t segment_length, uint32_t speed) {
|
||||
if(goingleft) {
|
||||
center -= speed_factor * speed;
|
||||
} else {
|
||||
center += speed_factor * speed;
|
||||
}
|
||||
|
||||
int32_t step = speed_factor * speed;
|
||||
center += goingleft ? -step : step;
|
||||
age++;
|
||||
|
||||
if(age > ttl) {
|
||||
if (age > ttl) {
|
||||
alive = false;
|
||||
} else {
|
||||
if(goingleft) {
|
||||
if(center + width < 0) {
|
||||
alive = false;
|
||||
}
|
||||
} else {
|
||||
if(center - width > segment_length) {
|
||||
alive = false;
|
||||
}
|
||||
}
|
||||
uint32_t width_scaled = (uint32_t)width << AW_SHIFT;
|
||||
uint32_t segment_length_scaled = segment_length << AW_SHIFT;
|
||||
|
||||
if (goingleft) {
|
||||
if (center < - (int32_t)width_scaled) {
|
||||
alive = false;
|
||||
}
|
||||
} else {
|
||||
if (center > (int32_t)segment_length_scaled + (int32_t)width_scaled) {
|
||||
alive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bool stillAlive() {
|
||||
return alive;
|
||||
};
|
||||
bool stillAlive() { return alive; }
|
||||
};
|
||||
|
||||
uint16_t mode_aurora(void) {
|
||||
AuroraWave* waves;
|
||||
SEGENV.aux1 = map(SEGMENT.intensity, 0, 255, 2, W_MAX_COUNT); // aux1 = Wavecount
|
||||
if(!SEGENV.allocateData(sizeof(AuroraWave) * SEGENV.aux1)) { // 20 on ESP32, 9 on ESP8266
|
||||
return mode_static(); //allocation failed
|
||||
if (!SEGENV.allocateData(sizeof(AuroraWave) * SEGENV.aux1)) {
|
||||
return mode_static();
|
||||
}
|
||||
waves = reinterpret_cast<AuroraWave*>(SEGENV.data);
|
||||
|
||||
if(SEGENV.call == 0) {
|
||||
for (int i = 0; i < SEGENV.aux1; i++) {
|
||||
waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(hw_random8(), false, false, hw_random8(0, 3))));
|
||||
}
|
||||
}
|
||||
|
||||
// note: on first call, SEGENV.data is zero -> all waves are dead and will be initialized
|
||||
for (int i = 0; i < SEGENV.aux1; i++) {
|
||||
//Update values of wave
|
||||
waves[i].update(SEGLEN, SEGMENT.speed);
|
||||
|
||||
if(!(waves[i].stillAlive())) {
|
||||
//If a wave dies, reinitialize it starts over.
|
||||
waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(hw_random8(), false, false, hw_random8(0, 3))));
|
||||
if (!(waves[i].stillAlive())) {
|
||||
waves[i].init(SEGLEN, SEGMENT.color_from_palette(hw_random8(), false, false, hw_random8(0, 3)));
|
||||
}
|
||||
waves[i].updateCachedValues();
|
||||
}
|
||||
|
||||
uint8_t backlight = 1; //dimmer backlight if less active colors
|
||||
uint8_t backlight = 0; // note: original code used 1, with inverse gamma applied background would never be black
|
||||
if (SEGCOLOR(0)) backlight++;
|
||||
if (SEGCOLOR(1)) backlight++;
|
||||
if (SEGCOLOR(2)) backlight++;
|
||||
//Loop through LEDs to determine color
|
||||
backlight = gamma8inv(backlight); // preserve backlight when using gamma correction
|
||||
|
||||
for (unsigned i = 0; i < SEGLEN; i++) {
|
||||
CRGB mixedRgb = CRGB(backlight, backlight, backlight);
|
||||
CRGBW mixedRgb = CRGBW(backlight, backlight, backlight);
|
||||
|
||||
//For each LED we must check each wave if it is "active" at this position.
|
||||
//If there are multiple waves active on a LED we multiply their values.
|
||||
for (int j = 0; j < SEGENV.aux1; j++) {
|
||||
CRGB rgb = waves[j].getColorForLED(i);
|
||||
|
||||
if(rgb != CRGB(0)) {
|
||||
mixedRgb += rgb;
|
||||
}
|
||||
for (int j = 0; j < SEGENV.aux1; j++) {
|
||||
CRGBW rgb = waves[j].getColorForLED(i);
|
||||
mixedRgb = color_add(mixedRgb, rgb); // sum all waves influencing this pixel
|
||||
}
|
||||
|
||||
SEGMENT.setPixelColor(i, mixedRgb[0], mixedRgb[1], mixedRgb[2]);
|
||||
SEGMENT.setPixelColor(i, mixedRgb);
|
||||
}
|
||||
|
||||
return FRAMETIME;
|
||||
}
|
||||
|
||||
static const char _data_FX_MODE_AURORA[] PROGMEM = "Aurora@!,!;1,2,3;!;;sx=24,pal=50";
|
||||
|
||||
// WLED-SR effects
|
||||
@@ -5196,112 +5214,162 @@ static const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = "Frizzles@X frequency,Y f
|
||||
///////////////////////////////////////////
|
||||
// 2D Cellular Automata Game of life //
|
||||
///////////////////////////////////////////
|
||||
typedef struct ColorCount {
|
||||
CRGB color;
|
||||
int8_t count;
|
||||
} colorCount;
|
||||
typedef struct Cell {
|
||||
uint8_t alive : 1, faded : 1, toggleStatus : 1, edgeCell: 1, oscillatorCheck : 1, spaceshipCheck : 1, unused : 2;
|
||||
} Cell;
|
||||
|
||||
uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ and https://github.com/DougHaber/nlife-color
|
||||
uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/
|
||||
// and https://github.com/DougHaber/nlife-color , Modified By: Brandon Butler
|
||||
if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up
|
||||
const int cols = SEG_W, rows = SEG_H;
|
||||
const unsigned maxIndex = cols * rows;
|
||||
|
||||
const int cols = SEG_W;
|
||||
const int rows = SEG_H;
|
||||
const auto XY = [&](int x, int y) { return (x%cols) + (y%rows) * cols; };
|
||||
const unsigned dataSize = sizeof(CRGB) * SEGMENT.length(); // using width*height prevents reallocation if mirroring is enabled
|
||||
const int crcBufferLen = 2; //(SEGMENT.width() + SEGMENT.height())*71/100; // roughly sqrt(2)/2 for better repetition detection (Ewowi)
|
||||
if (!SEGENV.allocateData(SEGMENT.length() * sizeof(Cell))) return mode_static(); // allocation failed
|
||||
|
||||
if (!SEGENV.allocateData(dataSize + sizeof(uint16_t)*crcBufferLen)) return mode_static(); //allocation failed
|
||||
CRGB *prevLeds = reinterpret_cast<CRGB*>(SEGENV.data);
|
||||
uint16_t *crcBuffer = reinterpret_cast<uint16_t*>(SEGENV.data + dataSize);
|
||||
Cell *cells = reinterpret_cast<Cell*> (SEGENV.data);
|
||||
|
||||
CRGB backgroundColor = SEGCOLOR(1);
|
||||
uint16_t& generation = SEGENV.aux0, &gliderLength = SEGENV.aux1; // rename aux variables for clarity
|
||||
bool mutate = SEGMENT.check3;
|
||||
uint8_t blur = map(SEGMENT.custom1, 0, 255, 255, 4);
|
||||
|
||||
if (SEGENV.call == 0 || strip.now - SEGMENT.step > 3000) {
|
||||
SEGENV.step = strip.now;
|
||||
SEGENV.aux0 = 0;
|
||||
uint32_t bgColor = SEGCOLOR(1);
|
||||
uint32_t birthColor = SEGMENT.color_from_palette(128, false, PALETTE_SOLID_WRAP, 255);
|
||||
|
||||
//give the leds random state and colors (based on intensity, colors from palette or all posible colors are chosen)
|
||||
for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) {
|
||||
unsigned state = hw_random8()%2;
|
||||
if (state == 0)
|
||||
SEGMENT.setPixelColorXY(x,y, backgroundColor);
|
||||
else
|
||||
SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 255));
|
||||
bool setup = SEGENV.call == 0;
|
||||
if (setup) {
|
||||
// Calculate glider length LCM(rows,cols)*4 once
|
||||
unsigned a = rows, b = cols;
|
||||
while (b) { unsigned t = b; b = a % b; a = t; }
|
||||
gliderLength = (cols * rows / a) << 2;
|
||||
}
|
||||
|
||||
if (abs(long(strip.now) - long(SEGENV.step)) > 2000) SEGENV.step = 0; // Timebase jump fix
|
||||
bool paused = SEGENV.step > strip.now;
|
||||
|
||||
// Setup New Game of Life
|
||||
if ((!paused && generation == 0) || setup) {
|
||||
SEGENV.step = strip.now + 1280; // show initial state for 1.28 seconds
|
||||
generation = 1;
|
||||
paused = true;
|
||||
//Setup Grid
|
||||
memset(cells, 0, maxIndex * sizeof(Cell));
|
||||
|
||||
for (unsigned i = 0; i < maxIndex; i++) {
|
||||
bool isAlive = !hw_random8(3); // ~33%
|
||||
cells[i].alive = isAlive;
|
||||
cells[i].faded = !isAlive;
|
||||
unsigned x = i % cols, y = i / cols;
|
||||
cells[i].edgeCell = (x == 0 || x == cols-1 || y == 0 || y == rows-1);
|
||||
|
||||
SEGMENT.setPixelColor(i, isAlive ? SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 0) : bgColor);
|
||||
}
|
||||
}
|
||||
|
||||
for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) prevLeds[XY(x,y)] = CRGB::Black;
|
||||
memset(crcBuffer, 0, sizeof(uint16_t)*crcBufferLen);
|
||||
} else if (strip.now - SEGENV.step < FRAMETIME_FIXED * (uint32_t)map(SEGMENT.speed,0,255,64,4)) {
|
||||
// update only when appropriate time passes (in 42 FPS slots)
|
||||
if (paused || (strip.now - SEGENV.step < 1000 / map(SEGMENT.speed,0,255,1,42))) {
|
||||
// Redraw if paused or between updates to remove blur
|
||||
for (unsigned i = maxIndex; i--; ) {
|
||||
if (!cells[i].alive) {
|
||||
uint32_t cellColor = SEGMENT.getPixelColor(i);
|
||||
if (cellColor != bgColor) {
|
||||
uint32_t newColor;
|
||||
bool needsColor = false;
|
||||
if (cells[i].faded) { newColor = bgColor; needsColor = true; }
|
||||
else {
|
||||
uint32_t blended = color_blend(cellColor, bgColor, 2);
|
||||
if (blended == cellColor) { blended = bgColor; cells[i].faded = 1; }
|
||||
newColor = blended; needsColor = true;
|
||||
}
|
||||
if (needsColor) SEGMENT.setPixelColor(i, newColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
return FRAMETIME;
|
||||
}
|
||||
|
||||
//copy previous leds (save previous generation)
|
||||
//NOTE: using lossy getPixelColor() is a benefit as endlessly repeating patterns will eventually fade out causing a reset
|
||||
for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) prevLeds[XY(x,y)] = SEGMENT.getPixelColorXY(x,y);
|
||||
// Repeat detection
|
||||
bool updateOscillator = generation % 16 == 0;
|
||||
bool updateSpaceship = gliderLength && generation % gliderLength == 0;
|
||||
bool repeatingOscillator = true, repeatingSpaceship = true, emptyGrid = true;
|
||||
|
||||
//calculate new leds
|
||||
for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) {
|
||||
unsigned cIndex = maxIndex-1;
|
||||
for (unsigned y = rows; y--; ) for (unsigned x = cols; x--; cIndex--) {
|
||||
Cell& cell = cells[cIndex];
|
||||
|
||||
colorCount colorsCount[9]; // count the different colors in the 3*3 matrix
|
||||
for (int i=0; i<9; i++) colorsCount[i] = {backgroundColor, 0}; // init colorsCount
|
||||
if (cell.alive) emptyGrid = false;
|
||||
if (cell.oscillatorCheck != cell.alive) repeatingOscillator = false;
|
||||
if (cell.spaceshipCheck != cell.alive) repeatingSpaceship = false;
|
||||
if (updateOscillator) cell.oscillatorCheck = cell.alive;
|
||||
if (updateSpaceship) cell.spaceshipCheck = cell.alive;
|
||||
|
||||
// iterate through neighbors and count them and their different colors
|
||||
int neighbors = 0;
|
||||
for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) { // iterate through 3*3 matrix
|
||||
if (i==0 && j==0) continue; // ignore itself
|
||||
// wrap around segment
|
||||
int xx = x+i, yy = y+j;
|
||||
if (x+i < 0) xx = cols-1; else if (x+i >= cols) xx = 0;
|
||||
if (y+j < 0) yy = rows-1; else if (y+j >= rows) yy = 0;
|
||||
|
||||
unsigned xy = XY(xx, yy); // previous cell xy to check
|
||||
// count different neighbours and colors
|
||||
if (prevLeds[xy] != backgroundColor) {
|
||||
neighbors++;
|
||||
bool colorFound = false;
|
||||
int k;
|
||||
for (k=0; k<9 && colorsCount[k].count != 0; k++)
|
||||
if (colorsCount[k].color == prevLeds[xy]) {
|
||||
colorsCount[k].count++;
|
||||
colorFound = true;
|
||||
}
|
||||
if (!colorFound) colorsCount[k] = {prevLeds[xy], 1}; //add new color found in the array
|
||||
unsigned neighbors = 0, aliveParents = 0, parentIdx[3];
|
||||
// Count alive neighbors
|
||||
for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) if (i || j) {
|
||||
int nX = x + j, nY = y + i;
|
||||
if (cell.edgeCell) {
|
||||
nX = (nX + cols) % cols;
|
||||
nY = (nY + rows) % rows;
|
||||
}
|
||||
unsigned nIndex = nX + nY * cols;
|
||||
Cell& neighbor = cells[nIndex];
|
||||
if (neighbor.alive) {
|
||||
neighbors++;
|
||||
if (!neighbor.toggleStatus && neighbors < 4) { // Alive and not dying
|
||||
parentIdx[aliveParents++] = nIndex;
|
||||
}
|
||||
}
|
||||
} // i,j
|
||||
|
||||
// Rules of Life
|
||||
uint32_t col = uint32_t(prevLeds[XY(x,y)]) & 0x00FFFFFF; // uint32_t operator returns RGBA, we want RGBW -> cut off "alpha" byte
|
||||
uint32_t bgc = RGBW32(backgroundColor.r, backgroundColor.g, backgroundColor.b, 0);
|
||||
if ((col != bgc) && (neighbors < 2)) SEGMENT.setPixelColorXY(x,y, bgc); // Loneliness
|
||||
else if ((col != bgc) && (neighbors > 3)) SEGMENT.setPixelColorXY(x,y, bgc); // Overpopulation
|
||||
else if ((col == bgc) && (neighbors == 3)) { // Reproduction
|
||||
// find dominant color and assign it to a cell
|
||||
colorCount dominantColorCount = {backgroundColor, 0};
|
||||
for (int i=0; i<9 && colorsCount[i].count != 0; i++)
|
||||
if (colorsCount[i].count > dominantColorCount.count) dominantColorCount = colorsCount[i];
|
||||
// assign the dominant color w/ a bit of randomness to avoid "gliders"
|
||||
if (dominantColorCount.count > 0 && hw_random8(128)) SEGMENT.setPixelColorXY(x,y, dominantColorCount.color);
|
||||
} else if ((col == bgc) && (neighbors == 2) && !hw_random8(128)) { // Mutation
|
||||
SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 255));
|
||||
}
|
||||
// else do nothing!
|
||||
} //x,y
|
||||
|
||||
// calculate CRC16 of leds
|
||||
uint16_t crc = crc16((const unsigned char*)prevLeds, dataSize);
|
||||
// check if we had same CRC and reset if needed
|
||||
bool repetition = false;
|
||||
for (int i=0; i<crcBufferLen && !repetition; i++) repetition = (crc == crcBuffer[i]); // (Ewowi)
|
||||
// same CRC would mean image did not change or was repeating itself
|
||||
if (!repetition) SEGENV.step = strip.now; //if no repetition avoid reset
|
||||
// remember CRCs across frames
|
||||
crcBuffer[SEGENV.aux0] = crc;
|
||||
++SEGENV.aux0 %= crcBufferLen;
|
||||
uint32_t newColor;
|
||||
bool needsColor = false;
|
||||
|
||||
if (cell.alive && (neighbors < 2 || neighbors > 3)) { // Loneliness or Overpopulation
|
||||
cell.toggleStatus = 1;
|
||||
if (blur == 255) cell.faded = 1;
|
||||
newColor = cell.faded ? bgColor : color_blend(SEGMENT.getPixelColor(cIndex), bgColor, blur);
|
||||
needsColor = true;
|
||||
}
|
||||
else if (!cell.alive) {
|
||||
byte mutationRoll = mutate ? hw_random8(128) : 1; // if 0: 3 neighbor births fail and 2 neighbor births mutate
|
||||
if ((neighbors == 3 && mutationRoll) || (mutate && neighbors == 2 && !mutationRoll)) { // Reproduction or Mutation
|
||||
cell.toggleStatus = 1;
|
||||
cell.faded = 0;
|
||||
|
||||
if (aliveParents) {
|
||||
// Set color based on random neighbor
|
||||
unsigned parentIndex = parentIdx[random8(aliveParents)];
|
||||
birthColor = SEGMENT.getPixelColor(parentIndex);
|
||||
}
|
||||
newColor = birthColor;
|
||||
needsColor = true;
|
||||
}
|
||||
else if (!cell.faded) {// No change, fade dead cells
|
||||
uint32_t cellColor = SEGMENT.getPixelColor(cIndex);
|
||||
uint32_t blended = color_blend(cellColor, bgColor, blur);
|
||||
if (blended == cellColor) { blended = bgColor; cell.faded = 1; }
|
||||
newColor = blended;
|
||||
needsColor = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsColor) SEGMENT.setPixelColor(cIndex, newColor);
|
||||
}
|
||||
// Loop through cells, if toggle, swap alive status
|
||||
for (unsigned i = maxIndex; i--; ) {
|
||||
cells[i].alive ^= cells[i].toggleStatus;
|
||||
cells[i].toggleStatus = 0;
|
||||
}
|
||||
|
||||
if (repeatingOscillator || repeatingSpaceship || emptyGrid) {
|
||||
generation = 0; // reset on next call
|
||||
SEGENV.step += 1024; // pause final generation for ~1 second
|
||||
}
|
||||
else {
|
||||
++generation;
|
||||
SEGENV.step = strip.now;
|
||||
}
|
||||
return FRAMETIME;
|
||||
} // mode_2Dgameoflife()
|
||||
static const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = "Game Of Life@!;!,!;!;2";
|
||||
static const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = "Game Of Life@!,,Blur,,,,,Mutation;!,!;!;2;pal=11,sx=128";
|
||||
|
||||
|
||||
/////////////////////////
|
||||
@@ -5984,7 +6052,7 @@ uint16_t mode_2Dcrazybees(void) {
|
||||
static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur,,,,Smear;;!;2;pal=11,ix=0";
|
||||
#undef MAX_BEES
|
||||
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
#ifdef WLED_PS_DONT_REPLACE_2D_FX
|
||||
/////////////////////////
|
||||
// 2D Ghost Rider //
|
||||
/////////////////////////
|
||||
@@ -6172,7 +6240,7 @@ uint16_t mode_2Dfloatingblobs(void) {
|
||||
}
|
||||
static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8";
|
||||
#undef MAX_BLOBS
|
||||
#endif // WLED_PS_DONT_REPLACE_FX
|
||||
#endif // WLED_PS_DONT_REPLACE_2D_FX
|
||||
|
||||
////////////////////////////
|
||||
// 2D Scrolling text //
|
||||
@@ -9628,11 +9696,11 @@ uint16_t mode_particleFireworks1D(void) {
|
||||
|
||||
PartSys->sources[0].sourceFlags.custom1 = 0; //flag used for rocket state
|
||||
PartSys->sources[0].source.hue = hw_random16(); // different color for each launch
|
||||
PartSys->sources[0].var = 10; // emit variation
|
||||
PartSys->sources[0].v = -10; // emit speed
|
||||
PartSys->sources[0].minLife = 30;
|
||||
PartSys->sources[0].maxLife = SEGMENT.check2 ? 400 : 60;
|
||||
PartSys->sources[0].source.x = 0; // start from bottom
|
||||
PartSys->sources[0].var = 10 * SEGMENT.check2; // emit variation, 0 if trail mode is off
|
||||
PartSys->sources[0].v = -10 * SEGMENT.check2; // emit speed, 0 if trail mode is off
|
||||
PartSys->sources[0].minLife = 180;
|
||||
PartSys->sources[0].maxLife = SEGMENT.check2 ? 700 : 240; // exhaust particle life
|
||||
PartSys->sources[0].source.x = SEGENV.aux0 * PartSys->maxX; // start from bottom or top
|
||||
uint32_t speed = sqrt((gravity * ((PartSys->maxX >> 2) + hw_random16(PartSys->maxX >> 1))) >> 4); // set speed such that rocket explods in frame
|
||||
PartSys->sources[0].source.vx = min(speed, (uint32_t)127);
|
||||
PartSys->sources[0].source.ttl = 4000;
|
||||
@@ -9642,7 +9710,7 @@ uint16_t mode_particleFireworks1D(void) {
|
||||
|
||||
if (SEGENV.aux0) { // inverted rockets launch from end
|
||||
PartSys->sources[0].sourceFlags.reversegrav = true;
|
||||
PartSys->sources[0].source.x = PartSys->maxX; // start from top
|
||||
//PartSys->sources[0].source.x = PartSys->maxX; // start from top
|
||||
PartSys->sources[0].source.vx = -PartSys->sources[0].source.vx; // revert direction
|
||||
PartSys->sources[0].v = -PartSys->sources[0].v; // invert exhaust emit speed
|
||||
}
|
||||
@@ -9661,18 +9729,20 @@ uint16_t mode_particleFireworks1D(void) {
|
||||
uint32_t rocketheight = SEGENV.aux0 ? PartSys->maxX - PartSys->sources[0].source.x : PartSys->sources[0].source.x;
|
||||
|
||||
if (currentspeed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee
|
||||
PartSys->sources[0].source.ttl = min((uint32_t)50, rocketheight >> (PS_P_RADIUS_SHIFT_1D + 3)); // alive for a few more frames
|
||||
PartSys->sources[0].source.ttl = 50 - gravity;// min((uint32_t)50, 15 + (rocketheight >> (PS_P_RADIUS_SHIFT_1D + 3))); // alive for a few more frames
|
||||
|
||||
if (PartSys->sources[0].source.ttl < 2) { // explode
|
||||
PartSys->sources[0].sourceFlags.custom1 = 1; // set standby state
|
||||
PartSys->sources[0].var = 5 + ((((PartSys->maxX >> 1) + rocketheight) * (200 + SEGMENT.intensity)) / (PartSys->maxX << 2)); // set explosion particle speed
|
||||
PartSys->sources[0].minLife = 600;
|
||||
PartSys->sources[0].maxLife = 1300;
|
||||
PartSys->sources[0].var = 5 + ((((PartSys->maxX >> 1) + rocketheight) * (20 + (SEGMENT.intensity << 1))) / (PartSys->maxX << 2)); // set explosion particle speed
|
||||
PartSys->sources[0].minLife = 1200;
|
||||
PartSys->sources[0].maxLife = 2600;
|
||||
PartSys->sources[0].source.ttl = 100 + hw_random16(64 - (SEGMENT.speed >> 2)); // standby time til next launch
|
||||
PartSys->sources[0].sat = SEGMENT.custom3 < 16 ? 10 + (SEGMENT.custom3 << 4) : 255; //color saturation
|
||||
PartSys->sources[0].size = SEGMENT.check3 ? hw_random16(SEGMENT.intensity) : 0; // random particle size in explosion
|
||||
uint32_t explosionsize = 8 + (PartSys->maxXpixel >> 2) + (PartSys->sources[0].source.x >> (PS_P_RADIUS_SHIFT_1D - 1));
|
||||
explosionsize += hw_random16((explosionsize * SEGMENT.intensity) >> 8);
|
||||
PartSys->setColorByAge(false); // disable
|
||||
PartSys->setColorByPosition(false); // disable
|
||||
for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles
|
||||
int idx = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle
|
||||
if(SEGMENT.custom3 > 23) {
|
||||
@@ -9692,16 +9762,16 @@ uint16_t mode_particleFireworks1D(void) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false && PartSys->sources[0].source.ttl > 50) // every second frame and not in standby and not about to explode
|
||||
if ((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false) // every second frame and not in standby
|
||||
PartSys->sprayEmit(PartSys->sources[0]); // emit exhaust particle
|
||||
|
||||
if ((SEGMENT.call & 0x03) == 0) // every fourth frame
|
||||
PartSys->applyFriction(1); // apply friction to all particles
|
||||
|
||||
PartSys->update(); // update and render
|
||||
|
||||
|
||||
for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
|
||||
if (PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan
|
||||
if (PartSys->particles[i].ttl > 20) PartSys->particles[i].ttl -= 20; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan
|
||||
else PartSys->particles[i].ttl = 0;
|
||||
}
|
||||
return FRAMETIME;
|
||||
@@ -10822,16 +10892,18 @@ void WS2812FX::setupEffectData() {
|
||||
addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS);
|
||||
addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE);
|
||||
addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET);
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET);
|
||||
addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS);
|
||||
#if defined(WLED_PS_DONT_REPLACE_1D_FX) || defined(WLED_PS_DONT_REPLACE_2D_FX)
|
||||
addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012);
|
||||
addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS);
|
||||
#endif
|
||||
addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE);
|
||||
addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER);
|
||||
addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER);
|
||||
addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET);
|
||||
#ifdef WLED_PS_DONT_REPLACE_1D_FX
|
||||
addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS);
|
||||
addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST);
|
||||
addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS);
|
||||
addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012);
|
||||
addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS);
|
||||
#endif
|
||||
addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE);
|
||||
addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS);
|
||||
@@ -10895,7 +10967,7 @@ void WS2812FX::setupEffectData() {
|
||||
addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS);
|
||||
addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES);
|
||||
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
#ifdef WLED_PS_DONT_REPLACE_2D_FX
|
||||
addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER);
|
||||
addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS);
|
||||
#endif
|
||||
|
||||
@@ -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,12 +1681,17 @@ 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!
|
||||
while (isServicing() && maxWait > millis()) delay(1);
|
||||
unsigned long waitStart = millis();
|
||||
unsigned long maxWait = 2*getFrameTime() + 100; // TODO: this needs a proper fix for timeout! see #4779
|
||||
while (isServicing() && (millis() - waitStart < maxWait)) delay(1); // safe even when millis() rolls over
|
||||
#ifdef WLED_DEBUG
|
||||
if (millis() >= maxWait) DEBUG_PRINTLN(F("Waited for strip to finish servicing."));
|
||||
if (millis()-waitStart >= maxWait) DEBUG_PRINTLN(F("Waited for strip to finish servicing."));
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -1810,7 +1815,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 +1827,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 +1899,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 +1962,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,15 +1992,13 @@ 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())) {
|
||||
Segment::maxWidth = min(max(root[F("width")].as<int>(), 1), 255);
|
||||
Segment::maxHeight = min(max(root[F("height")].as<int>(), 1), 255);
|
||||
isMatrix = true;
|
||||
DEBUG_PRINTF_P(PSTR("LED map width=%d, height=%d\n"), Segment::maxWidth, Segment::maxHeight);
|
||||
}
|
||||
|
||||
d_free(customMappingTable);
|
||||
@@ -2012,9 +2022,9 @@ bool WS2812FX::deserializeMap(unsigned n) {
|
||||
} while (i < 32);
|
||||
if (!foundDigit) break;
|
||||
int index = atoi(number);
|
||||
if (index < 0 || index > 16384) index = 0xFFFF;
|
||||
if (index < 0 || index > 65535) index = 0xFFFF; // prevent integer wrap around
|
||||
customMappingTable[customMappingSize++] = index;
|
||||
if (customMappingSize > getLengthTotal()) break;
|
||||
if (customMappingSize >= getLengthTotal()) break;
|
||||
} else break; // there was nothing to read, stop
|
||||
}
|
||||
currentLedmap = n;
|
||||
@@ -2024,7 +2034,7 @@ bool WS2812FX::deserializeMap(unsigned n) {
|
||||
DEBUG_PRINT(F("Loaded ledmap:"));
|
||||
for (unsigned i=0; i<customMappingSize; i++) {
|
||||
if (!(i%Segment::maxWidth)) DEBUG_PRINTLN();
|
||||
DEBUG_PRINTF_P(PSTR("%4d,"), customMappingTable[i]);
|
||||
DEBUG_PRINTF_P(PSTR("%4d,"), customMappingTable[i] < 0xFFFFU ? customMappingTable[i] : -1);
|
||||
}
|
||||
DEBUG_PRINTLN();
|
||||
#endif
|
||||
@@ -2040,8 +2050,6 @@ bool WS2812FX::deserializeMap(unsigned n) {
|
||||
DEBUG_PRINTLN(F("ERROR LED map allocation error."));
|
||||
}
|
||||
|
||||
resume();
|
||||
|
||||
releaseJSONBufferLock();
|
||||
return (customMappingSize > 0);
|
||||
}
|
||||
|
||||
@@ -17,13 +17,13 @@ static bool buttonBriDirection = false; // true: increase brightness, false: dec
|
||||
|
||||
void shortPressAction(uint8_t b)
|
||||
{
|
||||
if (!macroButton[b]) {
|
||||
if (!buttons[b].macroButton) {
|
||||
switch (b) {
|
||||
case 0: toggleOnOff(); stateUpdated(CALL_MODE_BUTTON); break;
|
||||
case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break;
|
||||
}
|
||||
} else {
|
||||
applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET);
|
||||
applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET);
|
||||
}
|
||||
|
||||
#ifndef WLED_DISABLE_MQTT
|
||||
@@ -38,7 +38,7 @@ void shortPressAction(uint8_t b)
|
||||
|
||||
void longPressAction(uint8_t b)
|
||||
{
|
||||
if (!macroLongPress[b]) {
|
||||
if (!buttons[b].macroLongPress) {
|
||||
switch (b) {
|
||||
case 0: setRandomColor(colPri); colorUpdated(CALL_MODE_BUTTON); break;
|
||||
case 1:
|
||||
@@ -52,11 +52,11 @@ void longPressAction(uint8_t b)
|
||||
else bri -= WLED_LONG_BRI_STEPS;
|
||||
}
|
||||
stateUpdated(CALL_MODE_BUTTON);
|
||||
buttonPressedTime[b] = millis();
|
||||
buttons[b].pressedTime = millis();
|
||||
break; // repeatable action
|
||||
}
|
||||
} else {
|
||||
applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET);
|
||||
applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET);
|
||||
}
|
||||
|
||||
#ifndef WLED_DISABLE_MQTT
|
||||
@@ -71,13 +71,13 @@ void longPressAction(uint8_t b)
|
||||
|
||||
void doublePressAction(uint8_t b)
|
||||
{
|
||||
if (!macroDoublePress[b]) {
|
||||
if (!buttons[b].macroDoublePress) {
|
||||
switch (b) {
|
||||
//case 0: toggleOnOff(); colorUpdated(CALL_MODE_BUTTON); break; //instant short press on button 0 if no macro set
|
||||
case 1: ++effectPalette %= getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break;
|
||||
}
|
||||
} else {
|
||||
applyPreset(macroDoublePress[b], CALL_MODE_BUTTON_PRESET);
|
||||
applyPreset(buttons[b].macroDoublePress, CALL_MODE_BUTTON_PRESET);
|
||||
}
|
||||
|
||||
#ifndef WLED_DISABLE_MQTT
|
||||
@@ -92,10 +92,10 @@ void doublePressAction(uint8_t b)
|
||||
|
||||
bool isButtonPressed(uint8_t b)
|
||||
{
|
||||
if (btnPin[b]<0) return false;
|
||||
unsigned pin = btnPin[b];
|
||||
if (buttons[b].pin < 0) return false;
|
||||
unsigned pin = buttons[b].pin;
|
||||
|
||||
switch (buttonType[b]) {
|
||||
switch (buttons[b].type) {
|
||||
case BTN_TYPE_NONE:
|
||||
case BTN_TYPE_RESERVED:
|
||||
break;
|
||||
@@ -113,7 +113,7 @@ bool isButtonPressed(uint8_t b)
|
||||
#ifdef SOC_TOUCH_VERSION_2 //ESP32 S2 and S3 provide a function to check touch state (state is updated in interrupt)
|
||||
if (touchInterruptGetLastStatus(pin)) return true;
|
||||
#else
|
||||
if (digitalPinToTouchChannel(btnPin[b]) >= 0 && touchRead(pin) <= touchThreshold) return true;
|
||||
if (digitalPinToTouchChannel(pin) >= 0 && touchRead(pin) <= touchThreshold) return true;
|
||||
#endif
|
||||
#endif
|
||||
break;
|
||||
@@ -124,25 +124,25 @@ bool isButtonPressed(uint8_t b)
|
||||
void handleSwitch(uint8_t b)
|
||||
{
|
||||
// isButtonPressed() handles inverted/noninverted logic
|
||||
if (buttonPressedBefore[b] != isButtonPressed(b)) {
|
||||
if (buttons[b].pressedBefore != isButtonPressed(b)) {
|
||||
DEBUG_PRINTF_P(PSTR("Switch: State changed %u\n"), b);
|
||||
buttonPressedTime[b] = millis();
|
||||
buttonPressedBefore[b] = !buttonPressedBefore[b];
|
||||
buttons[b].pressedTime = millis();
|
||||
buttons[b].pressedBefore = !buttons[b].pressedBefore; // toggle pressed state
|
||||
}
|
||||
|
||||
if (buttonLongPressed[b] == buttonPressedBefore[b]) return;
|
||||
if (buttons[b].longPressed == buttons[b].pressedBefore) return;
|
||||
|
||||
if (millis() - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
|
||||
if (millis() - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
|
||||
DEBUG_PRINTF_P(PSTR("Switch: Activating %u\n"), b);
|
||||
if (!buttonPressedBefore[b]) { // on -> off
|
||||
if (!buttons[b].pressedBefore) { // on -> off
|
||||
DEBUG_PRINTF_P(PSTR("Switch: On -> Off (%u)\n"), b);
|
||||
if (macroButton[b]) applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET);
|
||||
if (buttons[b].macroButton) applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET);
|
||||
else { //turn on
|
||||
if (!bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);}
|
||||
}
|
||||
} else { // off -> on
|
||||
DEBUG_PRINTF_P(PSTR("Switch: Off -> On (%u)\n"), b);
|
||||
if (macroLongPress[b]) applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET);
|
||||
if (buttons[b].macroLongPress) applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET);
|
||||
else { //turn off
|
||||
if (bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);}
|
||||
}
|
||||
@@ -152,13 +152,13 @@ void handleSwitch(uint8_t b)
|
||||
// publish MQTT message
|
||||
if (buttonPublishMqtt && WLED_MQTT_CONNECTED) {
|
||||
char subuf[MQTT_MAX_TOPIC_LEN + 32];
|
||||
if (buttonType[b] == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b);
|
||||
if (buttons[b].type == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b);
|
||||
else sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b);
|
||||
mqtt->publish(subuf, 0, false, !buttonPressedBefore[b] ? "off" : "on");
|
||||
mqtt->publish(subuf, 0, false, !buttons[b].pressedBefore ? "off" : "on");
|
||||
}
|
||||
#endif
|
||||
|
||||
buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state
|
||||
buttons[b].longPressed = buttons[b].pressedBefore; //save the last "long term" switch state
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,17 +178,17 @@ void handleAnalog(uint8_t b)
|
||||
#ifdef ESP8266
|
||||
rawReading = analogRead(A0) << 2; // convert 10bit read to 12bit
|
||||
#else
|
||||
if ((btnPin[b] < 0) /*|| (digitalPinToAnalogChannel(btnPin[b]) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise
|
||||
rawReading = analogRead(btnPin[b]); // collect at full 12bit resolution
|
||||
if ((buttons[b].pin < 0) /*|| (digitalPinToAnalogChannel(buttons[b].pin) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise
|
||||
rawReading = analogRead(buttons[b].pin); // collect at full 12bit resolution
|
||||
#endif
|
||||
yield(); // keep WiFi task running - analog read may take several millis on ESP8266
|
||||
|
||||
filteredReading[b] += POT_SMOOTHING * ((float(rawReading) / 16.0f) - filteredReading[b]); // filter raw input, and scale to [0..255]
|
||||
unsigned aRead = max(min(int(filteredReading[b]), 255), 0); // squash into 8bit
|
||||
if(aRead <= POT_SENSITIVITY) aRead = 0; // make sure that 0 and 255 are used
|
||||
if(aRead >= 255-POT_SENSITIVITY) aRead = 255;
|
||||
if (aRead <= POT_SENSITIVITY) aRead = 0; // make sure that 0 and 255 are used
|
||||
if (aRead >= 255-POT_SENSITIVITY) aRead = 255;
|
||||
|
||||
if (buttonType[b] == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead;
|
||||
if (buttons[b].type == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead;
|
||||
|
||||
// remove noise & reduce frequency of UI updates
|
||||
if (abs(int(aRead) - int(oldRead[b])) <= POT_SENSITIVITY) return; // no significant change in reading
|
||||
@@ -206,10 +206,10 @@ void handleAnalog(uint8_t b)
|
||||
oldRead[b] = aRead;
|
||||
|
||||
// if no macro for "short press" and "long press" is defined use brightness control
|
||||
if (!macroButton[b] && !macroLongPress[b]) {
|
||||
DEBUG_PRINTF_P(PSTR("Analog: Action = %u\n"), macroDoublePress[b]);
|
||||
if (!buttons[b].macroButton && !buttons[b].macroLongPress) {
|
||||
DEBUG_PRINTF_P(PSTR("Analog: Action = %u\n"), buttons[b].macroDoublePress);
|
||||
// if "double press" macro defines which option to change
|
||||
if (macroDoublePress[b] >= 250) {
|
||||
if (buttons[b].macroDoublePress >= 250) {
|
||||
// global brightness
|
||||
if (aRead == 0) {
|
||||
briLast = bri;
|
||||
@@ -218,27 +218,30 @@ void handleAnalog(uint8_t b)
|
||||
if (bri == 0) strip.restartRuntime();
|
||||
bri = aRead;
|
||||
}
|
||||
} else if (macroDoublePress[b] == 249) {
|
||||
} else if (buttons[b].macroDoublePress == 249) {
|
||||
// effect speed
|
||||
effectSpeed = aRead;
|
||||
} else if (macroDoublePress[b] == 248) {
|
||||
} else if (buttons[b].macroDoublePress == 248) {
|
||||
// effect intensity
|
||||
effectIntensity = aRead;
|
||||
} else if (macroDoublePress[b] == 247) {
|
||||
} else if (buttons[b].macroDoublePress == 247) {
|
||||
// selected palette
|
||||
effectPalette = map(aRead, 0, 252, 0, getPaletteCount()-1);
|
||||
effectPalette = constrain(effectPalette, 0, getPaletteCount()-1); // map is allowed to "overshoot", so we need to contrain the result
|
||||
} else if (macroDoublePress[b] == 200) {
|
||||
} else if (buttons[b].macroDoublePress == 200) {
|
||||
// primary color, hue, full saturation
|
||||
colorHStoRGB(aRead*256,255,colPri);
|
||||
colorHStoRGB(aRead*256, 255, colPri);
|
||||
} else {
|
||||
// otherwise use "double press" for segment selection
|
||||
Segment& seg = strip.getSegment(macroDoublePress[b]);
|
||||
Segment& seg = strip.getSegment(buttons[b].macroDoublePress);
|
||||
if (aRead == 0) {
|
||||
seg.setOption(SEG_OPTION_ON, false); // off (use transition)
|
||||
seg.on = false; // do not use transition
|
||||
//seg.setOption(SEG_OPTION_ON, false); // off (use transition)
|
||||
} else {
|
||||
seg.setOpacity(aRead);
|
||||
seg.setOption(SEG_OPTION_ON, true); // on (use transition)
|
||||
seg.opacity = aRead; // set brightness (opacity) of segment
|
||||
seg.on = true;
|
||||
//seg.setOpacity(aRead);
|
||||
//seg.setOption(SEG_OPTION_ON, true); // on (use transition)
|
||||
}
|
||||
// this will notify clients of update (websockets,mqtt,etc)
|
||||
updateInterfaces(CALL_MODE_BUTTON);
|
||||
@@ -261,16 +264,16 @@ void handleButton()
|
||||
if (strip.isUpdating() && (now - lastRun < ANALOG_BTN_READ_CYCLE+1)) return; // don't interfere with strip update (unless strip is updating continuously, e.g. very long strips)
|
||||
lastRun = now;
|
||||
|
||||
for (unsigned b=0; b<WLED_MAX_BUTTONS; b++) {
|
||||
for (unsigned b = 0; b < buttons.size(); b++) {
|
||||
#ifdef ESP8266
|
||||
if ((btnPin[b]<0 && !(buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED)) || buttonType[b] == BTN_TYPE_NONE) continue;
|
||||
if ((buttons[b].pin < 0 && !(buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED)) || buttons[b].type == BTN_TYPE_NONE) continue;
|
||||
#else
|
||||
if (btnPin[b]<0 || buttonType[b] == BTN_TYPE_NONE) continue;
|
||||
if (buttons[b].pin < 0 || buttons[b].type == BTN_TYPE_NONE) continue;
|
||||
#endif
|
||||
|
||||
if (UsermodManager::handleButton(b)) continue; // did usermod handle buttons
|
||||
|
||||
if (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer
|
||||
if (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer
|
||||
if (now - lastAnalogRead > ANALOG_BTN_READ_CYCLE) {
|
||||
handleAnalog(b);
|
||||
}
|
||||
@@ -278,7 +281,7 @@ void handleButton()
|
||||
}
|
||||
|
||||
// button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0)
|
||||
if (buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_TOUCH_SWITCH || buttonType[b] == BTN_TYPE_PIR_SENSOR) {
|
||||
if (buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_TOUCH_SWITCH || buttons[b].type == BTN_TYPE_PIR_SENSOR) {
|
||||
handleSwitch(b);
|
||||
continue;
|
||||
}
|
||||
@@ -287,40 +290,39 @@ void handleButton()
|
||||
if (isButtonPressed(b)) { // pressed
|
||||
|
||||
// if all macros are the same, fire action immediately on rising edge
|
||||
if (macroButton[b] && macroButton[b] == macroLongPress[b] && macroButton[b] == macroDoublePress[b]) {
|
||||
if (!buttonPressedBefore[b])
|
||||
shortPressAction(b);
|
||||
buttonPressedBefore[b] = true;
|
||||
buttonPressedTime[b] = now; // continually update (for debouncing to work in release handler)
|
||||
if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) {
|
||||
if (!buttons[b].pressedBefore) shortPressAction(b);
|
||||
buttons[b].pressedBefore = true;
|
||||
buttons[b].pressedTime = now; // continually update (for debouncing to work in release handler)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!buttonPressedBefore[b]) buttonPressedTime[b] = now;
|
||||
buttonPressedBefore[b] = true;
|
||||
if (!buttons[b].pressedBefore) buttons[b].pressedTime = now;
|
||||
buttons[b].pressedBefore = true;
|
||||
|
||||
if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press
|
||||
if (!buttonLongPressed[b]) {
|
||||
if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press
|
||||
if (!buttons[b].longPressed) {
|
||||
buttonBriDirection = !buttonBriDirection; //toggle brightness direction on long press
|
||||
longPressAction(b);
|
||||
} else if (b) { //repeatable action (~5 times per s) on button > 0
|
||||
longPressAction(b);
|
||||
buttonPressedTime[b] = now - WLED_LONG_REPEATED_ACTION; //200ms
|
||||
buttons[b].pressedTime = now - WLED_LONG_REPEATED_ACTION; //200ms
|
||||
}
|
||||
buttonLongPressed[b] = true;
|
||||
buttons[b].longPressed = true;
|
||||
}
|
||||
|
||||
} else if (buttonPressedBefore[b]) { //released
|
||||
long dur = now - buttonPressedTime[b];
|
||||
} else if (buttons[b].pressedBefore) { //released
|
||||
long dur = now - buttons[b].pressedTime;
|
||||
|
||||
// released after rising-edge short press action
|
||||
if (macroButton[b] && macroButton[b] == macroLongPress[b] && macroButton[b] == macroDoublePress[b]) {
|
||||
if (dur > WLED_DEBOUNCE_THRESHOLD) buttonPressedBefore[b] = false; // debounce, blocks button for 50 ms once it has been released
|
||||
if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) {
|
||||
if (dur > WLED_DEBOUNCE_THRESHOLD) buttons[b].pressedBefore = false; // debounce, blocks button for 50 ms once it has been released
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dur < WLED_DEBOUNCE_THRESHOLD) {buttonPressedBefore[b] = false; continue;} // too short "press", debounce
|
||||
bool doublePress = buttonWaitTime[b]; //did we have a short press before?
|
||||
buttonWaitTime[b] = 0;
|
||||
if (dur < WLED_DEBOUNCE_THRESHOLD) {buttons[b].pressedBefore = false; continue;} // too short "press", debounce
|
||||
bool doublePress = buttons[b].waitTime; //did we have a short press before?
|
||||
buttons[b].waitTime = 0;
|
||||
|
||||
if (b == 0 && dur > WLED_LONG_AP) { // long press on button 0 (when released)
|
||||
if (dur > WLED_LONG_FACTORY_RESET) { // factory reset if pressed > 10 seconds
|
||||
@@ -332,25 +334,25 @@ void handleButton()
|
||||
} else {
|
||||
WLED::instance().initAP(true);
|
||||
}
|
||||
} else if (!buttonLongPressed[b]) { //short press
|
||||
} else if (!buttons[b].longPressed) { //short press
|
||||
//NOTE: this interferes with double click handling in usermods so usermod needs to implement full button handling
|
||||
if (b != 1 && !macroDoublePress[b]) { //don't wait for double press on buttons without a default action if no double press macro set
|
||||
if (b != 1 && !buttons[b].macroDoublePress) { //don't wait for double press on buttons without a default action if no double press macro set
|
||||
shortPressAction(b);
|
||||
} else { //double press if less than 350 ms between current press and previous short press release (buttonWaitTime!=0)
|
||||
if (doublePress) {
|
||||
doublePressAction(b);
|
||||
} else {
|
||||
buttonWaitTime[b] = now;
|
||||
buttons[b].waitTime = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
buttonPressedBefore[b] = false;
|
||||
buttonLongPressed[b] = false;
|
||||
buttons[b].pressedBefore = false;
|
||||
buttons[b].longPressed = false;
|
||||
}
|
||||
|
||||
//if 350ms elapsed since last short press release it is a short press
|
||||
if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS && !buttonPressedBefore[b]) {
|
||||
buttonWaitTime[b] = 0;
|
||||
if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS && !buttons[b].pressedBefore) {
|
||||
buttons[b].waitTime = 0;
|
||||
shortPressAction(b);
|
||||
}
|
||||
}
|
||||
|
||||
130
wled00/cfg.cpp
130
wled00/cfg.cpp
@@ -345,97 +345,91 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
JsonArray hw_btn_ins = btn_obj["ins"];
|
||||
if (!hw_btn_ins.isNull()) {
|
||||
// deallocate existing button pins
|
||||
for (unsigned b = 0; b < WLED_MAX_BUTTONS; b++) PinManager::deallocatePin(btnPin[b], PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button
|
||||
for (const auto &button : buttons) PinManager::deallocatePin(button.pin, PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button
|
||||
buttons.clear(); // clear existing buttons
|
||||
unsigned s = 0;
|
||||
for (JsonObject btn : hw_btn_ins) {
|
||||
CJSON(buttonType[s], btn["type"]);
|
||||
int8_t pin = btn["pin"][0] | -1;
|
||||
uint8_t type = btn["type"] | BTN_TYPE_NONE;
|
||||
int8_t pin = btn["pin"][0] | -1;
|
||||
if (pin > -1 && PinManager::allocatePin(pin, false, PinOwner::Button)) {
|
||||
btnPin[s] = pin;
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
// ESP32 only: check that analog button pin is a valid ADC gpio
|
||||
if ((buttonType[s] == BTN_TYPE_ANALOG) || (buttonType[s] == BTN_TYPE_ANALOG_INVERTED)) {
|
||||
if (digitalPinToAnalogChannel(btnPin[s]) < 0) {
|
||||
if ((type == BTN_TYPE_ANALOG) || (type == BTN_TYPE_ANALOG_INVERTED)) {
|
||||
if (digitalPinToAnalogChannel(pin) < 0) {
|
||||
// not an ADC analog pin
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[s], s);
|
||||
btnPin[s] = -1;
|
||||
PinManager::deallocatePin(pin,PinOwner::Button);
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), pin, s);
|
||||
PinManager::deallocatePin(pin, PinOwner::Button);
|
||||
pin = -1;
|
||||
continue;
|
||||
} else {
|
||||
analogReadResolution(12); // see #4040
|
||||
}
|
||||
}
|
||||
else if ((buttonType[s] == BTN_TYPE_TOUCH || buttonType[s] == BTN_TYPE_TOUCH_SWITCH))
|
||||
{
|
||||
if (digitalPinToTouchChannel(btnPin[s]) < 0) {
|
||||
} else if ((type == BTN_TYPE_TOUCH || type == BTN_TYPE_TOUCH_SWITCH)) {
|
||||
if (digitalPinToTouchChannel(pin) < 0) {
|
||||
// not a touch pin
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\n"), btnPin[s], s);
|
||||
btnPin[s] = -1;
|
||||
PinManager::deallocatePin(pin,PinOwner::Button);
|
||||
}
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\n"), pin, s);
|
||||
PinManager::deallocatePin(pin, PinOwner::Button);
|
||||
pin = -1;
|
||||
continue;
|
||||
}
|
||||
//if touch pin, enable the touch interrupt on ESP32 S2 & S3
|
||||
#ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state but need to attach an interrupt to do so
|
||||
else
|
||||
{
|
||||
touchAttachInterrupt(btnPin[s], touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
|
||||
}
|
||||
else touchAttachInterrupt(pin, touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
|
||||
#endif
|
||||
}
|
||||
else
|
||||
#endif
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
// regular buttons and switches
|
||||
if (disablePullUp) {
|
||||
pinMode(btnPin[s], INPUT);
|
||||
pinMode(pin, INPUT);
|
||||
} else {
|
||||
#ifdef ESP32
|
||||
pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
|
||||
pinMode(pin, type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
|
||||
#else
|
||||
pinMode(btnPin[s], INPUT_PULLUP);
|
||||
pinMode(pin, INPUT_PULLUP);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
} else {
|
||||
btnPin[s] = -1;
|
||||
JsonArray hw_btn_ins_0_macros = btn["macros"];
|
||||
uint8_t press = hw_btn_ins_0_macros[0] | 0;
|
||||
uint8_t longPress = hw_btn_ins_0_macros[1] | 0;
|
||||
uint8_t doublePress = hw_btn_ins_0_macros[2] | 0;
|
||||
buttons.emplace_back(pin, type, press, longPress, doublePress); // add button to vector
|
||||
}
|
||||
JsonArray hw_btn_ins_0_macros = btn["macros"];
|
||||
CJSON(macroButton[s], hw_btn_ins_0_macros[0]);
|
||||
CJSON(macroLongPress[s],hw_btn_ins_0_macros[1]);
|
||||
CJSON(macroDoublePress[s], hw_btn_ins_0_macros[2]);
|
||||
if (++s >= WLED_MAX_BUTTONS) break; // max buttons reached
|
||||
}
|
||||
// clear remaining buttons
|
||||
for (; s<WLED_MAX_BUTTONS; s++) {
|
||||
btnPin[s] = -1;
|
||||
buttonType[s] = BTN_TYPE_NONE;
|
||||
macroButton[s] = 0;
|
||||
macroLongPress[s] = 0;
|
||||
macroDoublePress[s] = 0;
|
||||
}
|
||||
} else if (fromFS) {
|
||||
// new install/missing configuration (button 0 has defaults)
|
||||
// relies upon only being called once with fromFS == true, which is currently true.
|
||||
for (size_t s = 0; s < WLED_MAX_BUTTONS; s++) {
|
||||
if (buttonType[s] == BTN_TYPE_NONE || btnPin[s] < 0 || !PinManager::allocatePin(btnPin[s], false, PinOwner::Button)) {
|
||||
btnPin[s] = -1;
|
||||
buttonType[s] = BTN_TYPE_NONE;
|
||||
constexpr uint8_t defTypes[] = {BTNTYPE};
|
||||
constexpr int8_t defPins[] = {BTNPIN};
|
||||
constexpr unsigned numTypes = (sizeof(defTypes) / sizeof(defTypes[0]));
|
||||
constexpr unsigned numPins = (sizeof(defPins) / sizeof(defPins[0]));
|
||||
// check if the number of pins and types are valid; count of pins must be greater than or equal to types
|
||||
static_assert(numTypes <= numPins, "The default button pins defined in BTNPIN do not match the button types defined in BTNTYPE");
|
||||
|
||||
uint8_t type = BTN_TYPE_NONE;
|
||||
buttons.clear(); // clear existing buttons (just in case)
|
||||
for (size_t s = 0; s < WLED_MAX_BUTTONS && s < numPins; s++) {
|
||||
type = defTypes[s < numTypes ? s : numTypes - 1]; // use last known type to set current type if types less than pins
|
||||
if (type == BTN_TYPE_NONE || defPins[s] < 0 || !PinManager::allocatePin(defPins[s], false, PinOwner::Button)) {
|
||||
if (buttons.size() == 0) buttons.emplace_back(-1, BTN_TYPE_NONE); // add disabled button to vector (so we have at least one button defined)
|
||||
continue; // pin not available or invalid, skip configuring this GPIO
|
||||
}
|
||||
if (btnPin[s] >= 0) {
|
||||
if (disablePullUp) {
|
||||
pinMode(btnPin[s], INPUT);
|
||||
} else {
|
||||
#ifdef ESP32
|
||||
pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
|
||||
#else
|
||||
pinMode(btnPin[s], INPUT_PULLUP);
|
||||
#endif
|
||||
}
|
||||
if (disablePullUp) {
|
||||
pinMode(defPins[s], INPUT);
|
||||
} else {
|
||||
#ifdef ESP32
|
||||
pinMode(defPins[s], type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
|
||||
#else
|
||||
pinMode(defPins[s], INPUT_PULLUP);
|
||||
#endif
|
||||
}
|
||||
macroButton[s] = 0;
|
||||
macroLongPress[s] = 0;
|
||||
macroDoublePress[s] = 0;
|
||||
buttons.emplace_back(defPins[s], type); // add button to vector
|
||||
}
|
||||
}
|
||||
|
||||
CJSON(buttonPublishMqtt,btn_obj["mqtt"]);
|
||||
CJSON(buttonPublishMqtt, btn_obj["mqtt"]);
|
||||
|
||||
#ifndef WLED_DISABLE_INFRARED
|
||||
int hw_ir_pin = hw["ir"]["pin"] | -2; // 4
|
||||
@@ -777,6 +771,10 @@ bool verifyConfig() {
|
||||
return validateJsonFile(s_cfg_json);
|
||||
}
|
||||
|
||||
bool configBackupExists() {
|
||||
return checkBackupExists(s_cfg_json);
|
||||
}
|
||||
|
||||
// rename config file and reboot
|
||||
// if the cfg file doesn't exist, such as after a reset, do nothing
|
||||
void resetConfig() {
|
||||
@@ -1012,15 +1010,15 @@ void serializeConfig(JsonObject root) {
|
||||
JsonArray hw_btn_ins = hw_btn.createNestedArray("ins");
|
||||
|
||||
// configuration for all buttons
|
||||
for (int i = 0; i < WLED_MAX_BUTTONS; i++) {
|
||||
for (const auto &button : buttons) {
|
||||
JsonObject hw_btn_ins_0 = hw_btn_ins.createNestedObject();
|
||||
hw_btn_ins_0["type"] = buttonType[i];
|
||||
hw_btn_ins_0["type"] = button.type;
|
||||
JsonArray hw_btn_ins_0_pin = hw_btn_ins_0.createNestedArray("pin");
|
||||
hw_btn_ins_0_pin.add(btnPin[i]);
|
||||
hw_btn_ins_0_pin.add(button.pin);
|
||||
JsonArray hw_btn_ins_0_macros = hw_btn_ins_0.createNestedArray("macros");
|
||||
hw_btn_ins_0_macros.add(macroButton[i]);
|
||||
hw_btn_ins_0_macros.add(macroLongPress[i]);
|
||||
hw_btn_ins_0_macros.add(macroDoublePress[i]);
|
||||
hw_btn_ins_0_macros.add(button.macroButton);
|
||||
hw_btn_ins_0_macros.add(button.macroLongPress);
|
||||
hw_btn_ins_0_macros.add(button.macroDoublePress);
|
||||
}
|
||||
|
||||
hw_btn[F("tt")] = touchThreshold;
|
||||
|
||||
@@ -102,9 +102,9 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
|
||||
#ifndef WLED_MAX_BUTTONS
|
||||
#ifdef ESP8266
|
||||
#define WLED_MAX_BUTTONS 2
|
||||
#define WLED_MAX_BUTTONS 10
|
||||
#else
|
||||
#define WLED_MAX_BUTTONS 4
|
||||
#define WLED_MAX_BUTTONS 32
|
||||
#endif
|
||||
#else
|
||||
#if WLED_MAX_BUTTONS < 2
|
||||
|
||||
@@ -116,3 +116,62 @@ function uploadFile(fileObj, name) {
|
||||
fileObj.value = '';
|
||||
return false;
|
||||
}
|
||||
// connect to WebSocket, use parent WS or open new
|
||||
function connectWs(onOpen) {
|
||||
try {
|
||||
if (top.window.ws && top.window.ws.readyState === WebSocket.OPEN) {
|
||||
if (onOpen) onOpen();
|
||||
return top.window.ws;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
getLoc(); // ensure globals (loc, locip, locproto) are up to date
|
||||
let url = loc ? getURL('/ws').replace("http","ws") : "ws://"+window.location.hostname+"/ws";
|
||||
let ws = new WebSocket(url);
|
||||
ws.binaryType = "arraybuffer";
|
||||
if (onOpen) { ws.onopen = onOpen; }
|
||||
try { top.window.ws = ws; } catch (e) {} // store in parent for reuse
|
||||
return ws;
|
||||
}
|
||||
|
||||
// send LED colors to ESP using WebSocket and DDP protocol (RGB)
|
||||
// ws: WebSocket object
|
||||
// start: start pixel index
|
||||
// len: number of pixels to send
|
||||
// colors: Uint8Array with RGB values (3*len bytes)
|
||||
function sendDDP(ws, start, len, colors) {
|
||||
if (!colors || colors.length < len * 3) return false; // not enough color data
|
||||
let maxDDPpx = 472; // must fit into one WebSocket frame of 1428 bytes, DDP header is 10+1 bytes -> 472 RGB pixels
|
||||
//let maxDDPpx = 172; // ESP8266: must fit into one WebSocket frame of 528 bytes -> 172 RGB pixels TODO: add support for ESP8266?
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
|
||||
// send in chunks of maxDDPpx
|
||||
for (let i = 0; i < len; i += maxDDPpx) {
|
||||
let cnt = Math.min(maxDDPpx, len - i);
|
||||
let off = (start + i) * 3; // DDP pixel offset in bytes
|
||||
let dLen = cnt * 3;
|
||||
let cOff = i * 3; // offset in color buffer
|
||||
let pkt = new Uint8Array(11 + dLen); // DDP header is 10 bytes, plus 1 byte for WLED websocket protocol indicator
|
||||
pkt[0] = 0x02; // DDP protocol indicator for WLED websocket. Note: below DDP protocol bytes are offset by 1
|
||||
pkt[1] = 0x40; // flags: 0x40 = no push, 0x41 = push (i.e. render), note: this is DDP protocol byte 0
|
||||
pkt[2] = 0x00; // reserved
|
||||
pkt[3] = 0x01; // 1 = RGB (currently only supported mode)
|
||||
pkt[4] = 0x01; // destination id (not used but 0x01 is default output)
|
||||
pkt[5] = (off >> 24) & 255; // DDP protocol 4-7 is offset
|
||||
pkt[6] = (off >> 16) & 255;
|
||||
pkt[7] = (off >> 8) & 255;
|
||||
pkt[8] = off & 255;
|
||||
pkt[9] = (dLen >> 8) & 255; // DDP protocol 8-9 is data length
|
||||
pkt[10] = dLen & 255;
|
||||
pkt.set(colors.subarray(cOff, cOff + dLen), 11);
|
||||
if(i + cnt >= len) {
|
||||
pkt[1] = 0x41; //if this is last packet, set the "push" flag to render the frame
|
||||
}
|
||||
try {
|
||||
ws.send(pkt.buffer);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
1065
wled00/data/edit.htm
1065
wled00/data/edit.htm
File diff suppressed because it is too large
Load Diff
@@ -794,7 +794,7 @@ input[type=range]::-moz-range-thumb {
|
||||
/* buttons */
|
||||
.btn {
|
||||
padding: 8px;
|
||||
/*margin: 10px 4px;*/
|
||||
margin: 10px 4px;
|
||||
width: 230px;
|
||||
font-size: 19px;
|
||||
color: var(--c-d);
|
||||
|
||||
@@ -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;
|
||||
@@ -694,6 +693,8 @@ function parseInfo(i) {
|
||||
// gId("filterVol").classList.add("hide"); hideModes(" ♪"); // hide volume reactive effects
|
||||
// gId("filterFreq").classList.add("hide"); hideModes(" ♫"); // hide frequency reactive effects
|
||||
// }
|
||||
// Check for version upgrades on page load
|
||||
checkVersionUpgrade(i);
|
||||
}
|
||||
|
||||
//https://stackoverflow.com/questions/2592092/executing-script-elements-inserted-with-innerhtml
|
||||
@@ -3151,7 +3152,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%';
|
||||
@@ -3306,6 +3306,195 @@ function simplifyUI() {
|
||||
gId("btns").style.display = "none";
|
||||
}
|
||||
|
||||
// Version reporting feature
|
||||
var versionCheckDone = false;
|
||||
|
||||
function checkVersionUpgrade(info) {
|
||||
// Only check once per page load
|
||||
if (versionCheckDone) return;
|
||||
versionCheckDone = true;
|
||||
|
||||
// Suppress feature if in AP mode (no internet connection available)
|
||||
if (info.wifi && info.wifi.ap) return;
|
||||
|
||||
// Fetch version-info.json using existing /edit endpoint
|
||||
fetch(getURL('/edit?func=edit&path=/version-info.json'), {
|
||||
method: 'get'
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status === 404) {
|
||||
// File doesn't exist - first install, show install prompt
|
||||
showVersionUpgradePrompt(info, null, info.ver);
|
||||
return null;
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch version-info.json');
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(versionInfo => {
|
||||
if (!versionInfo) return; // 404 case already handled
|
||||
|
||||
// Check if user opted out
|
||||
if (versionInfo.neverAsk) return;
|
||||
|
||||
// Check if version has changed
|
||||
const currentVersion = info.ver;
|
||||
const storedVersion = versionInfo.version || '';
|
||||
|
||||
if (storedVersion && storedVersion !== currentVersion) {
|
||||
// Version has changed, show upgrade prompt
|
||||
showVersionUpgradePrompt(info, storedVersion, currentVersion);
|
||||
} else if (!storedVersion) {
|
||||
// Empty version in file, show install prompt
|
||||
showVersionUpgradePrompt(info, null, currentVersion);
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
console.log('Failed to load version-info.json', e);
|
||||
// On error, save current version for next time
|
||||
if (info && info.ver) {
|
||||
updateVersionInfo(info.ver, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showVersionUpgradePrompt(info, oldVersion, newVersion) {
|
||||
// Determine if this is an install or upgrade
|
||||
const isInstall = !oldVersion;
|
||||
|
||||
// Create overlay and dialog
|
||||
const overlay = d.createElement('div');
|
||||
overlay.id = 'versionUpgradeOverlay';
|
||||
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;';
|
||||
|
||||
const dialog = d.createElement('div');
|
||||
dialog.style.cssText = 'background:var(--c-1);border-radius:10px;padding:25px;max-width:500px;margin:20px;box-shadow:0 4px 6px rgba(0,0,0,0.3);';
|
||||
|
||||
// Build contextual message based on install vs upgrade
|
||||
const title = isInstall
|
||||
? '🎉 Thank you for installing WLED!'
|
||||
: '🎉 WLED Upgrade Detected!';
|
||||
|
||||
const description = isInstall
|
||||
? `You are now running WLED <strong style="text-wrap: nowrap">${newVersion}</strong>.`
|
||||
: `Your WLED has been upgraded from <strong style="text-wrap: nowrap">${oldVersion}</strong> to <strong style="text-wrap: nowrap">${newVersion}</strong>.`;
|
||||
|
||||
const question = 'Help make WLED better with a one-time hardware report? It includes only device details like chip type, LED count, etc. — never personal data or your activities.'
|
||||
|
||||
dialog.innerHTML = `
|
||||
<h2 style="margin-top:0;color:var(--c-f);">${title}</h2>
|
||||
<p style="color:var(--c-f);">${description}</p>
|
||||
<p style="color:var(--c-f);">${question}</p>
|
||||
<p style="color:var(--c-f);font-size:0.9em;">
|
||||
<a href="https://kno.wled.ge/about/privacy-policy/" target="_blank" style="color:var(--c-6);">Learn more about what data is collected and why</a>
|
||||
</p>
|
||||
<div style="margin-top:20px;">
|
||||
<button id="versionReportYes" class="btn">Yes</button>
|
||||
<button id="versionReportNo" class="btn">Not Now</button>
|
||||
<button id="versionReportNever" class="btn">Never Ask</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
overlay.appendChild(dialog);
|
||||
d.body.appendChild(overlay);
|
||||
|
||||
// Add event listeners
|
||||
gId('versionReportYes').addEventListener('click', () => {
|
||||
reportUpgradeEvent(info, oldVersion);
|
||||
d.body.removeChild(overlay);
|
||||
});
|
||||
|
||||
gId('versionReportNo').addEventListener('click', () => {
|
||||
// Don't update version, will ask again on next load
|
||||
d.body.removeChild(overlay);
|
||||
});
|
||||
|
||||
gId('versionReportNever').addEventListener('click', () => {
|
||||
updateVersionInfo(newVersion, true);
|
||||
d.body.removeChild(overlay);
|
||||
showToast('You will not be asked again.');
|
||||
});
|
||||
}
|
||||
|
||||
function reportUpgradeEvent(info, oldVersion) {
|
||||
showToast('Reporting upgrade...');
|
||||
|
||||
// Fetch fresh data from /json/info endpoint as requested
|
||||
fetch(getURL('/json/info'), {
|
||||
method: 'get'
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(infoData => {
|
||||
// Map to UpgradeEventRequest structure per OpenAPI spec
|
||||
// Required fields: deviceId, version, previousVersion, releaseName, chip, ledCount, isMatrix, bootloaderSHA256
|
||||
const upgradeData = {
|
||||
deviceId: infoData.deviceId, // Use anonymous unique device ID
|
||||
version: infoData.ver || '', // Current version string
|
||||
previousVersion: oldVersion || '', // Previous version from version-info.json
|
||||
releaseName: infoData.release || '', // Release name (e.g., "WLED 0.15.0")
|
||||
chip: infoData.arch || '', // Chip architecture (esp32, esp8266, etc)
|
||||
ledCount: infoData.leds ? infoData.leds.count : 0, // Number of LEDs
|
||||
isMatrix: !!(infoData.leds && infoData.leds.matrix), // Whether it's a 2D matrix setup
|
||||
bootloaderSHA256: infoData.bootloaderSHA256 || '', // Bootloader SHA256 hash
|
||||
brand: infoData.brand, // Device brand (always present)
|
||||
product: infoData.product, // Product name (always present)
|
||||
flashSize: infoData.flash // Flash size (always present)
|
||||
};
|
||||
|
||||
// Add optional fields if available
|
||||
if (infoData.psram !== undefined) upgradeData.psramSize = Math.round(infoData.psram / (1024 * 1024)); // convert bytes to MB
|
||||
// Note: partitionSizes not currently available in /json/info endpoint
|
||||
|
||||
// Make AJAX call to postUpgradeEvent API
|
||||
return fetch('https://usage.wled.me/api/usage/upgrade', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(upgradeData)
|
||||
});
|
||||
})
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
showToast('Thank you for reporting!');
|
||||
updateVersionInfo(info.ver, false);
|
||||
} else {
|
||||
showToast('Report failed. Please try again later.', true);
|
||||
// Do NOT update version info on failure - user will be prompted again
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
console.log('Failed to report upgrade', e);
|
||||
showToast('Report failed. Please try again later.', true);
|
||||
// Do NOT update version info on error - user will be prompted again
|
||||
});
|
||||
}
|
||||
|
||||
function updateVersionInfo(version, neverAsk) {
|
||||
const versionInfo = {
|
||||
version: version,
|
||||
neverAsk: neverAsk
|
||||
};
|
||||
|
||||
// Create a Blob with JSON content and use /upload endpoint
|
||||
const blob = new Blob([JSON.stringify(versionInfo)], {type: 'application/json'});
|
||||
const formData = new FormData();
|
||||
formData.append('data', blob, 'version-info.json');
|
||||
|
||||
fetch(getURL('/upload'), {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(res => res.text())
|
||||
.then(data => {
|
||||
console.log('Version info updated', data);
|
||||
})
|
||||
.catch(e => {
|
||||
console.log('Failed to update version-info.json', e);
|
||||
});
|
||||
}
|
||||
|
||||
size();
|
||||
_C.style.setProperty('--n', N);
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
<script src="common.js"></script>
|
||||
<script>
|
||||
var d = document;
|
||||
var ws;
|
||||
var tmout = null;
|
||||
var c;
|
||||
@@ -62,32 +62,14 @@
|
||||
if (window.location.href.indexOf("?ws") == -1) {update(); return;}
|
||||
|
||||
// Initialize WebSocket connection
|
||||
try {
|
||||
ws = top.window.ws;
|
||||
} catch (e) {}
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
//console.info("Peek uses top WS");
|
||||
ws.send("{'lv':true}");
|
||||
} else {
|
||||
//console.info("Peek WS opening");
|
||||
let l = window.location;
|
||||
let pathn = l.pathname;
|
||||
let paths = pathn.slice(1,pathn.endsWith('/')?-1:undefined).split("/");
|
||||
let url = l.origin.replace("http","ws");
|
||||
if (paths.length > 1) {
|
||||
url += "/" + paths[0];
|
||||
}
|
||||
ws = new WebSocket(url+"/ws");
|
||||
ws.onopen = function () {
|
||||
//console.info("Peek WS open");
|
||||
ws.send("{'lv':true}");
|
||||
}
|
||||
}
|
||||
ws.binaryType = "arraybuffer";
|
||||
ws = connectWs(function () {
|
||||
//console.info("Peek WS open");
|
||||
ws.send('{"lv":true}');
|
||||
});
|
||||
ws.addEventListener('message', (e) => {
|
||||
try {
|
||||
if (toString.call(e.data) === '[object ArrayBuffer]') {
|
||||
let leds = new Uint8Array(event.data);
|
||||
let leds = new Uint8Array(e.data);
|
||||
if (leds[0] != 76) return; //'L'
|
||||
// leds[1] = 1: 1D; leds[1] = 2: 1D/2D (leds[2]=w, leds[3]=h)
|
||||
draw(leds[1]==2 ? 4 : 2, 3, leds, (a,i) => `rgb(${a[i]},${a[i+1]},${a[i+2]})`);
|
||||
@@ -102,4 +84,4 @@
|
||||
<body onload="S()">
|
||||
<canvas id="canv"></canvas>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -10,6 +10,7 @@
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<script src="common.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canv"></canvas>
|
||||
@@ -26,30 +27,13 @@
|
||||
var ctx = c.getContext('2d');
|
||||
if (ctx) { // Access the rendering context
|
||||
// use parent WS or open new
|
||||
var ws;
|
||||
try {
|
||||
ws = top.window.ws;
|
||||
} catch (e) {}
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send("{'lv':true}");
|
||||
} else {
|
||||
let l = window.location;
|
||||
let pathn = l.pathname;
|
||||
let paths = pathn.slice(1,pathn.endsWith('/')?-1:undefined).split("/");
|
||||
let url = l.origin.replace("http","ws");
|
||||
if (paths.length > 1) {
|
||||
url += "/" + paths[0];
|
||||
}
|
||||
ws = new WebSocket(url+"/ws");
|
||||
ws.onopen = ()=>{
|
||||
ws.send("{'lv':true}");
|
||||
}
|
||||
}
|
||||
ws.binaryType = "arraybuffer";
|
||||
var ws = connectWs(()=>{
|
||||
ws.send('{"lv":true}');
|
||||
});
|
||||
ws.addEventListener('message',(e)=>{
|
||||
try {
|
||||
if (toString.call(e.data) === '[object ArrayBuffer]') {
|
||||
let leds = new Uint8Array(event.data);
|
||||
let leds = new Uint8Array(e.data);
|
||||
if (leds[0] != 76 || leds[1] != 2 || !ctx) return; //'L', set in ws.cpp
|
||||
let mW = leds[2]; // matrix width
|
||||
let mH = leds[3]; // matrix height
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>LED Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<script>
|
||||
var maxB=1,maxD=1,maxA=1,maxV=0,maxM=4000,maxPB=2048,maxL=1664,maxCO=5; //maximum bytes for LED allocation: 4kB for 8266, 32kB for 32
|
||||
var maxB=1,maxD=1,maxA=1,maxV=0,maxM=4000,maxPB=2048,maxL=1664,maxCO=5,maxBT=4; //maximum bytes for LED allocation: 4kB for 8266, 32kB for 32
|
||||
var customStarts=false,startsDirty=[];
|
||||
function off(n) { gN(n).value = -1;}
|
||||
// these functions correspond to C macros found in const.h
|
||||
@@ -43,7 +43,7 @@
|
||||
}); // If we set async false, file is loaded and executed, then next statement is processed
|
||||
if (loc) d.Sf.action = getURL('/settings/leds');
|
||||
}
|
||||
function bLimits(b,v,p,m,l,o=5,d=2,a=6) {
|
||||
function bLimits(b,v,p,m,l,o=5,d=2,a=6,n=4) {
|
||||
maxB = b; // maxB - max physical (analog + digital) buses: 32 - ESP32, 14 - S3/S2, 6 - C3, 4 - 8266
|
||||
maxV = v; // maxV - min virtual buses: 6 - ESP32/S3, 4 - S2/C3, 3 - ESP8266 (only used to distinguish S2/S3)
|
||||
maxPB = p; // maxPB - max LEDs per bus
|
||||
@@ -52,6 +52,7 @@
|
||||
maxCO = o; // maxCO - max Color Order mappings
|
||||
maxD = d; // maxD - max digital channels (can be changed if using ESP32 parallel I2S): 16 - ESP32, 12 - S3/S2, 2 - C3, 3 - 8266
|
||||
maxA = a; // maxA - max analog channels: 16 - ESP32, 8 - S3/S2, 6 - C3, 5 - 8266
|
||||
maxBT = n; // maxBT - max buttons
|
||||
}
|
||||
function is8266() { return maxA == 5 && maxD == 3; } // NOTE: see const.h
|
||||
function is32() { return maxA == 16 && maxD == 16; } // NOTE: see const.h
|
||||
@@ -600,9 +601,9 @@ Swap: <select id="xw${s}" name="XW${s}">
|
||||
}
|
||||
|
||||
function addBtn(i,p,t) {
|
||||
var c = gId("btns").innerHTML;
|
||||
var b = gId("btns");
|
||||
var s = chrID(i);
|
||||
c += `Button ${i} GPIO: <input type="number" name="BT${s}" onchange="UI()" class="xs" value="${p}">`;
|
||||
var c = `<div id="btn${i}">#${i} GPIO: <input type="number" name="BT${s}" onchange="UI()" min="-1" max="${d.max_gpio}" class="xs" value="${p}">`;
|
||||
c += ` <select name="BE${s}">`
|
||||
c += `<option value="0" ${t==0?"selected":""}>Disabled</option>`;
|
||||
c += `<option value="2" ${t==2?"selected":""}>Pushbutton</option>`;
|
||||
@@ -614,8 +615,24 @@ Swap: <select id="xw${s}" name="XW${s}">
|
||||
c += `<option value="8" ${t==8?"selected":""}>Analog inverted</option>`;
|
||||
c += `<option value="9" ${t==9?"selected":""}>Touch (switch)</option>`;
|
||||
c += `</select>`;
|
||||
c += `<span style="cursor: pointer;" onclick="off('BT${s}')"> ✕</span><br>`;
|
||||
gId("btns").innerHTML = c;
|
||||
c += `<span style="cursor: pointer;" onclick="off('BT${s}')"> ✕</span><br></div>`;
|
||||
b.insertAdjacentHTML("beforeend", c);
|
||||
btnBtn();
|
||||
pinDropdowns();
|
||||
UI();
|
||||
}
|
||||
function remBtn() {
|
||||
var b = gId("btns");
|
||||
if (b.children.length <= 1) return;
|
||||
b.lastElementChild.remove();
|
||||
btnBtn();
|
||||
pinDropdowns();
|
||||
UI();
|
||||
}
|
||||
function btnBtn() {
|
||||
var b = gId("btns");
|
||||
gId("btn_rem").style.display = (b.children.length > 1) ? "inline" : "none";
|
||||
gId("btn_add").style.display = (b.children.length < maxBT) ? "inline" : "none";
|
||||
}
|
||||
function tglSi(cs) {
|
||||
customStarts = cs;
|
||||
@@ -867,10 +884,16 @@ Swap: <select id="xw${s}" name="XW${s}">
|
||||
<div id="com_entries"></div>
|
||||
<hr class="sml">
|
||||
<button type="button" id="com_add" onclick="addCOM()">+</button>
|
||||
<button type="button" id="com_rem" onclick="remCOM()">-</button><br>
|
||||
<button type="button" id="com_rem" onclick="remCOM()">-</button>
|
||||
</div>
|
||||
<hr class="sml">
|
||||
<div id="btns"></div>
|
||||
<div id="btn_wrap">
|
||||
Buttons:
|
||||
<div id="btns"></div>
|
||||
<hr class="sml">
|
||||
<button type="button" id="btn_add" onclick="addBtn(gId('btns').children.length,-1,0)">+</button>
|
||||
<button type="button" id="btn_rem" onclick="remBtn()">-</button>
|
||||
</div>
|
||||
Disable internal pull-up/down: <input type="checkbox" name="IP"><br>
|
||||
Touch threshold: <input type="number" class="s" min="0" max="100" name="TT" required><br>
|
||||
<hr class="sml">
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -30,11 +30,19 @@ void handleDDPPacket(e131_packet_t* p) {
|
||||
|
||||
uint32_t start = htonl(p->channelOffset) / ddpChannelsPerLed;
|
||||
start += DMXAddress / ddpChannelsPerLed;
|
||||
unsigned stop = start + htons(p->dataLen) / ddpChannelsPerLed;
|
||||
uint16_t dataLen = htons(p->dataLen);
|
||||
unsigned stop = start + dataLen / ddpChannelsPerLed;
|
||||
uint8_t* data = p->data;
|
||||
unsigned c = 0;
|
||||
if (p->flags & DDP_TIMECODE_FLAG) c = 4; //packet has timecode flag, we do not support it, but data starts 4 bytes later
|
||||
|
||||
unsigned numLeds = stop - start; // stop >= start is guaranteed
|
||||
unsigned maxDataIndex = c + numLeds * ddpChannelsPerLed; // validate bounds before accessing data array
|
||||
if (maxDataIndex > dataLen) {
|
||||
DEBUG_PRINTLN(F("DDP packet data bounds exceeded, rejecting."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (realtimeMode != REALTIME_MODE_DDP) ddpSeenPush = false; // just starting, no push yet
|
||||
realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP);
|
||||
|
||||
@@ -414,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);
|
||||
|
||||
@@ -27,6 +27,7 @@ void IRAM_ATTR touchButtonISR();
|
||||
bool backupConfig();
|
||||
bool restoreConfig();
|
||||
bool verifyConfig();
|
||||
bool configBackupExists();
|
||||
void resetConfig();
|
||||
bool deserializeConfig(JsonObject doc, bool fromFS = false);
|
||||
bool deserializeConfigFromFS();
|
||||
@@ -103,6 +104,7 @@ inline bool readObjectFromFile(const String &file, const char* key, JsonDocument
|
||||
bool copyFile(const char* src_path, const char* dst_path);
|
||||
bool backupFile(const char* filename);
|
||||
bool restoreFile(const char* filename);
|
||||
bool checkBackupExists(const char* filename);
|
||||
bool validateJsonFile(const char* filename);
|
||||
void dumpFilesToSerial();
|
||||
|
||||
@@ -399,6 +401,8 @@ uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxL
|
||||
int16_t extractModeDefaults(uint8_t mode, const char *segVar);
|
||||
void checkSettingsPIN(const char *pin);
|
||||
uint16_t crc16(const unsigned char* data_p, size_t length);
|
||||
String computeSHA1(const String& input);
|
||||
String getDeviceId();
|
||||
uint16_t beatsin88_t(accum88 beats_per_minute_88, uint16_t lowest = 0, uint16_t highest = 65535, uint32_t timebase = 0, uint16_t phase_offset = 0);
|
||||
uint16_t beatsin16_t(accum88 beats_per_minute, uint16_t lowest = 0, uint16_t highest = 65535, uint32_t timebase = 0, uint16_t phase_offset = 0);
|
||||
uint8_t beatsin8_t(accum88 beats_per_minute, uint8_t lowest = 0, uint8_t highest = 255, uint32_t timebase = 0, uint8_t phase_offset = 0);
|
||||
@@ -539,7 +543,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);
|
||||
|
||||
@@ -557,6 +557,12 @@ bool restoreFile(const char* filename) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool checkBackupExists(const char* filename) {
|
||||
char backupname[32];
|
||||
snprintf_P(backupname, sizeof(backupname), s_backup_fmt, filename + 1); // skip leading '/' in filename
|
||||
return WLED_FS.exists(backupname);
|
||||
}
|
||||
|
||||
bool validateJsonFile(const char* filename) {
|
||||
if (!WLED_FS.exists(filename)) return false;
|
||||
File file = WLED_FS.open(filename, "r");
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
* Functions to render images from filesystem to segments, used by the "Image" effect
|
||||
*/
|
||||
|
||||
File file;
|
||||
char lastFilename[34] = "/";
|
||||
GifDecoder<320,320,12,true> decoder;
|
||||
bool gifDecodeFailed = false;
|
||||
unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0;
|
||||
static File file;
|
||||
static char lastFilename[WLED_MAX_SEGNAME_LEN+2] = "/"; // enough space for "/" + seg.name + '\0'
|
||||
static GifDecoder<320,320,12,true> decoder; // this creates the basic object; parameter lzwMaxBits is not used; decoder.alloc() always allocated "everything else" = 24Kb
|
||||
static bool gifDecodeFailed = false;
|
||||
static unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0;
|
||||
|
||||
bool fileSeekCallback(unsigned long position) {
|
||||
return file.seek(position);
|
||||
@@ -35,29 +35,62 @@ int fileSizeCallback(void) {
|
||||
return file.size();
|
||||
}
|
||||
|
||||
bool openGif(const char *filename) {
|
||||
bool openGif(const char *filename) { // side-effect: updates "file"
|
||||
file = WLED_FS.open(filename, "r");
|
||||
DEBUG_PRINTF_P(PSTR("opening GIF file %s\n"), filename);
|
||||
|
||||
if (!file) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
Segment* activeSeg;
|
||||
uint16_t gifWidth, gifHeight;
|
||||
static Segment* activeSeg;
|
||||
static uint16_t gifWidth, gifHeight;
|
||||
static int lastCoordinate; // last coordinate (x+y) that was set, used to reduce redundant pixel writes
|
||||
static uint16_t perPixelX, perPixelY; // scaling factors when upscaling
|
||||
|
||||
void screenClearCallback(void) {
|
||||
activeSeg->fill(0);
|
||||
}
|
||||
|
||||
void updateScreenCallback(void) {}
|
||||
// this callback runs when the decoder has finished painting all pixels
|
||||
void updateScreenCallback(void) {
|
||||
// perfect time for adding blur
|
||||
if (activeSeg->intensity > 1) {
|
||||
uint8_t blurAmount = activeSeg->intensity;
|
||||
if ((blurAmount < 24) && (activeSeg->is2D())) activeSeg->blurRows(activeSeg->intensity); // some blur - fast
|
||||
else activeSeg->blur(blurAmount); // more blur - slower
|
||||
}
|
||||
lastCoordinate = -1; // invalidate last position
|
||||
}
|
||||
|
||||
void drawPixelCallback(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) {
|
||||
// simple nearest-neighbor scaling
|
||||
int16_t outY = y * activeSeg->height() / gifHeight;
|
||||
int16_t outX = x * activeSeg->width() / gifWidth;
|
||||
// note: GifDecoder drawing is done top right to bottom left, line by line
|
||||
|
||||
// callbacks to draw a pixel at (x,y) without scaling: used if GIF size matches (virtual)segment size (faster) works for 1D and 2D segments
|
||||
void drawPixelCallbackNoScale(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) {
|
||||
activeSeg->setPixelColor(y * gifWidth + x, red, green, blue);
|
||||
}
|
||||
|
||||
void drawPixelCallback1D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) {
|
||||
// 1D strip: load pixel-by-pixel left to right, top to bottom (0/0 = top-left in gifs)
|
||||
int totalImgPix = (int)gifWidth * gifHeight;
|
||||
int start = ((int)y * gifWidth + (int)x) * activeSeg->vLength() / totalImgPix; // simple nearest-neighbor scaling
|
||||
if (start == lastCoordinate) return; // skip setting same coordinate again
|
||||
lastCoordinate = start;
|
||||
for (int i = 0; i < perPixelX; i++) {
|
||||
activeSeg->setPixelColor(start + i, red, green, blue);
|
||||
}
|
||||
}
|
||||
|
||||
void drawPixelCallback2D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) {
|
||||
// simple nearest-neighbor scaling
|
||||
int outY = (int)y * activeSeg->vHeight() / gifHeight;
|
||||
int outX = (int)x * activeSeg->vWidth() / gifWidth;
|
||||
// Pack coordinates uniquely: outY into upper 16 bits, outX into lower 16 bits
|
||||
if (((outY << 16) | outX) == lastCoordinate) return; // skip setting same coordinate again
|
||||
lastCoordinate = (outY << 16) | outX; // since input is a "scanline" this is sufficient to identify a "unique" coordinate
|
||||
// set multiple pixels if upscaling
|
||||
for (int16_t i = 0; i < (activeSeg->width()+(gifWidth-1)) / gifWidth; i++) {
|
||||
for (int16_t j = 0; j < (activeSeg->height()+(gifHeight-1)) / gifHeight; j++) {
|
||||
for (int i = 0; i < perPixelX; i++) {
|
||||
for (int j = 0; j < perPixelY; j++) {
|
||||
activeSeg->setPixelColorXY(outX + i, outY + j, red, green, blue);
|
||||
}
|
||||
}
|
||||
@@ -79,31 +112,88 @@ byte renderImageToSegment(Segment &seg) {
|
||||
if (!seg.name) return IMAGE_ERROR_NO_NAME;
|
||||
// disable during effect transition, causes flickering, multiple allocations and depending on image, part of old FX remaining
|
||||
//if (seg.mode != seg.currentMode()) return IMAGE_ERROR_WAITING;
|
||||
if (activeSeg && activeSeg != &seg) return IMAGE_ERROR_SEG_LIMIT; // only one segment at a time
|
||||
if (activeSeg && activeSeg != &seg) { // only one segment at a time
|
||||
if (!seg.isActive()) return IMAGE_ERROR_SEG_LIMIT; // sanity check: calling segment must be active
|
||||
if (gifDecodeFailed || !activeSeg->isActive()) // decoder failed, or last segment became inactive
|
||||
endImagePlayback(activeSeg); // => allow takeover but clean up first
|
||||
else
|
||||
return IMAGE_ERROR_SEG_LIMIT;
|
||||
}
|
||||
|
||||
activeSeg = &seg;
|
||||
|
||||
if (strncmp(lastFilename +1, seg.name, 32) != 0) { // segment name changed, load new image
|
||||
strncpy(lastFilename +1, seg.name, 32);
|
||||
if (strncmp(lastFilename +1, seg.name, WLED_MAX_SEGNAME_LEN) != 0) { // segment name changed, load new image
|
||||
strcpy(lastFilename, "/"); // filename always starts with '/'
|
||||
strncpy(lastFilename +1, seg.name, WLED_MAX_SEGNAME_LEN);
|
||||
lastFilename[WLED_MAX_SEGNAME_LEN+1] ='\0'; // ensure proper string termination when segment name was truncated
|
||||
gifDecodeFailed = false;
|
||||
if (strcmp(lastFilename + strlen(lastFilename) - 4, ".gif") != 0) {
|
||||
size_t fnameLen = strlen(lastFilename);
|
||||
if ((fnameLen < 4) || strcmp(lastFilename + fnameLen - 4, ".gif") != 0) { // empty segment name, name too short, or name not ending in .gif
|
||||
gifDecodeFailed = true;
|
||||
DEBUG_PRINTF_P(PSTR("GIF decoder unsupported file: %s\n"), lastFilename);
|
||||
return IMAGE_ERROR_UNSUPPORTED_FORMAT;
|
||||
}
|
||||
if (file) file.close();
|
||||
openGif(lastFilename);
|
||||
if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; }
|
||||
if (!openGif(lastFilename)) {
|
||||
gifDecodeFailed = true;
|
||||
DEBUG_PRINTF_P(PSTR("GIF file not found: %s\n"), lastFilename);
|
||||
return IMAGE_ERROR_FILE_MISSING;
|
||||
}
|
||||
lastCoordinate = -1;
|
||||
decoder.setScreenClearCallback(screenClearCallback);
|
||||
decoder.setUpdateScreenCallback(updateScreenCallback);
|
||||
decoder.setDrawPixelCallback(drawPixelCallback);
|
||||
decoder.setDrawPixelCallback(drawPixelCallbackNoScale); // default: use "fast path" callback without scaling
|
||||
decoder.setFileSeekCallback(fileSeekCallback);
|
||||
decoder.setFilePositionCallback(filePositionCallback);
|
||||
decoder.setFileReadCallback(fileReadCallback);
|
||||
decoder.setFileReadBlockCallback(fileReadBlockCallback);
|
||||
decoder.setFileSizeCallback(fileSizeCallback);
|
||||
decoder.alloc();
|
||||
#if __cpp_exceptions // use exception handler if we can (some targets don't support exceptions)
|
||||
try {
|
||||
#endif
|
||||
decoder.alloc(); // this function may throw out-of memory and cause a crash
|
||||
#if __cpp_exceptions
|
||||
} catch (...) { // if we arrive here, the decoder has thrown an OOM exception
|
||||
gifDecodeFailed = true;
|
||||
errorFlag = ERR_NORAM_PX;
|
||||
DEBUG_PRINTLN(F("\nGIF decoder out of memory. Please try a smaller image file.\n"));
|
||||
return IMAGE_ERROR_DECODER_ALLOC;
|
||||
// decoder cleanup (hi @coderabbitai): No additonal cleanup necessary - decoder.alloc() ultimately uses "new AnimatedGIF".
|
||||
// If new throws, no pointer is assigned, previous decoder state (if any) has already been deleted inside alloc(), so calling decoder.dealloc() here is unnecessary.
|
||||
}
|
||||
#endif
|
||||
DEBUG_PRINTLN(F("Starting decoding"));
|
||||
if(decoder.startDecoding() < 0) { gifDecodeFailed = true; return IMAGE_ERROR_GIF_DECODE; }
|
||||
int decoderError = decoder.startDecoding();
|
||||
if(decoderError < 0) {
|
||||
DEBUG_PRINTF_P(PSTR("GIF Decoding error %d in startDecoding().\n"), decoderError);
|
||||
errorFlag = ERR_NORAM_PX;
|
||||
gifDecodeFailed = true;
|
||||
return IMAGE_ERROR_GIF_DECODE;
|
||||
}
|
||||
DEBUG_PRINTLN(F("Decoding started"));
|
||||
// after startDecoding, we can get GIF size, update static variables and callbacks
|
||||
decoder.getSize(&gifWidth, &gifHeight);
|
||||
if (gifWidth == 0 || gifHeight == 0) { // bad gif size: prevent division by zero
|
||||
gifDecodeFailed = true;
|
||||
DEBUG_PRINTF_P(PSTR("Invalid GIF dimensions: %dx%d\n"), gifWidth, gifHeight);
|
||||
return IMAGE_ERROR_GIF_DECODE;
|
||||
}
|
||||
if (activeSeg->is2D()) {
|
||||
perPixelX = (activeSeg->vWidth() + gifWidth -1) / gifWidth;
|
||||
perPixelY = (activeSeg->vHeight() + gifHeight-1) / gifHeight;
|
||||
if (activeSeg->vWidth() != gifWidth || activeSeg->vHeight() != gifHeight) {
|
||||
decoder.setDrawPixelCallback(drawPixelCallback2D); // use 2D callback with scaling
|
||||
//DEBUG_PRINTLN(F("scaling image"));
|
||||
}
|
||||
} else {
|
||||
int totalImgPix = (int)gifWidth * gifHeight;
|
||||
if (totalImgPix - activeSeg->vLength() == 1) totalImgPix--; // handle off-by-one: skip last pixel instead of first (gifs constructed from 1D input pad last pixel if length is odd)
|
||||
perPixelX = (activeSeg->vLength() + totalImgPix-1) / totalImgPix;
|
||||
if (totalImgPix != activeSeg->vLength()) {
|
||||
decoder.setDrawPixelCallback(drawPixelCallback1D); // use 1D callback with scaling
|
||||
//DEBUG_PRINTLN(F("scaling image"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (gifDecodeFailed) return IMAGE_ERROR_PREV;
|
||||
@@ -117,10 +207,12 @@ byte renderImageToSegment(Segment &seg) {
|
||||
// TODO consider handling this on FX level with a different frametime, but that would cause slow gifs to speed up during transitions
|
||||
if (millis() - lastFrameDisplayTime < wait) return IMAGE_ERROR_WAITING;
|
||||
|
||||
decoder.getSize(&gifWidth, &gifHeight);
|
||||
|
||||
int result = decoder.decodeFrame(false);
|
||||
if (result < 0) { gifDecodeFailed = true; return IMAGE_ERROR_FRAME_DECODE; }
|
||||
if (result < 0) {
|
||||
DEBUG_PRINTF_P(PSTR("GIF Decoding error %d in decodeFrame().\n"), result);
|
||||
gifDecodeFailed = true;
|
||||
return IMAGE_ERROR_FRAME_DECODE;
|
||||
}
|
||||
|
||||
currentFrameDelay = decoder.getFrameDelay_ms();
|
||||
unsigned long tooSlowBy = (millis() - lastFrameDisplayTime) - wait; // if last frame was longer than intended, compensate
|
||||
@@ -137,7 +229,8 @@ void endImagePlayback(Segment *seg) {
|
||||
decoder.dealloc();
|
||||
gifDecodeFailed = false;
|
||||
activeSeg = nullptr;
|
||||
lastFilename[1] = '\0';
|
||||
strcpy(lastFilename, "/"); // reset filename
|
||||
gifWidth = gifHeight = 0; // reset dimensions
|
||||
DEBUG_PRINTLN(F("Image playback ended"));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "wled.h"
|
||||
|
||||
|
||||
#define JSON_PATH_STATE 1
|
||||
#define JSON_PATH_INFO 2
|
||||
#define JSON_PATH_STATE_INFO 3
|
||||
@@ -51,6 +52,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;
|
||||
|
||||
@@ -687,6 +691,7 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void serializeInfo(JsonObject root)
|
||||
{
|
||||
root[F("ver")] = versionString;
|
||||
@@ -694,6 +699,7 @@ void serializeInfo(JsonObject root)
|
||||
root[F("cn")] = F(WLED_CODENAME);
|
||||
root[F("release")] = releaseString;
|
||||
root[F("repo")] = repoString;
|
||||
root[F("deviceId")] = getDeviceId();
|
||||
|
||||
JsonObject leds = root.createNestedObject(F("leds"));
|
||||
leds[F("count")] = strip.getLengthTotal();
|
||||
@@ -817,6 +823,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();
|
||||
|
||||
741
wled00/ota_update.cpp
Normal file
741
wled00/ota_update.cpp
Normal file
@@ -0,0 +1,741 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
|
||||
void markOTAvalid() {
|
||||
#ifndef ESP8266
|
||||
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) {
|
||||
esp_ota_mark_app_valid_cancel_rollback(); // only needs to be called once, it marks the ota_state as ESP_OTA_IMG_VALID
|
||||
DEBUG_PRINTLN(F("Current firmware validated"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#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
|
||||
120
wled00/ota_update.h
Normal file
120
wled00/ota_update.h
Normal file
@@ -0,0 +1,120 @@
|
||||
// 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);
|
||||
|
||||
/**
|
||||
* Mark currently running firmware as valid to prevent auto-rollback on reboot.
|
||||
* This option can be enabled in some builds/bootloaders, it is an sdkconfig flag.
|
||||
*/
|
||||
void markOTAvalid();
|
||||
|
||||
#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
|
||||
|
||||
@@ -128,12 +128,12 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
|
||||
PinManager::deallocatePin(irPin, PinOwner::IR);
|
||||
}
|
||||
#endif
|
||||
for (unsigned s=0; s<WLED_MAX_BUTTONS; s++) {
|
||||
if (btnPin[s]>=0 && PinManager::isPinAllocated(btnPin[s], PinOwner::Button)) {
|
||||
PinManager::deallocatePin(btnPin[s], PinOwner::Button);
|
||||
for (const auto &button : buttons) {
|
||||
if (button.pin >= 0 && PinManager::isPinAllocated(button.pin, PinOwner::Button)) {
|
||||
PinManager::deallocatePin(button.pin, PinOwner::Button);
|
||||
#ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state, detach interrupt
|
||||
if (digitalPinToTouchChannel(btnPin[s]) >= 0) // if touch capable pin
|
||||
touchDetachInterrupt(btnPin[s]); // if not assigned previously, this will do nothing
|
||||
if (digitalPinToTouchChannel(button.pin) >= 0) // if touch capable pin
|
||||
touchDetachInterrupt(button.pin); // if not assigned previously, this will do nothing
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -280,54 +280,56 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
|
||||
char bt[4] = "BT"; bt[2] = offset+i; bt[3] = 0; // button pin (use A,B,C,... if WLED_MAX_BUTTONS>10)
|
||||
char be[4] = "BE"; be[2] = offset+i; be[3] = 0; // button type (use A,B,C,... if WLED_MAX_BUTTONS>10)
|
||||
int hw_btn_pin = request->arg(bt).toInt();
|
||||
if (hw_btn_pin >= 0 && PinManager::allocatePin(hw_btn_pin,false,PinOwner::Button)) {
|
||||
btnPin[i] = hw_btn_pin;
|
||||
buttonType[i] = request->arg(be).toInt();
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
if (i >= buttons.size()) buttons.emplace_back(hw_btn_pin, request->arg(be).toInt()); // add button to vector
|
||||
else {
|
||||
buttons[i].pin = hw_btn_pin;
|
||||
buttons[i].type = request->arg(be).toInt();
|
||||
}
|
||||
if (buttons[i].pin >= 0 && PinManager::allocatePin(buttons[i].pin, false, PinOwner::Button)) {
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
// ESP32 only: check that button pin is a valid gpio
|
||||
if ((buttonType[i] == BTN_TYPE_ANALOG) || (buttonType[i] == BTN_TYPE_ANALOG_INVERTED))
|
||||
{
|
||||
if (digitalPinToAnalogChannel(btnPin[i]) < 0) {
|
||||
if ((buttons[i].type == BTN_TYPE_ANALOG) || (buttons[i].type == BTN_TYPE_ANALOG_INVERTED)) {
|
||||
if (digitalPinToAnalogChannel(buttons[i].pin) < 0) {
|
||||
// not an ADC analog pin
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[i], i);
|
||||
btnPin[i] = -1;
|
||||
PinManager::deallocatePin(hw_btn_pin,PinOwner::Button);
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), buttons[i].pin, i);
|
||||
PinManager::deallocatePin(buttons[i].pin, PinOwner::Button);
|
||||
buttons[i].type = BTN_TYPE_NONE;
|
||||
} else {
|
||||
analogReadResolution(12); // see #4040
|
||||
}
|
||||
}
|
||||
else if ((buttonType[i] == BTN_TYPE_TOUCH || buttonType[i] == BTN_TYPE_TOUCH_SWITCH))
|
||||
{
|
||||
if (digitalPinToTouchChannel(btnPin[i]) < 0)
|
||||
{
|
||||
} else if ((buttons[i].type == BTN_TYPE_TOUCH || buttons[i].type == BTN_TYPE_TOUCH_SWITCH)) {
|
||||
if (digitalPinToTouchChannel(buttons[i].pin) < 0) {
|
||||
// not a touch pin
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), btnPin[i], i);
|
||||
btnPin[i] = -1;
|
||||
PinManager::deallocatePin(hw_btn_pin,PinOwner::Button);
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), buttons[i].pin, i);
|
||||
PinManager::deallocatePin(buttons[i].pin, PinOwner::Button);
|
||||
buttons[i].type = BTN_TYPE_NONE;
|
||||
}
|
||||
#ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a fucntion to check touch state but need to attach an interrupt to do so
|
||||
else
|
||||
{
|
||||
touchAttachInterrupt(btnPin[i], touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else
|
||||
#endif
|
||||
else touchAttachInterrupt(buttons[i].pin, touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
|
||||
#endif
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
// regular buttons and switches
|
||||
if (disablePullUp) {
|
||||
pinMode(btnPin[i], INPUT);
|
||||
pinMode(buttons[i].pin, INPUT);
|
||||
} else {
|
||||
#ifdef ESP32
|
||||
pinMode(btnPin[i], buttonType[i]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
|
||||
pinMode(buttons[i].pin, buttons[i].type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
|
||||
#else
|
||||
pinMode(btnPin[i], INPUT_PULLUP);
|
||||
pinMode(buttons[i].pin, INPUT_PULLUP);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
} else {
|
||||
btnPin[i] = -1;
|
||||
buttonType[i] = BTN_TYPE_NONE;
|
||||
buttons[i].pin = -1;
|
||||
buttons[i].type = BTN_TYPE_NONE;
|
||||
}
|
||||
}
|
||||
// we should remove all unused buttons from the vector
|
||||
for (int i = buttons.size()-1; i > 0; i--) {
|
||||
if (buttons[i].pin < 0 && buttons[i].type == BTN_TYPE_NONE) {
|
||||
buttons.erase(buttons.begin() + i); // remove button from vector
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,14 +533,16 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
|
||||
macroAlexaOff = request->arg(F("A1")).toInt();
|
||||
macroCountdown = request->arg(F("MC")).toInt();
|
||||
macroNl = request->arg(F("MN")).toInt();
|
||||
for (unsigned i=0; i<WLED_MAX_BUTTONS; i++) {
|
||||
char mp[4] = "MP"; mp[2] = (i<10?48:55)+i; mp[3] = 0; // short
|
||||
char ml[4] = "ML"; ml[2] = (i<10?48:55)+i; ml[3] = 0; // long
|
||||
char md[4] = "MD"; md[2] = (i<10?48:55)+i; md[3] = 0; // double
|
||||
int i = 0;
|
||||
for (auto &button : buttons) {
|
||||
char mp[4] = "MP"; mp[2] = (i<10?'0':'A'-10)+i; mp[3] = 0; // short
|
||||
char ml[4] = "ML"; ml[2] = (i<10?'0':'A'-10)+i; ml[3] = 0; // long
|
||||
char md[4] = "MD"; md[2] = (i<10?'0':'A'-10)+i; md[3] = 0; // double
|
||||
//if (!request->hasArg(mp)) break;
|
||||
macroButton[i] = request->arg(mp).toInt(); // these will default to 0 if not present
|
||||
macroLongPress[i] = request->arg(ml).toInt();
|
||||
macroDoublePress[i] = request->arg(md).toInt();
|
||||
button.macroButton = request->arg(mp).toInt(); // these will default to 0 if not present
|
||||
button.macroLongPress = request->arg(ml).toInt();
|
||||
button.macroDoublePress = request->arg(md).toInt();
|
||||
i++;
|
||||
}
|
||||
|
||||
char k[3]; k[2] = 0;
|
||||
@@ -613,7 +617,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 +818,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
|
||||
|
||||
|
||||
107
wled00/util.cpp
107
wled00/util.cpp
@@ -3,6 +3,7 @@
|
||||
#include "const.h"
|
||||
#ifdef ESP8266
|
||||
#include "user_interface.h" // for bootloop detection
|
||||
#include <Hash.h> // for SHA1 on ESP8266
|
||||
#else
|
||||
#include <Update.h>
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
|
||||
@@ -10,6 +11,8 @@
|
||||
#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 0)
|
||||
#include "soc/rtc.h"
|
||||
#endif
|
||||
#include "mbedtls/sha1.h" // for SHA1 on ESP32
|
||||
#include "esp_efuse.h"
|
||||
#endif
|
||||
|
||||
|
||||
@@ -369,7 +372,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();
|
||||
}
|
||||
|
||||
@@ -634,10 +636,12 @@ int32_t hw_random(int32_t lowerlimit, int32_t upperlimit) {
|
||||
#if defined(IDF_TARGET_ESP32C3) || defined(ESP8266)
|
||||
#error "ESP32-C3 and ESP8266 with PSRAM is not supported, please remove BOARD_HAS_PSRAM definition"
|
||||
#else
|
||||
// BOARD_HAS_PSRAM also means that compiler flag "-mfix-esp32-psram-cache-issue" has to be used
|
||||
#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) // PSRAM fix only needed for classic esp32
|
||||
// BOARD_HAS_PSRAM also means that compiler flag "-mfix-esp32-psram-cache-issue" has to be used for old "rev.1" esp32
|
||||
#warning "BOARD_HAS_PSRAM defined, make sure to use -mfix-esp32-psram-cache-issue to prevent issues on rev.1 ESP32 boards \
|
||||
see https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html#esp32-rev-v1-0"
|
||||
#endif
|
||||
#endif
|
||||
#else
|
||||
#if !defined(IDF_TARGET_ESP32C3) && !defined(ESP8266)
|
||||
#pragma message("BOARD_HAS_PSRAM not defined, not using PSRAM.")
|
||||
@@ -648,7 +652,8 @@ int32_t hw_random(int32_t lowerlimit, int32_t upperlimit) {
|
||||
#ifdef ESP8266
|
||||
static void *validateFreeHeap(void *buffer) {
|
||||
// make sure there is enough free heap left if buffer was allocated in DRAM region, free it if not
|
||||
if (getContiguousFreeHeap() < MIN_HEAP_SIZE) {
|
||||
// note: ESP826 needs very little contiguous heap for webserver, checking total free heap works better
|
||||
if (getFreeHeapSize() < MIN_HEAP_SIZE) {
|
||||
free(buffer);
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1125,4 +1130,98 @@ uint8_t perlin8(uint16_t x, uint16_t y) {
|
||||
|
||||
uint8_t perlin8(uint16_t x, uint16_t y, uint16_t z) {
|
||||
return (((perlin3D_raw((uint32_t)x << 8, (uint32_t)y << 8, (uint32_t)z << 8, true) * 2015) >> 10) + 33168) >> 8; //scale to 16 bit, offset, then scale to 8bit
|
||||
}
|
||||
}
|
||||
|
||||
// Platform-agnostic SHA1 computation from String input
|
||||
String computeSHA1(const String& input) {
|
||||
#ifdef ESP8266
|
||||
return sha1(input); // ESP8266 has built-in sha1() function
|
||||
#else
|
||||
// ESP32: Compute SHA1 hash using mbedtls
|
||||
unsigned char shaResult[20]; // SHA1 produces 20 bytes
|
||||
mbedtls_sha1_context ctx;
|
||||
|
||||
mbedtls_sha1_init(&ctx);
|
||||
mbedtls_sha1_starts_ret(&ctx);
|
||||
mbedtls_sha1_update_ret(&ctx, (const unsigned char*)input.c_str(), input.length());
|
||||
mbedtls_sha1_finish_ret(&ctx, shaResult);
|
||||
mbedtls_sha1_free(&ctx);
|
||||
|
||||
// Convert to hexadecimal string
|
||||
char hexString[41];
|
||||
for (int i = 0; i < 20; i++) {
|
||||
sprintf(&hexString[i*2], "%02x", shaResult[i]);
|
||||
}
|
||||
hexString[40] = '\0';
|
||||
|
||||
return String(hexString);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef ESP32
|
||||
#include "esp_adc_cal.h"
|
||||
String generateDeviceFingerprint() {
|
||||
uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint
|
||||
esp_chip_info_t chip_info;
|
||||
esp_chip_info(&chip_info);
|
||||
esp_efuse_mac_get_default((uint8_t*)fp);
|
||||
fp[1] ^= ESP.getFlashChipSize();
|
||||
fp[0] ^= chip_info.full_revision | (chip_info.model << 16);
|
||||
// mix in ADC calibration data:
|
||||
esp_adc_cal_characteristics_t ch;
|
||||
#if SOC_ADC_MAX_BITWIDTH == 13 // S2 has 13 bit ADC
|
||||
#define BIT_WIDTH ADC_WIDTH_BIT_13
|
||||
#else
|
||||
#define BIT_WIDTH ADC_WIDTH_BIT_12
|
||||
#endif
|
||||
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, BIT_WIDTH, 1100, &ch);
|
||||
fp[0] ^= ch.coeff_a;
|
||||
fp[1] ^= ch.coeff_b;
|
||||
if (ch.low_curve) {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
fp[0] ^= ch.low_curve[i];
|
||||
}
|
||||
}
|
||||
if (ch.high_curve) {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
fp[1] ^= ch.high_curve[i];
|
||||
}
|
||||
}
|
||||
char fp_string[17]; // 16 hex chars + null terminator
|
||||
sprintf(fp_string, "%08X%08X", fp[1], fp[0]);
|
||||
return String(fp_string);
|
||||
}
|
||||
#else // ESP8266
|
||||
String generateDeviceFingerprint() {
|
||||
uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint
|
||||
WiFi.macAddress((uint8_t*)&fp); // use MAC address as fingerprint base
|
||||
fp[0] ^= ESP.getFlashChipId();
|
||||
fp[1] ^= ESP.getFlashChipSize() | ESP.getFlashChipVendorId() << 16;
|
||||
char fp_string[17]; // 16 hex chars + null terminator
|
||||
sprintf(fp_string, "%08X%08X", fp[1], fp[0]);
|
||||
return String(fp_string);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Generate a device ID based on SHA1 hash of MAC address salted with other unique device info
|
||||
// Returns: original SHA1 + last 2 chars of double-hashed SHA1 (42 chars total)
|
||||
String getDeviceId() {
|
||||
static String cachedDeviceId = "";
|
||||
if (cachedDeviceId.length() > 0) return cachedDeviceId;
|
||||
// The device string is deterministic as it needs to be consistent for the same device, even after a full flash erase
|
||||
// MAC is salted with other consistent device info to avoid rainbow table attacks.
|
||||
// If the MAC address is known by malicious actors, they could precompute SHA1 hashes to impersonate devices,
|
||||
// but as WLED developers are just looking at statistics and not authenticating devices, this is acceptable.
|
||||
// If the usage data was exfiltrated, you could not easily determine the MAC from the device ID without brute forcing SHA1
|
||||
|
||||
String firstHash = computeSHA1(generateDeviceFingerprint());
|
||||
|
||||
// Second hash: SHA1 of the first hash
|
||||
String secondHash = computeSHA1(firstHash);
|
||||
|
||||
// Concatenate first hash + last 2 chars of second hash
|
||||
cachedDeviceId = firstHash + secondHash.substring(38);
|
||||
|
||||
return cachedDeviceId;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -166,16 +167,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();
|
||||
@@ -474,7 +474,7 @@ void WLED::setup()
|
||||
|
||||
if (needsCfgSave) serializeConfigToFS(); // usermods required new parameters; need to wait for strip to be initialised #4752
|
||||
|
||||
if (strcmp(multiWiFi[0].clientSSID, DEFAULT_CLIENT_SSID) == 0)
|
||||
if (strcmp(multiWiFi[0].clientSSID, DEFAULT_CLIENT_SSID) == 0 && !configBackupExists())
|
||||
showWelcomePage = true;
|
||||
WiFi.persistent(false);
|
||||
WiFi.onEvent(WiFiEvent);
|
||||
@@ -555,6 +555,7 @@ void WLED::setup()
|
||||
#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DISABLE_BROWNOUT_DET)
|
||||
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 1); //enable brownout detector
|
||||
#endif
|
||||
markOTAvalid();
|
||||
}
|
||||
|
||||
void WLED::beginStrip()
|
||||
|
||||
@@ -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!)
|
||||
@@ -296,10 +286,10 @@ WLED_GLOBAL char otaPass[33] _INIT(DEFAULT_OTA_PASS);
|
||||
|
||||
// Hardware and pin config
|
||||
#ifndef BTNPIN
|
||||
#define BTNPIN 0,-1
|
||||
#define BTNPIN 0
|
||||
#endif
|
||||
#ifndef BTNTYPE
|
||||
#define BTNTYPE BTN_TYPE_PUSH,BTN_TYPE_NONE
|
||||
#define BTNTYPE BTN_TYPE_PUSH
|
||||
#endif
|
||||
#ifndef RLYPIN
|
||||
WLED_GLOBAL int8_t rlyPin _INIT(-1);
|
||||
@@ -375,7 +365,7 @@ WLED_GLOBAL wifi_options_t wifiOpt _INIT_N(({0, 1, false, AP_BEHAVIOR_BOOT_NO_CO
|
||||
#define force802_3g wifiOpt.force802_3g
|
||||
#else
|
||||
WLED_GLOBAL int8_t selectedWiFi _INIT(0);
|
||||
WLED_GLOBAL byte apChannel _INIT(1); // 2.4GHz WiFi AP channel (1-13)
|
||||
WLED_GLOBAL byte apChannel _INIT(6); // 2.4GHz WiFi AP channel (1-13)
|
||||
WLED_GLOBAL byte apHide _INIT(0); // hidden AP SSID
|
||||
WLED_GLOBAL byte apBehavior _INIT(AP_BEHAVIOR_BOOT_NO_CONN); // access point opens when no connection after boot by default
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
@@ -581,9 +571,6 @@ WLED_GLOBAL byte countdownMin _INIT(0) , countdownSec _INIT(0);
|
||||
WLED_GLOBAL byte macroNl _INIT(0); // after nightlight delay over
|
||||
WLED_GLOBAL byte macroCountdown _INIT(0);
|
||||
WLED_GLOBAL byte macroAlexaOn _INIT(0), macroAlexaOff _INIT(0);
|
||||
WLED_GLOBAL byte macroButton[WLED_MAX_BUTTONS] _INIT({0});
|
||||
WLED_GLOBAL byte macroLongPress[WLED_MAX_BUTTONS] _INIT({0});
|
||||
WLED_GLOBAL byte macroDoublePress[WLED_MAX_BUTTONS] _INIT({0});
|
||||
|
||||
// Security CONFIG
|
||||
#ifdef WLED_OTA_PASS
|
||||
@@ -649,13 +636,32 @@ WLED_GLOBAL byte briLast _INIT(128); // brightness before
|
||||
WLED_GLOBAL byte whiteLast _INIT(128); // white channel before turned off. Used for toggle function in ir.cpp
|
||||
|
||||
// button
|
||||
WLED_GLOBAL int8_t btnPin[WLED_MAX_BUTTONS] _INIT({BTNPIN});
|
||||
WLED_GLOBAL byte buttonType[WLED_MAX_BUTTONS] _INIT({BTNTYPE});
|
||||
struct Button {
|
||||
unsigned long pressedTime; // time button was pressed
|
||||
unsigned long waitTime; // time to wait for next button press
|
||||
int8_t pin; // pin number
|
||||
struct {
|
||||
uint8_t type : 6; // button type (push, long, double, etc.)
|
||||
bool pressedBefore : 1; // button was pressed before
|
||||
bool longPressed : 1; // button was long pressed
|
||||
};
|
||||
uint8_t macroButton; // macro/preset to call on button press
|
||||
uint8_t macroLongPress; // macro/preset to call on long press
|
||||
uint8_t macroDoublePress; // macro/preset to call on double press
|
||||
|
||||
Button(int8_t p, uint8_t t, uint8_t mB = 0, uint8_t mLP = 0, uint8_t mDP = 0)
|
||||
: pressedTime(0)
|
||||
, waitTime(0)
|
||||
, pin(p)
|
||||
, type(t)
|
||||
, pressedBefore(false)
|
||||
, longPressed(false)
|
||||
, macroButton(mB)
|
||||
, macroLongPress(mLP)
|
||||
, macroDoublePress(mDP) {}
|
||||
};
|
||||
WLED_GLOBAL std::vector<Button> buttons; // vector of button structs
|
||||
WLED_GLOBAL bool buttonPublishMqtt _INIT(false);
|
||||
WLED_GLOBAL bool buttonPressedBefore[WLED_MAX_BUTTONS] _INIT({false});
|
||||
WLED_GLOBAL bool buttonLongPressed[WLED_MAX_BUTTONS] _INIT({false});
|
||||
WLED_GLOBAL unsigned long buttonPressedTime[WLED_MAX_BUTTONS] _INIT({0});
|
||||
WLED_GLOBAL unsigned long buttonWaitTime[WLED_MAX_BUTTONS] _INIT({0});
|
||||
WLED_GLOBAL bool disablePullUp _INIT(false);
|
||||
WLED_GLOBAL byte touchThreshold _INIT(TOUCH_THRESHOLD);
|
||||
|
||||
|
||||
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,17 @@ 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_list[] PROGMEM = "list";
|
||||
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++) {
|
||||
@@ -60,7 +67,7 @@ static bool inLocalSubnet(const IPAddress &client) {
|
||||
*/
|
||||
|
||||
static void generateEtag(char *etag, uint16_t eTagSuffix) {
|
||||
sprintf_P(etag, PSTR("%7d-%02x-%04x"), VERSION, cacheInvalidate, eTagSuffix);
|
||||
sprintf_P(etag, PSTR("%u-%02x-%04x"), WEB_BUILD_TIME, cacheInvalidate, eTagSuffix);
|
||||
}
|
||||
|
||||
static void setStaticContentCacheHeaders(AsyncWebServerResponse *response, int code, uint16_t eTagSuffix = 0) {
|
||||
@@ -71,9 +78,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 +183,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 +206,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 +215,98 @@ 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));
|
||||
bool legacyList = false;
|
||||
if (request->hasArg(FPSTR(s_list))) {
|
||||
legacyList = true; // support for '?list=/'
|
||||
}
|
||||
|
||||
if(func.length() == 0 && !legacyList) {
|
||||
// default: serve the editor page
|
||||
handleStaticContent(request, FPSTR(_edit_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_edit, PAGE_edit_length);
|
||||
return;
|
||||
}
|
||||
|
||||
if (func == FPSTR(s_list) || legacyList) {
|
||||
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 +472,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 +485,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 +535,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 +685,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 +810,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;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
*/
|
||||
#ifdef WLED_ENABLE_WEBSOCKETS
|
||||
|
||||
// define some constants for binary protocols, dont use defines but C++ style constexpr
|
||||
constexpr uint8_t BINARY_PROTOCOL_GENERIC = 0xFF; // generic / auto detect NOT IMPLEMENTED
|
||||
constexpr uint8_t BINARY_PROTOCOL_E131 = P_E131; // = 0, untested!
|
||||
constexpr uint8_t BINARY_PROTOCOL_ARTNET = P_ARTNET; // = 1, untested!
|
||||
constexpr uint8_t BINARY_PROTOCOL_DDP = P_DDP; // = 2
|
||||
|
||||
uint16_t wsLiveClientId = 0;
|
||||
unsigned long wsLastLiveTime = 0;
|
||||
//uint8_t* wsFrameBuffer = nullptr;
|
||||
@@ -25,7 +31,7 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp
|
||||
// data packet
|
||||
AwsFrameInfo * info = (AwsFrameInfo*)arg;
|
||||
if(info->final && info->index == 0 && info->len == len){
|
||||
// the whole message is in a single frame and we got all of its data (max. 1450 bytes)
|
||||
// the whole message is in a single frame and we got all of its data (max. 1428 bytes / ESP8266: 528 bytes)
|
||||
if(info->opcode == WS_TEXT)
|
||||
{
|
||||
if (len > 0 && len < 10 && data[0] == 'p') {
|
||||
@@ -71,8 +77,29 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp
|
||||
// force broadcast in 500ms after updating client
|
||||
//lastInterfaceUpdate = millis() - (INTERFACE_UPDATE_COOLDOWN -500); // ESP8266 does not like this
|
||||
}
|
||||
}else if (info->opcode == WS_BINARY) {
|
||||
// first byte determines protocol. Note: since e131_packet_t is "packed", the compiler handles alignment issues
|
||||
//DEBUG_PRINTF_P(PSTR("WS binary message: len %u, byte0: %u\n"), len, data[0]);
|
||||
int offset = 1; // offset to skip protocol byte
|
||||
switch (data[0]) {
|
||||
case BINARY_PROTOCOL_E131:
|
||||
handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_E131);
|
||||
break;
|
||||
case BINARY_PROTOCOL_ARTNET:
|
||||
handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_ARTNET);
|
||||
break;
|
||||
case BINARY_PROTOCOL_DDP:
|
||||
if (len < 10 + offset) return; // DDP header is 10 bytes (+1 protocol byte)
|
||||
size_t ddpDataLen = (data[8+offset] << 8) | data[9+offset]; // data length in bytes from DDP header
|
||||
uint8_t flags = data[0+offset];
|
||||
if ((flags & DDP_TIMECODE_FLAG) ) ddpDataLen += 4; // timecode flag adds 4 bytes to data length
|
||||
if (len < (10 + offset + ddpDataLen)) return; // not enough data, prevent out of bounds read
|
||||
// could be a valid DDP packet, forward to handler
|
||||
handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_DDP);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DEBUG_PRINTF_P(PSTR("WS multipart message: final %u index %u len %u total %u\n"), info->final, info->index, len, (uint32_t)info->len);
|
||||
//message is comprised of multiple frames or the frame is split into multiple packets
|
||||
//if(info->index == 0){
|
||||
//if (!wsFrameBuffer && len < 4096) wsFrameBuffer = new uint8_t[4096];
|
||||
|
||||
@@ -291,7 +291,7 @@ void getSettingsJS(byte subPage, Print& settingsScript)
|
||||
settingsScript.printf_P(PSTR("d.ledTypes=%s;"), BusManager::getLEDTypesJSONString().c_str());
|
||||
|
||||
// set limits
|
||||
settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d);"),
|
||||
settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d,%d);"),
|
||||
WLED_MAX_BUSSES,
|
||||
WLED_MIN_VIRTUAL_BUSSES, // irrelevant, but kept to distinguish S2/S3 in UI
|
||||
MAX_LEDS_PER_BUS,
|
||||
@@ -299,7 +299,8 @@ void getSettingsJS(byte subPage, Print& settingsScript)
|
||||
MAX_LEDS,
|
||||
WLED_MAX_COLOR_ORDER_MAPPINGS,
|
||||
WLED_MAX_DIGITAL_CHANNELS,
|
||||
WLED_MAX_ANALOG_CHANNELS
|
||||
WLED_MAX_ANALOG_CHANNELS,
|
||||
WLED_MAX_BUTTONS
|
||||
);
|
||||
|
||||
printSetFormCheckbox(settingsScript,PSTR("MS"),strip.autoSegments);
|
||||
@@ -403,8 +404,9 @@ void getSettingsJS(byte subPage, Print& settingsScript)
|
||||
printSetFormValue(settingsScript,PSTR("RL"),rlyPin);
|
||||
printSetFormCheckbox(settingsScript,PSTR("RM"),rlyMde);
|
||||
printSetFormCheckbox(settingsScript,PSTR("RO"),rlyOpenDrain);
|
||||
for (int i = 0; i < WLED_MAX_BUTTONS; i++) {
|
||||
settingsScript.printf_P(PSTR("addBtn(%d,%d,%d);"), i, btnPin[i], buttonType[i]);
|
||||
int i = 0;
|
||||
for (const auto &button : buttons) {
|
||||
settingsScript.printf_P(PSTR("addBtn(%d,%d,%d);"), i++, button.pin, button.type);
|
||||
}
|
||||
printSetFormCheckbox(settingsScript,PSTR("IP"),disablePullUp);
|
||||
printSetFormValue(settingsScript,PSTR("TT"),touchThreshold);
|
||||
@@ -578,8 +580,9 @@ void getSettingsJS(byte subPage, Print& settingsScript)
|
||||
printSetFormValue(settingsScript,PSTR("A1"),macroAlexaOff);
|
||||
printSetFormValue(settingsScript,PSTR("MC"),macroCountdown);
|
||||
printSetFormValue(settingsScript,PSTR("MN"),macroNl);
|
||||
for (unsigned i=0; i<WLED_MAX_BUTTONS; i++) {
|
||||
settingsScript.printf_P(PSTR("addRow(%d,%d,%d,%d);"), i, macroButton[i], macroLongPress[i], macroDoublePress[i]);
|
||||
int i = 0;
|
||||
for (const auto &button : buttons) {
|
||||
settingsScript.printf_P(PSTR("addRow(%d,%d,%d,%d);"), i++, button.macroButton, button.macroLongPress, button.macroDoublePress);
|
||||
}
|
||||
|
||||
char k[4];
|
||||
@@ -671,16 +674,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