mirror of
https://github.com/wled/WLED.git
synced 2025-12-27 10:28:00 +00:00
Compare commits
244 Commits
coderabbit
...
palettes-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acfb8b24ab | ||
|
|
97d3792a77 | ||
|
|
304c59e09b | ||
|
|
fdb85d82da | ||
|
|
c8a03817ed | ||
|
|
dc76ff669b | ||
|
|
eca002b655 | ||
|
|
71c8a30f42 | ||
|
|
65f1d8d836 | ||
|
|
624763cbc8 | ||
|
|
af7c91057e | ||
|
|
dd3edf1a3c | ||
|
|
7f4e0f74ba | ||
|
|
b452370be7 | ||
|
|
913c7316f2 | ||
|
|
bb6114e8aa | ||
|
|
c097cb1f27 | ||
|
|
9094b3130d | ||
|
|
32b104e1a9 | ||
|
|
d1260ccf8b | ||
|
|
c35140e763 | ||
|
|
6632a35339 | ||
|
|
6388b8f4bb | ||
|
|
f9a8b3021f | ||
|
|
6a8c6c1f58 | ||
|
|
19bc3c513a | ||
|
|
fa3a94e33e | ||
|
|
5c2177e8d5 | ||
|
|
6e39969cdc | ||
|
|
4b0cf874c9 | ||
|
|
2ff4ee0e1b | ||
|
|
f2a3502445 | ||
|
|
1fee9d4c29 | ||
|
|
b4d3a279e3 | ||
|
|
4684e092a8 | ||
|
|
2a53f415ea | ||
|
|
e074d19593 | ||
|
|
14a728084c | ||
|
|
ead1d6b5f8 | ||
|
|
a421cfeabe | ||
|
|
6f6ac066c9 | ||
|
|
a55a32cc7e | ||
|
|
474c84c9e6 | ||
|
|
a2b64ad332 | ||
|
|
cc5b504771 | ||
|
|
fe33709eb0 | ||
|
|
41b51edbdd | ||
|
|
e6b5429873 | ||
|
|
8cbc76540f | ||
|
|
de01c5e61f | ||
|
|
c114ea6b30 | ||
|
|
bdea3d4959 | ||
|
|
e403f4e0d0 | ||
|
|
3bac2ddae2 | ||
|
|
b98b1b4c78 | ||
|
|
5885a9cc63 | ||
|
|
e306e14b22 | ||
|
|
7b9d643dcd | ||
|
|
f70b359631 | ||
|
|
ae37f4268c | ||
|
|
7c6a1d717d | ||
|
|
a2c1ad01da | ||
|
|
a947e8f35e | ||
|
|
653e03921e | ||
|
|
a0eec81c8a | ||
|
|
9eda32b93a | ||
|
|
e1f5bbf895 | ||
|
|
5d4fdb171e | ||
|
|
4fa4bc8d4b | ||
|
|
b5f13e4331 | ||
|
|
a897271a03 | ||
|
|
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 | ||
|
|
d1ed5844e3 | ||
|
|
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 | ||
|
|
5bf1fc38b1 | ||
|
|
d55a3f078a | ||
|
|
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 | ||
|
|
bc5d4fed3c |
8
.github/workflows/usermods.yml
vendored
8
.github/workflows/usermods.yml
vendored
@@ -1,10 +1,6 @@
|
||||
name: Usermod CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- usermods/**
|
||||
- .github/workflows/usermods.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- usermods/**
|
||||
@@ -12,6 +8,8 @@ on:
|
||||
jobs:
|
||||
|
||||
get_usermod_envs:
|
||||
# Only run for pull requests from forks (not from branches within wled/WLED)
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
name: Gather Usermods
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -31,6 +29,8 @@ jobs:
|
||||
|
||||
|
||||
build:
|
||||
# Only run for pull requests from forks (not from branches within wled/WLED)
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
name: Build Enviornments
|
||||
runs-on: ubuntu-latest
|
||||
needs: get_usermod_envs
|
||||
|
||||
@@ -26,9 +26,28 @@ Github will pick up the changes so your PR stays up-to-date.
|
||||
> It has many subtle and unexpected consequences on our github reposistory.
|
||||
> For example, we regularly lost review comments when the PR author force-pushes code changes. So, pretty please, do not force-push.
|
||||
|
||||
> [!TIP]
|
||||
> use [cherry-picking](https://docs.github.com/en/desktop/managing-commits/cherry-picking-a-commit-in-github-desktop) to copy commits from one branch to another.
|
||||
|
||||
You can find a collection of very useful tips and tricks here: https://github.com/wled-dev/WLED/wiki/How-to-properly-submit-a-PR
|
||||
|
||||
### Source Code from an AI agent or bot
|
||||
> [!IMPORTANT]
|
||||
> Its OK if you took help from an AI for writing your source code.
|
||||
>
|
||||
> However, we expect a few things from you as the person making a contribution to WLED:
|
||||
* Make sure you really understand the code suggested by the AI, and don't just accept it because it "seems to work".
|
||||
* Don't let the AI change existing code without double-checking by you as the contributor. Often, the result will not be complete. For example, previous source code comments may be lost.
|
||||
* Remember that AI are still "Often-Wrong" ;-)
|
||||
* If you don't feel very confident using English, you can use AI for translating code comments and descriptions into English. AI bots are very good at understanding language. However, always check if the results is correct. The translation might still have wrong technical terms, or errors in some details.
|
||||
|
||||
#### best practice with AI:
|
||||
* As the person who contributes source code to WLED, make sure you understand exactly what the AI generated code does
|
||||
* best practice: add a comment like ``'// below section of my code was generated by an AI``, when larger parts of your source code were not written by you personally.
|
||||
* always review translations and code comments for correctness
|
||||
* always review AI generated source code
|
||||
* If the AI has rewritten existing code, check that the change is necessary and that nothing has been lost or broken. Also check that previous code comments are still intact.
|
||||
|
||||
|
||||
### Code style
|
||||
|
||||
|
||||
58
boards/adafruit_matrixportal_esp32s3_wled.json
Normal file
58
boards/adafruit_matrixportal_esp32s3_wled.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino":{
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default_8MB.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 for WLED",
|
||||
"upload": {
|
||||
"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"
|
||||
}
|
||||
@@ -14,14 +14,14 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wled-dev/WLED.git"
|
||||
"url": "git+https://github.com/wled/WLED.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wled-dev/WLED/issues"
|
||||
"url": "https://github.com/wled/WLED/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wled-dev/WLED#readme",
|
||||
"homepage": "https://github.com/wled/WLED#readme",
|
||||
"dependencies": {
|
||||
"clean-css": "^5.3.3",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
@@ -31,4 +31,4 @@
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}"])
|
||||
100
platformio.ini
100
platformio.ini
@@ -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
|
||||
@@ -100,6 +120,7 @@ build_flags =
|
||||
-D DECODE_SAMSUNG=true
|
||||
-D DECODE_LG=true
|
||||
-DWLED_USE_MY_CONFIG
|
||||
-D WLED_PS_DONT_REPLACE_FX ; PS replacement FX are purely a flash memory saving feature, do not replace classic FX until we run out of flash
|
||||
|
||||
build_unflags =
|
||||
|
||||
@@ -110,8 +131,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
|
||||
@@ -141,7 +161,7 @@ lib_compat_mode = strict
|
||||
lib_deps =
|
||||
fastled/FastLED @ 3.6.0
|
||||
IRremoteESP8266 @ 2.8.2
|
||||
makuna/NeoPixelBus @ 2.8.3
|
||||
https://github.com/Makuna/NeoPixelBus.git#a0919d1c10696614625978dd6fb750a1317a14ce
|
||||
https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.2
|
||||
marvinroger/AsyncMqttClient @ 0.9.0
|
||||
# for I2C interface
|
||||
@@ -237,7 +257,7 @@ lib_deps_compat =
|
||||
lib_deps =
|
||||
esp32async/AsyncTCP @ 3.4.7
|
||||
bitbank2/AnimatedGIF@^1.4.7
|
||||
https://github.com/Aircoookie/GifDecoder#bc3af18
|
||||
https://github.com/Aircoookie/GifDecoder.git#bc3af189b6b1e06946569f6b4287f0b79a860f8e
|
||||
build_flags =
|
||||
-D CONFIG_ASYNC_TCP_USE_WDT=0
|
||||
-D CONFIG_ASYNC_TCP_STACK_SIZE=8192
|
||||
@@ -265,12 +285,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
|
||||
@@ -279,12 +301,13 @@ build_flags = -g
|
||||
-D WLED_ENABLE_DMX_INPUT
|
||||
lib_deps =
|
||||
${esp32_all_variants.lib_deps}
|
||||
https://github.com/someweisguy/esp_dmx.git#47db25d
|
||||
https://github.com/someweisguy/esp_dmx.git#47db25d8c515e76fabcf5fc5ab0b786f98eeade0
|
||||
${env.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 +326,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 +345,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 +454,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 +490,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 +506,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 +526,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 +534,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 +546,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 +580,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 +599,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 +621,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 +659,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 +686,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
|
||||
board = adafruit_matrixportal_esp32s3_wled ; modified board definition: removed flash section that causes FS erase on upload
|
||||
;; 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_pixelforge.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()}
|
||||
|
||||
`;
|
||||
|
||||
@@ -126,12 +134,13 @@ async function minify(str, type = "plain") {
|
||||
throw new Error("Unknown filter: " + type);
|
||||
}
|
||||
|
||||
async function writeHtmlGzipped(sourceFile, resultFile, page) {
|
||||
async function writeHtmlGzipped(sourceFile, resultFile, page, inlineCss = true) {
|
||||
console.info("Reading " + sourceFile);
|
||||
inline.html({
|
||||
fileContent: fs.readFileSync(sourceFile, "utf8"),
|
||||
relativeTo: path.dirname(sourceFile),
|
||||
strict: true,
|
||||
strict: inlineCss, // when not inlining css, ignore errors (enables linking style.css from subfolder htm files)
|
||||
stylesheets: inlineCss // when true (default), css is inlined
|
||||
},
|
||||
async function (error, html) {
|
||||
if (error) throw error;
|
||||
@@ -244,8 +253,23 @@ if (isAlreadyBuilt("wled00/data") && process.argv[2] !== '--force' && process.ar
|
||||
|
||||
writeHtmlGzipped("wled00/data/index.htm", "wled00/html_ui.h", 'index');
|
||||
writeHtmlGzipped("wled00/data/pixart/pixart.htm", "wled00/html_pixart.h", 'pixart');
|
||||
//writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal');
|
||||
writeHtmlGzipped("wled00/data/pxmagic/pxmagic.htm", "wled00/html_pxmagic.h", 'pxmagic');
|
||||
writeHtmlGzipped("wled00/data/pixelforge/pixelforge.htm", "wled00/html_pixelforge.h", 'pixelforge', false); // do not inline css
|
||||
//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 +412,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
#include <driver/i2s.h>
|
||||
#include <driver/adc.h>
|
||||
|
||||
#ifdef WLED_ENABLE_DMX
|
||||
#error This audio reactive usermod is not compatible with DMX Out.
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG))
|
||||
@@ -1227,7 +1223,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 +1230,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
|
||||
|
||||
@@ -1321,7 +1331,7 @@ class AudioReactive : public Usermod {
|
||||
disableSoundProcessing = true;
|
||||
} else {
|
||||
#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG)
|
||||
if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource->isInitialized()) { // we just switched to "enabled"
|
||||
if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource && audioSource->isInitialized()) { // we just switched to "enabled"
|
||||
DEBUG_PRINTLN(F("[AR userLoop] realtime mode ended - audio processing resumed."));
|
||||
DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride));
|
||||
}
|
||||
@@ -1333,7 +1343,7 @@ class AudioReactive : public Usermod {
|
||||
if (audioSyncEnabled & 0x02) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode
|
||||
if (audioSyncEnabled & 0x01) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
if (!audioSource->isInitialized()) disableSoundProcessing = true; // no audio source
|
||||
if (!audioSource || !audioSource->isInitialized()) disableSoundProcessing = true; // no audio source
|
||||
|
||||
|
||||
// Only run the sampling code IF we're not in Receive mode or realtime mode
|
||||
@@ -1530,7 +1540,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -401,19 +401,19 @@ void RotaryEncoderUIUsermod::sortModesAndPalettes() {
|
||||
re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT);
|
||||
|
||||
DEBUG_PRINT(F("Sorting palettes: ")); DEBUG_PRINT(getPaletteCount()); DEBUG_PRINT('/'); DEBUG_PRINTLN(customPalettes.size());
|
||||
palettes_qstrings = re_findModeStrings(JSON_palette_names, getPaletteCount());
|
||||
palettes_alpha_indexes = re_initIndexArray(getPaletteCount());
|
||||
palettes_qstrings = re_findModeStrings(JSON_palette_names, getPaletteCount()); // allocates memory for all palette names
|
||||
palettes_alpha_indexes = re_initIndexArray(getPaletteCount()); // allocates memory for all palette indexes
|
||||
if (customPalettes.size()) {
|
||||
for (int i=0; i<customPalettes.size(); i++) {
|
||||
palettes_alpha_indexes[getPaletteCount()-customPalettes.size()+i] = 255-i;
|
||||
palettes_qstrings[getPaletteCount()-customPalettes.size()+i] = PSTR("~Custom~");
|
||||
palettes_alpha_indexes[FIXED_PALETTE_COUNT+i] = 255-i;
|
||||
palettes_qstrings[FIXED_PALETTE_COUNT+i] = PSTR("~Custom~");
|
||||
}
|
||||
}
|
||||
// How many palette names start with '*' and should not be sorted?
|
||||
// (Also skipping the first one, 'Default').
|
||||
int skipPaletteCount = 1;
|
||||
while (pgm_read_byte_near(palettes_qstrings[skipPaletteCount]) == '*') skipPaletteCount++;
|
||||
re_sortModes(palettes_qstrings, palettes_alpha_indexes, getPaletteCount()-customPalettes.size(), skipPaletteCount);
|
||||
int skipPaletteCount = 1; // could use DYNAMIC_PALETTE_COUNT instead
|
||||
while (pgm_read_byte_near(palettes_qstrings[skipPaletteCount]) == '*') skipPaletteCount++; // legacy code
|
||||
re_sortModes(palettes_qstrings, palettes_alpha_indexes, FIXED_PALETTE_COUNT, skipPaletteCount); // only sort fixed palettes (skip dynamic)
|
||||
}
|
||||
|
||||
byte *RotaryEncoderUIUsermod::re_initIndexArray(int numModes) {
|
||||
|
||||
891
wled00/FX.cpp
891
wled00/FX.cpp
File diff suppressed because it is too large
Load Diff
@@ -310,6 +310,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex()
|
||||
#define FX_MODE_2DFIRENOISE 149
|
||||
#define FX_MODE_2DSQUAREDSWIRL 150
|
||||
// #define FX_MODE_2DFIRE2012 151
|
||||
#define FX_MODE_PACMAN 151 // gap fill (non-SR). Do NOT renumber; SR-ID range must remain stable.
|
||||
#define FX_MODE_2DDNA 152
|
||||
#define FX_MODE_2DMATRIX 153
|
||||
#define FX_MODE_2DMETABALLS 154
|
||||
@@ -965,7 +966,8 @@ class WS2812FX {
|
||||
};
|
||||
|
||||
unsigned long now, timebase;
|
||||
inline uint32_t getPixelColor(unsigned n) const { return (n < getLengthTotal()) ? _pixels[n] : 0; } // returns color of pixel n
|
||||
inline uint32_t getPixelColor(unsigned n) const { return (getMappedPixelIndex(n) < getLengthTotal()) ? _pixels[n] : 0; } // returns color of pixel n, black if out of (mapped) bounds
|
||||
inline uint32_t getPixelColorNoMap(unsigned n) const { return (n < getLengthTotal()) ? _pixels[n] : 0; } // ignores mapping table
|
||||
inline uint32_t getLastShow() const { return _lastShow; } // returns millis() timestamp of last strip.show() call
|
||||
|
||||
const char *getModeData(unsigned id = 0) const { return (id && id < _modeCount) ? _modeData[id] : PSTR("Solid"); }
|
||||
|
||||
@@ -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:"));
|
||||
|
||||
@@ -230,7 +230,7 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
|
||||
// then come the custom palettes (255,254,...) growing downwards from 255 (255 being 1st custom palette)
|
||||
// palette 0 is a varying palette depending on effect and may be replaced by segment's color if so
|
||||
// instructed in color_from_palette()
|
||||
if (pal > FIXED_PALETTE_COUNT && pal <= 255-customPalettes.size()) pal = 0; // out of bounds palette
|
||||
if (pal >= FIXED_PALETTE_COUNT && pal <= 255-customPalettes.size()) pal = 0; // out of bounds palette
|
||||
//default palette. Differs depending on effect
|
||||
if (pal == 0) pal = _default_palette; // _default_palette is set in setMode()
|
||||
switch (pal) {
|
||||
@@ -268,11 +268,11 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
|
||||
default: //progmem palettes
|
||||
if (pal > 255 - customPalettes.size()) {
|
||||
targetPalette = customPalettes[255-pal]; // we checked bounds above
|
||||
} else if (pal < DYNAMIC_PALETTE_COUNT+FASTLED_PALETTE_COUNT+1) { // palette 6 - 12, fastled palettes
|
||||
targetPalette = *fastledPalettes[pal-DYNAMIC_PALETTE_COUNT-1];
|
||||
} else if (pal < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) { // palette 6 - 12, fastled palettes
|
||||
targetPalette = *fastledPalettes[pal - DYNAMIC_PALETTE_COUNT];
|
||||
} else {
|
||||
byte tcp[72];
|
||||
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal-(DYNAMIC_PALETTE_COUNT+FASTLED_PALETTE_COUNT)-1])), 72);
|
||||
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal - (DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT)])), sizeof(tcp));
|
||||
targetPalette.loadDynamicGradientPalette(tcp);
|
||||
}
|
||||
break;
|
||||
@@ -458,7 +458,7 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
|
||||
return;
|
||||
}
|
||||
if (i1 < Segment::maxWidth || (i1 >= Segment::maxWidth*Segment::maxHeight && i1 < strip.getLengthTotal())) start = i1; // Segment::maxWidth equals strip.getLengthTotal() for 1D
|
||||
stop = i2 > Segment::maxWidth*Segment::maxHeight ? MIN(i2,strip.getLengthTotal()) : constrain(i2, 1, Segment::maxWidth);
|
||||
stop = i2 > Segment::maxWidth*Segment::maxHeight && i1 >= Segment::maxWidth*Segment::maxHeight ? MIN(i2,strip.getLengthTotal()) : constrain(i2, 1, Segment::maxWidth); // check for 2D trailing strip
|
||||
startY = 0;
|
||||
stopY = 1;
|
||||
#ifndef WLED_DISABLE_2D
|
||||
@@ -1502,10 +1502,11 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
|
||||
// we need to blend old segment using fade as pixels are not clipped
|
||||
c_a = color_blend16(c_a, segO->getPixelColorRaw(x + y*oCols), progInv);
|
||||
} else if (blendingStyle != BLEND_STYLE_FADE) {
|
||||
// if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp)
|
||||
// workaround for On/Off transition
|
||||
// (bri != briT) && !bri => from On to Off
|
||||
// (bri != briT) && bri => from Off to On
|
||||
if ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri)) c_a = BLACK;
|
||||
if ((briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK;
|
||||
}
|
||||
// map it into frame buffer
|
||||
x = c; // restore coordiates if we were PUSHing
|
||||
@@ -1572,10 +1573,11 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
|
||||
// we need to blend old segment using fade as pixels are not clipped
|
||||
c_a = color_blend16(c_a, segO->getPixelColorRaw(i), progInv);
|
||||
} else if (blendingStyle != BLEND_STYLE_FADE) {
|
||||
// if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp)
|
||||
// workaround for On/Off transition
|
||||
// (bri != briT) && !bri => from On to Off
|
||||
// (bri != briT) && bri => from Off to On
|
||||
if ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri)) c_a = BLACK;
|
||||
if ((briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK;
|
||||
}
|
||||
// map into frame buffer
|
||||
i = k; // restore index if we were PUSHing
|
||||
@@ -1681,12 +1683,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 +1817,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 +1829,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 +1901,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 +1964,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 +1994,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 +2024,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 +2036,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 +2052,6 @@ bool WS2812FX::deserializeMap(unsigned n) {
|
||||
DEBUG_PRINTLN(F("ERROR LED map allocation error."));
|
||||
}
|
||||
|
||||
resume();
|
||||
|
||||
releaseJSONBufferLock();
|
||||
return (customMappingSize > 0);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@
|
||||
#include <stdint.h>
|
||||
#include "wled.h"
|
||||
|
||||
#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8)
|
||||
#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8), limiting below 127 to avoid overflows in collisions due to rounding errors
|
||||
#define MAX_MEMIDLE 10 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!)
|
||||
|
||||
//#define WLED_DEBUG_PS // note: enabling debug uses ~3k of flash
|
||||
@@ -103,7 +103,7 @@ typedef union {
|
||||
|
||||
// struct for additional particle settings (option)
|
||||
typedef struct { // 2 bytes
|
||||
uint8_t size; // particle size, 255 means 10 pixels in diameter, 0 means use global size (including single pixel rendering)
|
||||
uint8_t size; // particle size, 255 means 10 pixels in diameter, set perParticleSize = true to enable
|
||||
uint8_t forcecounter; // counter for applying forces to individual particles
|
||||
} PSadvancedParticle;
|
||||
|
||||
@@ -190,16 +190,18 @@ public:
|
||||
int32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1
|
||||
uint32_t numSources; // number of sources
|
||||
uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles'
|
||||
bool perParticleSize; // if true, uses individual particle sizes from advPartProps if available (disabled when calling setParticleSize())
|
||||
//note: some variables are 32bit for speed and code size at the cost of ram
|
||||
|
||||
private:
|
||||
//rendering functions
|
||||
void render();
|
||||
[[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY);
|
||||
void renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY);
|
||||
//paricle physics applied by system if flags are set
|
||||
void applyGravity(); // applies gravity to all particles
|
||||
void handleCollisions();
|
||||
[[gnu::hot]] void collideParticles(PSparticle &particle1, PSparticle &particle2, const int32_t dx, const int32_t dy, const uint32_t collDistSq);
|
||||
void collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const uint32_t collDistSq, int32_t massratio1, int32_t massratio2);
|
||||
void fireParticleupdate();
|
||||
//utility functions
|
||||
void updatePSpointers(const bool isadvanced, const bool sizecontrol); // update the data pointers to current segment data space
|
||||
@@ -226,12 +228,26 @@ private:
|
||||
uint8_t smearBlur; // 2D smeared blurring of full frame
|
||||
};
|
||||
|
||||
void blur2D(uint32_t *colorbuffer, const uint32_t xsize, uint32_t ysize, const uint32_t xblur, const uint32_t yblur, const uint32_t xstart = 0, uint32_t ystart = 0, const bool isparticle = false);
|
||||
// initialization functions (not part of class)
|
||||
bool initParticleSystem2D(ParticleSystem2D *&PartSys, const uint32_t requestedsources, const uint32_t additionalbytes = 0, const bool advanced = false, const bool sizecontrol = false);
|
||||
uint32_t calculateNumberOfParticles2D(const uint32_t pixels, const bool advanced, const bool sizecontrol);
|
||||
uint32_t calculateNumberOfSources2D(const uint32_t pixels, const uint32_t requestedsources);
|
||||
bool allocateParticleSystemMemory2D(const uint32_t numparticles, const uint32_t numsources, const bool advanced, const bool sizecontrol, const uint32_t additionalbytes);
|
||||
|
||||
// distance-based brightness for ellipse rendering, returns brightness (0-255) based on distance from ellipse center
|
||||
inline uint8_t calculateEllipseBrightness(int32_t dx, int32_t dy, int32_t rxsq, int32_t rysq, uint8_t maxBrightness) {
|
||||
// square the distances
|
||||
uint32_t dx_sq = dx * dx;
|
||||
uint32_t dy_sq = dy * dy;
|
||||
|
||||
uint32_t dist_sq = ((dx_sq << 8) / rxsq) + ((dy_sq << 8) / rysq); // normalized squared distance in fixed point: (dx²/rx²) * 256 + (dy²/ry²) * 256
|
||||
|
||||
if (dist_sq >= 256) return 0; // pixel is outside the ellipse, unit radius in fixed point: 256 = 1.0
|
||||
//if (dist_sq <= 96) return maxBrightness; // core at full brightness
|
||||
int32_t falloff = 256 - dist_sq;
|
||||
return (maxBrightness * falloff) >> 8; // linear falloff
|
||||
//return (maxBrightness * falloff * falloff) >> 16; // squared falloff for even softer edges
|
||||
}
|
||||
#endif // WLED_DISABLE_PARTICLESYSTEM2D
|
||||
|
||||
////////////////////////
|
||||
@@ -301,9 +317,9 @@ typedef union {
|
||||
|
||||
// struct for additional particle settings (optional)
|
||||
typedef struct {
|
||||
uint8_t sat; //color saturation
|
||||
uint8_t sat; // color saturation
|
||||
uint8_t size; // particle size, 255 means 10 pixels in diameter, this overrides global size setting
|
||||
uint8_t forcecounter;
|
||||
uint8_t forcecounter; // counter for applying forces to individual particles
|
||||
} PSadvancedParticle1D;
|
||||
|
||||
//struct for a particle source (20 bytes)
|
||||
@@ -346,10 +362,11 @@ public:
|
||||
void setColorByPosition(const bool enable);
|
||||
void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero
|
||||
void setSmearBlur(const uint8_t bluramount); // enable 1D smeared blurring of full frame
|
||||
void setParticleSize(const uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels, is overruled if advanced particle is used
|
||||
void setParticleSize(const uint8_t size); // particle diameter: size 0 = 1 pixel, size 1 = 2 pixels, size = 255 = 10 pixels, disables per particle size control if called
|
||||
void setGravity(int8_t force = 8);
|
||||
void enableParticleCollisions(bool enable, const uint8_t hardness = 255);
|
||||
|
||||
|
||||
PSparticle1D *particles; // pointer to particle array
|
||||
PSparticleFlags1D *particleFlags; // pointer to particle flags array
|
||||
PSsource1D *sources; // pointer to sources
|
||||
@@ -360,16 +377,18 @@ public:
|
||||
int32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1
|
||||
uint32_t numSources; // number of sources
|
||||
uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles'
|
||||
bool perParticleSize; // if true, uses individual particle sizes from advPartProps if available (disabled when calling setParticleSize())
|
||||
|
||||
private:
|
||||
//rendering functions
|
||||
void render(void);
|
||||
[[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap);
|
||||
void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap);
|
||||
void renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrap);
|
||||
|
||||
//paricle physics applied by system if flags are set
|
||||
void applyGravity(); // applies gravity to all particles
|
||||
void handleCollisions();
|
||||
[[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const uint32_t collisiondistance);
|
||||
void collideParticles(uint32_t partIdx1, uint32_t partIdx2, int32_t dx, uint32_t collisiondistance);
|
||||
|
||||
//utility functions
|
||||
void updatePSpointers(const bool isadvanced); // update the data pointers to current segment data space
|
||||
@@ -397,5 +416,5 @@ bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedso
|
||||
uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced);
|
||||
uint32_t calculateNumberOfSources1D(const uint32_t requestedsources);
|
||||
bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes);
|
||||
void blur1D(uint32_t *colorbuffer, uint32_t size, uint32_t blur, uint32_t start);
|
||||
|
||||
#endif // WLED_DISABLE_PARTICLESYSTEM1D
|
||||
|
||||
@@ -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,70 +290,66 @@ 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
|
||||
WLED_FS.format();
|
||||
#ifdef WLED_ADD_EEPROM_SUPPORT
|
||||
clearEEPROM();
|
||||
#endif
|
||||
doReboot = true;
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
135
wled00/cfg.cpp
135
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() {
|
||||
@@ -791,11 +789,6 @@ void resetConfig() {
|
||||
|
||||
bool deserializeConfigFromFS() {
|
||||
[[maybe_unused]] bool success = deserializeConfigSec();
|
||||
#ifdef WLED_ADD_EEPROM_SUPPORT
|
||||
if (!success) { //if file does not exist, try reading from EEPROM
|
||||
deEEPSettings();
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!requestJSONBufferLock(1)) return false;
|
||||
|
||||
@@ -1012,15 +1005,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;
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
* Readability defines and their associated numerical values + compile-time constants
|
||||
*/
|
||||
|
||||
constexpr size_t FASTLED_PALETTE_COUNT = 7; // = sizeof(fastledPalettes) / sizeof(fastledPalettes[0]);
|
||||
constexpr size_t GRADIENT_PALETTE_COUNT = 59; // = sizeof(gGradientPalettes) / sizeof(gGradientPalettes[0]);
|
||||
constexpr size_t DYNAMIC_PALETTE_COUNT = 5; // 1-5 are dynamic palettes (1=random,2=primary,3=primary+secondary,4=primary+secondary+tertiary,5=primary+secondary(+tertiary if not black)
|
||||
constexpr size_t FASTLED_PALETTE_COUNT = 7; // 6-12 = sizeof(fastledPalettes) / sizeof(fastledPalettes[0]);
|
||||
constexpr size_t GRADIENT_PALETTE_COUNT = 59; // 13-72 = sizeof(gGradientPalettes) / sizeof(gGradientPalettes[0]);
|
||||
constexpr size_t DYNAMIC_PALETTE_COUNT = 6; // 0- 5 = dynamic palettes (0=default(virtual),1=random,2=primary,3=primary+secondary,4=primary+secondary+tertiary,5=primary+secondary(+tertiary if not black)
|
||||
constexpr size_t FIXED_PALETTE_COUNT = DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT + GRADIENT_PALETTE_COUNT; // total number of fixed palettes
|
||||
#ifndef ESP8266
|
||||
#define WLED_MAX_CUSTOM_PALETTES (255 - FIXED_PALETTE_COUNT) // allow up to 255 total palettes, user is warned about stability issues when adding more than 10
|
||||
@@ -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
|
||||
@@ -366,7 +366,8 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define BTN_TYPE_TOUCH_SWITCH 9
|
||||
|
||||
//Ethernet board types
|
||||
#define WLED_NUM_ETH_TYPES 13
|
||||
#define WLED_NUM_ETH_TYPES 14
|
||||
|
||||
|
||||
#define WLED_ETH_NONE 0
|
||||
#define WLED_ETH_WT32_ETH01 1
|
||||
@@ -381,6 +382,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define WLED_ETH_SERG74 10
|
||||
#define WLED_ETH_ESP32_POE_WROVER 11
|
||||
#define WLED_ETH_LILYGO_T_POE_PRO 12
|
||||
#define WLED_ETH_GLEDOPTO 13
|
||||
|
||||
//Hue error codes
|
||||
#define HUE_ERROR_INACTIVE 0
|
||||
|
||||
@@ -51,6 +51,38 @@ function tooltip(cont=null) {
|
||||
});
|
||||
});
|
||||
};
|
||||
// sequential loading of external resources (JS or CSS) with retry, calls init() when done
|
||||
function loadResources(files, init) {
|
||||
let i = 0;
|
||||
const loadNext = () => {
|
||||
if (i >= files.length) {
|
||||
if (init) {
|
||||
d.documentElement.style.visibility = 'visible'; // make page visible after all files are loaded if it was hidden (prevent ugly display)
|
||||
d.readyState === 'complete' ? init() : window.addEventListener('load', init);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const file = files[i++];
|
||||
const isCSS = file.endsWith('.css');
|
||||
const el = d.createElement(isCSS ? 'link' : 'script');
|
||||
if (isCSS) {
|
||||
el.rel = 'stylesheet';
|
||||
el.href = file;
|
||||
const st = d.head.querySelector('style');
|
||||
if (st) d.head.insertBefore(el, st); // insert before any <style> to allow overrides
|
||||
else d.head.appendChild(el);
|
||||
} else {
|
||||
el.src = file;
|
||||
d.head.appendChild(el);
|
||||
}
|
||||
el.onload = () => { loadNext(); };
|
||||
el.onerror = () => {
|
||||
i--; // load this file again
|
||||
setTimeout(loadNext, 100);
|
||||
};
|
||||
};
|
||||
loadNext();
|
||||
}
|
||||
// https://www.educative.io/edpresso/how-to-dynamically-load-a-js-file-in-javascript
|
||||
function loadJS(FILE_URL, async = true, preGetV = undefined, postGetV = undefined) {
|
||||
let scE = d.createElement("script");
|
||||
@@ -116,3 +148,63 @@ function uploadFile(fileObj, name) {
|
||||
fileObj.value = '';
|
||||
return false;
|
||||
}
|
||||
// connect to WebSocket, use parent WS or open new, callback function gets passed the new WS object
|
||||
function connectWs(onOpen) {
|
||||
let ws;
|
||||
try { ws = top.window.ws;} catch (e) {}
|
||||
// reuse if open
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
if (onOpen) onOpen(ws);
|
||||
} else {
|
||||
// create new ws connection
|
||||
getLoc(); // ensure globals are up to date
|
||||
let url = loc ? getURL('/ws').replace("http", "ws")
|
||||
: "ws://" + window.location.hostname + "/ws";
|
||||
ws = new WebSocket(url);
|
||||
ws.binaryType = "arraybuffer";
|
||||
if (onOpen) ws.onopen = () => onOpen(ws);
|
||||
}
|
||||
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);
|
||||
@@ -1476,7 +1476,7 @@ dialog {
|
||||
.expanded {
|
||||
display: inline-block !important;
|
||||
}
|
||||
.hide, .expanded .segin.hide, .expanded .presin.hide, .expanded .sbs.hide, .expanded .frz, .expanded .g-icon {
|
||||
.hide, .expanded .segin.hide, .expanded .presin.hide, .expanded .sbs.hide, .expanded .g-icon {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
</div>
|
||||
<div style="padding: 8px 0;" id="btns">
|
||||
<button class="btn btn-xs" title="File editor" type="button" id="edit" onclick="window.location.href=getURL('/edit')"><i class="icons btn-icon"></i></button>
|
||||
<button class="btn btn-xs" title="Pixel Magic Tool" type="button" id="pxmb" onclick="window.location.href=getURL('/pxmagic.htm')"><i class="icons btn-icon"></i></button>
|
||||
<button class="btn btn-xs" title="PixelForge" type="button" onclick="window.location.href=getURL('/pixelforge.htm')"><i class="icons btn-icon"></i></button>
|
||||
<button class="btn btn-xs" title="Add custom palette" type="button" id="adPal" onclick="window.location.href=getURL('/cpal.htm')"><i class="icons btn-icon"></i></button>
|
||||
<button class="btn btn-xs" title="Remove last custom palette" type="button" id="rmPal" onclick="palettesData=null;localStorage.removeItem('wledPalx');requestJson({rmcpal:true});setTimeout(loadPalettes,250,loadPalettesData);"><i class="icons btn-icon"></i></button>
|
||||
</div>
|
||||
|
||||
@@ -16,13 +16,12 @@ var simplifiedUI = false;
|
||||
var tr = 7;
|
||||
var d = document;
|
||||
const ranges = RangeTouch.setup('input[type="range"]', {});
|
||||
var retry = false;
|
||||
var palettesData;
|
||||
var fxdata = [];
|
||||
var pJson = {}, eJson = {}, lJson = {};
|
||||
var plJson = {}; // array of playlists
|
||||
var pN = "", pI = 0, pNum = 0;
|
||||
var pmt = 1, pmtLS = 0, pmtLast = 0;
|
||||
var pmt = 1, pmtLS = 0;
|
||||
var lastinfo = {};
|
||||
var isM = false, mw = 0, mh=0;
|
||||
var ws, wsRpt=0;
|
||||
@@ -200,19 +199,17 @@ function loadBg() {
|
||||
});
|
||||
}
|
||||
|
||||
function loadSkinCSS(cId)
|
||||
{
|
||||
if (!gId(cId)) // check if element exists
|
||||
{
|
||||
var h = d.getElementsByTagName('head')[0];
|
||||
var l = d.createElement('link');
|
||||
l.id = cId;
|
||||
l.rel = 'stylesheet';
|
||||
l.type = 'text/css';
|
||||
function loadSkinCSS(cId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (gId(cId)) return resolve();
|
||||
const l = d.createElement('link');
|
||||
l.id = cId;
|
||||
l.rel = 'stylesheet';
|
||||
l.href = getURL('/skin.css');
|
||||
l.media = 'all';
|
||||
h.appendChild(l);
|
||||
}
|
||||
l.onload = resolve;
|
||||
l.onerror = reject;
|
||||
d.head.appendChild(l);
|
||||
});
|
||||
}
|
||||
|
||||
function getURL(path) {
|
||||
@@ -278,19 +275,23 @@ function onLoad()
|
||||
cpick.on("color:change", () => {updatePSliders()});
|
||||
pmtLS = localStorage.getItem('wledPmt');
|
||||
|
||||
// Load initial data
|
||||
loadPalettes(()=>{
|
||||
// fill effect extra data array
|
||||
loadFXData(()=>{
|
||||
// load and populate effects
|
||||
setTimeout(()=>{loadFX(()=>{
|
||||
loadPalettesData(()=>{
|
||||
requestJson();// will load presets and create WS
|
||||
if (cfg.comp.css) setTimeout(()=>{loadSkinCSS('skinCss')},50);
|
||||
});
|
||||
})},50);
|
||||
});
|
||||
});
|
||||
// Load initial data sequentially, no parallel requests to avoid "503" errors when heap is low (slower but much more reliable)
|
||||
(async ()=>{
|
||||
try {
|
||||
await loadPalettes(); // loads base palettes and builds #pallist (safe first)
|
||||
await loadFXData(); // loads fx data
|
||||
await loadFX(); // populates effect list
|
||||
await requestJson(); // updates info variables
|
||||
await loadPalettesData(); // fills palettesData[] for previews
|
||||
populatePalettes(); // repopulate with custom palettes now that cpalcount is known
|
||||
if(pmt == pmtLS) populatePresets(true); // load presets from localStorage if signature matches (i.e. no device reboot)
|
||||
else await loadPresets(); // load and populate presets
|
||||
if (cfg.comp.css) await loadSkinCSS('skinCss');
|
||||
if (!ws) makeWS();
|
||||
} catch(e) {
|
||||
showToast("Init failed: " + e, true);
|
||||
}
|
||||
})();
|
||||
resetUtil();
|
||||
|
||||
d.addEventListener("visibilitychange", handleVisibilityChange, false);
|
||||
@@ -448,7 +449,7 @@ function presetError(empty)
|
||||
if (bckstr.length > 10) hasBackup = true;
|
||||
} catch (e) {}
|
||||
|
||||
var cn = `<div class="pres c" style="padding:8px;margin-bottom:8px;${empty?'':'cursor:pointer;'}" ${empty?'':'onclick="pmtLast=0;loadPresets();"'}>`;
|
||||
var cn = `<div class="pres c" style="padding:8px;margin-bottom:8px;${empty?'':'cursor:pointer;'}" ${empty?'':'onclick="loadPresets();"'}>`;
|
||||
if (empty)
|
||||
cn += `You have no presets yet!`;
|
||||
else
|
||||
@@ -481,123 +482,81 @@ function restore(txt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function loadPresets(callback = null)
|
||||
{
|
||||
// 1st boot (because there is a callback)
|
||||
if (callback && pmt == pmtLS && pmt > 0) {
|
||||
// we have a copy of the presets in local storage and don't need to fetch another one
|
||||
populatePresets(true);
|
||||
pmtLast = pmt;
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// afterwards
|
||||
if (!callback && pmt == pmtLast) return;
|
||||
|
||||
fetch(getURL('/presets.json'), {
|
||||
method: 'get'
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status=="404") return {"0":{}};
|
||||
//if (!res.ok) showErrorToast();
|
||||
return res.json();
|
||||
})
|
||||
.then(json => {
|
||||
pJson = json;
|
||||
pmtLast = pmt;
|
||||
populatePresets();
|
||||
})
|
||||
.catch((e)=>{
|
||||
//showToast(e, true);
|
||||
presetError(false);
|
||||
})
|
||||
.finally(()=>{
|
||||
if (callback) setTimeout(callback,99);
|
||||
async function loadPresets() {
|
||||
return new Promise((resolve) => {
|
||||
fetch(getURL('/presets.json'), {method: 'get'})
|
||||
.then(res => res.status=="404" ? {"0":{}} : res.json())
|
||||
.then(json => {
|
||||
pJson = json;
|
||||
populatePresets();
|
||||
resolve();
|
||||
})
|
||||
.catch(() => {
|
||||
presetError(false);
|
||||
resolve();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function loadPalettes(callback = null)
|
||||
{
|
||||
fetch(getURL('/json/palettes'), {
|
||||
method: 'get'
|
||||
})
|
||||
.then((res)=>{
|
||||
if (!res.ok) showErrorToast();
|
||||
return res.json();
|
||||
})
|
||||
.then((json)=>{
|
||||
lJson = Object.entries(json);
|
||||
populatePalettes();
|
||||
retry = false;
|
||||
})
|
||||
.catch((e)=>{
|
||||
if (!retry) {
|
||||
retry = true;
|
||||
setTimeout(loadPalettes, 500); // retry
|
||||
}
|
||||
showToast(e, true);
|
||||
})
|
||||
.finally(()=>{
|
||||
if (callback) callback();
|
||||
updateUI();
|
||||
async function loadPalettes(retry=0) {
|
||||
return new Promise((resolve) => {
|
||||
fetch(getURL('/json/palettes'), {method: 'get'})
|
||||
.then(res => res.ok ? res.json() : Promise.reject())
|
||||
.then(json => {
|
||||
lJson = Object.entries(json);
|
||||
populatePalettes();
|
||||
resolve();
|
||||
})
|
||||
.catch((e) => {
|
||||
if (retry<5) {
|
||||
setTimeout(() => loadPalettes(retry+1).then(resolve), 100);
|
||||
} else {
|
||||
showToast(e, true);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadFX(callback = null)
|
||||
{
|
||||
fetch(getURL('/json/effects'), {
|
||||
method: 'get'
|
||||
})
|
||||
.then((res)=>{
|
||||
if (!res.ok) showErrorToast();
|
||||
return res.json();
|
||||
})
|
||||
.then((json)=>{
|
||||
eJson = Object.entries(json);
|
||||
populateEffects();
|
||||
retry = false;
|
||||
})
|
||||
.catch((e)=>{
|
||||
if (!retry) {
|
||||
retry = true;
|
||||
setTimeout(loadFX, 500); // retry
|
||||
}
|
||||
showToast(e, true);
|
||||
})
|
||||
.finally(()=>{
|
||||
if (callback) callback();
|
||||
updateUI();
|
||||
async function loadFX(retry=0) {
|
||||
return new Promise((resolve) => {
|
||||
fetch(getURL('/json/effects'), {method: 'get'})
|
||||
.then(res => res.ok ? res.json() : Promise.reject())
|
||||
.then(json => {
|
||||
eJson = Object.entries(json);
|
||||
populateEffects();
|
||||
resolve();
|
||||
})
|
||||
.catch((e) => {
|
||||
if (retry<5) {
|
||||
setTimeout(() => loadFX(retry+1).then(resolve), 100);
|
||||
} else {
|
||||
showToast(e, true);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadFXData(callback = null)
|
||||
{
|
||||
fetch(getURL('/json/fxdata'), {
|
||||
method: 'get'
|
||||
})
|
||||
.then((res)=>{
|
||||
if (!res.ok) showErrorToast();
|
||||
return res.json();
|
||||
})
|
||||
.then((json)=>{
|
||||
fxdata = json||[];
|
||||
// add default value for Solid
|
||||
fxdata.shift()
|
||||
fxdata.unshift(";!;");
|
||||
retry = false;
|
||||
})
|
||||
.catch((e)=>{
|
||||
fxdata = [];
|
||||
if (!retry) {
|
||||
retry = true;
|
||||
setTimeout(()=>{loadFXData(loadFX);}, 500); // retry
|
||||
}
|
||||
showToast(e, true);
|
||||
})
|
||||
.finally(()=>{
|
||||
if (callback) callback();
|
||||
updateUI();
|
||||
async function loadFXData(retry=0) {
|
||||
return new Promise((resolve) => {
|
||||
fetch(getURL('/json/fxdata'), {method: 'get'})
|
||||
.then(res => res.ok ? res.json() : Promise.reject())
|
||||
.then(json => {
|
||||
fxdata = json||[];
|
||||
fxdata.shift();
|
||||
fxdata.unshift(";!;");
|
||||
resolve();
|
||||
})
|
||||
.catch((e) => {
|
||||
fxdata = [];
|
||||
if (retry<5) {
|
||||
setTimeout(() => loadFXData(retry+1).then(resolve), 100);
|
||||
} else {
|
||||
showToast(e, true);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -619,7 +578,7 @@ function populateQL()
|
||||
function populatePresets(fromls)
|
||||
{
|
||||
if (fromls) pJson = JSON.parse(localStorage.getItem("wledP"));
|
||||
if (!pJson) {setTimeout(loadPresets,250); return;}
|
||||
if (!pJson) {loadPresets(); return;} // note: no await as this is a fallback that should not be needed as init function fetches pJson
|
||||
delete pJson["0"];
|
||||
var cn = "";
|
||||
var arr = Object.entries(pJson).sort(cmpP);
|
||||
@@ -672,7 +631,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,16 +652,18 @@ 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
|
||||
//var setInnerHTML = function(elm, html) {
|
||||
// elm.innerHTML = html;
|
||||
// Array.from(elm.querySelectorAll("script")).forEach( oldScript => {
|
||||
// const newScript = document.createElement("script");
|
||||
// const newScript = d.createElement("script");
|
||||
// Array.from(oldScript.attributes)
|
||||
// .forEach( attr => newScript.setAttribute(attr.name, attr.value) );
|
||||
// newScript.appendChild(document.createTextNode(oldScript.innerHTML));
|
||||
// newScript.appendChild(d.createTextNode(oldScript.innerHTML));
|
||||
// oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||
// });
|
||||
//}
|
||||
@@ -905,6 +865,7 @@ function populateSegments(s)
|
||||
gId("segcont").classList.remove("hide");
|
||||
let noNewSegs = (lowestUnused >= maxSeg);
|
||||
resetUtil(noNewSegs);
|
||||
if (segCount === 0) return; // no segments to populate
|
||||
for (var i = 0; i <= lSeg; i++) {
|
||||
if (!gId(`seg${i}`)) continue;
|
||||
updateLen(i);
|
||||
@@ -1275,7 +1236,6 @@ function updateUI()
|
||||
gId('buttonPower').className = (isOn) ? 'active':'';
|
||||
gId('buttonNl').className = (nlA) ? 'active':'';
|
||||
gId('buttonSync').className = (syncSend) ? 'active':'';
|
||||
gId('pxmb').style.display = (isM) ? "inline-block" : "none";
|
||||
|
||||
updateSelectedFx();
|
||||
updateSelectedPalette(selectedPal); // must be after updateSelectedFx() to un-hide color slots for * palettes
|
||||
@@ -1323,7 +1283,8 @@ function updateSelectedPalette(s)
|
||||
if (selElement) selElement.classList.remove('selected');
|
||||
|
||||
var selectedPalette = parent.querySelector(`.lstI[data-id="${s}"]`);
|
||||
if (selectedPalette) parent.querySelector(`.lstI[data-id="${s}"]`).classList.add('selected');
|
||||
if (!selectedPalette) return; // palette not yet loaded (custom palette on initial load)
|
||||
selectedPalette.classList.add('selected');
|
||||
|
||||
// Display selected palette name on button in simplified UI
|
||||
let selectedName = selectedPalette.querySelector(".lstIname").innerText;
|
||||
@@ -1437,7 +1398,7 @@ function makeWS() {
|
||||
};
|
||||
ws.onclose = (e)=>{
|
||||
gId('connind').style.backgroundColor = "var(--c-r)";
|
||||
if (wsRpt++ < 5) setTimeout(makeWS,1500); // retry WS connection
|
||||
if (wsRpt++ < 10) setTimeout(makeWS,wsRpt * 200); // retry WS connection
|
||||
ws = null;
|
||||
}
|
||||
ws.onopen = (e)=>{
|
||||
@@ -1470,6 +1431,7 @@ function readState(s,command=false)
|
||||
|
||||
populateSegments(s);
|
||||
hasRGB = hasWhite = hasCCT = has2D = false;
|
||||
segLmax = 0; // reset max selected segment length
|
||||
let i = {};
|
||||
// determine light capabilities from selected segments
|
||||
for (let seg of (s.seg||[])) {
|
||||
@@ -1709,77 +1671,68 @@ function setEffectParameters(idx)
|
||||
|
||||
var jsonTimeout;
|
||||
var reqsLegal = false;
|
||||
async function requestJson(command=null, retry=0) {
|
||||
return new Promise((resolve, reject) => {
|
||||
gId('connind').style.backgroundColor = "var(--c-y)";
|
||||
if (command && !reqsLegal) {resolve(); return;}
|
||||
if (!jsonTimeout) jsonTimeout = setTimeout(()=>{if (ws) ws.close(); ws=null; showErrorToast()}, 3000);
|
||||
|
||||
function requestJson(command=null)
|
||||
{
|
||||
gId('connind').style.backgroundColor = "var(--c-y)";
|
||||
if (command && !reqsLegal) return; // stop post requests from chrome onchange event on page restore
|
||||
if (!jsonTimeout) jsonTimeout = setTimeout(()=>{if (ws) ws.close(); ws=null; showErrorToast()}, 3000);
|
||||
var req = null;
|
||||
var useWs = (ws && ws.readyState === WebSocket.OPEN);
|
||||
var type = command ? 'post':'get';
|
||||
if (command) {
|
||||
command.v = true; // force complete /json/si API response
|
||||
command.time = Math.floor(Date.now() / 1000);
|
||||
var t = gId('tt');
|
||||
if (t.validity.valid && command.transition==null) {
|
||||
var tn = parseInt(t.value*10);
|
||||
if (tn != tr) command.transition = tn;
|
||||
var useWs = (ws && ws.readyState === WebSocket.OPEN);
|
||||
var req = null;
|
||||
if (command) {
|
||||
command.v = true;
|
||||
command.time = Math.floor(Date.now() / 1000);
|
||||
var t = gId('tt');
|
||||
if (t && t.validity.valid && command.transition==null) {
|
||||
var tn = parseInt(t.value*10);
|
||||
if (tn != tr) command.transition = tn;
|
||||
}
|
||||
req = JSON.stringify(command);
|
||||
if (req.length > 1340) useWs = false;
|
||||
if (req.length > 500 && lastinfo && lastinfo.arch == "esp8266") useWs = false;
|
||||
}
|
||||
//command.bs = parseInt(gId('bs').value);
|
||||
req = JSON.stringify(command);
|
||||
if (req.length > 1340) useWs = false; // do not send very long requests over websocket
|
||||
if (req.length > 500 && lastinfo && lastinfo.arch == "esp8266") useWs = false; // esp8266 can only handle 500 bytes
|
||||
};
|
||||
|
||||
if (useWs) {
|
||||
ws.send(req?req:'{"v":true}');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(getURL('/json/si'), {
|
||||
method: type,
|
||||
headers: {"Content-Type": "application/json; charset=UTF-8"},
|
||||
body: req
|
||||
})
|
||||
.then(res => {
|
||||
clearTimeout(jsonTimeout);
|
||||
jsonTimeout = null;
|
||||
if (!res.ok) showErrorToast();
|
||||
return res.json();
|
||||
})
|
||||
.then(json => {
|
||||
lastUpdate = new Date();
|
||||
clearErrorToast(3000);
|
||||
gId('connind').style.backgroundColor = "var(--c-g)";
|
||||
if (!json) { showToast('Empty response', true); return; }
|
||||
if (json.success) return;
|
||||
if (json.info) {
|
||||
let i = json.info;
|
||||
parseInfo(i);
|
||||
populatePalettes(i);
|
||||
if (isInfo) populateInfo(i);
|
||||
if (simplifiedUI) simplifyUI();
|
||||
if (useWs) {
|
||||
ws.send(req?req:'{"v":true}');
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
var s = json.state ? json.state : json;
|
||||
readState(s);
|
||||
|
||||
//load presets and open websocket sequentially
|
||||
if (!pJson || isEmpty(pJson)) setTimeout(()=>{
|
||||
loadPresets(()=>{
|
||||
wsRpt = 0;
|
||||
if (!(ws && ws.readyState === WebSocket.OPEN)) makeWS();
|
||||
});
|
||||
},25);
|
||||
reqsLegal = true;
|
||||
retry = false;
|
||||
})
|
||||
.catch((e)=>{
|
||||
if (!retry) {
|
||||
retry = true;
|
||||
setTimeout(requestJson,500);
|
||||
}
|
||||
showToast(e, true);
|
||||
fetch(getURL('/json/si'), {
|
||||
method: command ? 'post' : 'get',
|
||||
headers: {"Content-Type": "application/json; charset=UTF-8"},
|
||||
body: req
|
||||
})
|
||||
.then(res => {
|
||||
clearTimeout(jsonTimeout);
|
||||
jsonTimeout = null;
|
||||
return res.ok ? res.json() : Promise.reject();
|
||||
})
|
||||
.then(json => {
|
||||
lastUpdate = new Date();
|
||||
clearErrorToast(3000);
|
||||
gId('connind').style.backgroundColor = "var(--c-g)";
|
||||
if (!json) { showToast('Empty response', true); resolve(); return; }
|
||||
if (json.success) {resolve(); return;}
|
||||
if (json.info) {
|
||||
parseInfo(json.info);
|
||||
if (isInfo) populateInfo(json.info);
|
||||
if (simplifiedUI) simplifyUI();
|
||||
}
|
||||
var s = json.state ? json.state : json;
|
||||
readState(s);
|
||||
|
||||
reqsLegal = true;
|
||||
resolve();
|
||||
})
|
||||
.catch((e)=>{
|
||||
if (retry<10) {
|
||||
setTimeout(() => requestJson(command,retry+1).then(resolve).catch(reject), retry*50);
|
||||
} else {
|
||||
showToast(e, true);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2554,7 +2507,7 @@ function saveP(i,pl)
|
||||
}
|
||||
populatePresets();
|
||||
resetPUtil();
|
||||
setTimeout(()=>{pmtLast=0; loadPresets();}, 750); // force reloading of presets
|
||||
setTimeout(()=>{loadPresets();}, 750); // force reloading of presets
|
||||
}
|
||||
|
||||
function testPl(i,bt) {
|
||||
@@ -2820,56 +2773,51 @@ function rSegs()
|
||||
requestJson(obj);
|
||||
}
|
||||
|
||||
function loadPalettesData(callback = null)
|
||||
{
|
||||
if (palettesData) return;
|
||||
const lsKey = "wledPalx";
|
||||
var lsPalData = localStorage.getItem(lsKey);
|
||||
if (lsPalData) {
|
||||
try {
|
||||
var d = JSON.parse(lsPalData);
|
||||
if (d && d.vid == d.vid) {
|
||||
palettesData = d.p;
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
function loadPalettesData() {
|
||||
return new Promise((resolve) => {
|
||||
if (palettesData) return resolve(); // already loaded
|
||||
var lsPalData = localStorage.getItem("wledPalx");
|
||||
if (lsPalData) {
|
||||
try {
|
||||
var d = JSON.parse(lsPalData);
|
||||
if (d && d.vid == lastinfo.vid) {
|
||||
palettesData = d.p;
|
||||
redrawPalPrev();
|
||||
return resolve();
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
palettesData = {};
|
||||
getPalettesData(0, ()=>{
|
||||
localStorage.setItem(lsKey, JSON.stringify({
|
||||
p: palettesData,
|
||||
vid: lastinfo.vid
|
||||
}));
|
||||
redrawPalPrev();
|
||||
if (callback) setTimeout(callback, 99);
|
||||
palettesData = {};
|
||||
getPalettesData(0, () => {
|
||||
localStorage.setItem("wledPalx", JSON.stringify({
|
||||
p: palettesData,
|
||||
vid: lastinfo.vid
|
||||
}));
|
||||
redrawPalPrev();
|
||||
setTimeout(resolve, 99); // delay optional
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getPalettesData(page, callback)
|
||||
{
|
||||
fetch(getURL(`/json/palx?page=${page}`), {
|
||||
method: 'get'
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) showErrorToast();
|
||||
return res.json();
|
||||
})
|
||||
function getPalettesData(page, callback, retry=0) {
|
||||
fetch(getURL(`/json/palx?page=${page}`), {method: 'get'})
|
||||
.then(res => res.ok ? res.json() : Promise.reject())
|
||||
.then(json => {
|
||||
retry = false;
|
||||
palettesData = Object.assign({}, palettesData, json.p);
|
||||
if (page < json.m) setTimeout(()=>{ getPalettesData(page + 1, callback); }, 75);
|
||||
else callback();
|
||||
})
|
||||
.catch((error)=>{
|
||||
if (!retry) {
|
||||
retry = true;
|
||||
setTimeout(()=>{getPalettesData(page,callback);}, 500); // retry
|
||||
if (retry<5) {
|
||||
setTimeout(()=>{getPalettesData(page,callback,retry+1);}, 100);
|
||||
} else {
|
||||
showToast(error, true);
|
||||
callback();
|
||||
}
|
||||
showToast(error, true);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
function hideModes(txt)
|
||||
{
|
||||
@@ -2971,7 +2919,7 @@ function filterFocus(e) {
|
||||
}
|
||||
if (e.type === "blur") {
|
||||
setTimeout(() => {
|
||||
if (e.target === document.activeElement && document.hasFocus()) return;
|
||||
if (e.target === d.activeElement && d.hasFocus()) return;
|
||||
// do not hide if filter is active
|
||||
if (!c) {
|
||||
// compute sticky top
|
||||
@@ -3151,7 +3099,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%';
|
||||
@@ -3219,7 +3166,7 @@ function simplifyUI() {
|
||||
// Create dropdown dialog
|
||||
function createDropdown(id, buttonText, dialogElements = null) {
|
||||
// Create dropdown dialog
|
||||
const dialog = document.createElement("dialog");
|
||||
const dialog = d.createElement("dialog");
|
||||
// Move every dialogElement to the dropdown dialog or if none are given, move all children of the element with the given id
|
||||
if (dialogElements) {
|
||||
dialogElements.forEach((e) => {
|
||||
@@ -3232,7 +3179,7 @@ function simplifyUI() {
|
||||
}
|
||||
|
||||
// Create button for the dropdown
|
||||
const btn = document.createElement("button");
|
||||
const btn = d.createElement("button");
|
||||
btn.id = id + "btn";
|
||||
btn.classList.add("btn");
|
||||
btn.innerText = buttonText;
|
||||
@@ -3280,7 +3227,7 @@ function simplifyUI() {
|
||||
|
||||
// Hide palette label
|
||||
gId("pall").style.display = "none";
|
||||
gId("Colors").insertBefore(document.createElement("br"), gId("pall"));
|
||||
gId("Colors").insertBefore(d.createElement("br"), gId("pall"));
|
||||
// Hide effect label
|
||||
gId("modeLabel").style.display = "none";
|
||||
|
||||
@@ -3292,7 +3239,7 @@ function simplifyUI() {
|
||||
|
||||
// Hide bottom bar
|
||||
gId("bot").style.display = "none";
|
||||
document.documentElement.style.setProperty('--bh', '0px');
|
||||
d.documentElement.style.setProperty('--bh', '0px');
|
||||
|
||||
// Hide other tabs
|
||||
gId("Effects").style.display = "none";
|
||||
@@ -3306,6 +3253,197 @@ 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)
|
||||
repo: infoData.repo // GitHub repository (always present)
|
||||
};
|
||||
|
||||
// Add optional fields if available
|
||||
if (infoData.psramPresent !== undefined) upgradeData.psramPresent = infoData.psramPresent; // Whether device has PSRAM
|
||||
if (infoData.psramSize !== undefined) upgradeData.psramSize = infoData.psramSize; // Total PSRAM size in 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);
|
||||
|
||||
|
||||
@@ -18,7 +18,14 @@
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var d = document;
|
||||
// load common.js with retry on error
|
||||
(function common() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => S();
|
||||
l.onerror = () => setTimeout(common, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
var ws;
|
||||
var tmout = null;
|
||||
var c;
|
||||
@@ -31,7 +38,7 @@
|
||||
ctx.fillRect(Math.round((i - start) * w / skip), 0, Math.ceil(w), c.height);
|
||||
}
|
||||
}
|
||||
function update() { // via HTTP (/json/live)
|
||||
function update(retry=0) { // via HTTP (/json/live)
|
||||
if (d.hidden) {
|
||||
clearTimeout(tmout);
|
||||
tmout = setTimeout(update, 250);
|
||||
@@ -53,7 +60,7 @@
|
||||
.catch((error)=>{
|
||||
//console.error("Peek HTTP error:",error);
|
||||
clearTimeout(tmout);
|
||||
tmout = setTimeout(update, 2500);
|
||||
if (retry<5) tmout = setTimeout(() => update(retry+1), 2500); // stop endlessly bugging the ESP if resource is not available
|
||||
})
|
||||
}
|
||||
function S() { // Startup function (onload)
|
||||
@@ -62,32 +69,11 @@
|
||||
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(ws => 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]})`);
|
||||
@@ -99,7 +85,7 @@
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<canvas id="canv"></canvas>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -14,6 +14,14 @@
|
||||
<body>
|
||||
<canvas id="canv"></canvas>
|
||||
<script>
|
||||
// load common.js with retry on error
|
||||
(function common() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => S();
|
||||
l.onerror = () => setTimeout(common, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
var c = document.getElementById('canv');
|
||||
var leds = "";
|
||||
var throttled = false;
|
||||
@@ -21,53 +29,35 @@
|
||||
c.width = window.innerWidth * 0.98; //remove scroll bars
|
||||
c.height = window.innerHeight * 0.98; //remove scroll bars
|
||||
}
|
||||
setCanvas();
|
||||
// Check for canvas support
|
||||
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";
|
||||
ws.addEventListener('message',(e)=>{
|
||||
try {
|
||||
if (toString.call(e.data) === '[object ArrayBuffer]') {
|
||||
let leds = new Uint8Array(event.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
|
||||
let pPL = Math.min(c.width / mW, c.height / mH); // pixels per LED (width of circle)
|
||||
let lOf = Math.floor((c.width - pPL*mW)/2); //left offset (to center matrix)
|
||||
var i = 4;
|
||||
for (y=0.5;y<mH;y++) for (x=0.5; x<mW; x++) {
|
||||
ctx.fillStyle = `rgb(${leds[i]},${leds[i+1]},${leds[i+2]})`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x*pPL+lOf, y*pPL, pPL*0.4, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
i+=3;
|
||||
function S() { // Startup function (onload)
|
||||
setCanvas();
|
||||
// Check for canvas support
|
||||
var ctx = c.getContext('2d');
|
||||
if (ctx) { // Access the rendering context
|
||||
ws = connectWs(ws => ws.send('{"lv":true}')); // use parent WS or open new
|
||||
ws.addEventListener('message',(e)=>{
|
||||
try {
|
||||
if (toString.call(e.data) === '[object ArrayBuffer]') {
|
||||
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
|
||||
let pPL = Math.min(c.width / mW, c.height / mH); // pixels per LED (width of circle)
|
||||
let lOf = Math.floor((c.width - pPL*mW)/2); //left offset (to center matrix)
|
||||
var i = 4;
|
||||
for (y=0.5;y<mH;y++) for (x=0.5; x<mW; x++) {
|
||||
ctx.fillStyle = `rgb(${leds[i]},${leds[i+1]},${leds[i+2]})`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x*pPL+lOf, y*pPL, pPL*0.4, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
i+=3;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Peek WS error:",err);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Peek WS error:",err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
// window.resize event listener
|
||||
window.addEventListener('resize', (e)=>{
|
||||
|
||||
809
wled00/data/pixelforge/omggif.js
Normal file
809
wled00/data/pixelforge/omggif.js
Normal file
@@ -0,0 +1,809 @@
|
||||
// (c) Dean McNamee <dean@gmail.com>, 2013.
|
||||
//
|
||||
// https://github.com/deanm/omggif
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
//
|
||||
// omggif is a JavaScript implementation of a GIF 89a encoder and decoder,
|
||||
// including animation and compression. It does not rely on any specific
|
||||
// underlying system, so should run in the browser, Node, or Plask.
|
||||
|
||||
"use strict";
|
||||
|
||||
function GifWriter(buf, width, height, gopts) {
|
||||
var p = 0;
|
||||
|
||||
var gopts = gopts === undefined ? { } : gopts;
|
||||
var loop_count = gopts.loop === undefined ? null : gopts.loop;
|
||||
var global_palette = gopts.palette === undefined ? null : gopts.palette;
|
||||
|
||||
if (width <= 0 || height <= 0 || width > 65535 || height > 65535)
|
||||
throw new Error("Width/Height invalid.");
|
||||
|
||||
function check_palette_and_num_colors(palette) {
|
||||
var num_colors = palette.length;
|
||||
if (num_colors < 2 || num_colors > 256 || num_colors & (num_colors-1)) {
|
||||
throw new Error(
|
||||
"Invalid code/color length, must be power of 2 and 2 .. 256.");
|
||||
}
|
||||
return num_colors;
|
||||
}
|
||||
|
||||
// - Header.
|
||||
buf[p++] = 0x47; buf[p++] = 0x49; buf[p++] = 0x46; // GIF
|
||||
buf[p++] = 0x38; buf[p++] = 0x39; buf[p++] = 0x61; // 89a
|
||||
|
||||
// Handling of Global Color Table (palette) and background index.
|
||||
var gp_num_colors_pow2 = 0;
|
||||
var background = 0;
|
||||
if (global_palette !== null) {
|
||||
var gp_num_colors = check_palette_and_num_colors(global_palette);
|
||||
while (gp_num_colors >>= 1) ++gp_num_colors_pow2;
|
||||
gp_num_colors = 1 << gp_num_colors_pow2;
|
||||
--gp_num_colors_pow2;
|
||||
if (gopts.background !== undefined) {
|
||||
background = gopts.background;
|
||||
if (background >= gp_num_colors)
|
||||
throw new Error("Background index out of range.");
|
||||
// The GIF spec states that a background index of 0 should be ignored, so
|
||||
// this is probably a mistake and you really want to set it to another
|
||||
// slot in the palette. But actually in the end most browsers, etc end
|
||||
// up ignoring this almost completely (including for dispose background).
|
||||
if (background === 0)
|
||||
throw new Error("Background index explicitly passed as 0.");
|
||||
}
|
||||
}
|
||||
|
||||
// - Logical Screen Descriptor.
|
||||
// NOTE(deanm): w/h apparently ignored by implementations, but set anyway.
|
||||
buf[p++] = width & 0xff; buf[p++] = width >> 8 & 0xff;
|
||||
buf[p++] = height & 0xff; buf[p++] = height >> 8 & 0xff;
|
||||
// NOTE: Indicates 0-bpp original color resolution (unused?).
|
||||
buf[p++] = (global_palette !== null ? 0x80 : 0) | // Global Color Table Flag.
|
||||
gp_num_colors_pow2; // NOTE: No sort flag (unused?).
|
||||
buf[p++] = background; // Background Color Index.
|
||||
buf[p++] = 0; // Pixel aspect ratio (unused?).
|
||||
|
||||
// - Global Color Table
|
||||
if (global_palette !== null) {
|
||||
for (var i = 0, il = global_palette.length; i < il; ++i) {
|
||||
var rgb = global_palette[i];
|
||||
buf[p++] = rgb >> 16 & 0xff;
|
||||
buf[p++] = rgb >> 8 & 0xff;
|
||||
buf[p++] = rgb & 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
if (loop_count !== null) { // Netscape block for looping.
|
||||
if (loop_count < 0 || loop_count > 65535)
|
||||
throw new Error("Loop count invalid.")
|
||||
// Extension code, label, and length.
|
||||
buf[p++] = 0x21; buf[p++] = 0xff; buf[p++] = 0x0b;
|
||||
// NETSCAPE2.0
|
||||
buf[p++] = 0x4e; buf[p++] = 0x45; buf[p++] = 0x54; buf[p++] = 0x53;
|
||||
buf[p++] = 0x43; buf[p++] = 0x41; buf[p++] = 0x50; buf[p++] = 0x45;
|
||||
buf[p++] = 0x32; buf[p++] = 0x2e; buf[p++] = 0x30;
|
||||
// Sub-block
|
||||
buf[p++] = 0x03; buf[p++] = 0x01;
|
||||
buf[p++] = loop_count & 0xff; buf[p++] = loop_count >> 8 & 0xff;
|
||||
buf[p++] = 0x00; // Terminator.
|
||||
}
|
||||
|
||||
|
||||
var ended = false;
|
||||
|
||||
this.addFrame = function(x, y, w, h, indexed_pixels, opts) {
|
||||
if (ended === true) { --p; ended = false; } // Un-end.
|
||||
|
||||
opts = opts === undefined ? { } : opts;
|
||||
|
||||
// TODO(deanm): Bounds check x, y. Do they need to be within the virtual
|
||||
// canvas width/height, I imagine?
|
||||
if (x < 0 || y < 0 || x > 65535 || y > 65535)
|
||||
throw new Error("x/y invalid.")
|
||||
|
||||
if (w <= 0 || h <= 0 || w > 65535 || h > 65535)
|
||||
throw new Error("Width/Height invalid.")
|
||||
|
||||
if (indexed_pixels.length < w * h)
|
||||
throw new Error("Not enough pixels for the frame size.");
|
||||
|
||||
var using_local_palette = true;
|
||||
var palette = opts.palette;
|
||||
if (palette === undefined || palette === null) {
|
||||
using_local_palette = false;
|
||||
palette = global_palette;
|
||||
}
|
||||
|
||||
if (palette === undefined || palette === null)
|
||||
throw new Error("Must supply either a local or global palette.");
|
||||
|
||||
var num_colors = check_palette_and_num_colors(palette);
|
||||
|
||||
// Compute the min_code_size (power of 2), destroying num_colors.
|
||||
var min_code_size = 0;
|
||||
while (num_colors >>= 1) ++min_code_size;
|
||||
num_colors = 1 << min_code_size; // Now we can easily get it back.
|
||||
|
||||
var delay = opts.delay === undefined ? 0 : opts.delay;
|
||||
|
||||
// From the spec:
|
||||
// 0 - No disposal specified. The decoder is
|
||||
// not required to take any action.
|
||||
// 1 - Do not dispose. The graphic is to be left
|
||||
// in place.
|
||||
// 2 - Restore to background color. The area used by the
|
||||
// graphic must be restored to the background color.
|
||||
// 3 - Restore to previous. The decoder is required to
|
||||
// restore the area overwritten by the graphic with
|
||||
// what was there prior to rendering the graphic.
|
||||
// 4-7 - To be defined.
|
||||
// NOTE(deanm): Dispose background doesn't really work, apparently most
|
||||
// browsers ignore the background palette index and clear to transparency.
|
||||
var disposal = opts.disposal === undefined ? 0 : opts.disposal;
|
||||
if (disposal < 0 || disposal > 3) // 4-7 is reserved.
|
||||
throw new Error("Disposal out of range.");
|
||||
|
||||
var use_transparency = false;
|
||||
var transparent_index = 0;
|
||||
if (opts.transparent !== undefined && opts.transparent !== null) {
|
||||
use_transparency = true;
|
||||
transparent_index = opts.transparent;
|
||||
if (transparent_index < 0 || transparent_index >= num_colors)
|
||||
throw new Error("Transparent color index.");
|
||||
}
|
||||
|
||||
if (disposal !== 0 || use_transparency || delay !== 0) {
|
||||
// - Graphics Control Extension
|
||||
buf[p++] = 0x21; buf[p++] = 0xf9; // Extension / Label.
|
||||
buf[p++] = 4; // Byte size.
|
||||
|
||||
buf[p++] = disposal << 2 | (use_transparency === true ? 1 : 0);
|
||||
buf[p++] = delay & 0xff; buf[p++] = delay >> 8 & 0xff;
|
||||
buf[p++] = transparent_index; // Transparent color index.
|
||||
buf[p++] = 0; // Block Terminator.
|
||||
}
|
||||
|
||||
// - Image Descriptor
|
||||
buf[p++] = 0x2c; // Image Seperator.
|
||||
buf[p++] = x & 0xff; buf[p++] = x >> 8 & 0xff; // Left.
|
||||
buf[p++] = y & 0xff; buf[p++] = y >> 8 & 0xff; // Top.
|
||||
buf[p++] = w & 0xff; buf[p++] = w >> 8 & 0xff;
|
||||
buf[p++] = h & 0xff; buf[p++] = h >> 8 & 0xff;
|
||||
// NOTE: No sort flag (unused?).
|
||||
// TODO(deanm): Support interlace.
|
||||
buf[p++] = using_local_palette === true ? (0x80 | (min_code_size-1)) : 0;
|
||||
|
||||
// - Local Color Table
|
||||
if (using_local_palette === true) {
|
||||
for (var i = 0, il = palette.length; i < il; ++i) {
|
||||
var rgb = palette[i];
|
||||
buf[p++] = rgb >> 16 & 0xff;
|
||||
buf[p++] = rgb >> 8 & 0xff;
|
||||
buf[p++] = rgb & 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
p = GifWriterOutputLZWCodeStream(
|
||||
buf, p, min_code_size < 2 ? 2 : min_code_size, indexed_pixels);
|
||||
|
||||
return p;
|
||||
};
|
||||
|
||||
this.end = function() {
|
||||
if (ended === false) {
|
||||
buf[p++] = 0x3b; // Trailer.
|
||||
ended = true;
|
||||
}
|
||||
return p;
|
||||
};
|
||||
|
||||
this.getOutputBuffer = function() { return buf; };
|
||||
this.setOutputBuffer = function(v) { buf = v; };
|
||||
this.getOutputBufferPosition = function() { return p; };
|
||||
this.setOutputBufferPosition = function(v) { p = v; };
|
||||
}
|
||||
|
||||
// Main compression routine, palette indexes -> LZW code stream.
|
||||
// |index_stream| must have at least one entry.
|
||||
function GifWriterOutputLZWCodeStream(buf, p, min_code_size, index_stream) {
|
||||
buf[p++] = min_code_size;
|
||||
var cur_subblock = p++; // Pointing at the length field.
|
||||
|
||||
var clear_code = 1 << min_code_size;
|
||||
var code_mask = clear_code - 1;
|
||||
var eoi_code = clear_code + 1;
|
||||
var next_code = eoi_code + 1;
|
||||
|
||||
var cur_code_size = min_code_size + 1; // Number of bits per code.
|
||||
var cur_shift = 0;
|
||||
// We have at most 12-bit codes, so we should have to hold a max of 19
|
||||
// bits here (and then we would write out).
|
||||
var cur = 0;
|
||||
|
||||
function emit_bytes_to_buffer(bit_block_size) {
|
||||
while (cur_shift >= bit_block_size) {
|
||||
buf[p++] = cur & 0xff;
|
||||
cur >>= 8; cur_shift -= 8;
|
||||
if (p === cur_subblock + 256) { // Finished a subblock.
|
||||
buf[cur_subblock] = 255;
|
||||
cur_subblock = p++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function emit_code(c) {
|
||||
cur |= c << cur_shift;
|
||||
cur_shift += cur_code_size;
|
||||
emit_bytes_to_buffer(8);
|
||||
}
|
||||
|
||||
// I am not an expert on the topic, and I don't want to write a thesis.
|
||||
// However, it is good to outline here the basic algorithm and the few data
|
||||
// structures and optimizations here that make this implementation fast.
|
||||
// The basic idea behind LZW is to build a table of previously seen runs
|
||||
// addressed by a short id (herein called output code). All data is
|
||||
// referenced by a code, which represents one or more values from the
|
||||
// original input stream. All input bytes can be referenced as the same
|
||||
// value as an output code. So if you didn't want any compression, you
|
||||
// could more or less just output the original bytes as codes (there are
|
||||
// some details to this, but it is the idea). In order to achieve
|
||||
// compression, values greater then the input range (codes can be up to
|
||||
// 12-bit while input only 8-bit) represent a sequence of previously seen
|
||||
// inputs. The decompressor is able to build the same mapping while
|
||||
// decoding, so there is always a shared common knowledge between the
|
||||
// encoding and decoder, which is also important for "timing" aspects like
|
||||
// how to handle variable bit width code encoding.
|
||||
//
|
||||
// One obvious but very important consequence of the table system is there
|
||||
// is always a unique id (at most 12-bits) to map the runs. 'A' might be
|
||||
// 4, then 'AA' might be 10, 'AAA' 11, 'AAAA' 12, etc. This relationship
|
||||
// can be used for an effecient lookup strategy for the code mapping. We
|
||||
// need to know if a run has been seen before, and be able to map that run
|
||||
// to the output code. Since we start with known unique ids (input bytes),
|
||||
// and then from those build more unique ids (table entries), we can
|
||||
// continue this chain (almost like a linked list) to always have small
|
||||
// integer values that represent the current byte chains in the encoder.
|
||||
// This means instead of tracking the input bytes (AAAABCD) to know our
|
||||
// current state, we can track the table entry for AAAABC (it is guaranteed
|
||||
// to exist by the nature of the algorithm) and the next character D.
|
||||
// Therefor the tuple of (table_entry, byte) is guaranteed to also be
|
||||
// unique. This allows us to create a simple lookup key for mapping input
|
||||
// sequences to codes (table indices) without having to store or search
|
||||
// any of the code sequences. So if 'AAAA' has a table entry of 12, the
|
||||
// tuple of ('AAAA', K) for any input byte K will be unique, and can be our
|
||||
// key. This leads to a integer value at most 20-bits, which can always
|
||||
// fit in an SMI value and be used as a fast sparse array / object key.
|
||||
|
||||
// Output code for the current contents of the index buffer.
|
||||
var ib_code = index_stream[0] & code_mask; // Load first input index.
|
||||
var code_table = { }; // Key'd on our 20-bit "tuple".
|
||||
|
||||
emit_code(clear_code); // Spec says first code should be a clear code.
|
||||
|
||||
// First index already loaded, process the rest of the stream.
|
||||
for (var i = 1, il = index_stream.length; i < il; ++i) {
|
||||
var k = index_stream[i] & code_mask;
|
||||
var cur_key = ib_code << 8 | k; // (prev, k) unique tuple.
|
||||
var cur_code = code_table[cur_key]; // buffer + k.
|
||||
|
||||
// Check if we have to create a new code table entry.
|
||||
if (cur_code === undefined) { // We don't have buffer + k.
|
||||
// Emit index buffer (without k).
|
||||
// This is an inline version of emit_code, because this is the core
|
||||
// writing routine of the compressor (and V8 cannot inline emit_code
|
||||
// because it is a closure here in a different context). Additionally
|
||||
// we can call emit_byte_to_buffer less often, because we can have
|
||||
// 30-bits (from our 31-bit signed SMI), and we know our codes will only
|
||||
// be 12-bits, so can safely have 18-bits there without overflow.
|
||||
// emit_code(ib_code);
|
||||
cur |= ib_code << cur_shift;
|
||||
cur_shift += cur_code_size;
|
||||
while (cur_shift >= 8) {
|
||||
buf[p++] = cur & 0xff;
|
||||
cur >>= 8; cur_shift -= 8;
|
||||
if (p === cur_subblock + 256) { // Finished a subblock.
|
||||
buf[cur_subblock] = 255;
|
||||
cur_subblock = p++;
|
||||
}
|
||||
}
|
||||
|
||||
if (next_code === 4096) { // Table full, need a clear.
|
||||
emit_code(clear_code);
|
||||
next_code = eoi_code + 1;
|
||||
cur_code_size = min_code_size + 1;
|
||||
code_table = { };
|
||||
} else { // Table not full, insert a new entry.
|
||||
// Increase our variable bit code sizes if necessary. This is a bit
|
||||
// tricky as it is based on "timing" between the encoding and
|
||||
// decoder. From the encoders perspective this should happen after
|
||||
// we've already emitted the index buffer and are about to create the
|
||||
// first table entry that would overflow our current code bit size.
|
||||
if (next_code >= (1 << cur_code_size)) ++cur_code_size;
|
||||
code_table[cur_key] = next_code++; // Insert into code table.
|
||||
}
|
||||
|
||||
ib_code = k; // Index buffer to single input k.
|
||||
} else {
|
||||
ib_code = cur_code; // Index buffer to sequence in code table.
|
||||
}
|
||||
}
|
||||
|
||||
emit_code(ib_code); // There will still be something in the index buffer.
|
||||
emit_code(eoi_code); // End Of Information.
|
||||
|
||||
// Flush / finalize the sub-blocks stream to the buffer.
|
||||
emit_bytes_to_buffer(1);
|
||||
|
||||
// Finish the sub-blocks, writing out any unfinished lengths and
|
||||
// terminating with a sub-block of length 0. If we have already started
|
||||
// but not yet used a sub-block it can just become the terminator.
|
||||
if (cur_subblock + 1 === p) { // Started but unused.
|
||||
buf[cur_subblock] = 0;
|
||||
} else { // Started and used, write length and additional terminator block.
|
||||
buf[cur_subblock] = p - cur_subblock - 1;
|
||||
buf[p++] = 0;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
function GifReader(buf) {
|
||||
var p = 0;
|
||||
|
||||
// - Header (GIF87a or GIF89a).
|
||||
if (buf[p++] !== 0x47 || buf[p++] !== 0x49 || buf[p++] !== 0x46 ||
|
||||
buf[p++] !== 0x38 || (buf[p++]+1 & 0xfd) !== 0x38 || buf[p++] !== 0x61) {
|
||||
throw new Error("Invalid GIF 87a/89a header.");
|
||||
}
|
||||
|
||||
// - Logical Screen Descriptor.
|
||||
var width = buf[p++] | buf[p++] << 8;
|
||||
var height = buf[p++] | buf[p++] << 8;
|
||||
var pf0 = buf[p++]; // <Packed Fields>.
|
||||
var global_palette_flag = pf0 >> 7;
|
||||
var num_global_colors_pow2 = pf0 & 0x7;
|
||||
var num_global_colors = 1 << (num_global_colors_pow2 + 1);
|
||||
var background = buf[p++];
|
||||
buf[p++]; // Pixel aspect ratio (unused?).
|
||||
|
||||
var global_palette_offset = null;
|
||||
var global_palette_size = null;
|
||||
|
||||
if (global_palette_flag) {
|
||||
global_palette_offset = p;
|
||||
global_palette_size = num_global_colors;
|
||||
p += num_global_colors * 3; // Seek past palette.
|
||||
}
|
||||
|
||||
var no_eof = true;
|
||||
|
||||
var frames = [ ];
|
||||
|
||||
var delay = 0;
|
||||
var transparent_index = null;
|
||||
var disposal = 0; // 0 - No disposal specified.
|
||||
var loop_count = null;
|
||||
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
while (no_eof && p < buf.length) {
|
||||
switch (buf[p++]) {
|
||||
case 0x21: // Graphics Control Extension Block
|
||||
switch (buf[p++]) {
|
||||
case 0xff: // Application specific block
|
||||
// Try if it's a Netscape block (with animation loop counter).
|
||||
if (buf[p ] !== 0x0b || // 21 FF already read, check block size.
|
||||
// NETSCAPE2.0
|
||||
buf[p+1 ] == 0x4e && buf[p+2 ] == 0x45 && buf[p+3 ] == 0x54 &&
|
||||
buf[p+4 ] == 0x53 && buf[p+5 ] == 0x43 && buf[p+6 ] == 0x41 &&
|
||||
buf[p+7 ] == 0x50 && buf[p+8 ] == 0x45 && buf[p+9 ] == 0x32 &&
|
||||
buf[p+10] == 0x2e && buf[p+11] == 0x30 &&
|
||||
// Sub-block
|
||||
buf[p+12] == 0x03 && buf[p+13] == 0x01 && buf[p+16] == 0) {
|
||||
p += 14;
|
||||
loop_count = buf[p++] | buf[p++] << 8;
|
||||
p++; // Skip terminator.
|
||||
} else { // We don't know what it is, just try to get past it.
|
||||
p += 12;
|
||||
while (true) { // Seek through subblocks.
|
||||
var block_size = buf[p++];
|
||||
// Bad block size (ex: undefined from an out of bounds read).
|
||||
if (!(block_size >= 0)) throw Error("Invalid block size");
|
||||
if (block_size === 0) break; // 0 size is terminator
|
||||
p += block_size;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xf9: // Graphics Control Extension
|
||||
if (buf[p++] !== 0x4 || buf[p+4] !== 0)
|
||||
throw new Error("Invalid graphics extension block.");
|
||||
var pf1 = buf[p++];
|
||||
delay = buf[p++] | buf[p++] << 8;
|
||||
transparent_index = buf[p++];
|
||||
if ((pf1 & 1) === 0) transparent_index = null;
|
||||
disposal = pf1 >> 2 & 0x7;
|
||||
p++; // Skip terminator.
|
||||
break;
|
||||
|
||||
case 0xfe: // Comment Extension.
|
||||
while (true) { // Seek through subblocks.
|
||||
var block_size = buf[p++];
|
||||
// Bad block size (ex: undefined from an out of bounds read).
|
||||
if (!(block_size >= 0)) throw Error("Invalid block size");
|
||||
if (block_size === 0) break; // 0 size is terminator
|
||||
// console.log(buf.slice(p, p+block_size).toString('ascii'));
|
||||
p += block_size;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(
|
||||
"Unknown graphic control label: 0x" + buf[p-1].toString(16));
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x2c: // Image Descriptor.
|
||||
var x = buf[p++] | buf[p++] << 8;
|
||||
var y = buf[p++] | buf[p++] << 8;
|
||||
var w = buf[p++] | buf[p++] << 8;
|
||||
var h = buf[p++] | buf[p++] << 8;
|
||||
var pf2 = buf[p++];
|
||||
var local_palette_flag = pf2 >> 7;
|
||||
var interlace_flag = pf2 >> 6 & 1;
|
||||
var num_local_colors_pow2 = pf2 & 0x7;
|
||||
var num_local_colors = 1 << (num_local_colors_pow2 + 1);
|
||||
var palette_offset = global_palette_offset;
|
||||
var palette_size = global_palette_size;
|
||||
var has_local_palette = false;
|
||||
if (local_palette_flag) {
|
||||
var has_local_palette = true;
|
||||
palette_offset = p; // Override with local palette.
|
||||
palette_size = num_local_colors;
|
||||
p += num_local_colors * 3; // Seek past palette.
|
||||
}
|
||||
|
||||
var data_offset = p;
|
||||
|
||||
p++; // codesize
|
||||
while (true) {
|
||||
var block_size = buf[p++];
|
||||
// Bad block size (ex: undefined from an out of bounds read).
|
||||
if (!(block_size >= 0)) throw Error("Invalid block size");
|
||||
if (block_size === 0) break; // 0 size is terminator
|
||||
p += block_size;
|
||||
}
|
||||
|
||||
frames.push({x: x, y: y, width: w, height: h,
|
||||
has_local_palette: has_local_palette,
|
||||
palette_offset: palette_offset,
|
||||
palette_size: palette_size,
|
||||
data_offset: data_offset,
|
||||
data_length: p - data_offset,
|
||||
transparent_index: transparent_index,
|
||||
interlaced: !!interlace_flag,
|
||||
delay: delay,
|
||||
disposal: disposal});
|
||||
break;
|
||||
|
||||
case 0x3b: // Trailer Marker (end of file).
|
||||
no_eof = false;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Unknown gif block: 0x" + buf[p-1].toString(16));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.numFrames = function() {
|
||||
return frames.length;
|
||||
};
|
||||
|
||||
this.loopCount = function() {
|
||||
return loop_count;
|
||||
};
|
||||
|
||||
this.frameInfo = function(frame_num) {
|
||||
if (frame_num < 0 || frame_num >= frames.length)
|
||||
throw new Error("Frame index out of range.");
|
||||
return frames[frame_num];
|
||||
}
|
||||
|
||||
this.decodeAndBlitFrameBGRA = function(frame_num, pixels) {
|
||||
var frame = this.frameInfo(frame_num);
|
||||
var num_pixels = frame.width * frame.height;
|
||||
var index_stream = new Uint8Array(num_pixels); // At most 8-bit indices.
|
||||
GifReaderLZWOutputIndexStream(
|
||||
buf, frame.data_offset, index_stream, num_pixels);
|
||||
var palette_offset = frame.palette_offset;
|
||||
|
||||
// NOTE(deanm): It seems to be much faster to compare index to 256 than
|
||||
// to === null. Not sure why, but CompareStub_EQ_STRICT shows up high in
|
||||
// the profile, not sure if it's related to using a Uint8Array.
|
||||
var trans = frame.transparent_index;
|
||||
if (trans === null) trans = 256;
|
||||
|
||||
// We are possibly just blitting to a portion of the entire frame.
|
||||
// That is a subrect within the framerect, so the additional pixels
|
||||
// must be skipped over after we finished a scanline.
|
||||
var framewidth = frame.width;
|
||||
var framestride = width - framewidth;
|
||||
var xleft = framewidth; // Number of subrect pixels left in scanline.
|
||||
|
||||
// Output indicies of the top left and bottom right corners of the subrect.
|
||||
var opbeg = ((frame.y * width) + frame.x) * 4;
|
||||
var opend = ((frame.y + frame.height) * width + frame.x) * 4;
|
||||
var op = opbeg;
|
||||
|
||||
var scanstride = framestride * 4;
|
||||
|
||||
// Use scanstride to skip past the rows when interlacing. This is skipping
|
||||
// 7 rows for the first two passes, then 3 then 1.
|
||||
if (frame.interlaced === true) {
|
||||
scanstride += width * 4 * 7; // Pass 1.
|
||||
}
|
||||
|
||||
var interlaceskip = 8; // Tracking the row interval in the current pass.
|
||||
|
||||
for (var i = 0, il = index_stream.length; i < il; ++i) {
|
||||
var index = index_stream[i];
|
||||
|
||||
if (xleft === 0) { // Beginning of new scan line
|
||||
op += scanstride;
|
||||
xleft = framewidth;
|
||||
if (op >= opend) { // Catch the wrap to switch passes when interlacing.
|
||||
scanstride = framestride * 4 + width * 4 * (interlaceskip-1);
|
||||
// interlaceskip / 2 * 4 is interlaceskip << 1.
|
||||
op = opbeg + (framewidth + framestride) * (interlaceskip << 1);
|
||||
interlaceskip >>= 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (index === trans) {
|
||||
op += 4;
|
||||
} else {
|
||||
var r = buf[palette_offset + index * 3];
|
||||
var g = buf[palette_offset + index * 3 + 1];
|
||||
var b = buf[palette_offset + index * 3 + 2];
|
||||
pixels[op++] = b;
|
||||
pixels[op++] = g;
|
||||
pixels[op++] = r;
|
||||
pixels[op++] = 255;
|
||||
}
|
||||
--xleft;
|
||||
}
|
||||
};
|
||||
|
||||
// I will go to copy and paste hell one day...
|
||||
this.decodeAndBlitFrameRGBA = function(frame_num, pixels) {
|
||||
var frame = this.frameInfo(frame_num);
|
||||
var num_pixels = frame.width * frame.height;
|
||||
var index_stream = new Uint8Array(num_pixels); // At most 8-bit indices.
|
||||
GifReaderLZWOutputIndexStream(
|
||||
buf, frame.data_offset, index_stream, num_pixels);
|
||||
var palette_offset = frame.palette_offset;
|
||||
|
||||
// NOTE(deanm): It seems to be much faster to compare index to 256 than
|
||||
// to === null. Not sure why, but CompareStub_EQ_STRICT shows up high in
|
||||
// the profile, not sure if it's related to using a Uint8Array.
|
||||
var trans = frame.transparent_index;
|
||||
if (trans === null) trans = 256;
|
||||
|
||||
// We are possibly just blitting to a portion of the entire frame.
|
||||
// That is a subrect within the framerect, so the additional pixels
|
||||
// must be skipped over after we finished a scanline.
|
||||
var framewidth = frame.width;
|
||||
var framestride = width - framewidth;
|
||||
var xleft = framewidth; // Number of subrect pixels left in scanline.
|
||||
|
||||
// Output indicies of the top left and bottom right corners of the subrect.
|
||||
var opbeg = ((frame.y * width) + frame.x) * 4;
|
||||
var opend = ((frame.y + frame.height) * width + frame.x) * 4;
|
||||
var op = opbeg;
|
||||
|
||||
var scanstride = framestride * 4;
|
||||
|
||||
// Use scanstride to skip past the rows when interlacing. This is skipping
|
||||
// 7 rows for the first two passes, then 3 then 1.
|
||||
if (frame.interlaced === true) {
|
||||
scanstride += width * 4 * 7; // Pass 1.
|
||||
}
|
||||
|
||||
var interlaceskip = 8; // Tracking the row interval in the current pass.
|
||||
|
||||
for (var i = 0, il = index_stream.length; i < il; ++i) {
|
||||
var index = index_stream[i];
|
||||
|
||||
if (xleft === 0) { // Beginning of new scan line
|
||||
op += scanstride;
|
||||
xleft = framewidth;
|
||||
if (op >= opend) { // Catch the wrap to switch passes when interlacing.
|
||||
scanstride = framestride * 4 + width * 4 * (interlaceskip-1);
|
||||
// interlaceskip / 2 * 4 is interlaceskip << 1.
|
||||
op = opbeg + (framewidth + framestride) * (interlaceskip << 1);
|
||||
interlaceskip >>= 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (index === trans) {
|
||||
op += 4;
|
||||
} else {
|
||||
var r = buf[palette_offset + index * 3];
|
||||
var g = buf[palette_offset + index * 3 + 1];
|
||||
var b = buf[palette_offset + index * 3 + 2];
|
||||
pixels[op++] = r;
|
||||
pixels[op++] = g;
|
||||
pixels[op++] = b;
|
||||
pixels[op++] = 255;
|
||||
}
|
||||
--xleft;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function GifReaderLZWOutputIndexStream(code_stream, p, output, output_length) {
|
||||
var min_code_size = code_stream[p++];
|
||||
|
||||
var clear_code = 1 << min_code_size;
|
||||
var eoi_code = clear_code + 1;
|
||||
var next_code = eoi_code + 1;
|
||||
|
||||
var cur_code_size = min_code_size + 1; // Number of bits per code.
|
||||
// NOTE: This shares the same name as the encoder, but has a different
|
||||
// meaning here. Here this masks each code coming from the code stream.
|
||||
var code_mask = (1 << cur_code_size) - 1;
|
||||
var cur_shift = 0;
|
||||
var cur = 0;
|
||||
|
||||
var op = 0; // Output pointer.
|
||||
|
||||
var subblock_size = code_stream[p++];
|
||||
|
||||
// TODO(deanm): Would using a TypedArray be any faster? At least it would
|
||||
// solve the fast mode / backing store uncertainty.
|
||||
// var code_table = Array(4096);
|
||||
var code_table = new Int32Array(4096); // Can be signed, we only use 20 bits.
|
||||
|
||||
var prev_code = null; // Track code-1.
|
||||
|
||||
while (true) {
|
||||
// Read up to two bytes, making sure we always 12-bits for max sized code.
|
||||
while (cur_shift < 16) {
|
||||
if (subblock_size === 0) break; // No more data to be read.
|
||||
|
||||
cur |= code_stream[p++] << cur_shift;
|
||||
cur_shift += 8;
|
||||
|
||||
if (subblock_size === 1) { // Never let it get to 0 to hold logic above.
|
||||
subblock_size = code_stream[p++]; // Next subblock.
|
||||
} else {
|
||||
--subblock_size;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(deanm): We should never really get here, we should have received
|
||||
// and EOI.
|
||||
if (cur_shift < cur_code_size)
|
||||
break;
|
||||
|
||||
var code = cur & code_mask;
|
||||
cur >>= cur_code_size;
|
||||
cur_shift -= cur_code_size;
|
||||
|
||||
// TODO(deanm): Maybe should check that the first code was a clear code,
|
||||
// at least this is what you're supposed to do. But actually our encoder
|
||||
// now doesn't emit a clear code first anyway.
|
||||
if (code === clear_code) {
|
||||
// We don't actually have to clear the table. This could be a good idea
|
||||
// for greater error checking, but we don't really do any anyway. We
|
||||
// will just track it with next_code and overwrite old entries.
|
||||
|
||||
next_code = eoi_code + 1;
|
||||
cur_code_size = min_code_size + 1;
|
||||
code_mask = (1 << cur_code_size) - 1;
|
||||
|
||||
// Don't update prev_code ?
|
||||
prev_code = null;
|
||||
continue;
|
||||
} else if (code === eoi_code) {
|
||||
break;
|
||||
}
|
||||
|
||||
// We have a similar situation as the decoder, where we want to store
|
||||
// variable length entries (code table entries), but we want to do in a
|
||||
// faster manner than an array of arrays. The code below stores sort of a
|
||||
// linked list within the code table, and then "chases" through it to
|
||||
// construct the dictionary entries. When a new entry is created, just the
|
||||
// last byte is stored, and the rest (prefix) of the entry is only
|
||||
// referenced by its table entry. Then the code chases through the
|
||||
// prefixes until it reaches a single byte code. We have to chase twice,
|
||||
// first to compute the length, and then to actually copy the data to the
|
||||
// output (backwards, since we know the length). The alternative would be
|
||||
// storing something in an intermediate stack, but that doesn't make any
|
||||
// more sense. I implemented an approach where it also stored the length
|
||||
// in the code table, although it's a bit tricky because you run out of
|
||||
// bits (12 + 12 + 8), but I didn't measure much improvements (the table
|
||||
// entries are generally not the long). Even when I created benchmarks for
|
||||
// very long table entries the complexity did not seem worth it.
|
||||
// The code table stores the prefix entry in 12 bits and then the suffix
|
||||
// byte in 8 bits, so each entry is 20 bits.
|
||||
|
||||
var chase_code = code < next_code ? code : prev_code;
|
||||
|
||||
// Chase what we will output, either {CODE} or {CODE-1}.
|
||||
var chase_length = 0;
|
||||
var chase = chase_code;
|
||||
while (chase > clear_code) {
|
||||
chase = code_table[chase] >> 8;
|
||||
++chase_length;
|
||||
}
|
||||
|
||||
var k = chase;
|
||||
|
||||
var op_end = op + chase_length + (chase_code !== code ? 1 : 0);
|
||||
if (op_end > output_length) {
|
||||
console.log("Warning, gif stream longer than expected.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Already have the first byte from the chase, might as well write it fast.
|
||||
output[op++] = k;
|
||||
|
||||
op += chase_length;
|
||||
var b = op; // Track pointer, writing backwards.
|
||||
|
||||
if (chase_code !== code) // The case of emitting {CODE-1} + k.
|
||||
output[op++] = k;
|
||||
|
||||
chase = chase_code;
|
||||
while (chase_length--) {
|
||||
chase = code_table[chase];
|
||||
output[--b] = chase & 0xff; // Write backwards.
|
||||
chase >>= 8; // Pull down to the prefix code.
|
||||
}
|
||||
|
||||
if (prev_code !== null && next_code < 4096) {
|
||||
code_table[next_code++] = prev_code << 8 | k;
|
||||
// TODO(deanm): Figure out this clearing vs code growth logic better. I
|
||||
// have an feeling that it should just happen somewhere else, for now it
|
||||
// is awkward between when we grow past the max and then hit a clear code.
|
||||
// For now just check if we hit the max 12-bits (then a clear code should
|
||||
// follow, also of course encoded in 12-bits).
|
||||
if (next_code >= code_mask+1 && cur_code_size < 12) {
|
||||
++cur_code_size;
|
||||
code_mask = code_mask << 1 | 1;
|
||||
}
|
||||
}
|
||||
|
||||
prev_code = code;
|
||||
}
|
||||
|
||||
if (op !== output_length) {
|
||||
console.log("Warning, gif stream shorter than expected.");
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
// CommonJS.
|
||||
//try { exports.GifWriter = GifWriter; exports.GifReader = GifReader } catch(e) {}
|
||||
try { exports.GifWriter = GifWriter; } catch(e) {}
|
||||
1181
wled00/data/pixelforge/pixelforge.htm
Normal file
1181
wled00/data/pixelforge/pixelforge.htm
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1977,4 +1977,4 @@
|
||||
return isValid;
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
</html>
|
||||
@@ -4,17 +4,23 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<title>WLED Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<script>
|
||||
function S() {
|
||||
getLoc();
|
||||
loadJS(getURL('/settings/s.js?p=0'), false); // If we set async false, file is loaded and executed, then next statement is processed
|
||||
}
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
// load style.css then initialize
|
||||
l.onload = () => loadResources(['style.css'], () => {
|
||||
getLoc();
|
||||
loadJS(getURL('/settings/s.js?p=0'), false);
|
||||
});
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
@import url("style.css");
|
||||
body {
|
||||
text-align: center;
|
||||
background: #222;
|
||||
height: 100px;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -22,21 +28,17 @@
|
||||
--h: 9vh;
|
||||
}
|
||||
button {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
font-family: Verdana, Helvetica, sans-serif;
|
||||
display: block;
|
||||
border: 1px solid #333;
|
||||
border-radius: var(--h);
|
||||
font-size: 6vmin;
|
||||
height: var(--h);
|
||||
width: calc(100% - 40px);
|
||||
margin: 2vh auto 0;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<button type=submit id="b" onclick="window.location=getURL('/')">Back</button>
|
||||
<button type="submit" onclick="window.location=getURL('/settings/wifi')">WiFi Setup</button>
|
||||
<button type="submit" onclick="window.location=getURL('/settings/leds')">LED Preferences</button>
|
||||
|
||||
@@ -4,11 +4,20 @@
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<title>2D Set-up</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
var maxPanels=64;
|
||||
var ctx = null;
|
||||
function fS(){d.Sf.submit();} // <button type=submit> sometimes didn't work
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
|
||||
function S() {
|
||||
getLoc();
|
||||
loadJS(getURL('/settings/s.js?p=10'), false, undefined, ()=>{
|
||||
@@ -238,9 +247,8 @@ Y: <input name="P${i}Y" type="number" min="0" max="255" value="0" oninput="UI()"
|
||||
gId("MD").innerHTML = "Matrix Dimensions (W*H=LC): " + maxWidth + " x " + maxHeight + " = " + maxWidth * maxHeight;
|
||||
}
|
||||
</script>
|
||||
<style>@import url("style.css");</style>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<form id="form_s" name="Sf" method="post">
|
||||
<div class="toprow">
|
||||
<div class="helpB"><button type="button" onclick="H('features/2D')">?</button></div>
|
||||
|
||||
@@ -4,8 +4,16 @@
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<meta charset="utf-8">
|
||||
<title>DMX Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
function HW(){window.open("https://kno.wled.ge/interfaces/dmx-output/");}
|
||||
function GCH(num) {
|
||||
gId('dmxchannels').innerHTML += "";
|
||||
@@ -38,9 +46,8 @@
|
||||
if (loc) d.Sf.action = getURL('/settings/dmx');
|
||||
}
|
||||
</script>
|
||||
<style>@import url("style.css");</style>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<form id="form_s" name="Sf" method="post">
|
||||
<div class="toprow">
|
||||
<div class="helpB"><button type="button" onclick="HW()">?</button></div>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<title>LED Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<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
|
||||
@@ -26,6 +26,15 @@
|
||||
function numPins(t){ return Math.max(gT(t).t.length, 1); } // type length determines number of GPIO pins
|
||||
function chrID(x) { return String.fromCharCode((x<10?48:55)+x); }
|
||||
function toNum(c) { let n=c.charCodeAt(0); return (n>=48 && n<=57)?n-48:(n>=65 && n<=90)?n-55:0; } // convert char (0-9A-Z) to number (0-35)
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
|
||||
function S() {
|
||||
getLoc();
|
||||
loadJS(getURL('/settings/s.js?p=2'), false, ()=>{
|
||||
@@ -43,7 +52,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 +61,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
|
||||
@@ -267,10 +277,10 @@
|
||||
}
|
||||
|
||||
// enable/disable LED fields
|
||||
updateTypeDropdowns(); // restrict bus types in dropdowns to max allowed digital/analog buses
|
||||
let dC = 0; // count of digital buses (for parallel I2S)
|
||||
let LTs = d.Sf.querySelectorAll("#mLC select[name^=LT]");
|
||||
LTs.forEach((s,i)=>{
|
||||
if (i < LTs.length-1) s.disabled = true; // prevent changing type (as we can't update options)
|
||||
// is the field a LED type?
|
||||
var n = s.name.substring(2,3); // bus number (0-Z)
|
||||
var t = parseInt(s.value);
|
||||
@@ -447,17 +457,8 @@
|
||||
{
|
||||
var o = gEBCN("iST");
|
||||
var i = o.length;
|
||||
let disable = (sel,opt) => { sel.querySelectorAll(opt).forEach((o)=>{o.disabled=true;}); }
|
||||
|
||||
var f = gId("mLC");
|
||||
let digitalB = 0, analogB = 0, twopinB = 0, virtB = 0;
|
||||
f.querySelectorAll("select[name^=LT]").forEach((s)=>{
|
||||
let t = s.value;
|
||||
if (isDig(t) && !isD2P(t)) digitalB++;
|
||||
if (isD2P(t)) twopinB++;
|
||||
if (isPWM(t)) analogB += numPins(t); // each GPIO is assigned to a channel
|
||||
if (isVir(t)) virtB++;
|
||||
});
|
||||
|
||||
if ((n==1 && i>=36) || (n==-1 && i==0)) return; // used to be i>=maxB+maxV when virtual buses were limited (now :"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
var s = chrID(i);
|
||||
@@ -467,7 +468,7 @@
|
||||
var cn = `<div class="iST">
|
||||
<hr class="sml">
|
||||
${i+1}:
|
||||
<select name="LT${s}" onchange="UI(true)"></select><br>
|
||||
<select name="LT${s}" onchange="updateTypeDropdowns();UI(true)"></select><br>
|
||||
<div id="abl${s}">
|
||||
mA/LED: <select name="LAsel${s}" onchange="enLA(this,'${s}');UI();">
|
||||
<option value="55" selected>55mA (typ. 5V WS281x)</option>
|
||||
@@ -522,18 +523,15 @@ mA/LED: <select name="LAsel${s}" onchange="enLA(this,'${s}');UI();">
|
||||
}
|
||||
});
|
||||
enLA(d.Sf["LAsel"+s],s); // update LED mA
|
||||
// disable inappropriate LED types
|
||||
// temporarily set to virtual (network) type to avoid "same type" exception during dropdown update
|
||||
let sel = d.getElementsByName("LT"+s)[0];
|
||||
// 32 & S2 supports mono I2S as well as parallel so we need to take that into account; S3 only supports parallel
|
||||
let maxDB = maxD - (is32() || isS2() || isS3() ? (!d.Sf["PR"].checked)*8 - (!isS3()) : 0); // adjust max digital buses if parallel I2S is not used
|
||||
if (digitalB >= maxDB) disable(sel,'option[data-type="D"]'); // NOTE: see isDig()
|
||||
if (twopinB >= 2) disable(sel,'option[data-type="2P"]'); // NOTE: see isD2P() (we will only allow 2 2pin buses)
|
||||
disable(sel,`option[data-type^="${'A'.repeat(maxA-analogB+1)}"]`); // NOTE: see isPWM()
|
||||
sel.value = sel.querySelector('option[data-type="N"]').value;
|
||||
updateTypeDropdowns(); // update valid bus options including this new one
|
||||
sel.selectedIndex = sel.querySelector('option:not(:disabled)').index;
|
||||
updateTypeDropdowns(); // update again for the newly selected type
|
||||
}
|
||||
if (n==-1) {
|
||||
o[--i].remove();--i;
|
||||
o[i].querySelector("[name^=LT]").disabled = false;
|
||||
}
|
||||
|
||||
gId("+").style.display = (i<35) ? "inline":"none"; // was maxB+maxV-1 when virtual buses were limited (now :"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
@@ -600,9 +598,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 +612,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;
|
||||
@@ -812,10 +826,37 @@ Swap: <select id="xw${s}" name="XW${s}">
|
||||
}
|
||||
return opt;
|
||||
}
|
||||
// dynamically enforce bus type availability based on current usage
|
||||
function updateTypeDropdowns() {
|
||||
let LTs = d.Sf.querySelectorAll("#mLC select[name^=LT]");
|
||||
let digitalB = 0, analogB = 0, twopinB = 0, virtB = 0;
|
||||
// count currently used buses
|
||||
LTs.forEach(sel => {
|
||||
let t = parseInt(sel.value);
|
||||
if (isDig(t) && !isD2P(t)) digitalB++;
|
||||
if (isPWM(t)) analogB += numPins(t);
|
||||
if (isD2P(t)) twopinB++;
|
||||
if (isVir(t)) virtB++;
|
||||
});
|
||||
// enable/disable type options according to limits in dropdowns
|
||||
LTs.forEach(sel => {
|
||||
const curType = parseInt(sel.value);
|
||||
const disable = (q) => sel.querySelectorAll(q).forEach(o => o.disabled = true);
|
||||
const enable = (q) => sel.querySelectorAll(q).forEach(o => o.disabled = false);
|
||||
enable('option'); // reset all first
|
||||
// max digital buses: ESP32 & S2 support mono I2S as well as parallel so we need to take that into account; S3 only supports parallel
|
||||
// supported outputs using parallel I2S/mono I2S: S2: 12/5, S3: 12/4, ESP32: 16/9
|
||||
let maxDB = maxD - ((is32() || isS2() || isS3()) ? (!d.Sf["PR"].checked) * 8 - (!isS3()) : 0); // adjust max digital buses if parallel I2S is not used
|
||||
// disallow adding more of a type that has reached its limit but allow changing the current type
|
||||
if (digitalB >= maxDB && !(isDig(curType) && !isD2P(curType))) disable('option[data-type="D"]');
|
||||
if (twopinB >= 2 && !isD2P(curType)) disable('option[data-type="2P"]');
|
||||
// Disable PWM types that need more pins than available (accounting for current type's pins if PWM)
|
||||
disable(`option[data-type^="${'A'.repeat(maxA - analogB + (isPWM(curType)?numPins(curType):0) + 1)}"]`);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style>@import url("style.css");</style>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<form id="form_s" name="Sf" method="post">
|
||||
<div class="toprow">
|
||||
<div class="helpB"><button type="button" onclick="H('features/settings/#led-settings')">?</button></div>
|
||||
@@ -867,10 +908,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">
|
||||
|
||||
@@ -4,8 +4,16 @@
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<meta charset="utf-8">
|
||||
<title>Misc Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
function U() { window.open(getURL("/update"),"_self"); }
|
||||
function checkNum(o) {
|
||||
const specialkeys = ["Backspace", "Tab", "Enter", "Shift", "Control", "Alt", "Pause", "CapsLock", "Escape", "Space", "PageUp", "PageDown", "End", "Home", "ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown", "Insert", "Delete"];
|
||||
@@ -30,11 +38,8 @@
|
||||
if (loc) d.Sf.action = getURL('/settings/sec');
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
@import url("style.css");
|
||||
</style>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<form id="form_s" name="Sf" method="post">
|
||||
<div class="toprow">
|
||||
<div class="helpB"><button type="button" onclick="H('features/settings/#security-settings')">?</button></div>
|
||||
|
||||
@@ -4,8 +4,16 @@
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<meta charset="utf-8">
|
||||
<title>Sync Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
function adj(){if (d.Sf.DI.value == 6454) {if (d.Sf.EU.value == 1) d.Sf.EU.value = 0;}
|
||||
else if (d.Sf.DI.value == 5568) {if (d.Sf.DA.value == 0) d.Sf.DA.value = 1; if (d.Sf.EU.value == 0) d.Sf.EU.value = 1;} }
|
||||
function FC()
|
||||
@@ -43,9 +51,8 @@
|
||||
function hideDMXInput(){gId("dmxInput").style.display="none";}
|
||||
function hideNoDMXInput(){gId("dmxInputOff").style.display="none";}
|
||||
</script>
|
||||
<style>@import url("style.css");</style>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<form id="form_s" name="Sf" method="post" onsubmit="GC()">
|
||||
<div class="toprow">
|
||||
<div class="helpB"><button type="button" onclick="H('interfaces/udp-notifier/')">?</button></div>
|
||||
|
||||
@@ -4,10 +4,19 @@
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<meta charset="utf-8">
|
||||
<title>Time Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
var el=false;
|
||||
var ms=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
|
||||
function S() {
|
||||
getLoc();
|
||||
loadJS(getURL('/settings/s.js?p=5'), false, ()=>{BTa();}, ()=>{
|
||||
@@ -119,9 +128,8 @@
|
||||
if (parseFloat(d.Sf.LN.value)<0) { d.Sf.LNR.value = "W"; d.Sf.LN.value = -1*parseFloat(d.Sf.LN.value); } else d.Sf.LNR.value = "E";
|
||||
}
|
||||
</script>
|
||||
<style>@import url("style.css");</style>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<form id="form_s" name="Sf" method="post" onsubmit="Wd()">
|
||||
<div class="toprow">
|
||||
<div class="helpB"><button type="button" onclick="H('features/settings/#time-settings')">?</button></div>
|
||||
|
||||
@@ -4,10 +4,19 @@
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<title>UI Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
var initial_ds, initial_st, initial_su, oldUrl;
|
||||
var sett = null;
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
|
||||
var l = {
|
||||
"comp":{
|
||||
"labels":"Show button labels",
|
||||
@@ -208,9 +217,8 @@
|
||||
gId("theme_bg_rnd").checked = false;
|
||||
}
|
||||
</script>
|
||||
<style>@import url("style.css");</style>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<form id="form_s" name="Sf" method="post">
|
||||
<div class="toprow">
|
||||
<div class="helpB"><button type="button" onclick="H('features/settings/#user-interface-settings')">?</button></div>
|
||||
|
||||
@@ -4,12 +4,22 @@
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<title>Usermod Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
var umCfg = {};
|
||||
var pins = [], pinO = [], owner;
|
||||
var urows;
|
||||
var numM = 0;
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
|
||||
|
||||
function S() {
|
||||
getLoc();
|
||||
// load settings and insert values into DOM
|
||||
@@ -269,10 +279,9 @@
|
||||
if (d.Sf.checkValidity()) d.Sf.submit(); //https://stackoverflow.com/q/37323914
|
||||
}
|
||||
</script>
|
||||
<style>@import url("style.css");</style>
|
||||
</head>
|
||||
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<form id="form_s" name="Sf" method="post" onsubmit="svS(event)">
|
||||
<div class="toprow">
|
||||
<div class="helpB"><button type="button" onclick="H()">?</button></div>
|
||||
|
||||
@@ -4,8 +4,17 @@
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<title>WiFi Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
|
||||
var scanLoops = 0, preScanSSID = "";
|
||||
var maxNetworks = 3;
|
||||
function N() {
|
||||
@@ -178,9 +187,8 @@ Static subnet mask:<br>
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>@import url("style.css");</style>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<form id="form_s" name="Sf" method="post">
|
||||
<div class="toprow">
|
||||
<div class="helpB"><button type="button" onclick="H('features/settings/#wifi-settings')">?</button></div>
|
||||
@@ -270,6 +278,7 @@ Static subnet mask:<br>
|
||||
<option value="5">TwilightLord-ESP32</option>
|
||||
<option value="3">WESP32</option>
|
||||
<option value="1">WT32-ETH01</option>
|
||||
<option value="13">Gledopto</option>
|
||||
</select><br><br>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
@@ -36,6 +36,7 @@ button, .btn {
|
||||
min-width: 48px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
button.sml {
|
||||
padding: 8px;
|
||||
@@ -44,6 +45,11 @@ button.sml {
|
||||
min-width: 40px;
|
||||
margin: 0 0 0 10px;
|
||||
}
|
||||
button:hover, .btn:hover{
|
||||
background:#555;
|
||||
border-color:#555;
|
||||
}
|
||||
|
||||
#scan {
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -493,14 +497,6 @@ class JSONBufferGuard {
|
||||
inline void release() { if (holding_lock) releaseJSONBufferLock(); holding_lock = false; }
|
||||
};
|
||||
|
||||
#ifdef WLED_ADD_EEPROM_SUPPORT
|
||||
//wled_eeprom.cpp
|
||||
void applyMacro(byte index);
|
||||
void deEEP();
|
||||
void deEEPSettings();
|
||||
void clearEEPROM();
|
||||
#endif
|
||||
|
||||
//wled_math.cpp
|
||||
//float cos_t(float phi); // use float math
|
||||
//float sin_t(float phi);
|
||||
@@ -539,7 +535,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();
|
||||
@@ -830,8 +839,16 @@ void serializeInfo(JsonObject root)
|
||||
#endif
|
||||
|
||||
root[F("freeheap")] = getFreeHeapSize();
|
||||
#if defined(BOARD_HAS_PSRAM)
|
||||
root[F("psram")] = ESP.getFreePsram();
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
// Report PSRAM information
|
||||
bool hasPsram = psramFound();
|
||||
root[F("psramPresent")] = hasPsram;
|
||||
if (hasPsram) {
|
||||
#if defined(BOARD_HAS_PSRAM)
|
||||
root[F("psram")] = ESP.getFreePsram(); // Free PSRAM in bytes (backward compatibility)
|
||||
#endif
|
||||
root[F("psramSize")] = ESP.getPsramSize() / (1024UL * 1024UL); // Total PSRAM size in MB
|
||||
}
|
||||
#endif
|
||||
root[F("uptime")] = millis()/1000 + rolloverMillis*4294967;
|
||||
|
||||
@@ -928,26 +945,25 @@ void serializePalettes(JsonObject root, int page)
|
||||
{
|
||||
byte tcp[72];
|
||||
#ifdef ESP8266
|
||||
int itemPerPage = 5;
|
||||
constexpr int itemPerPage = 5;
|
||||
#else
|
||||
int itemPerPage = 8;
|
||||
constexpr int itemPerPage = 8;
|
||||
#endif
|
||||
|
||||
int customPalettesCount = customPalettes.size();
|
||||
int palettesCount = getPaletteCount() - customPalettesCount; // palettesCount is number of palettes, not palette index
|
||||
const int customPalettesCount = customPalettes.size();
|
||||
const int palettesCount = FIXED_PALETTE_COUNT; // palettesCount is number of palettes, not palette index
|
||||
|
||||
int maxPage = (palettesCount + customPalettesCount -1) / itemPerPage;
|
||||
const int maxPage = (palettesCount + customPalettesCount) / itemPerPage;
|
||||
if (page > maxPage) page = maxPage;
|
||||
|
||||
int start = itemPerPage * page;
|
||||
int end = start + itemPerPage;
|
||||
if (end > palettesCount + customPalettesCount) end = palettesCount + customPalettesCount;
|
||||
const int start = itemPerPage * page;
|
||||
int end = min(start + itemPerPage, palettesCount + customPalettesCount);
|
||||
|
||||
root[F("m")] = maxPage; // inform caller how many pages there are
|
||||
JsonObject palettes = root.createNestedObject("p");
|
||||
|
||||
for (int i = start; i <= end; i++) {
|
||||
JsonArray curPalette = palettes.createNestedArray(String(i<=palettesCount ? i : 255 - (i - (palettesCount + 1))));
|
||||
for (int i = start; i < end; i++) {
|
||||
JsonArray curPalette = palettes.createNestedArray(String(i >= palettesCount ? 255 - i + palettesCount : i));
|
||||
switch (i) {
|
||||
case 0: //default palette
|
||||
setPaletteColors(curPalette, PartyColors_p);
|
||||
@@ -976,12 +992,12 @@ void serializePalettes(JsonObject root, int page)
|
||||
curPalette.add("c1");
|
||||
break;
|
||||
default:
|
||||
if (i > palettesCount)
|
||||
setPaletteColors(curPalette, customPalettes[i - (palettesCount + 1)]);
|
||||
else if (i < 13) // palette 6 - 12, fastled palettes
|
||||
setPaletteColors(curPalette, *fastledPalettes[i-6]);
|
||||
if (i >= palettesCount) // custom palettes
|
||||
setPaletteColors(curPalette, customPalettes[i - palettesCount]);
|
||||
else if (i < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) // palette 6 - 12, fastled palettes
|
||||
setPaletteColors(curPalette, *fastledPalettes[i - DYNAMIC_PALETTE_COUNT]);
|
||||
else {
|
||||
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[i - 13])), 72);
|
||||
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[i - (DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT)])), sizeof(tcp));
|
||||
setPaletteColors(curPalette, tcp);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -144,7 +144,17 @@ const ethernet_settings ethernetBoards[] = {
|
||||
18, // eth_mdio,
|
||||
ETH_PHY_LAN8720, // eth_type,
|
||||
ETH_CLOCK_GPIO0_OUT // eth_clk_mode
|
||||
}
|
||||
},
|
||||
|
||||
// Gledopto Series With Ethernet
|
||||
{
|
||||
1, // eth_address,
|
||||
5, // eth_power,
|
||||
23, // eth_mdc,
|
||||
33, // eth_mdio,
|
||||
ETH_PHY_LAN8720, // eth_type,
|
||||
ETH_CLOCK_GPIO0_IN // eth_clk_mode
|
||||
},
|
||||
};
|
||||
|
||||
bool initEthernet()
|
||||
|
||||
751
wled00/ota_update.cpp
Normal file
751
wled00/ota_update.cpp
Normal file
@@ -0,0 +1,751 @@
|
||||
#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
|
||||
|
||||
// Bootloader is at fixed offset 0x1000 (4KB), 0x0000 (0KB), or 0x2000 (8KB), and is typically 32KB
|
||||
// Bootloader offsets for different MCUs => see https://github.com/wled/WLED/issues/5064
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
constexpr size_t BOOTLOADER_OFFSET = 0x0000; // esp32-S3, esp32-C3 and (future support) esp32-c6
|
||||
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32P4) || defined(CONFIG_IDF_TARGET_ESP32C5)
|
||||
constexpr size_t BOOTLOADER_OFFSET = 0x2000; // (future support) esp32-P4 and esp32-C5
|
||||
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
|
||||
#else
|
||||
constexpr size_t BOOTLOADER_OFFSET = 0x1000; // esp32 and esp32-s2
|
||||
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
|
||||
#endif
|
||||
|
||||
#elif defined(ESP8266)
|
||||
constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4KB offset
|
||||
#define UPDATE_ERROR getErrorString
|
||||
#endif
|
||||
constexpr size_t METADATA_SEARCH_RANGE = 512; // bytes
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// Calculate SHA256
|
||||
uint8_t sha256[32];
|
||||
mbedtls_sha256_context ctx;
|
||||
mbedtls_sha256_init(&ctx);
|
||||
mbedtls_sha256_starts(&ctx, 0); // 0 = SHA256 (not SHA224)
|
||||
|
||||
const size_t chunkSize = 256;
|
||||
uint8_t buffer[chunkSize];
|
||||
|
||||
for (uint32_t offset = 0; offset < BOOTLOADER_SIZE; offset += chunkSize) {
|
||||
size_t readSize = min((size_t)(BOOTLOADER_SIZE - offset), chunkSize);
|
||||
if (esp_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;
|
||||
@@ -572,9 +576,6 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
|
||||
if (request->hasArg(F("RS"))) //complete factory reset
|
||||
{
|
||||
WLED_FS.format();
|
||||
#ifdef WLED_ADD_EEPROM_SUPPORT
|
||||
clearEEPROM();
|
||||
#endif
|
||||
serveMessage(request, 200, F("All Settings erased."), F("Connect to WLED-AP to setup again"),255);
|
||||
doReboot = true; // may reboot immediately on dual-core system (race condition) which is desireable in this case
|
||||
}
|
||||
@@ -613,7 +614,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 +815,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
|
||||
constexpr auto myBIT_WIDTH = ADC_WIDTH_BIT_13;
|
||||
#else
|
||||
constexpr auto myBIT_WIDTH = ADC_WIDTH_BIT_12;
|
||||
#endif
|
||||
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, myBIT_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();
|
||||
@@ -430,12 +430,7 @@ void WLED::setup()
|
||||
}
|
||||
|
||||
handleBootLoop(); // check for bootloop and take action (requires WLED_FS)
|
||||
|
||||
#ifdef WLED_ADD_EEPROM_SUPPORT
|
||||
else deEEP();
|
||||
#else
|
||||
initPresetsFile();
|
||||
#endif
|
||||
updateFSInfo();
|
||||
|
||||
// generate module IDs must be done before AP setup
|
||||
@@ -474,7 +469,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 +550,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()
|
||||
@@ -669,7 +665,6 @@ void WLED::initConnection()
|
||||
if (!WLED_WIFI_CONFIGURED) {
|
||||
DEBUG_PRINTLN(F("No connection configured."));
|
||||
if (!apActive) initAP(); // instantly go to ap mode
|
||||
return;
|
||||
} else if (!apActive) {
|
||||
if (apBehavior == AP_BEHAVIOR_ALWAYS) {
|
||||
DEBUG_PRINTLN(F("Access point ALWAYS enabled."));
|
||||
|
||||
@@ -122,9 +122,6 @@
|
||||
#endif
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#ifdef WLED_ADD_EEPROM_SUPPORT
|
||||
#include <EEPROM.h>
|
||||
#endif
|
||||
#include <WiFiUdp.h>
|
||||
#include <DNSServer.h>
|
||||
#include <SPIFFSEditor.h>
|
||||
@@ -189,11 +186,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 +271,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 +283,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 +362,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 +568,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 +633,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);
|
||||
|
||||
@@ -816,7 +819,7 @@ WLED_GLOBAL byte timerHours[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }));
|
||||
WLED_GLOBAL int8_t timerMinutes[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }));
|
||||
WLED_GLOBAL byte timerMacro[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }));
|
||||
//weekdays to activate on, bit pattern of arr elem: 0b11111111: sun,sat,fri,thu,wed,tue,mon,validity
|
||||
WLED_GLOBAL byte timerWeekday[] _INIT_N(({ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }));
|
||||
WLED_GLOBAL byte timerWeekday[] _INIT_N(({ 254, 254, 254, 254, 254, 254, 254, 254, 254, 254 }));
|
||||
//upper 4 bits start, lower 4 bits end month (default 28: start month 1 and end month 12)
|
||||
WLED_GLOBAL byte timerMonth[] _INIT_N(({28,28,28,28,28,28,28,28}));
|
||||
WLED_GLOBAL byte timerDay[] _INIT_N(({1,1,1,1,1,1,1,1}));
|
||||
|
||||
@@ -1,469 +0,0 @@
|
||||
#ifdef WLED_ADD_EEPROM_SUPPORT
|
||||
#include <EEPROM.h>
|
||||
#include "wled.h"
|
||||
|
||||
#if defined(WLED_ENABLE_MQTT) && MQTT_MAX_TOPIC_LEN < 32
|
||||
#error "MQTT topics length < 32 is not supported by the EEPROM module!"
|
||||
#endif
|
||||
|
||||
/*
|
||||
* DEPRECATED, do not use for new settings
|
||||
* Only used to restore config from pre-0.11 installations using the deEEP() methods
|
||||
*
|
||||
* Methods to handle saving and loading to non-volatile memory
|
||||
* EEPROM Map: https://github.com/wled-dev/WLED/wiki/EEPROM-Map
|
||||
*/
|
||||
|
||||
//eeprom Version code, enables default settings instead of 0 init on update
|
||||
#define EEPVER 22
|
||||
#define EEPSIZE 2560 //Maximum is 4096
|
||||
//0 -> old version, default
|
||||
//1 -> 0.4p 1711272 and up
|
||||
//2 -> 0.4p 1711302 and up
|
||||
//3 -> 0.4 1712121 and up
|
||||
//4 -> 0.5.0 and up
|
||||
//5 -> 0.5.1 and up
|
||||
//6 -> 0.6.0 and up
|
||||
//7 -> 0.7.1 and up
|
||||
//8 -> 0.8.0-a and up
|
||||
//9 -> 0.8.0
|
||||
//10-> 0.8.2
|
||||
//11-> 0.8.5-dev #mqttauth @TimothyBrown
|
||||
//12-> 0.8.7-dev
|
||||
//13-> 0.9.0-dev
|
||||
//14-> 0.9.0-b1
|
||||
//15-> 0.9.0-b3
|
||||
//16-> 0.9.1
|
||||
//17-> 0.9.1-dmx
|
||||
//18-> 0.9.1-e131
|
||||
//19-> 0.9.1n
|
||||
//20-> 0.9.1p
|
||||
//21-> 0.10.1p
|
||||
//22-> 2009260
|
||||
|
||||
/*
|
||||
* Erase all (pre 0.11) configuration data on factory reset
|
||||
*/
|
||||
void clearEEPROM()
|
||||
{
|
||||
EEPROM.begin(EEPSIZE);
|
||||
for (int i = 0; i < EEPSIZE; i++)
|
||||
{
|
||||
EEPROM.write(i, 0);
|
||||
}
|
||||
EEPROM.end();
|
||||
}
|
||||
|
||||
|
||||
void readStringFromEEPROM(uint16_t pos, char* str, uint16_t len)
|
||||
{
|
||||
for (int i = 0; i < len; ++i)
|
||||
{
|
||||
str[i] = EEPROM.read(pos + i);
|
||||
if (str[i] == 0) return;
|
||||
}
|
||||
str[len] = 0; //make sure every string is properly terminated. str must be at least len +1 big.
|
||||
}
|
||||
|
||||
/*
|
||||
* Read all configuration from flash
|
||||
*/
|
||||
void loadSettingsFromEEPROM()
|
||||
{
|
||||
if (EEPROM.read(233) != 233) //first boot/reset to default
|
||||
{
|
||||
DEBUG_PRINTLN(F("EEPROM settings invalid, using defaults..."));
|
||||
return;
|
||||
}
|
||||
int lastEEPROMversion = EEPROM.read(377); //last EEPROM version before update
|
||||
|
||||
|
||||
readStringFromEEPROM( 0, multiWiFi[0].clientSSID, 32);
|
||||
readStringFromEEPROM( 32, multiWiFi[0].clientPass, 64);
|
||||
readStringFromEEPROM( 96, cmDNS, 32);
|
||||
readStringFromEEPROM(128, apSSID, 32);
|
||||
readStringFromEEPROM(160, apPass, 64);
|
||||
|
||||
nightlightDelayMinsDefault = EEPROM.read(224);
|
||||
nightlightDelayMins = nightlightDelayMinsDefault;
|
||||
nightlightMode = EEPROM.read(225);
|
||||
notifyDirect = EEPROM.read(226);
|
||||
sendNotificationsRT = notifyDirect;
|
||||
|
||||
apChannel = EEPROM.read(227);
|
||||
if (apChannel > 13 || apChannel < 1) apChannel = 1;
|
||||
apHide = EEPROM.read(228);
|
||||
if (apHide > 1) apHide = 1;
|
||||
uint16_t length = EEPROM.read(229) + ((EEPROM.read(398) << 8) & 0xFF00); //was ledCount
|
||||
if (length > MAX_LEDS || length == 0) length = 30;
|
||||
uint8_t pins[OUTPUT_MAX_PINS] = {2, 255, 255, 255, 255};
|
||||
uint8_t colorOrder = COL_ORDER_GRB;
|
||||
if (lastEEPROMversion > 9) colorOrder = EEPROM.read(383);
|
||||
if (colorOrder > COL_ORDER_GBR) colorOrder = COL_ORDER_GRB;
|
||||
bool skipFirst = EEPROM.read(2204);
|
||||
bool reversed = EEPROM.read(252);
|
||||
BusConfig bc = BusConfig(EEPROM.read(372) ? TYPE_SK6812_RGBW : TYPE_WS2812_RGB, pins, 0, length, colorOrder, reversed, skipFirst);
|
||||
BusManager::add(bc);
|
||||
|
||||
notifyButton = EEPROM.read(230);
|
||||
if (EEPROM.read(231)) udpNumRetries = 1;
|
||||
buttonType[0] = EEPROM.read(232) ? BTN_TYPE_PUSH : BTN_TYPE_NONE;
|
||||
|
||||
staticIP[0] = EEPROM.read(234);
|
||||
staticIP[1] = EEPROM.read(235);
|
||||
staticIP[2] = EEPROM.read(236);
|
||||
staticIP[3] = EEPROM.read(237);
|
||||
staticGateway[0] = EEPROM.read(238);
|
||||
staticGateway[1] = EEPROM.read(239);
|
||||
staticGateway[2] = EEPROM.read(240);
|
||||
staticGateway[3] = EEPROM.read(241);
|
||||
staticSubnet[0] = EEPROM.read(242);
|
||||
staticSubnet[1] = EEPROM.read(243);
|
||||
staticSubnet[2] = EEPROM.read(244);
|
||||
staticSubnet[3] = EEPROM.read(245);
|
||||
|
||||
briS = EEPROM.read(249); bri = briS;
|
||||
if (!EEPROM.read(369))
|
||||
{
|
||||
bri = 0; briLast = briS;
|
||||
}
|
||||
receiveNotificationBrightness = EEPROM.read(250);
|
||||
fadeTransition = EEPROM.read(251);
|
||||
transitionDelayDefault = EEPROM.read(253) + ((EEPROM.read(254) << 8) & 0xFF00);
|
||||
transitionDelay = transitionDelayDefault;
|
||||
briMultiplier = EEPROM.read(255);
|
||||
|
||||
readStringFromEEPROM(256, otaPass, 32);
|
||||
|
||||
nightlightTargetBri = EEPROM.read(288);
|
||||
otaLock = EEPROM.read(289);
|
||||
udpPort = EEPROM.read(290) + ((EEPROM.read(291) << 8) & 0xFF00);
|
||||
|
||||
readStringFromEEPROM(292, serverDescription, 32);
|
||||
|
||||
ntpEnabled = EEPROM.read(327);
|
||||
currentTimezone = EEPROM.read(328);
|
||||
useAMPM = EEPROM.read(329);
|
||||
gammaCorrectBri = EEPROM.read(330);
|
||||
gammaCorrectCol = EEPROM.read(331);
|
||||
overlayCurrent = EEPROM.read(332);
|
||||
|
||||
alexaEnabled = EEPROM.read(333);
|
||||
|
||||
readStringFromEEPROM(334, alexaInvocationName, 32);
|
||||
|
||||
notifyAlexa = EEPROM.read(366);
|
||||
arlsOffset = EEPROM.read(368);
|
||||
if (!EEPROM.read(367)) arlsOffset = -arlsOffset;
|
||||
turnOnAtBoot = EEPROM.read(369);
|
||||
//strip.isRgbw = EEPROM.read(372);
|
||||
//374 - strip.paletteFade
|
||||
|
||||
apBehavior = EEPROM.read(376);
|
||||
|
||||
//377 = lastEEPROMversion
|
||||
if (lastEEPROMversion > 3) {
|
||||
aOtaEnabled = EEPROM.read(390);
|
||||
receiveNotificationColor = EEPROM.read(391);
|
||||
receiveNotificationEffects = EEPROM.read(392);
|
||||
}
|
||||
|
||||
if (lastEEPROMversion > 4) {
|
||||
#ifndef WLED_DISABLE_HUESYNC
|
||||
huePollingEnabled = EEPROM.read(2048);
|
||||
//hueUpdatingEnabled = EEPROM.read(2049);
|
||||
for (int i = 2050; i < 2054; ++i)
|
||||
{
|
||||
hueIP[i-2050] = EEPROM.read(i);
|
||||
}
|
||||
|
||||
readStringFromEEPROM(2054, hueApiKey, 46);
|
||||
|
||||
huePollIntervalMs = EEPROM.read(2100) + ((EEPROM.read(2101) << 8) & 0xFF00);
|
||||
notifyHue = EEPROM.read(2102);
|
||||
hueApplyOnOff = EEPROM.read(2103);
|
||||
hueApplyBri = EEPROM.read(2104);
|
||||
hueApplyColor = EEPROM.read(2105);
|
||||
huePollLightId = EEPROM.read(2106);
|
||||
#endif
|
||||
}
|
||||
if (lastEEPROMversion > 5) {
|
||||
overlayMin = EEPROM.read(2150);
|
||||
overlayMax = EEPROM.read(2151);
|
||||
analogClock12pixel = EEPROM.read(2152);
|
||||
analogClock5MinuteMarks = EEPROM.read(2153);
|
||||
analogClockSecondsTrail = EEPROM.read(2154);
|
||||
countdownMode = EEPROM.read(2155);
|
||||
countdownYear = EEPROM.read(2156);
|
||||
countdownMonth = EEPROM.read(2157);
|
||||
countdownDay = EEPROM.read(2158);
|
||||
countdownHour = EEPROM.read(2159);
|
||||
countdownMin = EEPROM.read(2160);
|
||||
countdownSec = EEPROM.read(2161);
|
||||
setCountdown();
|
||||
|
||||
//macroBoot = EEPROM.read(2175);
|
||||
macroAlexaOn = EEPROM.read(2176);
|
||||
macroAlexaOff = EEPROM.read(2177);
|
||||
macroButton[0] = EEPROM.read(2178);
|
||||
macroLongPress[0] = EEPROM.read(2179);
|
||||
macroCountdown = EEPROM.read(2180);
|
||||
macroNl = EEPROM.read(2181);
|
||||
macroDoublePress[0] = EEPROM.read(2182);
|
||||
if (macroDoublePress[0] > 16) macroDoublePress[0] = 0;
|
||||
}
|
||||
|
||||
if (lastEEPROMversion > 6)
|
||||
{
|
||||
e131Universe = EEPROM.read(2190) + ((EEPROM.read(2191) << 8) & 0xFF00);
|
||||
e131Multicast = EEPROM.read(2192);
|
||||
realtimeTimeoutMs = EEPROM.read(2193) + ((EEPROM.read(2194) << 8) & 0xFF00);
|
||||
arlsForceMaxBri = EEPROM.read(2195);
|
||||
arlsDisableGammaCorrection = EEPROM.read(2196);
|
||||
}
|
||||
|
||||
if (lastEEPROMversion > 7)
|
||||
{
|
||||
//strip.paletteFade = EEPROM.read(374);
|
||||
paletteBlend = EEPROM.read(382);
|
||||
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
timerHours[i] = EEPROM.read(2260 + i);
|
||||
timerMinutes[i] = EEPROM.read(2270 + i);
|
||||
timerWeekday[i] = EEPROM.read(2280 + i);
|
||||
timerMacro[i] = EEPROM.read(2290 + i);
|
||||
if (timerMacro[i] > 0) timerMacro[i] += 16; //add 16 to work with macro --> preset mapping
|
||||
if (timerWeekday[i] == 0) timerWeekday[i] = 255;
|
||||
if (timerMacro[i] == 0) timerWeekday[i] = timerWeekday[i] & 0b11111110;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastEEPROMversion > 8)
|
||||
{
|
||||
readStringFromEEPROM(2300, mqttServer, 32);
|
||||
readStringFromEEPROM(2333, mqttDeviceTopic, 32);
|
||||
readStringFromEEPROM(2366, mqttGroupTopic, 32);
|
||||
}
|
||||
|
||||
if (lastEEPROMversion > 9)
|
||||
{
|
||||
//strip.setColorOrder(EEPROM.read(383));
|
||||
irEnabled = EEPROM.read(385);
|
||||
strip.ablMilliampsMax = EEPROM.read(387) + ((EEPROM.read(388) << 8) & 0xFF00);
|
||||
} else if (lastEEPROMversion > 1) //ABL is off by default when updating from version older than 0.8.2
|
||||
{
|
||||
strip.ablMilliampsMax = 65000;
|
||||
} else {
|
||||
strip.ablMilliampsMax = ABL_MILLIAMPS_DEFAULT;
|
||||
}
|
||||
|
||||
if (lastEEPROMversion > 10)
|
||||
{
|
||||
readStringFromEEPROM(2399, mqttUser, 40);
|
||||
readStringFromEEPROM(2440, mqttPass, 40);
|
||||
readStringFromEEPROM(2481, mqttClientID, 40);
|
||||
mqttPort = EEPROM.read(2522) + ((EEPROM.read(2523) << 8) & 0xFF00);
|
||||
}
|
||||
|
||||
if (lastEEPROMversion > 11)
|
||||
{
|
||||
strip.milliampsPerLed = EEPROM.read(375);
|
||||
} else if (strip.ablMilliampsMax == 65000) //65000 indicates disabled ABL in <0.8.7
|
||||
{
|
||||
strip.ablMilliampsMax = ABL_MILLIAMPS_DEFAULT;
|
||||
strip.milliampsPerLed = 0; //disable ABL
|
||||
}
|
||||
if (lastEEPROMversion > 12)
|
||||
{
|
||||
readStringFromEEPROM(990, ntpServerName, 32);
|
||||
}
|
||||
if (lastEEPROMversion > 13)
|
||||
{
|
||||
mqttEnabled = EEPROM.read(2299);
|
||||
//syncToggleReceive = EEPROM.read(397);
|
||||
} else {
|
||||
mqttEnabled = true;
|
||||
//syncToggleReceive = false;
|
||||
}
|
||||
|
||||
if (lastEEPROMversion > 14)
|
||||
{
|
||||
DMXAddress = EEPROM.read(2197) + ((EEPROM.read(2198) << 8) & 0xFF00);
|
||||
DMXMode = EEPROM.read(2199);
|
||||
} else {
|
||||
DMXAddress = 1;
|
||||
DMXMode = DMX_MODE_MULTIPLE_RGB;
|
||||
}
|
||||
|
||||
//if (lastEEPROMversion > 15)
|
||||
//{
|
||||
noWifiSleep = EEPROM.read(370);
|
||||
//}
|
||||
|
||||
if (lastEEPROMversion > 17)
|
||||
{
|
||||
e131SkipOutOfSequence = EEPROM.read(2189);
|
||||
} else {
|
||||
e131SkipOutOfSequence = true;
|
||||
}
|
||||
|
||||
if (lastEEPROMversion > 18)
|
||||
{
|
||||
e131Port = EEPROM.read(2187) + ((EEPROM.read(2188) << 8) & 0xFF00);
|
||||
}
|
||||
|
||||
#ifdef WLED_ENABLE_DMX
|
||||
if (lastEEPROMversion > 19)
|
||||
{
|
||||
e131ProxyUniverse = EEPROM.read(2185) + ((EEPROM.read(2186) << 8) & 0xFF00);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (lastEEPROMversion > 21) {
|
||||
udpPort2 = EEPROM.read(378) + ((EEPROM.read(379) << 8) & 0xFF00);
|
||||
}
|
||||
|
||||
receiveDirect = !EEPROM.read(2200);
|
||||
//notifyMacro = EEPROM.read(2201);
|
||||
|
||||
//strip.rgbwMode = EEPROM.read(2203);
|
||||
//skipFirstLed = EEPROM.read(2204);
|
||||
|
||||
bootPreset = EEPROM.read(389);
|
||||
wifiLock = EEPROM.read(393);
|
||||
utcOffsetSecs = EEPROM.read(394) + ((EEPROM.read(395) << 8) & 0xFF00);
|
||||
if (EEPROM.read(396)) utcOffsetSecs = -utcOffsetSecs; //negative
|
||||
//!EEPROM.read(399); was enableSecTransition
|
||||
|
||||
//favorite setting (preset) memory (25 slots/ each 20byte)
|
||||
//400 - 899 reserved
|
||||
|
||||
//custom macro memory (16 slots/ each 64byte)
|
||||
//1024-2047 reserved
|
||||
|
||||
#ifdef WLED_ENABLE_DMX
|
||||
// DMX (2530 - 2549)2535
|
||||
DMXChannels = EEPROM.read(2530);
|
||||
DMXGap = EEPROM.read(2531) + ((EEPROM.read(2532) << 8) & 0xFF00);
|
||||
DMXStart = EEPROM.read(2533) + ((EEPROM.read(2534) << 8) & 0xFF00);
|
||||
|
||||
for (int i=0;i<15;i++) {
|
||||
DMXFixtureMap[i] = EEPROM.read(2535+i);
|
||||
} //last used: 2549
|
||||
DMXStartLED = EEPROM.read(2550);
|
||||
#endif
|
||||
|
||||
//Usermod memory
|
||||
//2551 - 2559 reserved for Usermods, usable by default
|
||||
//2560 - 2943 usable, NOT reserved (need to increase EEPSIZE accordingly, new WLED core features may override this section)
|
||||
//2944 - 3071 reserved for Usermods (need to increase EEPSIZE to 3072 in const.h)
|
||||
}
|
||||
|
||||
|
||||
//provided for increased compatibility with usermods written for v0.10
|
||||
void applyMacro(byte index) {
|
||||
applyPreset(index+16);
|
||||
}
|
||||
|
||||
|
||||
// De-EEPROM routine, upgrade from previous versions to v0.11
|
||||
void deEEP() {
|
||||
if (WLED_FS.exists(FPSTR(getPresetsFileName()))) return;
|
||||
|
||||
DEBUG_PRINTLN(F("Preset file not found, attempting to load from EEPROM"));
|
||||
DEBUGFS_PRINTLN(F("Allocating saving buffer for dEEP"));
|
||||
if (!requestJSONBufferLock(8)) return;
|
||||
|
||||
JsonObject sObj = pDoc->to<JsonObject>();
|
||||
sObj.createNestedObject("0");
|
||||
|
||||
EEPROM.begin(EEPSIZE);
|
||||
if (EEPROM.read(233) == 233) { //valid EEPROM save
|
||||
for (uint16_t index = 1; index <= 16; index++) { //copy presets to presets.json
|
||||
uint16_t i = 380 + index*20;
|
||||
byte ver = EEPROM.read(i);
|
||||
|
||||
if ((index < 16 && ver != 1) || (index == 16 && (ver < 2 || ver > 3))) continue;
|
||||
|
||||
char nbuf[16];
|
||||
sprintf(nbuf, "%d", index);
|
||||
|
||||
JsonObject pObj = sObj.createNestedObject(nbuf);
|
||||
|
||||
sprintf_P(nbuf, (char*)F("Preset %d"), index);
|
||||
pObj["n"] = nbuf;
|
||||
|
||||
pObj["bri"] = EEPROM.read(i+1);
|
||||
|
||||
if (index < 16) {
|
||||
JsonObject segObj = pObj.createNestedObject("seg");
|
||||
|
||||
JsonArray colarr = segObj.createNestedArray("col");
|
||||
|
||||
byte numChannels = (strip.hasWhiteChannel())? 4:3;
|
||||
|
||||
for (uint8_t k = 0; k < 3; k++) //k=0 primary (i+2) k=1 secondary (i+6) k=2 tertiary color (i+12)
|
||||
{
|
||||
JsonArray colX = colarr.createNestedArray();
|
||||
uint16_t memloc = i + 6*k;
|
||||
if (k == 0) memloc += 2;
|
||||
|
||||
for (byte j = 0; j < numChannels; j++) colX.add(EEPROM.read(memloc + j));
|
||||
}
|
||||
|
||||
segObj["fx"] = EEPROM.read(i+10);
|
||||
segObj["sx"] = EEPROM.read(i+11);
|
||||
segObj["ix"] = EEPROM.read(i+16);
|
||||
segObj["pal"] = EEPROM.read(i+17);
|
||||
} else {
|
||||
Segment* seg = strip.getSegments();
|
||||
memcpy(seg, EEPROM.getDataPtr() +i+2, 240);
|
||||
if (ver == 2) { //versions before 2004230 did not have opacity
|
||||
for (byte j = 0; j < strip.getMaxSegments(); j++)
|
||||
{
|
||||
strip.getSegment(j).opacity = 255;
|
||||
strip.getSegment(j).setOption(SEG_OPTION_ON, true); // use transistion
|
||||
}
|
||||
}
|
||||
serializeState(pObj, true, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
for (uint16_t index = 1; index <= 16; index++) { //copy macros to presets.json
|
||||
char m[65];
|
||||
readStringFromEEPROM(1024+64*(index-1), m, 64);
|
||||
if (m[0]) { //macro exists
|
||||
char nbuf[16];
|
||||
sprintf(nbuf, "%d", index + 16);
|
||||
JsonObject pObj = sObj.createNestedObject(nbuf);
|
||||
sprintf_P(nbuf, "Z Macro %d", index);
|
||||
pObj["n"] = nbuf;
|
||||
pObj["win"] = m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EEPROM.end();
|
||||
|
||||
File f = WLED_FS.open(FPSTR(getPresetsFileName()), "w");
|
||||
if (!f) {
|
||||
errorFlag = ERR_FS_GENERAL;
|
||||
releaseJSONBufferLock();
|
||||
return;
|
||||
}
|
||||
serializeJson(*pDoc, f);
|
||||
f.close();
|
||||
|
||||
releaseJSONBufferLock();
|
||||
|
||||
DEBUG_PRINTLN(F("deEEP complete!"));
|
||||
}
|
||||
|
||||
void deEEPSettings() {
|
||||
DEBUG_PRINTLN(F("Restore settings from EEPROM"));
|
||||
EEPROM.begin(EEPSIZE);
|
||||
loadSettingsFromEEPROM();
|
||||
EEPROM.end();
|
||||
}
|
||||
#endif
|
||||
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"
|
||||
@@ -13,10 +9,15 @@
|
||||
#ifdef WLED_ENABLE_PIXART
|
||||
#include "html_pixart.h"
|
||||
#endif
|
||||
#ifndef WLED_DISABLE_PXMAGIC
|
||||
#ifdef WLED_ENABLE_PXMAGIC
|
||||
#include "html_pxmagic.h"
|
||||
#endif
|
||||
#ifndef WLED_DISABLE_PIXELFORGE
|
||||
#include "html_pixelforge.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 +27,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 +70,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 +81,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 +186,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 +209,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 +218,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 +475,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 +488,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 +538,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);
|
||||
@@ -489,12 +608,19 @@ void initServer()
|
||||
});
|
||||
#endif
|
||||
|
||||
#ifndef WLED_DISABLE_PXMAGIC
|
||||
#ifdef WLED_ENABLE_PXMAGIC
|
||||
static const char _pxmagic_htm[] PROGMEM = "/pxmagic.htm";
|
||||
server.on(_pxmagic_htm, HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
handleStaticContent(request, FPSTR(_pxmagic_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pxmagic, PAGE_pxmagic_length);
|
||||
});
|
||||
#endif
|
||||
|
||||
#ifndef WLED_DISABLE_PIXELFORGE
|
||||
static const char _pixelforge_htm[] PROGMEM = "/pixelforge.htm";
|
||||
server.on(_pixelforge_htm, HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
handleStaticContent(request, FPSTR(_pixelforge_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pixelforge, PAGE_pixelforge_length);
|
||||
});
|
||||
#endif
|
||||
#endif
|
||||
|
||||
static const char _cpal_htm[] PROGMEM = "/cpal.htm";
|
||||
@@ -569,8 +695,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 +820,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];
|
||||
@@ -130,12 +157,6 @@ void sendDataWs(AsyncWebSocketClient * client)
|
||||
// the following may no longer be necessary as heap management has been fixed by @willmmiles in AWS
|
||||
size_t heap1 = getFreeHeapSize();
|
||||
DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize());
|
||||
#ifdef ESP8266
|
||||
if (len>heap1) {
|
||||
DEBUG_PRINTLN(F("Out of memory (WS)!"));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
AsyncWebSocketBuffer buffer(len);
|
||||
#ifdef ESP8266
|
||||
size_t heap2 = getFreeHeapSize();
|
||||
@@ -209,7 +230,7 @@ bool sendLiveLedsWs(uint32_t wsClient)
|
||||
#ifndef WLED_DISABLE_2D
|
||||
if (strip.isMatrix && n>1 && (i/Segment::maxWidth)%n) i += Segment::maxWidth * (n-1);
|
||||
#endif
|
||||
uint32_t c = strip.getPixelColor(i);
|
||||
uint32_t c = strip.getPixelColor(i); // note: LEDs mapped outside of valid range are set to black
|
||||
uint8_t r = R(c);
|
||||
uint8_t g = G(c);
|
||||
uint8_t b = B(c);
|
||||
|
||||
@@ -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