mirror of
https://github.com/wled/WLED.git
synced 2026-06-27 15:21:40 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9672b9bee0 | |||
| 6d0921bdd7 | |||
| ea4661e907 |
@@ -30,27 +30,6 @@ The build has two main phases:
|
||||
- Common environments: `nodemcuv2`, `esp32dev`, `esp8266_2m`
|
||||
- List all targets: `pio run --list-targets`
|
||||
|
||||
## Before Finishing Work
|
||||
|
||||
**CRITICAL: You MUST complete ALL of these steps before marking your work as complete:**
|
||||
|
||||
1. **Run the test suite**: `npm test` -- Set timeout to 2+ minutes. NEVER CANCEL.
|
||||
- All tests MUST pass
|
||||
- If tests fail, fix the issue before proceeding
|
||||
|
||||
2. **Build at least one hardware environment**: `pio run -e esp32dev` -- Set timeout to 30+ minutes. NEVER CANCEL.
|
||||
- Choose `esp32dev` as it's a common, representative environment
|
||||
- See "Hardware Compilation" section above for the full list of common environments
|
||||
- The build MUST complete successfully without errors
|
||||
- If the build fails, fix the issue before proceeding
|
||||
- **DO NOT skip this step** - it validates that firmware compiles with your changes
|
||||
|
||||
3. **For web UI changes only**: Manually test the interface
|
||||
- See "Manual Testing Scenarios" section below
|
||||
- Verify the UI loads and functions correctly
|
||||
|
||||
**If any of these validation steps fail, you MUST fix the issues before finishing. Do NOT mark work as complete with failing builds or tests.**
|
||||
|
||||
## Validation and Testing
|
||||
|
||||
### Web UI Testing
|
||||
@@ -65,7 +44,6 @@ The build has two main phases:
|
||||
- **Code style**: Use tabs for web files (.html/.css/.js), spaces (2 per level) for C++ files
|
||||
- **C++ formatting available**: `clang-format` is installed but not in CI
|
||||
- **Always run tests before finishing**: `npm test`
|
||||
- **MANDATORY: Always run a hardware build before finishing** (see "Before Finishing Work" section below)
|
||||
|
||||
### Manual Testing Scenarios
|
||||
After making changes to web UI, always test:
|
||||
@@ -121,16 +99,10 @@ package.json # Node.js dependencies and scripts
|
||||
|
||||
## Build Timing and Timeouts
|
||||
|
||||
**IMPORTANT: Use these timeout values when running builds:**
|
||||
|
||||
- **Web UI build** (`npm run build`): 3 seconds typical - Set timeout to 30 seconds minimum
|
||||
- **Test suite** (`npm test`): 40 seconds typical - Set timeout to 120 seconds (2 minutes) minimum
|
||||
- **Hardware builds** (`pio run -e [target]`): 15-20 minutes typical for first build - Set timeout to 1800 seconds (30 minutes) minimum
|
||||
- Subsequent builds are faster due to caching
|
||||
- First builds download toolchains and dependencies which takes significant time
|
||||
- **NEVER CANCEL long-running builds** - PlatformIO downloads and compilation require patience
|
||||
|
||||
**When validating your changes before finishing, you MUST wait for the hardware build to complete successfully. Set the timeout appropriately and be patient.**
|
||||
- **Web UI build**: 3 seconds - Set timeout to 30 seconds minimum
|
||||
- **Test suite**: 40 seconds - Set timeout to 2 minutes minimum
|
||||
- **Hardware builds**: 15+ minutes - Set timeout to 30+ minutes minimum
|
||||
- **NEVER CANCEL long-running builds** - PlatformIO downloads and compilation can take significant time
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -156,17 +128,11 @@ package.json # Node.js dependencies and scripts
|
||||
- **Hardware builds require appropriate ESP32/ESP8266 development board**
|
||||
|
||||
## CI/CD Pipeline
|
||||
|
||||
**The GitHub Actions CI workflow will:**
|
||||
The GitHub Actions workflow:
|
||||
1. Installs Node.js and Python dependencies
|
||||
2. Runs `npm test` to validate build system (MUST pass)
|
||||
3. Builds web UI with `npm run build` (automatically run by PlatformIO)
|
||||
4. Compiles firmware for ALL hardware targets listed in `default_envs` (MUST succeed for all)
|
||||
2. Runs `npm test` to validate build system
|
||||
3. Builds web UI with `npm run build`
|
||||
4. Compiles firmware for multiple hardware targets
|
||||
5. Uploads build artifacts
|
||||
|
||||
**To ensure CI success, you MUST locally:**
|
||||
- Run `npm test` and ensure it passes
|
||||
- Run `pio run -e esp32dev` (or another common environment from "Hardware Compilation" section) and ensure it completes successfully
|
||||
- If either fails locally, it WILL fail in CI
|
||||
|
||||
**Match this workflow in your local development to ensure CI success. Do not mark work complete until you have validated builds locally.**
|
||||
Match this workflow in your local development to ensure CI success.
|
||||
|
||||
@@ -33,6 +33,6 @@
|
||||
run: |
|
||||
jq -n \
|
||||
--arg content "Pull Request #${PR_NUMBER} \"${PR_TITLE}\" merged by ${ACTOR}
|
||||
${PR_URL} . It will be included in the next nightly builds, please test" \
|
||||
${PR_URL}. It will be included in the next nightly builds, please test" \
|
||||
'{content: $content}' \
|
||||
| curl -H "Content-Type: application/json" -d @- ${{ secrets.DISCORD_WEBHOOK_BETA_TESTERS }}
|
||||
|
||||
+1
-2
@@ -7,8 +7,6 @@
|
||||
.pioenvs
|
||||
.piolibdeps
|
||||
.vscode
|
||||
compile_commands.json
|
||||
__pycache__/
|
||||
|
||||
esp01-update.sh
|
||||
platformio_override.ini
|
||||
@@ -25,3 +23,4 @@ wled-update.sh
|
||||
/wled00/Release
|
||||
/wled00/wled00.ino.cpp
|
||||
/wled00/html_*.h
|
||||
compile_commands.json
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino":{
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "partitions-8MB-tinyuf2.csv"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1",
|
||||
"-DBOARD_HAS_PSRAM"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"hwids": [
|
||||
[
|
||||
"0x239A",
|
||||
"0x8125"
|
||||
],
|
||||
[
|
||||
"0x239A",
|
||||
"0x0125"
|
||||
],
|
||||
[
|
||||
"0x239A",
|
||||
"0x8126"
|
||||
]
|
||||
],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "adafruit_matrixportal_esp32s3"
|
||||
},
|
||||
"connectivity": [
|
||||
"bluetooth",
|
||||
"wifi"
|
||||
],
|
||||
"debug": {
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": [
|
||||
"arduino",
|
||||
"espidf"
|
||||
],
|
||||
"name": "Adafruit MatrixPortal ESP32-S3",
|
||||
"upload": {
|
||||
"arduino": {
|
||||
"flash_extra_images": [
|
||||
[
|
||||
"0x410000",
|
||||
"variants/adafruit_matrixportal_esp32s3/tinyuf2.bin"
|
||||
]
|
||||
]
|
||||
},
|
||||
"flash_size": "8MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 8388608,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"require_upload_port": true,
|
||||
"speed": 460800
|
||||
},
|
||||
"url": "https://www.adafruit.com/product/5778",
|
||||
"vendor": "Adafruit"
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino":{
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"memory_type": "qio_opi",
|
||||
"partitions": "default_16MB.csv"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DARDUINO_TTGO_T7_S3",
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_MODE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"hwids": [
|
||||
[
|
||||
"0X303A",
|
||||
"0x1001"
|
||||
]
|
||||
],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "esp32s3"
|
||||
},
|
||||
"connectivity": [
|
||||
"wifi",
|
||||
"bluetooth"
|
||||
],
|
||||
"debug": {
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": [
|
||||
"arduino",
|
||||
"espidf"
|
||||
],
|
||||
"name": "LILYGO T3-S3",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "https://www.aliexpress.us/item/3256804591247074.html",
|
||||
"vendor": "LILYGO"
|
||||
}
|
||||
@@ -2,7 +2,6 @@ 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")
|
||||
@@ -23,8 +22,7 @@ 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("\\\"", "")
|
||||
with open("package.json", "r") as package:
|
||||
version = json.load(package)["version"]
|
||||
version = _get_cpp_define_value(env, "WLED_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,116 +0,0 @@
|
||||
Import('env')
|
||||
import subprocess
|
||||
import json
|
||||
import re
|
||||
|
||||
def get_github_repo():
|
||||
"""Extract GitHub repository name from git remote URL.
|
||||
|
||||
Uses the remote that the current branch tracks, falling back to 'origin'.
|
||||
This handles cases where repositories have multiple remotes or where the
|
||||
main remote is not named 'origin'.
|
||||
|
||||
Returns:
|
||||
str: Repository name in 'owner/repo' format for GitHub repos,
|
||||
'unknown' for non-GitHub repos, missing git CLI, or any errors.
|
||||
"""
|
||||
try:
|
||||
remote_name = 'origin' # Default fallback
|
||||
|
||||
# Try to get the remote for the current branch
|
||||
try:
|
||||
# Get current branch name
|
||||
branch_result = subprocess.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
|
||||
capture_output=True, text=True, check=True)
|
||||
current_branch = branch_result.stdout.strip()
|
||||
|
||||
# Get the remote for the current branch
|
||||
remote_result = subprocess.run(['git', 'config', f'branch.{current_branch}.remote'],
|
||||
capture_output=True, text=True, check=True)
|
||||
tracked_remote = remote_result.stdout.strip()
|
||||
|
||||
# Use the tracked remote if we found one
|
||||
if tracked_remote:
|
||||
remote_name = tracked_remote
|
||||
except subprocess.CalledProcessError:
|
||||
# If branch config lookup fails, continue with 'origin' as fallback
|
||||
pass
|
||||
|
||||
# Get the remote URL for the determined remote
|
||||
result = subprocess.run(['git', 'remote', 'get-url', remote_name],
|
||||
capture_output=True, text=True, check=True)
|
||||
remote_url = result.stdout.strip()
|
||||
|
||||
# Check if it's a GitHub URL
|
||||
if 'github.com' not in remote_url.lower():
|
||||
return None
|
||||
|
||||
# Parse GitHub URL patterns:
|
||||
# https://github.com/owner/repo.git
|
||||
# git@github.com:owner/repo.git
|
||||
# https://github.com/owner/repo
|
||||
|
||||
# Remove .git suffix if present
|
||||
if remote_url.endswith('.git'):
|
||||
remote_url = remote_url[:-4]
|
||||
|
||||
# Handle HTTPS URLs
|
||||
https_match = re.search(r'github\.com/([^/]+/[^/]+)', remote_url, re.IGNORECASE)
|
||||
if https_match:
|
||||
return https_match.group(1)
|
||||
|
||||
# Handle SSH URLs
|
||||
ssh_match = re.search(r'github\.com:([^/]+/[^/]+)', remote_url, re.IGNORECASE)
|
||||
if ssh_match:
|
||||
return ssh_match.group(1)
|
||||
|
||||
return None
|
||||
|
||||
except FileNotFoundError:
|
||||
# Git CLI is not installed or not in PATH
|
||||
return None
|
||||
except subprocess.CalledProcessError:
|
||||
# Git command failed (e.g., not a git repo, no remote, etc.)
|
||||
return None
|
||||
except Exception:
|
||||
# Any other unexpected error
|
||||
return None
|
||||
|
||||
# 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"
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
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}"])
|
||||
+12
-82
@@ -10,27 +10,7 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# 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
|
||||
esp32dev_debug
|
||||
esp32_eth
|
||||
esp32_wrover
|
||||
lolin_s2_mini
|
||||
esp32c3dev
|
||||
esp32c3dev_qio
|
||||
esp32S3_wroom2
|
||||
esp32s3dev_16MB_opi
|
||||
esp32s3dev_8MB_opi
|
||||
esp32s3_4M_qspi
|
||||
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, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_16MB_opi, esp32s3dev_8MB_opi, esp32s3_4M_qspi, esp32_wrover, usermods
|
||||
|
||||
src_dir = ./wled00
|
||||
data_dir = ./wled00/data
|
||||
@@ -130,7 +110,7 @@ ldscript_4m1m = eagle.flash.4m1m.ld
|
||||
|
||||
[scripts_defaults]
|
||||
extra_scripts =
|
||||
pre:pio-scripts/set_metadata.py
|
||||
pre:pio-scripts/set_version.py
|
||||
post:pio-scripts/output_bins.py
|
||||
post:pio-scripts/strip-floats.py
|
||||
pre:pio-scripts/user_config_copy.py
|
||||
@@ -284,14 +264,12 @@ 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
|
||||
@@ -306,7 +284,6 @@ 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
|
||||
@@ -325,7 +302,6 @@ 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
|
||||
@@ -344,7 +320,6 @@ 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
|
||||
@@ -453,47 +428,35 @@ 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\" #-D WLED_DISABLE_BROWNOUT_DET
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_V4\" #-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}
|
||||
board_upload.flash_size = 8MB
|
||||
board_upload.maximum_size = 8388608
|
||||
; board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = dio
|
||||
; board_build.flash_mode = qio
|
||||
|
||||
[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}
|
||||
@@ -505,12 +468,10 @@ 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}
|
||||
@@ -525,7 +486,6 @@ 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}
|
||||
@@ -533,7 +493,6 @@ 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}
|
||||
@@ -545,26 +504,19 @@ 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 WLED_WATCHDOG_TIMEOUT=0
|
||||
-D CONFIG_LITTLEFS_FOR_IDF_3_2 -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}
|
||||
@@ -579,14 +531,13 @@ 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 WLED_WATCHDOG_TIMEOUT=0
|
||||
-D CONFIG_LITTLEFS_FOR_IDF_3_2 -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}
|
||||
@@ -598,20 +549,19 @@ 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 WLED_WATCHDOG_TIMEOUT=0
|
||||
-D CONFIG_LITTLEFS_FOR_IDF_3_2 -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}
|
||||
|
||||
@@ -620,33 +570,15 @@ 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
|
||||
@@ -658,7 +590,6 @@ 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
|
||||
@@ -685,7 +616,6 @@ 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
|
||||
|
||||
+12
-117
@@ -28,6 +28,7 @@ lib_deps = ${esp8266.lib_deps}
|
||||
; robtillaart/SHT85@~0.3.3
|
||||
; ;gmag11/QuickESPNow @ ~0.7.0 # will also load QuickDebug
|
||||
; https://github.com/blazoncek/QuickESPNow.git#optional-debug ;; exludes debug library
|
||||
; ${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE
|
||||
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags}
|
||||
@@ -139,6 +140,8 @@ build_flags = ${common.build_flags} ${esp8266.build_flags}
|
||||
; -D PIR_SENSOR_MAX_SENSORS=2 # max allowable sensors (uses OR logic for triggering)
|
||||
;
|
||||
; Use Audioreactive usermod and configure I2S microphone
|
||||
; ${esp32.AR_build_flags} ;; default flags required to properly configure ArduinoFFT
|
||||
; ;; don't forget to add ArduinoFFT to your libs_deps: ${esp32.AR_lib_deps}
|
||||
; -D AUDIOPIN=-1
|
||||
; -D DMTYPE=1 # 0-analog/disabled, 1-I2S generic, 2-ES7243, 3-SPH0645, 4-I2S+mclk, 5-I2S PDM
|
||||
; -D I2S_SDPIN=36
|
||||
@@ -177,7 +180,7 @@ build_flags = ${common.build_flags} ${esp8266.build_flags}
|
||||
;
|
||||
; enable IR by setting remote type
|
||||
; -D IRTYPE=0 # 0 Remote disabled | 1 24-key RGB | 2 24-key with CT | 3 40-key blue | 4 40-key RGB | 5 21-key RGB | 6 6-key black | 7 9-key red | 8 JSON remote
|
||||
;
|
||||
;
|
||||
; use PSRAM on classic ESP32 rev.1 (rev.3 or above has no issues)
|
||||
; -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue # needed only for classic ESP32 rev.1
|
||||
;
|
||||
@@ -191,22 +194,6 @@ 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
|
||||
@@ -254,7 +241,9 @@ lib_deps = ${esp8266.lib_deps}
|
||||
extends = env:esp32dev # we want to extend the existing esp32dev environment (and define only updated options)
|
||||
board = esp32dev
|
||||
build_flags = ${common.build_flags} ${esp32.build_flags} #-D WLED_DISABLE_BROWNOUT_DET
|
||||
${esp32.AR_build_flags} ;; optional - includes USERMOD_AUDIOREACTIVE
|
||||
lib_deps = ${esp32.lib_deps}
|
||||
${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
@@ -266,7 +255,9 @@ board_build.flash_mode = qio
|
||||
extends = esp32_idf_V4 # based on newer "esp-idf V4" platform environment
|
||||
board = esp32dev
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} #-D WLED_DISABLE_BROWNOUT_DET
|
||||
${esp32.AR_build_flags} ;; includes USERMOD_AUDIOREACTIVE
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.partitions = ${esp32.default_partitions} ;; if you get errors about "out of program space", change this to ${esp32.extended_partitions} or even ${esp32.big_partitions}
|
||||
board_build.f_flash = 80000000L
|
||||
@@ -384,9 +375,11 @@ build_flags = ${common.build_flags} ${esp32.build_flags}
|
||||
-D USERMOD_DALLASTEMPERATURE
|
||||
-D USERMOD_FOUR_LINE_DISPLAY
|
||||
-D TEMPERATURE_PIN=23
|
||||
${esp32.AR_build_flags} ;; includes USERMOD_AUDIOREACTIVE
|
||||
lib_deps = ${esp32.lib_deps}
|
||||
OneWire@~2.3.5 ;; needed for USERMOD_DALLASTEMPERATURE
|
||||
olikraus/U8g2 @ ^2.28.8 ;; needed for USERMOD_FOUR_LINE_DISPLAY
|
||||
${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
|
||||
[env:esp32_pico-D4]
|
||||
@@ -398,11 +391,13 @@ build_flags = ${common.build_flags} ${esp32.build_flags}
|
||||
-D WLED_DISABLE_ADALIGHT ;; no serial-to-USB chip on this board - better to disable serial protocols
|
||||
-D DATA_PINS=2,18 ;; LED pins
|
||||
-D RLYPIN=19 -D BTNPIN=0 -D IRPIN=-1 ;; no default pin for IR
|
||||
${esp32.AR_build_flags} ;; include USERMOD_AUDIOREACTIVE
|
||||
-D UM_AUDIOREACTIVE_ENABLE ;; enable AR by default
|
||||
;; Audioreactive settings for on-board microphone (ICS-43432)
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=25 -D I2S_WSPIN=15 -D I2S_CKPIN=14
|
||||
-D SR_SQUELCH=5 -D SR_GAIN=30
|
||||
lib_deps = ${esp32.lib_deps}
|
||||
${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.f_flash = 80000000L
|
||||
|
||||
@@ -530,7 +525,6 @@ build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_DISABLE_BROWNOU
|
||||
-D USER_SETUP_LOADED
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Usermod examples
|
||||
# ------------------------------------------------------------------------------
|
||||
@@ -541,102 +535,3 @@ extends = env:esp32dev
|
||||
build_flags = ${env:esp32dev.build_flags} -D USERMOD_RF433
|
||||
lib_deps = ${env:esp32dev.lib_deps}
|
||||
sui77/rc-switch @ 2.6.4
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Hub75 examples
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
[env:esp32dev_hub75]
|
||||
board = esp32dev
|
||||
upload_speed = 921600
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages =
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags}
|
||||
-D WLED_RELEASE_NAME=\"ESP32_hub75\"
|
||||
-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
|
||||
build_flags = ${common.build_flags}
|
||||
-D WLED_RELEASE_NAME=\"ESP32_hub75_forum_pinout\"
|
||||
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
|
||||
-D ESP32_FORUM_PINOUT ;; enable for SmartMatrix default pins
|
||||
-D WLED_DEBUG_BUS
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
|
||||
; -D WLED_DEBUG
|
||||
|
||||
|
||||
[env:adafruit_matrixportal_esp32s3]
|
||||
; ESP32-S3 processor, 8 MB flash, 2 MB of PSRAM, dedicated driver pins for HUB75
|
||||
board = adafruit_matrixportal_esp32s3
|
||||
;; adafruit recommends to use arduino-esp32 2.0.14
|
||||
;;platform = espressif32@ ~6.5.0
|
||||
;;platform_packages = platformio/framework-arduinoespressif32 @ 3.20014.231204 ;; arduino-esp32 2.0.14
|
||||
platform = ${esp32s3.platform}
|
||||
platform_packages =
|
||||
upload_speed = 921600
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_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
|
||||
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
|
||||
-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.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 (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\"
|
||||
${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
|
||||
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
|
||||
-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.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
|
||||
@@ -1,7 +0,0 @@
|
||||
# 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
|
||||
|
+9
-31
@@ -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_edit.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_pxmagic.h", "wled00/html_settings.h", "wled00/html_other.h"]
|
||||
|
||||
// \x1b[34m is blue, \x1b[36m is cyan, \x1b[0m is reset
|
||||
const wledBanner = `
|
||||
@@ -143,7 +143,7 @@ async function writeHtmlGzipped(sourceFile, resultFile, page) {
|
||||
console.info("Minified and compressed " + sourceFile + " from " + originalLength + " to " + result.length + " bytes");
|
||||
const array = hexdump(result);
|
||||
let src = singleHeader;
|
||||
src += `const uint16_t PAGE_${page}_length = ${result.length};\n`;
|
||||
src += `const uint16_t PAGE_${page}_L = ${result.length};\n`;
|
||||
src += `const uint8_t PAGE_${page}[] PROGMEM = {\n${array}\n};\n\n`;
|
||||
console.info("Writing " + resultFile);
|
||||
fs.writeFileSync(resultFile, src);
|
||||
@@ -244,36 +244,8 @@ if (isAlreadyBuilt("wled00/data") && process.argv[2] !== '--force' && process.ar
|
||||
|
||||
writeHtmlGzipped("wled00/data/index.htm", "wled00/html_ui.h", 'index');
|
||||
writeHtmlGzipped("wled00/data/pixart/pixart.htm", "wled00/html_pixart.h", 'pixart');
|
||||
//writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal');
|
||||
writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal');
|
||||
writeHtmlGzipped("wled00/data/pxmagic/pxmagic.htm", "wled00/html_pxmagic.h", 'pxmagic');
|
||||
//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",
|
||||
[
|
||||
{
|
||||
file: "cpal.htm",
|
||||
name: "PAGE_cpal",
|
||||
method: "gzip",
|
||||
filter: "html-minify"
|
||||
}
|
||||
],
|
||||
"wled00/html_cpal.h"
|
||||
);
|
||||
|
||||
writeChunks(
|
||||
"wled00/data",
|
||||
@@ -403,6 +375,12 @@ 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",
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# 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,
|
||||
|
@@ -1,11 +0,0 @@
|
||||
# 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,
|
||||
|
@@ -1,10 +0,0 @@
|
||||
# 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,
|
||||
|
+35
-78
@@ -28,78 +28,28 @@ log() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Fetch a URL to a destination file, validating status codes.
|
||||
# Usage: fetch "<url>" "<dest or empty>" "200 404"
|
||||
fetch() {
|
||||
local url="$1"
|
||||
local dest="$2"
|
||||
local accepted="${3:-200}"
|
||||
# Generic curl handler function
|
||||
curl_handler() {
|
||||
local command="$1"
|
||||
local hostname="$2"
|
||||
|
||||
# If no dest given, just discard body
|
||||
local out
|
||||
if [ -n "$dest" ]; then
|
||||
# Write to ".tmp" files first, then move when success, to ensure we don't write partial files
|
||||
out="${dest}.tmp"
|
||||
response=$($command -w "%{http_code}" -o /dev/null)
|
||||
curl_exit_code=$?
|
||||
|
||||
if [ "$response" -ge 200 ] && [ "$response" -lt 300 ]; then
|
||||
return 0
|
||||
elif [ $curl_exit_code -ne 0 ]; then
|
||||
log "ERROR" "$RED" "Connection error during request to $hostname (curl exit code: $curl_exit_code)."
|
||||
return 1
|
||||
elif [ "$response" -ge 400 ]; then
|
||||
log "ERROR" "$RED" "Server error during request to $hostname (HTTP status code: $response)."
|
||||
return 2
|
||||
else
|
||||
out="/dev/null"
|
||||
log "ERROR" "$RED" "Unexpected response from $hostname (HTTP status code: $response)."
|
||||
return 3
|
||||
fi
|
||||
|
||||
response=$(curl --connect-timeout 5 --max-time 30 -s -w "%{http_code}" -o "$out" "$url")
|
||||
local curl_exit_code=$?
|
||||
|
||||
if [ $curl_exit_code -ne 0 ]; then
|
||||
[ -n "$dest" ] && rm -f "$out"
|
||||
log "ERROR" "$RED" "Connection error during request to $url (curl exit code: $curl_exit_code)."
|
||||
return 1
|
||||
fi
|
||||
|
||||
for code in $accepted; do
|
||||
if [ "$response" = "$code" ]; then
|
||||
# Accepted; only persist body for 2xx responses
|
||||
if [ -n "$dest" ]; then
|
||||
if [[ "$response" =~ ^2 ]]; then
|
||||
mv "$out" "$dest"
|
||||
else
|
||||
rm -f "$out"
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
# not accepted
|
||||
[ -n "$dest" ] && rm -f "$out"
|
||||
log "ERROR" "$RED" "Unexpected response from $url (HTTP $response)."
|
||||
return 2
|
||||
}
|
||||
|
||||
|
||||
# POST a file to a URL, validating status codes.
|
||||
# Usage: post_file "<url>" "<file>" "200"
|
||||
post_file() {
|
||||
local url="$1"
|
||||
local file="$2"
|
||||
local accepted="${3:-200}"
|
||||
|
||||
response=$(curl --connect-timeout 5 --max-time 300 -s -w "%{http_code}" -o /dev/null -X POST -F "file=@$file" "$url")
|
||||
local curl_exit_code=$?
|
||||
|
||||
if [ $curl_exit_code -ne 0 ]; then
|
||||
log "ERROR" "$RED" "Connection error during POST to $url (curl exit code: $curl_exit_code)."
|
||||
return 1
|
||||
fi
|
||||
|
||||
for code in $accepted; do
|
||||
if [ "$response" -eq "$code" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
log "ERROR" "$RED" "Unexpected response from $url (HTTP $response)."
|
||||
return 2
|
||||
}
|
||||
|
||||
|
||||
# Print help message
|
||||
show_help() {
|
||||
cat << EOF
|
||||
@@ -159,27 +109,33 @@ backup_one() {
|
||||
local address="$2"
|
||||
local port="$3"
|
||||
|
||||
log "INFO" "$YELLOW" "Backing up device config/presets/ir: $hostname ($address:$port)"
|
||||
log "INFO" "$YELLOW" "Backing up device config/presets: $hostname ($address:$port)"
|
||||
|
||||
mkdir -p "$backup_dir"
|
||||
|
||||
local file_prefix="${backup_dir}/${hostname}"
|
||||
local cfg_url="http://$address:$port/cfg.json"
|
||||
local presets_url="http://$address:$port/presets.json"
|
||||
local cfg_dest="${backup_dir}/${hostname}.cfg.json"
|
||||
local presets_dest="${backup_dir}/${hostname}.presets.json"
|
||||
|
||||
if ! fetch "http://$address:$port/cfg.json" "${file_prefix}.cfg.json"; then
|
||||
# Write to ".tmp" files first, then move when success, to ensure we don't write partial files
|
||||
local curl_command_cfg="curl -s "$cfg_url" -o "$cfg_dest.tmp""
|
||||
local curl_command_presets="curl -s "$presets_url" -o "$presets_dest.tmp""
|
||||
|
||||
if ! curl_handler "$curl_command_cfg" "$hostname"; then
|
||||
log "ERROR" "$RED" "Failed to backup configuration for $hostname"
|
||||
rm -f "$cfg_dest.tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! fetch "http://$address:$port/presets.json" "${file_prefix}.presets.json"; then
|
||||
if ! curl_handler "$curl_command_presets" "$hostname"; then
|
||||
log "ERROR" "$RED" "Failed to backup presets for $hostname"
|
||||
rm -f "$presets_dest.tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ir.json is optional
|
||||
if ! fetch "http://$address:$port/ir.json" "${file_prefix}.ir.json" "200 404"; then
|
||||
log "ERROR" "$RED" "Failed to backup ir configs for $hostname"
|
||||
fi
|
||||
fi
|
||||
|
||||
mv "$cfg_dest.tmp" "$cfg_dest"
|
||||
mv "$presets_dest.tmp" "$presets_dest"
|
||||
log "INFO" "$GREEN" "Successfully backed up config and presets for $hostname"
|
||||
return 0
|
||||
}
|
||||
@@ -194,8 +150,9 @@ update_one() {
|
||||
log "INFO" "$YELLOW" "Starting firmware update for device: $hostname ($address:$port)"
|
||||
|
||||
local url="http://$address:$port/update"
|
||||
local curl_command="curl -s -X POST -F "file=@$firmware" "$url""
|
||||
|
||||
if ! post_file "$url" "$firmware" "200"; then
|
||||
if ! curl_handler "$curl_command" "$hostname"; then
|
||||
log "ERROR" "$RED" "Failed to update firmware for $hostname"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -1227,6 +1227,7 @@ 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);
|
||||
@@ -1234,25 +1235,10 @@ 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 && (dmType != 254)) enabled = false;// audio failed to initialise
|
||||
if (!audioSource) enabled = false; // audio failed to initialise
|
||||
#endif
|
||||
if (enabled) onUpdateBegin(false); // create FFT task, and initialize network
|
||||
|
||||
@@ -1995,7 +1981,7 @@ void AudioReactive::createAudioPalettes(void) {
|
||||
if (palettes) return;
|
||||
DEBUG_PRINTLN(F("Adding audio palettes."));
|
||||
for (int i=0; i<MAX_PALETTES; i++)
|
||||
if (customPalettes.size() < WLED_MAX_CUSTOM_PALETTES) {
|
||||
if (customPalettes.size() < 10) {
|
||||
customPalettes.push_back(CRGBPalette16(CRGB(BLACK)));
|
||||
palettes++;
|
||||
DEBUG_PRINTLN(palettes);
|
||||
|
||||
@@ -54,11 +54,7 @@ class RgbRotaryEncoderUsermod : public Usermod
|
||||
|
||||
void initLedBus()
|
||||
{
|
||||
// Initialize all pins to the sentinel value first…
|
||||
byte _pins[OUTPUT_MAX_PINS];
|
||||
std::fill(std::begin(_pins), std::end(_pins), 255);
|
||||
// …then set only the LED pin
|
||||
_pins[0] = static_cast<byte>(ledIo);
|
||||
byte _pins[5] = {(byte)ledIo, 255, 255, 255, 255};
|
||||
BusConfig busCfg = BusConfig(TYPE_WS2812_RGB, _pins, 0, numLeds, COL_ORDER_GRB, false, 0);
|
||||
|
||||
ledBus = new BusDigital(busCfg, WLED_MAX_BUSSES - 1);
|
||||
|
||||
@@ -25,51 +25,11 @@ class StairwayWipeUsermod : public Usermod {
|
||||
public:
|
||||
void setup() {
|
||||
}
|
||||
/**
|
||||
* @brief Drives the stairway wipe state machine and reacts to user variables.
|
||||
*
|
||||
* @details
|
||||
* Reads userVar0 (U0) and userVar1 (U1) to control a directional stairway color wipe:
|
||||
* - U0 = 0: off.
|
||||
* - U0 = 1: start/keep wipe from local side.
|
||||
* - U0 = 2: start/keep wipe from opposite side.
|
||||
* - U0 = 3: toggle mode for direction 1 (becomes 1 when off, 0 when on).
|
||||
* - U0 = 4: toggle mode for direction 2 (becomes 2 when off, 0 when on).
|
||||
*
|
||||
* Manages a small state machine:
|
||||
* - State 0: idle, will start a wipe.
|
||||
* - State 1: wiping; transitions to static when wipe completes.
|
||||
* - State 2: static/hold; transitions to off after U1 seconds if U1 > 0.
|
||||
* - State 3: prepare to wipe off (or immediately off if off-wipe is disabled).
|
||||
* - State 4: wiping off; turns fully off when wipe-off completes.
|
||||
*
|
||||
* The wipe duration and wipe-off timing are derived from the current effectSpeed. A change
|
||||
* in trigger side (previousUserVar0 differing from userVar0) forces the usermod to begin
|
||||
* turning off. When turning on/off the code invokes startWipe() or turnOff() and issues
|
||||
* color/state update notifications as appropriate.
|
||||
*
|
||||
* @note Defining STAIRCASE_WIPE_OFF enables a reverse color-wipe transition when turning off;
|
||||
* without it the lights fade off immediately.
|
||||
*/
|
||||
void loop() {
|
||||
void loop() {
|
||||
//userVar0 (U0 in HTTP API):
|
||||
//has to be set to 1 if movement is detected on the PIR that is the same side of the staircase as the ESP8266
|
||||
//has to be set to 2 if movement is detected on the PIR that is the opposite side
|
||||
//can be set to 0 if no movement is detected. Otherwise LEDs will turn off after a configurable timeout (userVar1 seconds)
|
||||
//U0 = 3: Toggle mode for direction 1 (if off, turn on with U0=1; if on, turn off with U0=0)
|
||||
//U0 = 4: Toggle mode for direction 2 (if off, turn on with U0=2; if on, turn off with U0=0)
|
||||
|
||||
// Handle toggle modes U0=3 and U0=4
|
||||
if (userVar0 == 3 || userVar0 == 4) {
|
||||
if (wipeState == 0 || wipeState == 3 || wipeState == 4) {
|
||||
// Lights are off or turning off, so turn them on
|
||||
wipeState = 0; // Reset state so the state machine starts fresh
|
||||
userVar0 = (userVar0 == 3) ? 1 : 2;
|
||||
} else {
|
||||
// Lights are on or turning on, so turn them off
|
||||
userVar0 = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (userVar0 > 0)
|
||||
{
|
||||
|
||||
+1
-501
@@ -1,504 +1,4 @@
|
||||
# Usermod user FX
|
||||
|
||||
This usermod is a common place to put various users’ WLED effects. It lets you load your own custom effects or bring back deprecated ones—without touching core WLED source code.
|
||||
This Usermod is a common place to put various user's LED effects.
|
||||
|
||||
Multiple Effects can be specified inside this single usermod, as we will illustrate below. You will be able to define them with custom names, sliders, etc. as with any other Effect.
|
||||
|
||||
* [How The Usermod Works](./README.md#how-the-usermod-works)
|
||||
* [Basic Syntax for WLED Effect Creation](./README.md#basic-syntax-for-wled-effect-creation)
|
||||
* [Understanding 2D WLED Effects](./README.md#understanding-2d-wled-effects)
|
||||
* [The Metadata String](./README.md#the-metadata-string)
|
||||
* [Understanding 1D WLED Effects](./README.md#understanding-1d-wled-effects)
|
||||
* [Combining Multiple Effects in this Usermod](./README.md#combining-multiple-effects-in-this-usermod)
|
||||
* [Compiling](./README.md#compiling)
|
||||
* [Change Log](./README.md#change-log)
|
||||
* [Contact Us](./README.md#contact-us)
|
||||
|
||||
## How The Usermod Works
|
||||
|
||||
The `user_fx.cpp` file can be broken down into four main parts:
|
||||
* **static effect definition** - This is a static LED setting that is displayed if an effect fails to initialize.
|
||||
* **User FX function definition(s)** - This area is where you place the FX code for all of the custom effects you want to use. This mainly includes the FX code and the static variable containing the [metadata string](https://kno.wled.ge/interfaces/json-api/#effect-metadata).
|
||||
* **Usermod Class definition(s)** - The class definition defines the blueprint from which all your custom Effects (or any usermod, for that matter) are created.
|
||||
* **Usermod registration** - All usermods have to be registered so that they are able to be compiled into your binary.
|
||||
|
||||
We will go into greater detail on how custom effects work in the usermod and how to go about creating your own in the section below.
|
||||
|
||||
|
||||
## Basic Syntax for WLED Effect Creation
|
||||
|
||||
WLED effects generally follow a certain procedure for their operation:
|
||||
1. Determine dimension of segment
|
||||
2. Calculate new state if needed
|
||||
3. Implement a loop that calculates color for each pixel and sets it using `SEGMENT.setPixelColor()`
|
||||
4. The function is called at current frame rate.
|
||||
|
||||
Below are some helpful variables and functions to know as you start your journey towards WLED effect creation:
|
||||
|
||||
| Syntax Element | Size | Description |
|
||||
| :---------------------------------------------- | :----- | :---------- |
|
||||
| [`SEGMENT.speed / intensity / custom1 / custom2`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L450) | 8-bit | These read-only variables help you control aspects of your custom effect using the UI sliders. You can edit these variables through the UI sliders when WLED is running your effect. (These variables can be controlled by the API as well.) Note that while `SEGMENT.intensity` through `SEGMENT.custom2` are 8-bit variables, `SEGMENT.custom3` is actually 5-bit. The other three bits are used by the boolean parameters `SEGMENT.check1` through `SEGMENT.check3` and are bit-packed to conserve data size and memory. |
|
||||
| [`SEGMENT.custom3`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L454) | 5-bit | Another optional UI slider for custom effect control. While `SEGMENT.speed` through `SEGMENT.custom2` are 8-bit variables, `SEGMENT.custom3` is actually 5-bit. |
|
||||
| [`SEGMENT.check1 / check2 / check3`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L455) | 1-bit | These variables are boolean parameters which show up as checkbox options in the User Interface. They are bit-packed along with `SEGMENT.custom3` to conserve data size and memory. |
|
||||
| [`SEGENV.aux0 / aux1`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L467) | 16-bit | These are state variables that persists between function calls, and they are free to be overwritten by the user for any use case. |
|
||||
| [`SEGENV.step`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L465) | 32-bit | This is a timestamp variable that contains the last update time. It is initially set during effect initialization to 0, and then it updates with the elapsed time after each frame runs. |
|
||||
| [`SEGENV.call`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L466) | 32-bit | A counter for how many times this effect function has been invoked since it started. |
|
||||
| [`strip.now`](https://github.com/wled/WLED/blob/main/wled00/FX.h) | 32-bit | Current timestamp in milliseconds. (Equivalent to `millis()`, but use `strip.now()` instead.) `strip.now` respects the timebase, which can be used to advance or reset effects in a preset. This can be useful to sync multiple segments. |
|
||||
| [`SEGLEN / SEG_W / SEG_H`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L116) | 16-bit | These variables are macros that help define the length and width of your LED strip/matrix segment. |
|
||||
| [`SEGPALETTE`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L115) | --- | Macro that gets the currently selected palette for the currently processing segment. |
|
||||
| [`hw_random8()`](https://github.com/wled/WLED/blob/7b0075d3754fa883fc1bbc9fbbe82aa23a9b97b8/wled00/fcn_declare.h#L548) | 8-bit | One of several functions that generates a random integer. (All of the "hw_" functions are similar to the FastLED library's random functions, but in WLED they use true hardware-based randomness instead of a pseudo random number. In short, they are better and faster.) |
|
||||
| [`SEGCOLOR(x)`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L114) | 32-bit | Macro that gets user-selected colors from UI, where x is an integer 1, 2, or 3 for primary, secondary, and tertiary colors, respectively. |
|
||||
| [`SEGMENT.setPixelColor`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp) / [`setPixelColorXY`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_2Dfcn.cpp) | 32-bit | Function that paints one pixel. `setPixelColor` is 1‑D; `setPixelColorXY` expects `(x, y)` and an RGBW color value. |
|
||||
| [`SEGMENT.color_wheel()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1092) | 32-bit | Input 0–255 to get a color. Transitions r→g→b→r. In HSV terms, `pos` is H. Note: only returns palette color unless the Default palette is selected. |
|
||||
| [`SEGMENT.color_from_palette()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1093) | 32-bit | Gets a single color from the currently selected palette for a segment. (This function which should be favoured over `ColorFromPalette()` because this function returns an RGBW color with white from the `SEGCOLOR` passed, while also respecting the setting for palette wrapping. On the other hand, `ColorFromPalette()` simply gets the RGB palette color.) |
|
||||
| [`fade_out()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1012) | --- | fade out function, higher rate = quicker fade. fading is highly dependent on frame rate (higher frame rates, faster fading). each frame will fade at max 9% or as little as 0.8%. |
|
||||
| [`fadeToBlackBy()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1043) | --- | can be used to fade all pixels to black. |
|
||||
| [`fadeToSecondaryBy()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1043) | --- | fades all pixels to secondary color. |
|
||||
| [`move()`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp) | --- | Moves/shifts pixels in the desired direction. |
|
||||
| [`blur / blur2d`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1053) | --- | Blurs all pixels for the desired segment. Blur also has the boolean option `smear`, which, when activated, does not fade the blurred pixel(s). |
|
||||
|
||||
You will see how these syntax elements work in the examples below.
|
||||
|
||||
|
||||
|
||||
## Understanding 2D WLED Effects
|
||||
|
||||
In this section we give some advice to those who are new to WLED Effect creation. We will illustrate how to load in multiple Effects using this single usermod, and we will do a deep dive into the anatomy of a 1D Effect as well as a 2D Effect.
|
||||
(Special thanks to @mryndzionek for offering this "Diffusion Fire" 2D Effect for this tutorial.)
|
||||
|
||||
### Imports
|
||||
The first line of the code imports the [wled.h](https://github.com/wled/WLED/blob/main/wled00/wled.h) file into this module. Importing `wled.h` brings all of the variables, files, and functions listed in the table above (and more) into your custom effect for you to use.
|
||||
|
||||
```cpp
|
||||
#include "wled.h"
|
||||
```
|
||||
|
||||
### Static Effect Definition
|
||||
The next code block is the `mode_static` definition. This is usually left as `SEGMENT.fill(SEGCOLOR(0));` to leave all pixels off if the effect fails to load, but in theory one could use this as a 'fallback effect' to take on a different behavior, such as displaying some other color instead of leaving the pixels off.
|
||||
|
||||
### User Effect Definitions
|
||||
Pre-loaded in this template is an example 2D Effect called "Diffusion Fire". (This is the name that would be shown in the UI once the binary is compiled and run on your device, as defined in the metadata string.)
|
||||
The effect starts off by checking to see if the segment that the effect is being applied to is a 2D Matrix, and if it is not, then it returns the static effect which displays no pattern:
|
||||
```cpp
|
||||
if (!strip.isMatrix || !SEGMENT.is2D())
|
||||
return mode_static(); // not a 2D set-up
|
||||
```
|
||||
The next code block contains several constant variable definitions which essentially serve to extract the dimensions of the user's 2D matrix and allow WLED to interpret the matrix as a 1D coordinate system (WLED must do this for all 2D animations):
|
||||
```cpp
|
||||
const int cols = SEG_W;
|
||||
const int rows = SEG_H;
|
||||
const auto XY = [&](int x, int y) { return x + y * cols; };
|
||||
```
|
||||
* The first line assigns the number of columns (width) in the active segment to cols.
|
||||
* SEG_W is a macro defined in WLED that expands to SEGMENT.width(). This value is the width of your 2D matrix segment, used to traverse the matrix correctly.
|
||||
* Next, we assign the number of rows (height) in the segment to rows.
|
||||
* SEG_H is a macro for SEGMENT.height(). Combined with cols, this allows pixel addressing in 2D (x, y) space.
|
||||
* The third line declares a lambda function named `XY` to map (x, y) matrix coordinates into a 1D index in the LED array. This assumes row-major order (left to right, top to bottom).
|
||||
* This lambda helps with mapping a local 1D array to a 2D one.
|
||||
|
||||
The next lines of code further the setup process by defining variables that allow the effect's settings to be configurable using the UI sliders (or alternatively, through API calls):
|
||||
```cpp
|
||||
const uint8_t refresh_hz = map(SEGMENT.speed, 0, 255, 20, 80);
|
||||
const unsigned refresh_ms = 1000 / refresh_hz;
|
||||
const int16_t diffusion = map(SEGMENT.custom1, 0, 255, 0, 100);
|
||||
const uint8_t spark_rate = SEGMENT.intensity;
|
||||
const uint8_t turbulence = SEGMENT.custom2;
|
||||
```
|
||||
* The first line maps the SEGMENT.speed (user-controllable parameter from 0–255) to a value between 20 and 80 Hz.
|
||||
* This determines how often the effect should refresh per second (Higher speed = more frames per second).
|
||||
* Next we convert refresh rate from Hz to milliseconds. (It’s easier to schedule animation updates in WLED using elapsed time in milliseconds.)
|
||||
* This value is used to time when to update the effect.
|
||||
* The third line utilizes the `custom1` control (0–255 range, usually exposed via sliders) to define the diffusion rate, mapped to 0–100.
|
||||
* This controls how much "heat" spreads to neighboring pixels — more diffusion = smoother flame spread.
|
||||
* Next we assign `SEGMENT.intensity` (user input 0–255) to a variable named `spark_rate`.
|
||||
* This controls how frequently new "spark" pixels appear at the bottom of the matrix.
|
||||
* A higher value means more frequent ignition of flame points.
|
||||
* The final line stores the user-defined `custom2` value to a variable called `turbulence`.
|
||||
* This is used to introduce randomness in spark generation or flow — more turbulence means more chaotic behavior.
|
||||
|
||||
Next we will look at some lines of code that handle memory allocation and effect initialization:
|
||||
|
||||
```cpp
|
||||
unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vWidth()*vHeight() for 2D
|
||||
```
|
||||
* This part calculates how much memory we need to represent per-pixel state.
|
||||
* `cols * rows` or `(or SEGLEN)` returns the total number of pixels in the current segment.
|
||||
* This fire effect models heat values per pixel (not just colors), so we need persistent storage — one uint8_t per pixel — for the entire effect.
|
||||
> **_NOTE:_** Virtual lengths `vWidth()` and `vHeight()` will be evaluated differently based on your own custom effect, and based on what other settings are active. For example: If you have an LED strip of length = 60 and you enable grouping = 2, then the virtual length will be 30, so the FX will render 30 pixels instead of 60. This is also true for mirroring or adding gaps--it halves the size. For a 1D strip mapped to 2D, the virtual length depends on selected mode. Keep these things in mind during your custom effect's creation.
|
||||
|
||||
```cpp
|
||||
if (!SEGENV.allocateData(dataSize))
|
||||
return mode_static(); // allocation failed
|
||||
```
|
||||
* Upon the first call, this section allocates a persistent data buffer tied to the segment environment (`SEGENV.data`). All subsequent calls simply ensure that the data is still valid.
|
||||
* The syntax `SEGENV.allocateData(n)` requests a buffer of size n bytes (1 byte per pixel here).
|
||||
* If allocation fails (e.g., out of memory), it returns false, and the effect can’t proceed.
|
||||
* It calls previously defined `mode_static()` fallback effect, which just fills the segment with a static color. We need to do this because WLED needs a fail-safe behavior if a custom effect can't run properly due to memory constraints.
|
||||
|
||||
|
||||
The next lines of code clear the LEDs and initialize timing:
|
||||
```cpp
|
||||
if (SEGENV.call == 0) {
|
||||
SEGMENT.fill(BLACK);
|
||||
SEGENV.step = 0;
|
||||
}
|
||||
```
|
||||
* The first line checks whether this is the first time the effect is being run; `SEGENV.call` is a counter for how many times this effect function has been invoked since it started.
|
||||
* If `SEGENV.call` equals 0 (which it does on the very first call, making it useful for initialization), then it clears the LED segment by filling it with black (turns off all LEDs).
|
||||
* This gives a clean starting point for the fire animation.
|
||||
* It also initializes `SEGENV.step`, a timing marker, to 0. This value is later used as a timestamp to control when the next animation frame should occur (based on elapsed time).
|
||||
|
||||
|
||||
The next block of code is where the animation update logic starts to kick in:
|
||||
```cpp
|
||||
if ((strip.now - SEGENV.step) >= refresh_ms) {
|
||||
uint8_t tmp_row[cols]; // Keep for ≤~1 KiB; otherwise consider heap or reuse SEGENV.data as scratch.
|
||||
SEGENV.step = strip.now;
|
||||
// scroll up
|
||||
for (unsigned y = 1; y < rows; y++)
|
||||
for (unsigned x = 0; x < cols; x++) {
|
||||
unsigned src = XY(x, y);
|
||||
unsigned dst = XY(x, y - 1);
|
||||
SEGENV.data[dst] = SEGENV.data[src];
|
||||
}
|
||||
```
|
||||
* The first line checks if it's time to update the effect frame. `strip.now` is the current timestamp in milliseconds; `SEGENV.step` is the last update time (set during initialization or previous frame). `refresh_ms` is how long to wait between frames, computed earlier based on SEGMENT.speed.
|
||||
* The conditional statement in the first line of code ensures the effect updates on a fixed interval — e.g., every 20 ms for 50 Hz.
|
||||
* The second line of code declares a temporary row buffer for intermediate diffusion results that is one byte per column (horizontal position), so this buffer holds one row's worth of heat values.
|
||||
* You'll see later that it writes results here before updating `SEGENV.data`.
|
||||
* Note: this is allocated on the stack each frame. Keep such VLAs ≤ ~1 KiB; for larger sizes, prefer a buffer in `SEGENV.data`.
|
||||
|
||||
> **_IMPORTANT NOTE:_** Creating variable‑length arrays (VLAs) is non‑standard C++, but this practice is used throughout WLED and works in practice. But be aware that VLAs live on the stack, which is limited. If the array scales with segment length (1D), it can overflow the stack and crash. Keep VLAs ≲ ~1 KiB; an array with 4000 LEDs is ~4 KiB and will likely crash. It’s worse with `uint16_t`. Anything larger than ~1 KiB should go into `SEGENV.data`, which has a higher limit.
|
||||
|
||||
|
||||
Now we get to the spark generation portion, where new bursts of heat appear at the bottom of the matrix:
|
||||
```cpp
|
||||
if (hw_random8() > turbulence) {
|
||||
// create new sparks at bottom row
|
||||
for (unsigned x = 0; x < cols; x++) {
|
||||
uint8_t p = hw_random8();
|
||||
if (p < spark_rate) {
|
||||
unsigned dst = XY(x, rows - 1);
|
||||
SEGENV.data[dst] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
* The first line randomizes whether we even attempt to spawn sparks this frame.
|
||||
* `hw_random8()` gives a random number between 0–255 using a fast hardware RNG.
|
||||
* `turbulence` is a user-controlled parameter (SEGMENT.custom2, set earlier).
|
||||
* Higher turbulence means this block is less likely to run (because `hw_random8()` is less likely to exceed a high threshold).
|
||||
* This adds randomness to when sparks appear — simulating natural flicker and chaotic fire.
|
||||
* The next line loops over all columns in the bottom row (row `rows - 1`).
|
||||
* Another random number, `p`, is used to probabilistically decide whether a spark appears at this (x, `rows-1`) position.
|
||||
* Next is a conditional statement. The lower spark_rate is, the fewer sparks will appear.
|
||||
* `spark_rate` comes from `SEGMENT.intensity` (0–255).
|
||||
* High intensity means more frequent ignition.
|
||||
* `dst` calculates the destination index in the bottom row at column x.
|
||||
* The final line here sets the heat at this pixel to maximum (255).
|
||||
* This simulates a fresh burst of flame, which will diffuse and move upward over time in subsequent frames.
|
||||
|
||||
Next we reach the first part of the core of the fire simulation, which is diffusion (how heat spreads to neighboring pixels):
|
||||
```cpp
|
||||
// diffuse
|
||||
for (unsigned y = 0; y < rows; y++) {
|
||||
for (unsigned x = 0; x < cols; x++) {
|
||||
unsigned v = SEGENV.data[XY(x, y)];
|
||||
if (x > 0) {
|
||||
v += SEGENV.data[XY(x - 1, y)];
|
||||
}
|
||||
if (x < (cols - 1)) {
|
||||
v += SEGENV.data[XY(x + 1, y)];
|
||||
}
|
||||
tmp_row[x] = min(255, (int)(v * 100 / (300 + diffusion)));
|
||||
}
|
||||
```
|
||||
* This block of code starts by looping over each row from top to bottom. (We will do diffusion for each pixel row.)
|
||||
* Next we start an inner loop which iterates across each column in the current row.
|
||||
* Starting with the current heat value of pixel (x, y) assigned `v`:
|
||||
* if there’s a pixel to the left, add its heat to the total.
|
||||
* If there’s a pixel to the right, add its heat as well.
|
||||
* So essentially, what the two `if` statements accomplish is: `v = center + left + right`.
|
||||
* The final line of code applies diffusion smoothing:
|
||||
* The denominator controls how much the neighboring heat contributes. `300 + diffusion` means that with higher diffusion, you get more smoothing (since the sum is divided more).
|
||||
* The `v * 100` scales things before dividing (preserving some dynamic range).
|
||||
* `min(255, ...)` clamps the result to 8-bit range.
|
||||
* This entire line of code stores the smoothed heat into the temporary row buffer.
|
||||
|
||||
After calculating tmp_row, we now handle rendering the pixels by updating the actual segment data and turning 'heat' into visible colors:
|
||||
```cpp
|
||||
for (unsigned x = 0; x < cols; x++) {
|
||||
SEGENV.data[XY(x, y)] = tmp_row[x];
|
||||
if (SEGMENT.check1) {
|
||||
uint32_t color = SEGMENT.color_from_palette(tmp_row[x], true, false, 0);
|
||||
SEGMENT.setPixelColorXY(x, y, color);
|
||||
} else {
|
||||
uint32_t base = SEGCOLOR(0);
|
||||
SEGMENT.setPixelColorXY(x, y, color_fade(base, tmp_row[x]));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
* This next loop starts iterating over each row from top to bottom. (We're now doing this for color-rendering for each pixel row.)
|
||||
* Next we update the main segment data with the smoothed value for this pixel.
|
||||
* The if statement creates a conditional rendering path — the user can toggle this. If `check1` is enabled in the effect metadata, we use a color palette to display the flame.
|
||||
* The next line converts the heat value (`tmp_row[x]`) into a `color` from the current palette with 255 brightness, and no wrapping in palette lookup.
|
||||
* This creates rich gradient flames (e.g., yellow → red → black).
|
||||
* Finally we set the rendered color for the pixel (x, y).
|
||||
* This repeats for each pixel in each row.
|
||||
* If palette use is disabled, we fallback to fading a base color.
|
||||
* `SEGCOLOR(0)` gets the first user-selected color for the segment.
|
||||
* The final line of code fades that base color according to the heat value (acts as brightness multiplier).
|
||||
|
||||
The final piece of this custom effect returns the frame time:
|
||||
```cpp
|
||||
}
|
||||
return FRAMETIME;
|
||||
}
|
||||
```
|
||||
* The first bracket closes the earlier `if ((strip.now - SEGENV.step) >= refresh_ms)` block.
|
||||
* It ensures that the fire simulation (scrolling, sparking, diffusion, rendering) only runs when enough time has passed since the last update.
|
||||
* returning the frame time tells WLED how soon this effect wants to be called again.
|
||||
* `FRAMETIME` is a predefined macro in WLED, typically set to ~16ms, corresponding to ~60 FPS (frames per second).
|
||||
* Even though the effect logic itself controls when to update based on refresh_ms, WLED will still call this function at roughly FRAMETIME intervals to check whether an update is needed.
|
||||
* ⚠️ Important: Because the actual frame logic is gated by strip.now - SEGENV.step, returning FRAMETIME here doesn’t cause excessive updates — it just keeps the engine responsive. **Also note that an Effect should ALWAYS return FRAMETIME. Not doing so can cause glitches.**
|
||||
* The final bracket closes the `mode_diffusionfire()` function itself.
|
||||
|
||||
|
||||
### The Metadata String
|
||||
At the end of every effect is an important line of code called the **metadata string**.
|
||||
It defines how the effect is to be interacted with in the UI:
|
||||
```cpp
|
||||
static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spark rate,Diffusion Speed,Turbulence,,Use palette;;Color;;2;pal=35";
|
||||
```
|
||||
This metadata string is passed into `strip.addEffect()` and parsed by WLED to determine how your effect appears and behaves in the UI.
|
||||
The string follows the syntax of `<Effect Parameters>;<Colors>;<Palette>;<Flags>;<Defaults>`, where Effect Parameters are specified by a comma-separated list.
|
||||
The values for Effect Parameters will always follow the convention in the table below:
|
||||
|
||||
| Parameter | Default tooltip label |
|
||||
| :-------- | :-------------------- |
|
||||
| sx | Effect Speed |
|
||||
| ix | Effect Intensity |
|
||||
| c1 | Custom 1 |
|
||||
| c2 | Custom 2 |
|
||||
| c3 | Custom 3 |
|
||||
| o1 | Checkbox 1 |
|
||||
| o2 | Checkbox 2 |
|
||||
| o3 | Checkbox 3 |
|
||||
|
||||
Using this info, let’s split the Metadata string above into logical sections:
|
||||
|
||||
| Syntax Element | Description |
|
||||
| :---------------------------------------------- | :---------- |
|
||||
| "Diffusion Fire@! | Name. (The @ symbol marks the end of the Effect Name, and the beginning of the Parameter String elements.) |
|
||||
| !, | Use default UI entry; for the first space, this will automatically create a slider for Speed |
|
||||
| Spark rate, Diffusion Speed, Turbulence, | UI sliders for Spark Rate, Diffusion Speed, and Turbulence. Defining slider 2 as "Spark Rate" overwrites the default value of Intensity. |
|
||||
| (blank), | unused (empty field with not even a space) |
|
||||
| Use palette; | This occupies the spot for the 6th effect parameter, which automatically makes this a checkbox argument `o1` called Use palette in the UI. When this is enabled, the effect uses `SEGMENT.color_from_palette(...)` (RGBW-aware, respects wrap), otherwise it fades from `SEGCOLOR(0)`. The first semicolon marks the end of the Effect Parameters and the beginning of the `Colors` parameter. |
|
||||
| Color; | Custom color field `(SEGCOLOR(0))` |
|
||||
| (blank); | Empty means the effect does not allow Palettes to be selected by the user. But used in conjunction with the checkbox argument, palette use can be turned on/off by the user. |
|
||||
| 2; | Flag specifying that the effect requires a 2D matrix setup |
|
||||
| pal=35" | Default Palette ID. this is the setting that the effect starts up with. |
|
||||
|
||||
More information on metadata strings can be found [here](https://kno.wled.ge/interfaces/json-api/#effect-metadata).
|
||||
|
||||
|
||||
## Understanding 1D WLED Effects
|
||||
|
||||
Next, we will look at a 1D WLED effect called `Sinelon`. This one is an especially interesting example because it shows how a single effect function can be used to create several different selectable effects in the UI.
|
||||
We will break this effect down step by step.
|
||||
(This effect was originally one of the FastLED example effects; more information on FastLED can be found [here](https://fastled.io/).)
|
||||
|
||||
```cpp
|
||||
static uint16_t sinelon_base(bool dual, bool rainbow=false) {
|
||||
```
|
||||
* The first line of code defines `sinelon base` as static helper function. This is how all effects are initially defined.
|
||||
* Notice that it has some optional flags; these parameters will allow us to easily define the effect in different ways in the UI.
|
||||
|
||||
```cpp
|
||||
if (SEGLEN <= 1) return mode_static();
|
||||
```
|
||||
* If segment length ≤ 1, there’s nothing to animate. Just show static mode.
|
||||
|
||||
The line of code helps create the "Fade Out" Trail:
|
||||
```cpp
|
||||
SEGMENT.fade_out(SEGMENT.intensity);
|
||||
```
|
||||
* Gradually dims all LEDs each frame using SEGMENT.intensity as fade amount.
|
||||
* Creates the trailing "comet" effect by leaving a fading path behind the moving dot.
|
||||
|
||||
Next, the effect computes some position information for the actively changing pixel, and the rest of the pixels as well:
|
||||
```cpp
|
||||
unsigned pos = beatsin16_t(SEGMENT.speed/10, 0, SEGLEN-1);
|
||||
if (SEGENV.call == 0) SEGENV.aux0 = pos;
|
||||
```
|
||||
* Calculates a sine-based oscillation to move the dot smoothly back and forth.
|
||||
* `beatsin16_t` is an improved version of FastLED’s beatsin16 function, generating smooth oscillations
|
||||
* SEGMENT.speed / 10: affects oscillation speed. Higher = faster.
|
||||
* 0: minimum position.
|
||||
* SEGLEN-1: maximum position.
|
||||
* On first call `(SEGENV.call == 0)`, stores initial position in `SEGENV.aux0`. (`SEGENV.aux0` is a temporary state variable to keep track of last position.)
|
||||
|
||||
The next lines of code help determine the colors to be used:
|
||||
```cpp
|
||||
uint32_t color1 = SEGMENT.color_from_palette(pos, true, false, 0);
|
||||
uint32_t color2 = SEGCOLOR(2);
|
||||
```
|
||||
* `color1`: main moving dot color, chosen from palette using the current position as index.
|
||||
* `color2`: secondary color from user-configured color slot 2.
|
||||
|
||||
The next part takes into account the optional argument for if a Rainbow colored palette is in use:
|
||||
```cpp
|
||||
if (rainbow) {
|
||||
color1 = SEGMENT.color_wheel((pos & 0x07) * 32);
|
||||
}
|
||||
```
|
||||
* If `rainbow` is true, override color1 using a rainbow wheel, producing rainbow cycling colors.
|
||||
* `(pos & 0x07) * 32` ensures the color changes gradually with position.
|
||||
|
||||
```cpp
|
||||
SEGMENT.setPixelColor(pos, color1);
|
||||
```
|
||||
* Lights up the computed position with the selected color.
|
||||
|
||||
The next line takes into account another one of the optional arguments for the effect to potentially handle dual mirrored dots which create the animation:
|
||||
```cpp
|
||||
if (dual) {
|
||||
if (!color2) color2 = SEGMENT.color_from_palette(pos, true, false, 0);
|
||||
if (rainbow) color2 = color1; // share rainbow color
|
||||
SEGMENT.setPixelColor(SEGLEN-1-pos, color2);
|
||||
}
|
||||
```
|
||||
* If dual is true:
|
||||
* Uses `color2` for mirrored dot on opposite side.
|
||||
* If `color2` is not set (0), fallback to same palette color as `color1`.
|
||||
* In `rainbow` mode, force both dots to share the rainbow color.
|
||||
* Sets pixel at `SEGLEN-1-pos` to `color2`.
|
||||
|
||||
This final part of the effect function will fill in the 'trailing' pixels to complete the animation:
|
||||
```cpp
|
||||
if (SEGENV.aux0 < pos) {
|
||||
for (unsigned i = SEGENV.aux0; i < pos ; i++) {
|
||||
SEGMENT.setPixelColor(i, color1);
|
||||
if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2);
|
||||
}
|
||||
} else {
|
||||
for (unsigned i = SEGENV.aux0; i > pos ; i--) {
|
||||
SEGMENT.setPixelColor(i, color1);
|
||||
if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2);
|
||||
}
|
||||
}
|
||||
SEGENV.aux0 = pos;
|
||||
}
|
||||
```
|
||||
* The first line checks if current position has changed since last frame. (Prevents holes if the dot moves quickly and "skips" pixels.) If the position has changed, then it will implement the logic to update the rest of the pixels.
|
||||
* Fills in all pixels between previous position (SEGENV.aux0) and new position (pos) to ensure smooth continuous trail.
|
||||
* Works in both directions: Forward (if new pos > old pos), and Backward (if new pos < old pos).
|
||||
* Updates `SEGENV.aux0` to current position at the end.
|
||||
|
||||
Finally, we return the `FRAMETIME`, as with all effect functions:
|
||||
```cpp
|
||||
return FRAMETIME;
|
||||
}
|
||||
```
|
||||
* Returns `FRAMETIME` constant to set effect update rate (usually ~16 ms).
|
||||
|
||||
The last part of this effect has the Wrapper functions for different Sinelon modes.
|
||||
Notice that there are three different modes that we can define from the single effect definition by leveraging the arguments in the function:
|
||||
```cpp
|
||||
uint16_t mode_sinelon(void) {
|
||||
return sinelon_base(false);
|
||||
}
|
||||
// Calls sinelon_base with dual = false and rainbow = false
|
||||
|
||||
uint16_t mode_sinelon_dual(void) {
|
||||
return sinelon_base(true);
|
||||
}
|
||||
// Calls sinelon_base with dual = true and rainbow = false
|
||||
|
||||
uint16_t mode_sinelon_rainbow(void) {
|
||||
return sinelon_base(false, true);
|
||||
}
|
||||
// Calls sinelon_base with dual = false and rainbow = true
|
||||
```
|
||||
|
||||
And then the last part defines the metadata strings for each effect to specify how it will be portrayed in the UI:
|
||||
```cpp
|
||||
static const char _data_FX_MODE_SINELON[] PROGMEM = "Sinelon@!,Trail;!,!,!;!";
|
||||
static const char _data_FX_MODE_SINELON_DUAL[] PROGMEM = "Sinelon Dual@!,Trail;!,!,!;!";
|
||||
static const char _data_FX_MODE_SINELON_RAINBOW[] PROGMEM = "Sinelon Rainbow@!,Trail;,,!;!";
|
||||
```
|
||||
Refer to the section above for guidance on understanding metadata strings.
|
||||
|
||||
|
||||
### The UserFxUsermod Class
|
||||
|
||||
The `UserFxUsermod` class registers the `mode_diffusionfire` effect with WLED. This section starts right after the effect function and metadata string, and is responsible for making the effect usable in the WLED interface:
|
||||
```cpp
|
||||
class UserFxUsermod : public Usermod {
|
||||
private:
|
||||
public:
|
||||
void setup() override {
|
||||
strip.addEffect(255, &mode_diffusionfire, _data_FX_MODE_DIFFUSIONFIRE);
|
||||
|
||||
////////////////////////////////////////
|
||||
// add your effect function(s) here //
|
||||
////////////////////////////////////////
|
||||
|
||||
// use id=255 for all custom user FX (the final id is assigned when adding the effect)
|
||||
|
||||
// strip.addEffect(255, &mode_your_effect, _data_FX_MODE_YOUR_EFFECT);
|
||||
// strip.addEffect(255, &mode_your_effect2, _data_FX_MODE_YOUR_EFFECT2);
|
||||
// strip.addEffect(255, &mode_your_effect3, _data_FX_MODE_YOUR_EFFECT3);
|
||||
}
|
||||
void loop() override {} // nothing to do in the loop
|
||||
uint16_t getId() override { return USERMOD_ID_USER_FX; }
|
||||
};
|
||||
```
|
||||
* The first line declares a new class called UserFxUsermod. It inherits from `Usermod`, which is the base class WLED uses for any pluggable user-defined modules.
|
||||
* This makes UserFxUsermod a valid WLED extension that can hook into `setup()`, `loop()`, and other lifecycle events.
|
||||
* The `void setup()` function runs once when WLED initializes the usermod.
|
||||
* It's where you should register your effects, initialize hardware, or do any other setup logic.
|
||||
* `override` ensures that this matches the Usermod base class definition.
|
||||
* The `strip.addEffect` line is an important one that registers the custom effect so WLED knows about it.
|
||||
* 255: Temporary ID — WLED will assign a unique ID automatically. (**Create all custom effects with the 255 ID.**)
|
||||
* `&mode_diffusionfire`: Pointer to the effect function.
|
||||
* `_data_FX_MODE_DIFFUSIONFIRE`: Metadata string stored in PROGMEM, describing the effect name and UI fields (like sliders).
|
||||
* After this, your custom effect shows up in the WLED effects list.
|
||||
* The `loop()` function remains empty because this usermod doesn’t need to do anything continuously. WLED still calls this every main loop, but nothing is done here.
|
||||
* If your usermod had to respond to input or update state, you'd do it here.
|
||||
* The last part returns a unique ID constant used to identify this usermod.
|
||||
* USERMOD_ID_USER_FX is defined in [const.h](https://github.com/wled/WLED/blob/main/wled00/const.h). WLED uses this for tracking, debugging, or referencing usermods internally.
|
||||
|
||||
The final part of this file handles instantiation and initialization:
|
||||
```cpp
|
||||
static UserFxUsermod user_fx;
|
||||
REGISTER_USERMOD(user_fx);
|
||||
```
|
||||
* The first line creates a single, global instance of your usermod class.
|
||||
* The last line is a macro that tells WLED: “This is a valid usermod — load it during startup.”
|
||||
* WLED adds it to the list of active usermods, calls `setup()` and `loop()`, and lets it interact with the system.
|
||||
|
||||
|
||||
|
||||
## Combining Multiple Effects in this Usermod
|
||||
|
||||
So now let's say that you wanted add the effects "Diffusion Fire" and "Sinelon" through this same Usermod file:
|
||||
* Navigate to [the code for Sinelon](https://github.com/wled/WLED/blob/7b0075d3754fa883fc1bbc9fbbe82aa23a9b97b8/wled00/FX.cpp#L3110).
|
||||
* Copy this code, and place it below the metadata string for Diffusion Fire. Be sure to get the metadata string as well--and to name it something different than what's already inside the core WLED code. (Refer to the metadata String section above for more information.)
|
||||
* Register the effect using the `addEffect` function in the Usermod class.
|
||||
* Compile the code!
|
||||
|
||||
## Compiling
|
||||
Compiling WLED yourself is beyond the scope of this tutorial, but [the complete guide to compiling WLED can be found here](https://kno.wled.ge/advanced/compiling-wled/), on the official WLED documentation website.
|
||||
|
||||
## Change Log
|
||||
|
||||
### Version 1.0.0
|
||||
|
||||
* First version of the custom effect creation guide
|
||||
|
||||
## Contact Us
|
||||
|
||||
This custom effect tutorial guide is still in development.
|
||||
If you have suggestions on what should be added, or if you've found any parts of this guide which seem incorrect, feel free to reach out [here](mailto:aregis1992@gmail.com) and help us improve this guide for future creators.
|
||||
|
||||
@@ -27,7 +27,7 @@ static uint16_t mode_diffusionfire(void) {
|
||||
const uint8_t spark_rate = SEGMENT.intensity;
|
||||
const uint8_t turbulence = SEGMENT.custom2;
|
||||
|
||||
unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vWidth()*vHeight() for 2D
|
||||
unsigned dataSize = SEGMENT.length(); // allocate persistent data for heat value for each pixel
|
||||
if (!SEGENV.allocateData(dataSize))
|
||||
return mode_static(); // allocation failed
|
||||
|
||||
@@ -37,7 +37,6 @@ unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vW
|
||||
}
|
||||
|
||||
if ((strip.now - SEGENV.step) >= refresh_ms) {
|
||||
// Keep for ≤~1 KiB; otherwise consider heap or reuse SEGENV.data as scratch.
|
||||
uint8_t tmp_row[cols];
|
||||
SEGENV.step = strip.now;
|
||||
// scroll up
|
||||
@@ -45,7 +44,7 @@ unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vW
|
||||
for (unsigned x = 0; x < cols; x++) {
|
||||
unsigned src = XY(x, y);
|
||||
unsigned dst = XY(x, y - 1);
|
||||
SEGENV.data[dst] = SEGENV.data[src];
|
||||
SEGMENT.data[dst] = SEGMENT.data[src];
|
||||
}
|
||||
|
||||
if (hw_random8() > turbulence) {
|
||||
@@ -54,7 +53,7 @@ unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vW
|
||||
uint8_t p = hw_random8();
|
||||
if (p < spark_rate) {
|
||||
unsigned dst = XY(x, rows - 1);
|
||||
SEGENV.data[dst] = 255;
|
||||
SEGMENT.data[dst] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,24 +61,24 @@ unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vW
|
||||
// diffuse
|
||||
for (unsigned y = 0; y < rows; y++) {
|
||||
for (unsigned x = 0; x < cols; x++) {
|
||||
unsigned v = SEGENV.data[XY(x, y)];
|
||||
unsigned v = SEGMENT.data[XY(x, y)];
|
||||
if (x > 0) {
|
||||
v += SEGENV.data[XY(x - 1, y)];
|
||||
v += SEGMENT.data[XY(x - 1, y)];
|
||||
}
|
||||
if (x < (cols - 1)) {
|
||||
v += SEGENV.data[XY(x + 1, y)];
|
||||
v += SEGMENT.data[XY(x + 1, y)];
|
||||
}
|
||||
tmp_row[x] = min(255, (int)(v * 100 / (300 + diffusion)));
|
||||
}
|
||||
|
||||
for (unsigned x = 0; x < cols; x++) {
|
||||
SEGENV.data[XY(x, y)] = tmp_row[x];
|
||||
SEGMENT.data[XY(x, y)] = tmp_row[x];
|
||||
if (SEGMENT.check1) {
|
||||
uint32_t color = SEGMENT.color_from_palette(tmp_row[x], true, false, 0);
|
||||
uint32_t color = ColorFromPalette(SEGPALETTE, tmp_row[x], 255, LINEARBLEND_NOWRAP);
|
||||
SEGMENT.setPixelColorXY(x, y, color);
|
||||
} else {
|
||||
uint32_t base = SEGCOLOR(0);
|
||||
SEGMENT.setPixelColorXY(x, y, color_fade(base, tmp_row[x]));
|
||||
uint32_t color = SEGCOLOR(0);
|
||||
SEGMENT.setPixelColorXY(x, y, color_fade(color, tmp_row[x]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+190
-324
@@ -135,8 +135,7 @@ uint16_t mode_copy_segment(void) {
|
||||
SEGMENT.fadeToBlackBy(5); // fade out
|
||||
return FRAMETIME;
|
||||
}
|
||||
Segment& sourcesegment = strip.getSegment(sourceid);
|
||||
|
||||
Segment sourcesegment = strip.getSegment(sourceid);
|
||||
if (sourcesegment.isActive()) {
|
||||
uint32_t sourcecolor;
|
||||
uint32_t destcolor;
|
||||
@@ -678,7 +677,7 @@ uint16_t mode_twinkle(void) {
|
||||
SEGENV.step = it;
|
||||
}
|
||||
|
||||
uint16_t PRNG16 = SEGENV.aux1;
|
||||
unsigned PRNG16 = SEGENV.aux1;
|
||||
|
||||
for (unsigned i = 0; i < SEGENV.aux0; i++)
|
||||
{
|
||||
@@ -1715,8 +1714,8 @@ static const char _data_FX_MODE_TRICOLOR_WIPE[] PROGMEM = "Tri Wipe@!;1,2,3;!";
|
||||
* Modified by Aircoookie
|
||||
*/
|
||||
uint16_t mode_tricolor_fade(void) {
|
||||
uint16_t counter = strip.now * ((SEGMENT.speed >> 3) +1);
|
||||
uint32_t prog = (counter * 768) >> 16;
|
||||
unsigned counter = strip.now * ((SEGMENT.speed >> 3) +1);
|
||||
uint16_t prog = (counter * 768) >> 16;
|
||||
|
||||
uint32_t color1 = 0, color2 = 0;
|
||||
unsigned stage = 0;
|
||||
@@ -2607,11 +2606,9 @@ static CRGB twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat)
|
||||
// This is like 'triwave8', which produces a
|
||||
// symmetrical up-and-down triangle sawtooth waveform, except that this
|
||||
// function produces a triangle wave with a faster attack and a slower decay
|
||||
if (cat) { //twinklecat, variant where the leds instantly turn on and fade off
|
||||
if (cat) //twinklecat, variant where the leds instantly turn on
|
||||
{
|
||||
bright = 255 - ph;
|
||||
if (SEGMENT.check2) { //reverse checkbox, reverses the leds to fade on and instantly turn off
|
||||
bright = ph;
|
||||
}
|
||||
} else { //vanilla twinklefox
|
||||
if (ph < 86) {
|
||||
bright = ph * 3;
|
||||
@@ -2719,7 +2716,7 @@ uint16_t mode_twinklecat()
|
||||
{
|
||||
return twinklefox_base(true);
|
||||
}
|
||||
static const char _data_FX_MODE_TWINKLECAT[] PROGMEM = "Twinklecat@!,Twinkle rate,,,,Cool,Reverse;!,!;!";
|
||||
static const char _data_FX_MODE_TWINKLECAT[] PROGMEM = "Twinklecat@!,Twinkle rate,,,,Cool;!,!;!";
|
||||
|
||||
|
||||
uint16_t mode_halloween_eyes()
|
||||
@@ -4509,7 +4506,7 @@ uint16_t mode_image(void) {
|
||||
// Serial.println(status);
|
||||
// }
|
||||
}
|
||||
static const char _data_FX_MODE_IMAGE[] PROGMEM = "Image@!,Blur,;;;12;sx=128,ix=0";
|
||||
static const char _data_FX_MODE_IMAGE[] PROGMEM = "Image@!,;;;12;sx=128";
|
||||
|
||||
/*
|
||||
Blends random colors across palette
|
||||
@@ -4669,8 +4666,7 @@ static const char _data_FX_MODE_TV_SIMULATOR[] PROGMEM = "TV Simulator@!,!;;!;01
|
||||
|
||||
|
||||
/*
|
||||
Aurora effect by @Mazen
|
||||
improved and converted to integer math by @dedehai
|
||||
Aurora effect
|
||||
*/
|
||||
|
||||
//CONFIG
|
||||
@@ -4682,138 +4678,140 @@ static const char _data_FX_MODE_TV_SIMULATOR[] PROGMEM = "TV Simulator@!,!;;!;01
|
||||
#define W_MAX_SPEED 6 //Higher number, higher speed
|
||||
#define W_WIDTH_FACTOR 6 //Higher number, smaller waves
|
||||
|
||||
// fixed-point math scaling
|
||||
#define AW_SHIFT 16
|
||||
#define AW_SCALE (1 << AW_SHIFT) // 65536 representing 1.0
|
||||
|
||||
// 32 bytes
|
||||
//24 bytes
|
||||
class AuroraWave {
|
||||
private:
|
||||
int32_t center; // scaled by AW_SCALE
|
||||
uint32_t ageFactor_cached; // cached age factor scaled by AW_SCALE
|
||||
uint16_t ttl;
|
||||
CRGB basecolor;
|
||||
float basealpha;
|
||||
uint16_t age;
|
||||
uint16_t width;
|
||||
uint16_t basealpha; // scaled by AW_SCALE
|
||||
uint16_t speed_factor; // scaled by AW_SCALE
|
||||
int16_t wave_start; // wave start LED index
|
||||
int16_t wave_end; // wave end LED index
|
||||
float center;
|
||||
bool goingleft;
|
||||
float speed_factor;
|
||||
bool alive = true;
|
||||
CRGBW basecolor;
|
||||
|
||||
public:
|
||||
void init(uint32_t segment_length, CRGBW color) {
|
||||
void init(uint32_t segment_length, CRGB color) {
|
||||
ttl = hw_random16(500, 1501);
|
||||
basecolor = color;
|
||||
basealpha = hw_random8(60, 100) * AW_SCALE / 100; // 0-99% note: if using 100% there is risk of integer overflow
|
||||
basealpha = hw_random8(60, 101) / (float)100;
|
||||
age = 0;
|
||||
width = hw_random16(segment_length / 20, segment_length / W_WIDTH_FACTOR) + 1;
|
||||
center = (((uint32_t)hw_random8(101) << AW_SHIFT) / 100) * segment_length; // 0-100%
|
||||
goingleft = hw_random8() & 0x01; // 50/50 chance
|
||||
speed_factor = (((uint32_t)hw_random8(10, 31) * W_MAX_SPEED) << AW_SHIFT) / (100 * 255);
|
||||
width = hw_random16(segment_length / 20, segment_length / W_WIDTH_FACTOR); //half of width to make math easier
|
||||
if (!width) width = 1;
|
||||
center = hw_random8(101) / (float)100 * segment_length;
|
||||
goingleft = hw_random8(0, 2) == 0;
|
||||
speed_factor = (hw_random8(10, 31) / (float)100 * W_MAX_SPEED / 255);
|
||||
alive = true;
|
||||
}
|
||||
|
||||
void updateCachedValues() {
|
||||
uint32_t half_ttl = ttl >> 1;
|
||||
if (age < half_ttl) {
|
||||
ageFactor_cached = ((uint32_t)age << AW_SHIFT) / half_ttl;
|
||||
} else {
|
||||
ageFactor_cached = ((uint32_t)(ttl - age) << AW_SHIFT) / half_ttl;
|
||||
}
|
||||
if (ageFactor_cached >= AW_SCALE) ageFactor_cached = AW_SCALE - 1; // prevent overflow
|
||||
CRGB getColorForLED(int ledIndex) {
|
||||
if(ledIndex < center - width || ledIndex > center + width) return 0; //Position out of range of this wave
|
||||
|
||||
uint32_t center_led = center >> AW_SHIFT;
|
||||
wave_start = (int16_t)center_led - (int16_t)width;
|
||||
wave_end = (int16_t)center_led + (int16_t)width;
|
||||
}
|
||||
CRGB rgb;
|
||||
|
||||
CRGBW getColorForLED(int ledIndex) {
|
||||
// linear brightness falloff from center to edge of wave
|
||||
if (ledIndex < wave_start || ledIndex > wave_end) return 0;
|
||||
int32_t ledIndex_scaled = (int32_t)ledIndex << AW_SHIFT;
|
||||
int32_t offset = ledIndex_scaled - center;
|
||||
//Offset of this led from center of wave
|
||||
//The further away from the center, the dimmer the LED
|
||||
float offset = ledIndex - center;
|
||||
if (offset < 0) offset = -offset;
|
||||
uint32_t offsetFactor = offset / width; // scaled by AW_SCALE
|
||||
if (offsetFactor > AW_SCALE) return 0; // outside of wave
|
||||
uint32_t brightness_factor = (AW_SCALE - offsetFactor);
|
||||
brightness_factor = (brightness_factor * ageFactor_cached) >> AW_SHIFT;
|
||||
brightness_factor = (brightness_factor * basealpha) >> AW_SHIFT;
|
||||
float offsetFactor = offset / width;
|
||||
|
||||
CRGBW rgb;
|
||||
rgb.r = (basecolor.r * brightness_factor) >> AW_SHIFT;
|
||||
rgb.g = (basecolor.g * brightness_factor) >> AW_SHIFT;
|
||||
rgb.b = (basecolor.b * brightness_factor) >> AW_SHIFT;
|
||||
rgb.w = (basecolor.w * brightness_factor) >> AW_SHIFT;
|
||||
//The age of the wave determines it brightness.
|
||||
//At half its maximum age it will be the brightest.
|
||||
float ageFactor = 0.1;
|
||||
if((float)age / ttl < 0.5) {
|
||||
ageFactor = (float)age / (ttl / 2);
|
||||
} else {
|
||||
ageFactor = (float)(ttl - age) / ((float)ttl * 0.5);
|
||||
}
|
||||
|
||||
//Calculate color based on above factors and basealpha value
|
||||
float factor = (1 - offsetFactor) * ageFactor * basealpha;
|
||||
rgb.r = basecolor.r * factor;
|
||||
rgb.g = basecolor.g * factor;
|
||||
rgb.b = basecolor.b * factor;
|
||||
|
||||
return rgb;
|
||||
};
|
||||
|
||||
//Change position and age of wave
|
||||
//Determine if its still "alive"
|
||||
//Determine if its sill "alive"
|
||||
void update(uint32_t segment_length, uint32_t speed) {
|
||||
int32_t step = speed_factor * speed;
|
||||
center += goingleft ? -step : step;
|
||||
if(goingleft) {
|
||||
center -= speed_factor * speed;
|
||||
} else {
|
||||
center += speed_factor * speed;
|
||||
}
|
||||
|
||||
age++;
|
||||
|
||||
if (age > ttl) {
|
||||
if(age > ttl) {
|
||||
alive = false;
|
||||
} else {
|
||||
uint32_t width_scaled = (uint32_t)width << AW_SHIFT;
|
||||
uint32_t segment_length_scaled = segment_length << AW_SHIFT;
|
||||
|
||||
if (goingleft) {
|
||||
if (center < - (int32_t)width_scaled) {
|
||||
alive = false;
|
||||
}
|
||||
} else {
|
||||
if (center > (int32_t)segment_length_scaled + (int32_t)width_scaled) {
|
||||
alive = false;
|
||||
}
|
||||
}
|
||||
if(goingleft) {
|
||||
if(center + width < 0) {
|
||||
alive = false;
|
||||
}
|
||||
} else {
|
||||
if(center - width > segment_length) {
|
||||
alive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bool stillAlive() { return alive; }
|
||||
bool stillAlive() {
|
||||
return alive;
|
||||
};
|
||||
};
|
||||
|
||||
uint16_t mode_aurora(void) {
|
||||
AuroraWave* waves;
|
||||
SEGENV.aux1 = map(SEGMENT.intensity, 0, 255, 2, W_MAX_COUNT); // aux1 = Wavecount
|
||||
if (!SEGENV.allocateData(sizeof(AuroraWave) * SEGENV.aux1)) {
|
||||
return mode_static();
|
||||
if(!SEGENV.allocateData(sizeof(AuroraWave) * SEGENV.aux1)) { // 20 on ESP32, 9 on ESP8266
|
||||
return mode_static(); //allocation failed
|
||||
}
|
||||
waves = reinterpret_cast<AuroraWave*>(SEGENV.data);
|
||||
|
||||
// note: on first call, SEGENV.data is zero -> all waves are dead and will be initialized
|
||||
for (int i = 0; i < SEGENV.aux1; i++) {
|
||||
waves[i].update(SEGLEN, SEGMENT.speed);
|
||||
if (!(waves[i].stillAlive())) {
|
||||
waves[i].init(SEGLEN, SEGMENT.color_from_palette(hw_random8(), false, false, hw_random8(0, 3)));
|
||||
if(SEGENV.call == 0) {
|
||||
for (int i = 0; i < SEGENV.aux1; i++) {
|
||||
waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(hw_random8(), false, false, hw_random8(0, 3))));
|
||||
}
|
||||
waves[i].updateCachedValues();
|
||||
}
|
||||
|
||||
uint8_t backlight = 0; // note: original code used 1, with inverse gamma applied background would never be black
|
||||
for (int i = 0; i < SEGENV.aux1; i++) {
|
||||
//Update values of wave
|
||||
waves[i].update(SEGLEN, SEGMENT.speed);
|
||||
|
||||
if(!(waves[i].stillAlive())) {
|
||||
//If a wave dies, reinitialize it starts over.
|
||||
waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(hw_random8(), false, false, hw_random8(0, 3))));
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t backlight = 1; //dimmer backlight if less active colors
|
||||
if (SEGCOLOR(0)) backlight++;
|
||||
if (SEGCOLOR(1)) backlight++;
|
||||
if (SEGCOLOR(2)) backlight++;
|
||||
backlight = gamma8inv(backlight); // preserve backlight when using gamma correction
|
||||
|
||||
//Loop through LEDs to determine color
|
||||
for (unsigned i = 0; i < SEGLEN; i++) {
|
||||
CRGBW mixedRgb = CRGBW(backlight, backlight, backlight);
|
||||
CRGB mixedRgb = CRGB(backlight, backlight, backlight);
|
||||
|
||||
for (int j = 0; j < SEGENV.aux1; j++) {
|
||||
CRGBW rgb = waves[j].getColorForLED(i);
|
||||
mixedRgb = color_add(mixedRgb, rgb); // sum all waves influencing this pixel
|
||||
//For each LED we must check each wave if it is "active" at this position.
|
||||
//If there are multiple waves active on a LED we multiply their values.
|
||||
for (int j = 0; j < SEGENV.aux1; j++) {
|
||||
CRGB rgb = waves[j].getColorForLED(i);
|
||||
|
||||
if(rgb != CRGB(0)) {
|
||||
mixedRgb += rgb;
|
||||
}
|
||||
}
|
||||
|
||||
SEGMENT.setPixelColor(i, mixedRgb);
|
||||
SEGMENT.setPixelColor(i, mixedRgb[0], mixedRgb[1], mixedRgb[2]);
|
||||
}
|
||||
|
||||
return FRAMETIME;
|
||||
}
|
||||
|
||||
static const char _data_FX_MODE_AURORA[] PROGMEM = "Aurora@!,!;1,2,3;!;;sx=24,pal=50";
|
||||
|
||||
// WLED-SR effects
|
||||
@@ -4877,78 +4875,6 @@ uint16_t mode_FlowStripe(void) {
|
||||
} // mode_FlowStripe()
|
||||
static const char _data_FX_MODE_FLOWSTRIPE[] PROGMEM = "Flow Stripe@Hue speed,Effect speed;;!;pal=11";
|
||||
|
||||
/*
|
||||
Shimmer effect: moves a gradient with optional modulators across the strip at a given interval, up to 60 seconds
|
||||
It can be used as an overlay to other effects or standalone
|
||||
by DedeHai (Damian Schneider), based on idea from @Charming-Lime (#4905)
|
||||
*/
|
||||
uint16_t mode_shimmer() {
|
||||
if(!SEGENV.allocateData(sizeof(uint32_t))) { return mode_static(); }
|
||||
uint32_t* lastTime = reinterpret_cast<uint32_t*>(SEGENV.data);
|
||||
|
||||
uint32_t radius = (SEGMENT.custom1 * SEGLEN >> 7) + 1; // [1, 2*SEGLEN+1] pixels
|
||||
uint32_t traversalDistance = (SEGLEN + 2 * radius) << 8; // total subpixels to cross, 1 pixel = 256 subpixels
|
||||
uint32_t traversalTime = 200 + (255 - SEGMENT.speed) * 80; // [200, 20600] ms
|
||||
uint32_t speed = ((traversalDistance << 5) / traversalTime); // subpixels/512ms
|
||||
int32_t position = static_cast<int32_t>(SEGENV.step); // current position in subpixels
|
||||
uint16_t inputstate = (uint16_t(SEGMENT.intensity) << 8) | uint16_t(SEGMENT.custom1); // current user input state
|
||||
|
||||
// init
|
||||
if (SEGENV.call == 0 || inputstate != SEGENV.aux1) {
|
||||
position = -(radius << 8);
|
||||
SEGENV.aux0 = 0; // aux0 is pause timer
|
||||
*lastTime = strip.now;
|
||||
SEGENV.aux1 = inputstate; // save user input state
|
||||
}
|
||||
|
||||
if(SEGMENT.speed) {
|
||||
uint32_t deltaTime = (strip.now - *lastTime) & 0x7F; // clamp to 127ms to avoid overflows. note: speed*deltaTime can still overflow for segments > ~10k pixels
|
||||
*lastTime = strip.now;
|
||||
|
||||
if (SEGENV.aux0 > 0) {
|
||||
SEGENV.aux0 = (SEGENV.aux0 > deltaTime) ? SEGENV.aux0 - deltaTime : 0;
|
||||
} else {
|
||||
// calculate movement step and update position
|
||||
int32_t step = 1 + ((speed * deltaTime) >> 5); // subpixels moved this frame. note >>5 as speed is in subpixels/512ms
|
||||
position += step;
|
||||
int endposition = (SEGLEN + radius) << 8;
|
||||
if (position > endposition) {
|
||||
SEGENV.aux0 = SEGMENT.intensity * 236; // [0, 60180] ms pause
|
||||
if(SEGMENT.check3) SEGENV.aux0 = hw_random(SEGENV.aux0 + 1000); // randomise interval, +1 second to affect low intensity values
|
||||
position = -(radius << 8); // reset to start position (out of frame)
|
||||
}
|
||||
SEGENV.step = (uint32_t)position; // save back
|
||||
}
|
||||
|
||||
if (SEGMENT.check2)
|
||||
position = (SEGLEN << 8) - position; // invert position (and direction)
|
||||
} else {
|
||||
position = (SEGLEN << 7); // at speed=0, make it static in the center (this enables to use modulators only)
|
||||
}
|
||||
|
||||
for (int i = 0; i < SEGLEN; i++) {
|
||||
uint32_t dist = abs(position - (i << 8));
|
||||
if (dist < (radius << 8)) {
|
||||
uint32_t color = SEGMENT.color_from_palette(i * 255 / SEGLEN, false, false, 0);
|
||||
uint8_t blend = dist / radius; // linear gradient note: dist is in subpixels, radius in pixels, result is [0, 255] since dist < radius*256
|
||||
if (SEGMENT.custom2) {
|
||||
uint8_t modVal; // modulation value
|
||||
if (SEGMENT.check1) {
|
||||
modVal = (sin16_t((i * SEGMENT.custom2 << 6) + (strip.now * SEGMENT.custom3 << 5)) >> 8) + 128; // sine modulation: regular "Zebra" stripes
|
||||
} else {
|
||||
modVal = perlin16((i * SEGMENT.custom2 << 7), strip.now * SEGMENT.custom3 << 5) >> 8; // perlin noise modulation
|
||||
}
|
||||
color = color_fade(color, modVal, true); // dim by modulator value
|
||||
}
|
||||
SEGMENT.setPixelColor(i, color_blend(color, SEGCOLOR(1), blend)); // blend to background color
|
||||
} else {
|
||||
SEGMENT.setPixelColor(i, SEGCOLOR(1));
|
||||
}
|
||||
}
|
||||
|
||||
return FRAMETIME;
|
||||
}
|
||||
static const char _data_FX_MODE_SHIMMER[] PROGMEM = "Shimmer@Speed,Interval,Size,Granular,Flow,Zebra,Reverse,Sporadic;Fx,Bg,Cx;!;1;pal=15,sx=220,ix=10,c2=0,c3=0";
|
||||
|
||||
#ifndef WLED_DISABLE_2D
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
@@ -5195,162 +5121,112 @@ static const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = "Frizzles@X frequency,Y f
|
||||
///////////////////////////////////////////
|
||||
// 2D Cellular Automata Game of life //
|
||||
///////////////////////////////////////////
|
||||
typedef struct Cell {
|
||||
uint8_t alive : 1, faded : 1, toggleStatus : 1, edgeCell: 1, oscillatorCheck : 1, spaceshipCheck : 1, unused : 2;
|
||||
} Cell;
|
||||
typedef struct ColorCount {
|
||||
CRGB color;
|
||||
int8_t count;
|
||||
} colorCount;
|
||||
|
||||
uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/
|
||||
// and https://github.com/DougHaber/nlife-color , Modified By: Brandon Butler
|
||||
uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ and https://github.com/DougHaber/nlife-color
|
||||
if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up
|
||||
const int cols = SEG_W, rows = SEG_H;
|
||||
const unsigned maxIndex = cols * rows;
|
||||
|
||||
if (!SEGENV.allocateData(SEGMENT.length() * sizeof(Cell))) return mode_static(); // allocation failed
|
||||
const int cols = SEG_W;
|
||||
const int rows = SEG_H;
|
||||
const auto XY = [&](int x, int y) { return (x%cols) + (y%rows) * cols; };
|
||||
const unsigned dataSize = sizeof(CRGB) * SEGMENT.length(); // using width*height prevents reallocation if mirroring is enabled
|
||||
const int crcBufferLen = 2; //(SEGMENT.width() + SEGMENT.height())*71/100; // roughly sqrt(2)/2 for better repetition detection (Ewowi)
|
||||
|
||||
Cell *cells = reinterpret_cast<Cell*> (SEGENV.data);
|
||||
if (!SEGENV.allocateData(dataSize + sizeof(uint16_t)*crcBufferLen)) return mode_static(); //allocation failed
|
||||
CRGB *prevLeds = reinterpret_cast<CRGB*>(SEGENV.data);
|
||||
uint16_t *crcBuffer = reinterpret_cast<uint16_t*>(SEGENV.data + dataSize);
|
||||
|
||||
uint16_t& generation = SEGENV.aux0, &gliderLength = SEGENV.aux1; // rename aux variables for clarity
|
||||
bool mutate = SEGMENT.check3;
|
||||
uint8_t blur = map(SEGMENT.custom1, 0, 255, 255, 4);
|
||||
CRGB backgroundColor = SEGCOLOR(1);
|
||||
|
||||
uint32_t bgColor = SEGCOLOR(1);
|
||||
uint32_t birthColor = SEGMENT.color_from_palette(128, false, PALETTE_SOLID_WRAP, 255);
|
||||
if (SEGENV.call == 0 || strip.now - SEGMENT.step > 3000) {
|
||||
SEGENV.step = strip.now;
|
||||
SEGENV.aux0 = 0;
|
||||
|
||||
bool setup = SEGENV.call == 0;
|
||||
if (setup) {
|
||||
// Calculate glider length LCM(rows,cols)*4 once
|
||||
unsigned a = rows, b = cols;
|
||||
while (b) { unsigned t = b; b = a % b; a = t; }
|
||||
gliderLength = (cols * rows / a) << 2;
|
||||
}
|
||||
|
||||
if (abs(long(strip.now) - long(SEGENV.step)) > 2000) SEGENV.step = 0; // Timebase jump fix
|
||||
bool paused = SEGENV.step > strip.now;
|
||||
|
||||
// Setup New Game of Life
|
||||
if ((!paused && generation == 0) || setup) {
|
||||
SEGENV.step = strip.now + 1280; // show initial state for 1.28 seconds
|
||||
generation = 1;
|
||||
paused = true;
|
||||
//Setup Grid
|
||||
memset(cells, 0, maxIndex * sizeof(Cell));
|
||||
|
||||
for (unsigned i = 0; i < maxIndex; i++) {
|
||||
bool isAlive = !hw_random8(3); // ~33%
|
||||
cells[i].alive = isAlive;
|
||||
cells[i].faded = !isAlive;
|
||||
unsigned x = i % cols, y = i / cols;
|
||||
cells[i].edgeCell = (x == 0 || x == cols-1 || y == 0 || y == rows-1);
|
||||
|
||||
SEGMENT.setPixelColor(i, isAlive ? SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 0) : bgColor);
|
||||
//give the leds random state and colors (based on intensity, colors from palette or all posible colors are chosen)
|
||||
for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) {
|
||||
unsigned state = hw_random8()%2;
|
||||
if (state == 0)
|
||||
SEGMENT.setPixelColorXY(x,y, backgroundColor);
|
||||
else
|
||||
SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 255));
|
||||
}
|
||||
}
|
||||
|
||||
if (paused || (strip.now - SEGENV.step < 1000 / map(SEGMENT.speed,0,255,1,42))) {
|
||||
// Redraw if paused or between updates to remove blur
|
||||
for (unsigned i = maxIndex; i--; ) {
|
||||
if (!cells[i].alive) {
|
||||
uint32_t cellColor = SEGMENT.getPixelColor(i);
|
||||
if (cellColor != bgColor) {
|
||||
uint32_t newColor;
|
||||
bool needsColor = false;
|
||||
if (cells[i].faded) { newColor = bgColor; needsColor = true; }
|
||||
else {
|
||||
uint32_t blended = color_blend(cellColor, bgColor, 2);
|
||||
if (blended == cellColor) { blended = bgColor; cells[i].faded = 1; }
|
||||
newColor = blended; needsColor = true;
|
||||
}
|
||||
if (needsColor) SEGMENT.setPixelColor(i, newColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) prevLeds[XY(x,y)] = CRGB::Black;
|
||||
memset(crcBuffer, 0, sizeof(uint16_t)*crcBufferLen);
|
||||
} else if (strip.now - SEGENV.step < FRAMETIME_FIXED * (uint32_t)map(SEGMENT.speed,0,255,64,4)) {
|
||||
// update only when appropriate time passes (in 42 FPS slots)
|
||||
return FRAMETIME;
|
||||
}
|
||||
|
||||
// Repeat detection
|
||||
bool updateOscillator = generation % 16 == 0;
|
||||
bool updateSpaceship = gliderLength && generation % gliderLength == 0;
|
||||
bool repeatingOscillator = true, repeatingSpaceship = true, emptyGrid = true;
|
||||
//copy previous leds (save previous generation)
|
||||
//NOTE: using lossy getPixelColor() is a benefit as endlessly repeating patterns will eventually fade out causing a reset
|
||||
for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) prevLeds[XY(x,y)] = SEGMENT.getPixelColorXY(x,y);
|
||||
|
||||
unsigned cIndex = maxIndex-1;
|
||||
for (unsigned y = rows; y--; ) for (unsigned x = cols; x--; cIndex--) {
|
||||
Cell& cell = cells[cIndex];
|
||||
//calculate new leds
|
||||
for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) {
|
||||
|
||||
if (cell.alive) emptyGrid = false;
|
||||
if (cell.oscillatorCheck != cell.alive) repeatingOscillator = false;
|
||||
if (cell.spaceshipCheck != cell.alive) repeatingSpaceship = false;
|
||||
if (updateOscillator) cell.oscillatorCheck = cell.alive;
|
||||
if (updateSpaceship) cell.spaceshipCheck = cell.alive;
|
||||
colorCount colorsCount[9]; // count the different colors in the 3*3 matrix
|
||||
for (int i=0; i<9; i++) colorsCount[i] = {backgroundColor, 0}; // init colorsCount
|
||||
|
||||
unsigned neighbors = 0, aliveParents = 0, parentIdx[3];
|
||||
// Count alive neighbors
|
||||
for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) if (i || j) {
|
||||
int nX = x + j, nY = y + i;
|
||||
if (cell.edgeCell) {
|
||||
nX = (nX + cols) % cols;
|
||||
nY = (nY + rows) % rows;
|
||||
}
|
||||
unsigned nIndex = nX + nY * cols;
|
||||
Cell& neighbor = cells[nIndex];
|
||||
if (neighbor.alive) {
|
||||
// iterate through neighbors and count them and their different colors
|
||||
int neighbors = 0;
|
||||
for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) { // iterate through 3*3 matrix
|
||||
if (i==0 && j==0) continue; // ignore itself
|
||||
// wrap around segment
|
||||
int xx = x+i, yy = y+j;
|
||||
if (x+i < 0) xx = cols-1; else if (x+i >= cols) xx = 0;
|
||||
if (y+j < 0) yy = rows-1; else if (y+j >= rows) yy = 0;
|
||||
|
||||
unsigned xy = XY(xx, yy); // previous cell xy to check
|
||||
// count different neighbours and colors
|
||||
if (prevLeds[xy] != backgroundColor) {
|
||||
neighbors++;
|
||||
if (!neighbor.toggleStatus && neighbors < 4) { // Alive and not dying
|
||||
parentIdx[aliveParents++] = nIndex;
|
||||
}
|
||||
bool colorFound = false;
|
||||
int k;
|
||||
for (k=0; k<9 && colorsCount[k].count != 0; k++)
|
||||
if (colorsCount[k].color == prevLeds[xy]) {
|
||||
colorsCount[k].count++;
|
||||
colorFound = true;
|
||||
}
|
||||
if (!colorFound) colorsCount[k] = {prevLeds[xy], 1}; //add new color found in the array
|
||||
}
|
||||
} // i,j
|
||||
|
||||
// Rules of Life
|
||||
uint32_t col = uint32_t(prevLeds[XY(x,y)]) & 0x00FFFFFF; // uint32_t operator returns RGBA, we want RGBW -> cut off "alpha" byte
|
||||
uint32_t bgc = RGBW32(backgroundColor.r, backgroundColor.g, backgroundColor.b, 0);
|
||||
if ((col != bgc) && (neighbors < 2)) SEGMENT.setPixelColorXY(x,y, bgc); // Loneliness
|
||||
else if ((col != bgc) && (neighbors > 3)) SEGMENT.setPixelColorXY(x,y, bgc); // Overpopulation
|
||||
else if ((col == bgc) && (neighbors == 3)) { // Reproduction
|
||||
// find dominant color and assign it to a cell
|
||||
colorCount dominantColorCount = {backgroundColor, 0};
|
||||
for (int i=0; i<9 && colorsCount[i].count != 0; i++)
|
||||
if (colorsCount[i].count > dominantColorCount.count) dominantColorCount = colorsCount[i];
|
||||
// assign the dominant color w/ a bit of randomness to avoid "gliders"
|
||||
if (dominantColorCount.count > 0 && hw_random8(128)) SEGMENT.setPixelColorXY(x,y, dominantColorCount.color);
|
||||
} else if ((col == bgc) && (neighbors == 2) && !hw_random8(128)) { // Mutation
|
||||
SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 255));
|
||||
}
|
||||
// else do nothing!
|
||||
} //x,y
|
||||
|
||||
uint32_t newColor;
|
||||
bool needsColor = false;
|
||||
// calculate CRC16 of leds
|
||||
uint16_t crc = crc16((const unsigned char*)prevLeds, dataSize);
|
||||
// check if we had same CRC and reset if needed
|
||||
bool repetition = false;
|
||||
for (int i=0; i<crcBufferLen && !repetition; i++) repetition = (crc == crcBuffer[i]); // (Ewowi)
|
||||
// same CRC would mean image did not change or was repeating itself
|
||||
if (!repetition) SEGENV.step = strip.now; //if no repetition avoid reset
|
||||
// remember CRCs across frames
|
||||
crcBuffer[SEGENV.aux0] = crc;
|
||||
++SEGENV.aux0 %= crcBufferLen;
|
||||
|
||||
if (cell.alive && (neighbors < 2 || neighbors > 3)) { // Loneliness or Overpopulation
|
||||
cell.toggleStatus = 1;
|
||||
if (blur == 255) cell.faded = 1;
|
||||
newColor = cell.faded ? bgColor : color_blend(SEGMENT.getPixelColor(cIndex), bgColor, blur);
|
||||
needsColor = true;
|
||||
}
|
||||
else if (!cell.alive) {
|
||||
byte mutationRoll = mutate ? hw_random8(128) : 1; // if 0: 3 neighbor births fail and 2 neighbor births mutate
|
||||
if ((neighbors == 3 && mutationRoll) || (mutate && neighbors == 2 && !mutationRoll)) { // Reproduction or Mutation
|
||||
cell.toggleStatus = 1;
|
||||
cell.faded = 0;
|
||||
|
||||
if (aliveParents) {
|
||||
// Set color based on random neighbor
|
||||
unsigned parentIndex = parentIdx[random8(aliveParents)];
|
||||
birthColor = SEGMENT.getPixelColor(parentIndex);
|
||||
}
|
||||
newColor = birthColor;
|
||||
needsColor = true;
|
||||
}
|
||||
else if (!cell.faded) {// No change, fade dead cells
|
||||
uint32_t cellColor = SEGMENT.getPixelColor(cIndex);
|
||||
uint32_t blended = color_blend(cellColor, bgColor, blur);
|
||||
if (blended == cellColor) { blended = bgColor; cell.faded = 1; }
|
||||
newColor = blended;
|
||||
needsColor = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsColor) SEGMENT.setPixelColor(cIndex, newColor);
|
||||
}
|
||||
// Loop through cells, if toggle, swap alive status
|
||||
for (unsigned i = maxIndex; i--; ) {
|
||||
cells[i].alive ^= cells[i].toggleStatus;
|
||||
cells[i].toggleStatus = 0;
|
||||
}
|
||||
|
||||
if (repeatingOscillator || repeatingSpaceship || emptyGrid) {
|
||||
generation = 0; // reset on next call
|
||||
SEGENV.step += 1024; // pause final generation for ~1 second
|
||||
}
|
||||
else {
|
||||
++generation;
|
||||
SEGENV.step = strip.now;
|
||||
}
|
||||
return FRAMETIME;
|
||||
} // mode_2Dgameoflife()
|
||||
static const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = "Game Of Life@!,,Blur,,,,,Mutation;!,!;!;2;pal=11,sx=128";
|
||||
static const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = "Game Of Life@!;!,!;!;2";
|
||||
|
||||
|
||||
/////////////////////////
|
||||
@@ -7397,7 +7273,6 @@ uint16_t mode_2DGEQ(void) { // By Will Tatam. Code reduction by Ewoud Wijma.
|
||||
if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up
|
||||
|
||||
const int NUM_BANDS = map(SEGMENT.custom1, 0, 255, 1, 16);
|
||||
const int CENTER_BIN = map(SEGMENT.custom3, 0, 31, 0, 15);
|
||||
const int cols = SEG_W;
|
||||
const int rows = SEG_H;
|
||||
|
||||
@@ -7419,14 +7294,8 @@ uint16_t mode_2DGEQ(void) { // By Will Tatam. Code reduction by Ewoud Wijma.
|
||||
if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fadeToBlackBy(SEGMENT.speed);
|
||||
|
||||
for (int x=0; x < cols; x++) {
|
||||
int band = map(x, 0, cols, 0, NUM_BANDS);
|
||||
if (NUM_BANDS < 16) {
|
||||
int startBin = constrain(CENTER_BIN - NUM_BANDS/2, 0, 15 - NUM_BANDS + 1);
|
||||
if(NUM_BANDS <= 1)
|
||||
band = CENTER_BIN; // map() does not work for single band
|
||||
else
|
||||
band = map(band, 0, NUM_BANDS - 1, startBin, startBin + NUM_BANDS - 1);
|
||||
}
|
||||
uint8_t band = map(x, 0, cols, 0, NUM_BANDS);
|
||||
if (NUM_BANDS < 16) band = map(band, 0, NUM_BANDS - 1, 0, 15); // always use full range. comment out this line to get the previous behaviour.
|
||||
band = constrain(band, 0, 15);
|
||||
unsigned colorIndex = band * 17;
|
||||
int barHeight = map(fftResult[band], 0, 255, 0, rows); // do not subtract -1 from rows here
|
||||
@@ -7448,7 +7317,7 @@ uint16_t mode_2DGEQ(void) { // By Will Tatam. Code reduction by Ewoud Wijma.
|
||||
|
||||
return FRAMETIME;
|
||||
} // mode_2DGEQ()
|
||||
static const char _data_FX_MODE_2DGEQ[] PROGMEM = "GEQ@Fade speed,Ripple decay,# of bands,,Bin,Color bars;!,,Peaks;!;2f;c1=255,c2=64,pal=11,si=0,c3=0";
|
||||
static const char _data_FX_MODE_2DGEQ[] PROGMEM = "GEQ@Fade speed,Ripple decay,# of bands,,,Color bars;!,,Peaks;!;2f;c1=255,c2=64,pal=11,si=0"; // Beatsin
|
||||
|
||||
|
||||
/////////////////////////
|
||||
@@ -9677,11 +9546,11 @@ uint16_t mode_particleFireworks1D(void) {
|
||||
|
||||
PartSys->sources[0].sourceFlags.custom1 = 0; //flag used for rocket state
|
||||
PartSys->sources[0].source.hue = hw_random16(); // different color for each launch
|
||||
PartSys->sources[0].var = 10 * SEGMENT.check2; // emit variation, 0 if trail mode is off
|
||||
PartSys->sources[0].v = -10 * SEGMENT.check2; // emit speed, 0 if trail mode is off
|
||||
PartSys->sources[0].minLife = 180;
|
||||
PartSys->sources[0].maxLife = SEGMENT.check2 ? 700 : 240; // exhaust particle life
|
||||
PartSys->sources[0].source.x = SEGENV.aux0 * PartSys->maxX; // start from bottom or top
|
||||
PartSys->sources[0].var = 10; // emit variation
|
||||
PartSys->sources[0].v = -10; // emit speed
|
||||
PartSys->sources[0].minLife = 30;
|
||||
PartSys->sources[0].maxLife = SEGMENT.check2 ? 400 : 60;
|
||||
PartSys->sources[0].source.x = 0; // start from bottom
|
||||
uint32_t speed = sqrt((gravity * ((PartSys->maxX >> 2) + hw_random16(PartSys->maxX >> 1))) >> 4); // set speed such that rocket explods in frame
|
||||
PartSys->sources[0].source.vx = min(speed, (uint32_t)127);
|
||||
PartSys->sources[0].source.ttl = 4000;
|
||||
@@ -9691,7 +9560,7 @@ uint16_t mode_particleFireworks1D(void) {
|
||||
|
||||
if (SEGENV.aux0) { // inverted rockets launch from end
|
||||
PartSys->sources[0].sourceFlags.reversegrav = true;
|
||||
//PartSys->sources[0].source.x = PartSys->maxX; // start from top
|
||||
PartSys->sources[0].source.x = PartSys->maxX; // start from top
|
||||
PartSys->sources[0].source.vx = -PartSys->sources[0].source.vx; // revert direction
|
||||
PartSys->sources[0].v = -PartSys->sources[0].v; // invert exhaust emit speed
|
||||
}
|
||||
@@ -9710,20 +9579,18 @@ uint16_t mode_particleFireworks1D(void) {
|
||||
uint32_t rocketheight = SEGENV.aux0 ? PartSys->maxX - PartSys->sources[0].source.x : PartSys->sources[0].source.x;
|
||||
|
||||
if (currentspeed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee
|
||||
PartSys->sources[0].source.ttl = 50 - gravity;// min((uint32_t)50, 15 + (rocketheight >> (PS_P_RADIUS_SHIFT_1D + 3))); // alive for a few more frames
|
||||
PartSys->sources[0].source.ttl = min((uint32_t)50, rocketheight >> (PS_P_RADIUS_SHIFT_1D + 3)); // alive for a few more frames
|
||||
|
||||
if (PartSys->sources[0].source.ttl < 2) { // explode
|
||||
PartSys->sources[0].sourceFlags.custom1 = 1; // set standby state
|
||||
PartSys->sources[0].var = 5 + ((((PartSys->maxX >> 1) + rocketheight) * (20 + (SEGMENT.intensity << 1))) / (PartSys->maxX << 2)); // set explosion particle speed
|
||||
PartSys->sources[0].minLife = 1200;
|
||||
PartSys->sources[0].maxLife = 2600;
|
||||
PartSys->sources[0].var = 5 + ((((PartSys->maxX >> 1) + rocketheight) * (200 + SEGMENT.intensity)) / (PartSys->maxX << 2)); // set explosion particle speed
|
||||
PartSys->sources[0].minLife = 600;
|
||||
PartSys->sources[0].maxLife = 1300;
|
||||
PartSys->sources[0].source.ttl = 100 + hw_random16(64 - (SEGMENT.speed >> 2)); // standby time til next launch
|
||||
PartSys->sources[0].sat = SEGMENT.custom3 < 16 ? 10 + (SEGMENT.custom3 << 4) : 255; //color saturation
|
||||
PartSys->sources[0].size = SEGMENT.check3 ? hw_random16(SEGMENT.intensity) : 0; // random particle size in explosion
|
||||
uint32_t explosionsize = 8 + (PartSys->maxXpixel >> 2) + (PartSys->sources[0].source.x >> (PS_P_RADIUS_SHIFT_1D - 1));
|
||||
explosionsize += hw_random16((explosionsize * SEGMENT.intensity) >> 8);
|
||||
PartSys->setColorByAge(false); // disable
|
||||
PartSys->setColorByPosition(false); // disable
|
||||
for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles
|
||||
int idx = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle
|
||||
if(SEGMENT.custom3 > 23) {
|
||||
@@ -9743,16 +9610,16 @@ uint16_t mode_particleFireworks1D(void) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false) // every second frame and not in standby
|
||||
if ((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false && PartSys->sources[0].source.ttl > 50) // every second frame and not in standby and not about to explode
|
||||
PartSys->sprayEmit(PartSys->sources[0]); // emit exhaust particle
|
||||
|
||||
if ((SEGMENT.call & 0x03) == 0) // every fourth frame
|
||||
PartSys->applyFriction(1); // apply friction to all particles
|
||||
|
||||
PartSys->update(); // update and render
|
||||
|
||||
|
||||
for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
|
||||
if (PartSys->particles[i].ttl > 20) PartSys->particles[i].ttl -= 20; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan
|
||||
if (PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan
|
||||
else PartSys->particles[i].ttl = 0;
|
||||
}
|
||||
return FRAMETIME;
|
||||
@@ -10938,7 +10805,6 @@ void WS2812FX::setupEffectData() {
|
||||
addEffect(FX_MODE_FLOWSTRIPE, &mode_FlowStripe, _data_FX_MODE_FLOWSTRIPE);
|
||||
addEffect(FX_MODE_WAVESINS, &mode_wavesins, _data_FX_MODE_WAVESINS);
|
||||
addEffect(FX_MODE_ROCKTAVES, &mode_rocktaves, _data_FX_MODE_ROCKTAVES);
|
||||
addEffect(FX_MODE_SHIMMER, &mode_shimmer, _data_FX_MODE_SHIMMER);
|
||||
|
||||
// --- 2D effects ---
|
||||
#ifndef WLED_DISABLE_2D
|
||||
|
||||
@@ -320,7 +320,6 @@ extern byte realtimeMode; // used in getMappedPixelIndex()
|
||||
#define FX_MODE_DJLIGHT 159
|
||||
#define FX_MODE_2DFUNKYPLANK 160
|
||||
//#define FX_MODE_2DCENTERBARS 161
|
||||
#define FX_MODE_SHIMMER 161 // gap fill, non SR 1D effect
|
||||
#define FX_MODE_2DPULSER 162
|
||||
#define FX_MODE_BLURZ 163
|
||||
#define FX_MODE_2DDRIFT 164
|
||||
@@ -625,9 +624,6 @@ class Segment {
|
||||
DEBUGFX_PRINTLN();
|
||||
#endif
|
||||
clearName();
|
||||
#ifdef WLED_ENABLE_GIF
|
||||
endImagePlayback(this);
|
||||
#endif
|
||||
deallocateData();
|
||||
p_free(pixels);
|
||||
}
|
||||
|
||||
+40
-29
@@ -8,6 +8,7 @@
|
||||
Parts of the code adapted from WLED Sound Reactive
|
||||
*/
|
||||
#include "wled.h"
|
||||
#include "palettes.h"
|
||||
|
||||
// setUpMatrix() - constructs ledmap array from matrix of panels with WxH pixels
|
||||
// this converts physical (possibly irregular) LED arrangement into well defined
|
||||
@@ -17,7 +18,6 @@
|
||||
// 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
|
||||
@@ -46,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) {
|
||||
@@ -114,6 +114,7 @@ void WS2812FX::setUpMatrix() {
|
||||
|
||||
// delete gap array as we no longer need it
|
||||
p_free(gapTable);
|
||||
resume();
|
||||
|
||||
#ifdef WLED_DEBUG
|
||||
DEBUG_PRINT(F("Matrix ledmap:"));
|
||||
@@ -145,7 +146,7 @@ void WS2812FX::setUpMatrix() {
|
||||
#ifndef WLED_DISABLE_2D
|
||||
// pixel is clipped if it falls outside clipping range
|
||||
// if clipping start > stop the clipping range is inverted
|
||||
bool Segment::isPixelXYClipped(int x, int y) const {
|
||||
bool IRAM_ATTR_YN Segment::isPixelXYClipped(int x, int y) const {
|
||||
if (blendingStyle != BLEND_STYLE_FADE && isInTransition() && _clipStart != _clipStop) {
|
||||
const bool invertX = _clipStart > _clipStop;
|
||||
const bool invertY = _clipStartY > _clipStopY;
|
||||
@@ -185,7 +186,7 @@ bool Segment::isPixelXYClipped(int x, int y) const {
|
||||
void IRAM_ATTR_YN Segment::setPixelColorXY(int x, int y, uint32_t col) const
|
||||
{
|
||||
if (!isActive()) return; // not active
|
||||
if ((unsigned)x >= vWidth() || (unsigned)y >= vHeight()) return; // if pixel would fall out of virtual segment just exit
|
||||
if (x >= (int)vWidth() || y >= (int)vHeight() || x < 0 || y < 0) return; // if pixel would fall out of virtual segment just exit
|
||||
setPixelColorXYRaw(x, y, col);
|
||||
}
|
||||
|
||||
@@ -235,7 +236,7 @@ void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) const
|
||||
// returns RGBW values of pixel
|
||||
uint32_t IRAM_ATTR_YN Segment::getPixelColorXY(int x, int y) const {
|
||||
if (!isActive()) return 0; // not active
|
||||
if ((unsigned)x >= vWidth() || (unsigned)y >= vHeight()) return 0; // if pixel would fall out of virtual segment just exit
|
||||
if (x >= (int)vWidth() || y >= (int)vHeight() || x<0 || y<0) return 0; // if pixel would fall out of virtual segment just exit
|
||||
return getPixelColorXYRaw(x,y);
|
||||
}
|
||||
|
||||
@@ -245,42 +246,52 @@ void Segment::blur2D(uint8_t blur_x, uint8_t blur_y, bool smear) const {
|
||||
const unsigned cols = vWidth();
|
||||
const unsigned rows = vHeight();
|
||||
const auto XY = [&](unsigned x, unsigned y){ return x + y*cols; };
|
||||
uint32_t lastnew; // not necessary to initialize lastnew and last, as both will be initialized by the first loop iteration
|
||||
uint32_t last;
|
||||
if (blur_x) {
|
||||
const uint8_t keepx = smear ? 255 : 255 - blur_x;
|
||||
const uint8_t seepx = blur_x >> 1;
|
||||
for (unsigned row = 0; row < rows; row++) { // blur rows (x direction)
|
||||
// handle first pixel in row to avoid conditional in loop (faster)
|
||||
uint32_t cur = getPixelColorRaw(XY(0, row));
|
||||
uint32_t carryover = fast_color_scale(cur, seepx);
|
||||
setPixelColorRaw(XY(0, row), fast_color_scale(cur, keepx));
|
||||
for (unsigned x = 1; x < cols; x++) {
|
||||
cur = getPixelColorRaw(XY(x, row));
|
||||
uint32_t part = fast_color_scale(cur, seepx);
|
||||
cur = fast_color_scale(cur, keepx);
|
||||
cur = color_add(cur, carryover);
|
||||
setPixelColorRaw(XY(x - 1, row), color_add(getPixelColorRaw(XY(x-1, row)), part)); // previous pixel
|
||||
setPixelColorRaw(XY(x, row), cur); // current pixel
|
||||
uint32_t carryover = BLACK;
|
||||
uint32_t curnew = BLACK;
|
||||
for (unsigned x = 0; x < cols; x++) {
|
||||
uint32_t cur = getPixelColorRaw(XY(x, row));
|
||||
uint32_t part = color_fade(cur, seepx);
|
||||
curnew = color_fade(cur, keepx);
|
||||
if (x > 0) {
|
||||
if (carryover) curnew = color_add(curnew, carryover);
|
||||
uint32_t prev = color_add(lastnew, part);
|
||||
// optimization: only set pixel if color has changed
|
||||
if (last != prev) setPixelColorRaw(XY(x - 1, row), prev);
|
||||
} else setPixelColorRaw(XY(x, row), curnew); // first pixel
|
||||
lastnew = curnew;
|
||||
last = cur; // save original value for comparison on next iteration
|
||||
carryover = part;
|
||||
}
|
||||
setPixelColorRaw(XY(cols-1, row), curnew); // set last pixel
|
||||
}
|
||||
}
|
||||
if (blur_y) {
|
||||
const uint8_t keepy = smear ? 255 : 255 - blur_y;
|
||||
const uint8_t seepy = blur_y >> 1;
|
||||
for (unsigned col = 0; col < cols; col++) {
|
||||
// handle first pixel in column
|
||||
uint32_t cur = getPixelColorRaw(XY(col, 0));
|
||||
uint32_t carryover = fast_color_scale(cur, seepy);
|
||||
setPixelColorRaw(XY(col, 0), fast_color_scale(cur, keepy));
|
||||
for (unsigned y = 1; y < rows; y++) {
|
||||
cur = getPixelColorRaw(XY(col, y));
|
||||
uint32_t part = fast_color_scale(cur, seepy);
|
||||
cur = fast_color_scale(cur, keepy);
|
||||
cur = color_add(cur, carryover);
|
||||
setPixelColorRaw(XY(col, y - 1), color_add(getPixelColorRaw(XY(col, y-1)), part)); // previous pixel
|
||||
setPixelColorRaw(XY(col, y), cur); // current pixel
|
||||
uint32_t carryover = BLACK;
|
||||
uint32_t curnew = BLACK;
|
||||
for (unsigned y = 0; y < rows; y++) {
|
||||
uint32_t cur = getPixelColorRaw(XY(col, y));
|
||||
uint32_t part = color_fade(cur, seepy);
|
||||
curnew = color_fade(cur, keepy);
|
||||
if (y > 0) {
|
||||
if (carryover) curnew = color_add(curnew, carryover);
|
||||
uint32_t prev = color_add(lastnew, part);
|
||||
// optimization: only set pixel if color has changed
|
||||
if (last != prev) setPixelColorRaw(XY(col, y - 1), prev);
|
||||
} else setPixelColorRaw(XY(col, y), curnew); // first pixel
|
||||
lastnew = curnew;
|
||||
last = cur; //save original value for comparison on next iteration
|
||||
carryover = part;
|
||||
}
|
||||
setPixelColorRaw(XY(col, rows - 1), curnew);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Regular → Executable
+45
-83
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
#include "wled.h"
|
||||
#include "FXparticleSystem.h" // TODO: better define the required function (mem service) in FX.h?
|
||||
#include "palettes.h"
|
||||
|
||||
/*
|
||||
Custom per-LED mapping has moved!
|
||||
@@ -225,12 +226,8 @@ void Segment::resetIfRequired() {
|
||||
}
|
||||
|
||||
CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
|
||||
// there is one randomy generated palette (1) followed by 4 palettes created from segment colors (2-5)
|
||||
// those are followed by 7 fastled palettes (6-12) and 59 gradient palettes (13-71)
|
||||
// then come the custom palettes (255,254,...) growing downwards from 255 (255 being 1st custom palette)
|
||||
// palette 0 is a varying palette depending on effect and may be replaced by segment's color if so
|
||||
// instructed in color_from_palette()
|
||||
if (pal > FIXED_PALETTE_COUNT && pal <= 255-customPalettes.size()) pal = 0; // out of bounds palette
|
||||
if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0;
|
||||
if (pal > 245 && (customPalettes.size() == 0 || 255U-pal > customPalettes.size()-1)) pal = 0;
|
||||
//default palette. Differs depending on effect
|
||||
if (pal == 0) pal = _default_palette; // _default_palette is set in setMode()
|
||||
switch (pal) {
|
||||
@@ -266,13 +263,13 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
|
||||
}
|
||||
break;}
|
||||
default: //progmem palettes
|
||||
if (pal > 255 - customPalettes.size()) {
|
||||
if (pal>245) {
|
||||
targetPalette = customPalettes[255-pal]; // we checked bounds above
|
||||
} else if (pal < DYNAMIC_PALETTE_COUNT+FASTLED_PALETTE_COUNT+1) { // palette 6 - 12, fastled palettes
|
||||
targetPalette = *fastledPalettes[pal-DYNAMIC_PALETTE_COUNT-1];
|
||||
} else if (pal < 13) { // palette 6 - 12, fastled palettes
|
||||
targetPalette = *fastledPalettes[pal-6];
|
||||
} else {
|
||||
byte tcp[72];
|
||||
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal-(DYNAMIC_PALETTE_COUNT+FASTLED_PALETTE_COUNT)-1])), 72);
|
||||
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal-13])), 72);
|
||||
targetPalette.loadDynamicGradientPalette(tcp);
|
||||
}
|
||||
break;
|
||||
@@ -448,9 +445,6 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
|
||||
|
||||
// apply change immediately
|
||||
if (i2 <= i1) { //disable segment
|
||||
#ifdef WLED_ENABLE_GIF
|
||||
endImagePlayback(this);
|
||||
#endif
|
||||
deallocateData();
|
||||
p_free(pixels);
|
||||
pixels = nullptr;
|
||||
@@ -469,9 +463,6 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
|
||||
#endif
|
||||
// safety check
|
||||
if (start >= stop || startY >= stopY) {
|
||||
#ifdef WLED_ENABLE_GIF
|
||||
endImagePlayback(this);
|
||||
#endif
|
||||
deallocateData();
|
||||
p_free(pixels);
|
||||
pixels = nullptr;
|
||||
@@ -485,9 +476,6 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
|
||||
pixels = static_cast<uint32_t*>(allocate_buffer(length() * sizeof(uint32_t), BFRALLOC_PREFER_PSRAM | BFRALLOC_NOBYTEACCESS));
|
||||
if (!pixels) {
|
||||
DEBUGFX_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!"));
|
||||
#ifdef WLED_ENABLE_GIF
|
||||
endImagePlayback(this);
|
||||
#endif
|
||||
deallocateData();
|
||||
errorFlag = ERR_NORAM_PX;
|
||||
stop = 0;
|
||||
@@ -585,7 +573,8 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
|
||||
}
|
||||
|
||||
Segment &Segment::setPalette(uint8_t pal) {
|
||||
if (pal <= 255-customPalettes.size() && pal > FIXED_PALETTE_COUNT) pal = 0; // not built in palette or custom palette
|
||||
if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; // built in palettes
|
||||
if (pal > 245 && (customPalettes.size() == 0 || 255U-pal > customPalettes.size()-1)) pal = 0; // custom palettes
|
||||
if (pal != palette) {
|
||||
//DEBUG_PRINTF_P(PSTR("- Starting palette transition: %d\n"), pal);
|
||||
startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change (no need to copy segment)
|
||||
@@ -691,7 +680,7 @@ uint16_t Segment::maxMappingLength() const {
|
||||
#endif
|
||||
// pixel is clipped if it falls outside clipping range
|
||||
// if clipping start > stop the clipping range is inverted
|
||||
bool Segment::isPixelClipped(int i) const {
|
||||
bool IRAM_ATTR_YN Segment::isPixelClipped(int i) const {
|
||||
if (blendingStyle != BLEND_STYLE_FADE && isInTransition() && _clipStart != _clipStop) {
|
||||
bool invert = _clipStart > _clipStop; // ineverted start & stop
|
||||
int start = invert ? _clipStop : _clipStart;
|
||||
@@ -709,7 +698,7 @@ bool Segment::isPixelClipped(int i) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
void WLED_O2_ATTR Segment::setPixelColor(int i, uint32_t col) const
|
||||
void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) const
|
||||
{
|
||||
if (!isActive() || i < 0) return; // not active or invalid index
|
||||
#ifndef WLED_DISABLE_2D
|
||||
@@ -922,7 +911,7 @@ void Segment::setPixelColor(float i, uint32_t col, bool aa) const
|
||||
}
|
||||
#endif
|
||||
|
||||
uint32_t WLED_O2_ATTR Segment::getPixelColor(int i) const
|
||||
uint32_t IRAM_ATTR_YN Segment::getPixelColor(int i) const
|
||||
{
|
||||
if (!isActive() || i < 0) return 0; // not active or invalid index
|
||||
|
||||
@@ -1061,7 +1050,7 @@ void Segment::fadeToSecondaryBy(uint8_t fadeBy) const {
|
||||
void Segment::fadeToBlackBy(uint8_t fadeBy) const {
|
||||
if (!isActive() || fadeBy == 0) return; // optimization - no scaling to apply
|
||||
const size_t rlength = rawLength(); // calculate only once
|
||||
for (unsigned i = 0; i < rlength; i++) setPixelColorRaw(i, fast_color_scale(getPixelColorRaw(i), 255-fadeBy));
|
||||
for (unsigned i = 0; i < rlength; i++) setPixelColorRaw(i, color_fade(getPixelColorRaw(i), 255-fadeBy));
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1081,19 +1070,25 @@ void Segment::blur(uint8_t blur_amount, bool smear) const {
|
||||
uint8_t keep = smear ? 255 : 255 - blur_amount;
|
||||
uint8_t seep = blur_amount >> 1;
|
||||
unsigned vlength = vLength();
|
||||
// handle first pixel to avoid conditional in loop (faster)
|
||||
uint32_t cur = getPixelColorRaw(0);
|
||||
uint32_t carryover = fast_color_scale(cur, seep);
|
||||
setPixelColorRaw(0, fast_color_scale(cur, keep));
|
||||
for (unsigned i = 1; i < vlength; i++) {
|
||||
cur = getPixelColorRaw(i);
|
||||
uint32_t part = fast_color_scale(cur, seep);
|
||||
cur = fast_color_scale(cur, keep);
|
||||
cur = color_add(cur, carryover);
|
||||
setPixelColorRaw(i - 1, color_add(getPixelColorRaw(i - 1), part)); // previous pixel
|
||||
setPixelColorRaw(i, cur); // current pixel
|
||||
uint32_t carryover = BLACK;
|
||||
uint32_t lastnew; // not necessary to initialize lastnew and last, as both will be initialized by the first loop iteration
|
||||
uint32_t last;
|
||||
uint32_t curnew = BLACK;
|
||||
for (unsigned i = 0; i < vlength; i++) {
|
||||
uint32_t cur = getPixelColorRaw(i);
|
||||
uint32_t part = color_fade(cur, seep);
|
||||
curnew = color_fade(cur, keep);
|
||||
if (i > 0) {
|
||||
if (carryover) curnew = color_add(curnew, carryover);
|
||||
uint32_t prev = color_add(lastnew, part);
|
||||
// optimization: only set pixel if color has changed
|
||||
if (last != prev) setPixelColorRaw(i - 1, prev);
|
||||
} else setPixelColorRaw(i, curnew); // first pixel
|
||||
lastnew = curnew;
|
||||
last = cur; // save original value for comparison on next iteration
|
||||
carryover = part;
|
||||
}
|
||||
setPixelColorRaw(vlength - 1, curnew);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1183,42 +1178,17 @@ void WS2812FX::finalizeInit() {
|
||||
digitalCount = 0;
|
||||
#endif
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), getFreeHeapSize());
|
||||
// create buses/outputs
|
||||
unsigned mem = 0;
|
||||
unsigned maxI2S = 0;
|
||||
for (const auto &bus : busConfigs) {
|
||||
unsigned memB = bus.memUsage(Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) ? digitalCount++ : 0); // does not include DMA/RMT buffer
|
||||
mem += memB;
|
||||
// estimate maximum I2S memory usage (only relevant for digital non-2pin busses)
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266)
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
const bool usesI2S = ((useParallelI2S && digitalCount <= 8) || (!useParallelI2S && digitalCount == 1));
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
const bool usesI2S = (useParallelI2S && digitalCount <= 8);
|
||||
#else
|
||||
const bool usesI2S = false;
|
||||
#endif
|
||||
if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && usesI2S) {
|
||||
#ifdef NPB_CONF_4STEP_CADENCE
|
||||
constexpr unsigned stepFactor = 4; // 4 step cadence (4 bits per pixel bit)
|
||||
#else
|
||||
constexpr unsigned stepFactor = 3; // 3 step cadence (3 bits per pixel bit)
|
||||
#endif
|
||||
unsigned i2sCommonSize = stepFactor * bus.count * (3*Bus::hasRGB(bus.type)+Bus::hasWhite(bus.type)+Bus::hasCCT(bus.type)) * (Bus::is16bit(bus.type)+1);
|
||||
if (i2sCommonSize > maxI2S) maxI2S = i2sCommonSize;
|
||||
}
|
||||
#endif
|
||||
if (mem + maxI2S <= MAX_LED_MEMORY) {
|
||||
BusManager::add(bus);
|
||||
DEBUG_PRINTF_P(PSTR("Bus memory: %uB\n"), memB);
|
||||
mem += bus.memUsage(Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) ? digitalCount++ : 0); // includes global buffer
|
||||
if (mem <= MAX_LED_MEMORY) {
|
||||
if (BusManager::add(bus) == -1) break;
|
||||
} else {
|
||||
errorFlag = ERR_NORAM_PX; // alert UI
|
||||
DEBUG_PRINTF_P(PSTR("Out of LED memory! Bus %d (%d) #%u not created."), (int)bus.type, (int)bus.count, digitalCount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
DEBUG_PRINTF_P(PSTR("LED buffer size: %uB/%uB\n"), mem + maxI2S, BusManager::memUsage());
|
||||
busConfigs.clear();
|
||||
busConfigs.shrink_to_fit();
|
||||
|
||||
@@ -1681,17 +1651,12 @@ void WS2812FX::setTransitionMode(bool t) {
|
||||
resume();
|
||||
}
|
||||
|
||||
// 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
|
||||
// wait until frame is over (service() has finished or time for 1 frame has passed; yield() crashes on 8266)
|
||||
void WS2812FX::waitForIt() {
|
||||
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
|
||||
unsigned long maxWait = millis() + getFrameTime() + 100; // TODO: this needs a proper fix for timeout!
|
||||
while (isServicing() && maxWait > millis()) delay(1);
|
||||
#ifdef WLED_DEBUG
|
||||
if (millis()-waitStart >= maxWait) DEBUG_PRINTLN(F("Waited for strip to finish servicing."));
|
||||
if (millis() >= maxWait) DEBUG_PRINTLN(F("Waited for strip to finish servicing."));
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -1815,11 +1780,7 @@ 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 ...
|
||||
@@ -1827,7 +1788,6 @@ 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};
|
||||
@@ -1899,7 +1859,6 @@ 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) {
|
||||
@@ -1962,7 +1921,6 @@ 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"));
|
||||
@@ -1992,13 +1950,15 @@ 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);
|
||||
@@ -2022,9 +1982,9 @@ bool WS2812FX::deserializeMap(unsigned n) {
|
||||
} while (i < 32);
|
||||
if (!foundDigit) break;
|
||||
int index = atoi(number);
|
||||
if (index < 0 || index > 65535) index = 0xFFFF; // prevent integer wrap around
|
||||
if (index < 0 || index > 16384) index = 0xFFFF;
|
||||
customMappingTable[customMappingSize++] = index;
|
||||
if (customMappingSize >= getLengthTotal()) break;
|
||||
if (customMappingSize > getLengthTotal()) break;
|
||||
} else break; // there was nothing to read, stop
|
||||
}
|
||||
currentLedmap = n;
|
||||
@@ -2034,7 +1994,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] < 0xFFFFU ? customMappingTable[i] : -1);
|
||||
DEBUG_PRINTF_P(PSTR("%4d,"), customMappingTable[i]);
|
||||
}
|
||||
DEBUG_PRINTLN();
|
||||
#endif
|
||||
@@ -2050,6 +2010,8 @@ bool WS2812FX::deserializeMap(unsigned n) {
|
||||
DEBUG_PRINTLN(F("ERROR LED map allocation error."));
|
||||
}
|
||||
|
||||
resume();
|
||||
|
||||
releaseJSONBufferLock();
|
||||
return (customMappingSize > 0);
|
||||
}
|
||||
|
||||
+55
-49
@@ -17,7 +17,8 @@
|
||||
// local shared functions (used both in 1D and 2D system)
|
||||
static int32_t calcForce_dv(const int8_t force, uint8_t &counter);
|
||||
static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap); // returns false if out of bounds by more than particleradius
|
||||
static uint32_t fast_color_scaleAdd(const uint32_t c1, const uint32_t c2, uint8_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding)
|
||||
static uint32_t fast_color_add(CRGBW c1, const CRGBW c2, uint8_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding)
|
||||
static uint32_t fast_color_scale(CRGBW c, const uint8_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255
|
||||
#endif
|
||||
|
||||
#ifndef WLED_DISABLE_PARTICLESYSTEM2D
|
||||
@@ -624,7 +625,7 @@ void ParticleSystem2D::render() {
|
||||
}
|
||||
|
||||
// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer
|
||||
void WLED_O2_ATTR ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY) {
|
||||
__attribute__((optimize("O2"))) void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY) {
|
||||
uint32_t size = particlesize;
|
||||
if (advPartProps && advPartProps[particleindex].size > 0) // use advanced size properties (0 means use global size including single pixel rendering)
|
||||
size = advPartProps[particleindex].size;
|
||||
@@ -634,7 +635,7 @@ void WLED_O2_ATTR ParticleSystem2D::renderParticle(const uint32_t particleindex,
|
||||
uint32_t y = particles[particleindex].y >> PS_P_RADIUS_SHIFT;
|
||||
if (x <= (uint32_t)maxXpixel && y <= (uint32_t)maxYpixel) {
|
||||
uint32_t index = x + (maxYpixel - y) * (maxXpixel + 1); // flip y coordinate (0,0 is bottom left in PS but top left in framebuffer)
|
||||
framebuffer[index] = fast_color_scaleAdd(framebuffer[index], color, brightness);
|
||||
framebuffer[index] = fast_color_add(framebuffer[index], color, brightness);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -686,10 +687,10 @@ void WLED_O2_ATTR ParticleSystem2D::renderParticle(const uint32_t particleindex,
|
||||
memset(renderbuffer, 0, sizeof(renderbuffer)); // clear buffer
|
||||
//particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10
|
||||
//first, render the pixel to the center of the renderbuffer, then apply 2D blurring
|
||||
renderbuffer[4 + (4 * 10)] = fast_color_scaleAdd(renderbuffer[4 + (4 * 10)], color, pxlbrightness[0]); // order is: bottom left, bottom right, top right, top left
|
||||
renderbuffer[5 + (4 * 10)] = fast_color_scaleAdd(renderbuffer[5 + (4 * 10)], color, pxlbrightness[1]);
|
||||
renderbuffer[5 + (5 * 10)] = fast_color_scaleAdd(renderbuffer[5 + (5 * 10)], color, pxlbrightness[2]);
|
||||
renderbuffer[4 + (5 * 10)] = fast_color_scaleAdd(renderbuffer[4 + (5 * 10)], color, pxlbrightness[3]);
|
||||
renderbuffer[4 + (4 * 10)] = fast_color_add(renderbuffer[4 + (4 * 10)], color, pxlbrightness[0]); // order is: bottom left, bottom right, top right, top left
|
||||
renderbuffer[5 + (4 * 10)] = fast_color_add(renderbuffer[5 + (4 * 10)], color, pxlbrightness[1]);
|
||||
renderbuffer[5 + (5 * 10)] = fast_color_add(renderbuffer[5 + (5 * 10)], color, pxlbrightness[2]);
|
||||
renderbuffer[4 + (5 * 10)] = fast_color_add(renderbuffer[4 + (5 * 10)], color, pxlbrightness[3]);
|
||||
uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4
|
||||
uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below)
|
||||
uint32_t maxsize = advPartProps[particleindex].size;
|
||||
@@ -747,7 +748,7 @@ void WLED_O2_ATTR ParticleSystem2D::renderParticle(const uint32_t particleindex,
|
||||
continue;
|
||||
}
|
||||
uint32_t idx = xfb + (maxYpixel - yfb) * (maxXpixel + 1); // flip y coordinate (0,0 is bottom left in PS but top left in framebuffer)
|
||||
framebuffer[idx] = fast_color_scaleAdd(framebuffer[idx], renderbuffer[xrb + yrb * 10]);
|
||||
framebuffer[idx] = fast_color_add(framebuffer[idx], renderbuffer[xrb + yrb * 10]);
|
||||
}
|
||||
}
|
||||
} else { // standard rendering (2x2 pixels)
|
||||
@@ -784,7 +785,7 @@ void WLED_O2_ATTR ParticleSystem2D::renderParticle(const uint32_t particleindex,
|
||||
for (uint32_t i = 0; i < 4; i++) {
|
||||
if (pixelvalid[i]) {
|
||||
uint32_t idx = pixco[i].x + (maxYpixel - pixco[i].y) * (maxXpixel + 1); // flip y coordinate (0,0 is bottom left in PS but top left in framebuffer)
|
||||
framebuffer[idx] = fast_color_scaleAdd(framebuffer[idx], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left
|
||||
framebuffer[idx] = fast_color_add(framebuffer[idx], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -856,7 +857,7 @@ void ParticleSystem2D::handleCollisions() {
|
||||
|
||||
// handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS
|
||||
// takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard)
|
||||
void WLED_O2_ATTR ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const uint32_t collDistSq) {
|
||||
__attribute__((optimize("O2"))) void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const uint32_t collDistSq) {
|
||||
int32_t distanceSquared = dx * dx + dy * dy;
|
||||
// Calculate relative velocity note: could zero check but that does not improve overall speed but deminish it as that is rarely the case and pushing is still required
|
||||
int32_t relativeVx = (int32_t)particle2.vx - (int32_t)particle1.vx;
|
||||
@@ -1027,8 +1028,9 @@ void blur2D(uint32_t *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblu
|
||||
for (uint32_t x = xstart; x < xstart + xsize; x++) {
|
||||
seeppart = fast_color_scale(colorbuffer[indexXY], seep); // scale it and seep to neighbours
|
||||
if (x > 0) {
|
||||
colorbuffer[indexXY - 1] = fast_color_scaleAdd(colorbuffer[indexXY - 1], seeppart);
|
||||
colorbuffer[indexXY] = fast_color_scaleAdd(colorbuffer[indexXY], carryover);
|
||||
colorbuffer[indexXY - 1] = fast_color_add(colorbuffer[indexXY - 1], seeppart);
|
||||
if (carryover.color32) // note: check adds overhead but is faster on average
|
||||
colorbuffer[indexXY] = fast_color_add(colorbuffer[indexXY], carryover);
|
||||
}
|
||||
carryover = seeppart;
|
||||
indexXY++; // next pixel in x direction
|
||||
@@ -1047,8 +1049,9 @@ void blur2D(uint32_t *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblu
|
||||
for (uint32_t y = ystart; y < ystart + ysize; y++) {
|
||||
seeppart = fast_color_scale(colorbuffer[indexXY], seep); // scale it and seep to neighbours
|
||||
if (y > 0) {
|
||||
colorbuffer[indexXY - width] = fast_color_scaleAdd(colorbuffer[indexXY - width], seeppart);
|
||||
colorbuffer[indexXY] = fast_color_scaleAdd(colorbuffer[indexXY], carryover);
|
||||
colorbuffer[indexXY - width] = fast_color_add(colorbuffer[indexXY - width], seeppart);
|
||||
if (carryover.color32) // note: check adds overhead but is faster on average
|
||||
colorbuffer[indexXY] = fast_color_add(colorbuffer[indexXY], carryover);
|
||||
}
|
||||
carryover = seeppart;
|
||||
indexXY += width; // next pixel in y direction
|
||||
@@ -1467,7 +1470,7 @@ void ParticleSystem1D::render() {
|
||||
CRGBW bg_color = SEGCOLOR(1);
|
||||
if (bg_color > 0) { //if not black
|
||||
for (int32_t i = 0; i <= maxXpixel; i++) {
|
||||
framebuffer[i] = fast_color_scaleAdd(framebuffer[i], bg_color);
|
||||
framebuffer[i] = fast_color_add(framebuffer[i], bg_color);
|
||||
}
|
||||
}
|
||||
#ifndef WLED_DISABLE_2D
|
||||
@@ -1482,7 +1485,7 @@ void ParticleSystem1D::render() {
|
||||
}
|
||||
|
||||
// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer
|
||||
void WLED_O2_ATTR ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap) {
|
||||
__attribute__((optimize("O2"))) void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap) {
|
||||
uint32_t size = particlesize;
|
||||
if (advPartProps) // use advanced size properties (1D system has no large size global rendering TODO: add large global rendering?)
|
||||
size = advPartProps[particleindex].size;
|
||||
@@ -1490,7 +1493,7 @@ void WLED_O2_ATTR ParticleSystem1D::renderParticle(const uint32_t particleindex,
|
||||
if (size == 0) { //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles (and updating it uses more code)
|
||||
uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D;
|
||||
if (x <= (uint32_t)maxXpixel) { //by making x unsigned there is no need to check < 0 as it will overflow
|
||||
framebuffer[x] = fast_color_scaleAdd(framebuffer[x], color, brightness);
|
||||
framebuffer[x] = fast_color_add(framebuffer[x], color, brightness);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1527,8 +1530,8 @@ void WLED_O2_ATTR ParticleSystem1D::renderParticle(const uint32_t particleindex,
|
||||
//render particle to a bigger size
|
||||
//particle size to pixels: 2 - 63 is 4 pixels, < 128 is 6pixels, < 192 is 8 pixels, bigger is 10 pixels
|
||||
//first, render the pixel to the center of the renderbuffer, then apply 1D blurring
|
||||
renderbuffer[4] = fast_color_scaleAdd(renderbuffer[4], color, pxlbrightness[0]);
|
||||
renderbuffer[5] = fast_color_scaleAdd(renderbuffer[5], color, pxlbrightness[1]);
|
||||
renderbuffer[4] = fast_color_add(renderbuffer[4], color, pxlbrightness[0]);
|
||||
renderbuffer[5] = fast_color_add(renderbuffer[5], color, pxlbrightness[1]);
|
||||
uint32_t rendersize = 2; // initialize render size, minimum is 4 pixels, it is incremented int he loop below to start with 4
|
||||
uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below)
|
||||
uint32_t blurpasses = size/64 + 1; // number of blur passes depends on size, four passes max
|
||||
@@ -1562,7 +1565,7 @@ void WLED_O2_ATTR ParticleSystem1D::renderParticle(const uint32_t particleindex,
|
||||
#ifdef ESP8266 // no local buffer on ESP8266
|
||||
SEGMENT.addPixelColor(xfb, renderbuffer[xrb], true);
|
||||
#else
|
||||
framebuffer[xfb] = fast_color_scaleAdd(framebuffer[xfb], renderbuffer[xrb]);
|
||||
framebuffer[xfb] = fast_color_add(framebuffer[xfb], renderbuffer[xrb]);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1582,7 +1585,7 @@ void WLED_O2_ATTR ParticleSystem1D::renderParticle(const uint32_t particleindex,
|
||||
}
|
||||
for (uint32_t i = 0; i < 2; i++) {
|
||||
if (pxlisinframe[i]) {
|
||||
framebuffer[pixco[i]] = fast_color_scaleAdd(framebuffer[pixco[i]], color, pxlbrightness[i]);
|
||||
framebuffer[pixco[i]] = fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1645,7 +1648,7 @@ void ParticleSystem1D::handleCollisions() {
|
||||
}
|
||||
// handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS
|
||||
// takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard)
|
||||
void WLED_O2_ATTR ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const uint32_t collisiondistance) {
|
||||
__attribute__((optimize("O2"))) void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const uint32_t collisiondistance) {
|
||||
int32_t dv = particle2.vx - particle1.vx;
|
||||
int32_t dotProduct = (dx * dv); // is always negative if moving towards each other
|
||||
|
||||
@@ -1834,8 +1837,9 @@ void blur1D(uint32_t *colorbuffer, uint32_t size, uint32_t blur, uint32_t start)
|
||||
for (uint32_t x = start; x < start + size; x++) {
|
||||
seeppart = fast_color_scale(colorbuffer[x], seep); // scale it and seep to neighbours
|
||||
if (x > 0) {
|
||||
colorbuffer[x-1] = fast_color_scaleAdd(colorbuffer[x-1], seeppart);
|
||||
colorbuffer[x] = fast_color_scaleAdd(colorbuffer[x], carryover); // is black on first pass
|
||||
colorbuffer[x-1] = fast_color_add(colorbuffer[x-1], seeppart);
|
||||
if (carryover.color32) // note: check adds overhead but is faster on average
|
||||
colorbuffer[x] = fast_color_add(colorbuffer[x], carryover); // is black on first pass
|
||||
}
|
||||
carryover = seeppart;
|
||||
}
|
||||
@@ -1884,34 +1888,36 @@ static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32
|
||||
return true; // particle is in bounds
|
||||
}
|
||||
|
||||
// this is a fast version for RGB color adding ignoring white channel (PS does not handle white) including scaling of second color
|
||||
// this is a fast version for CRGBW color adding ignoring white channel (PS does not handle white) including scaling of second color
|
||||
// note: function is mainly used to add scaled colors, so checking if one color is black is slower
|
||||
static uint32_t fast_color_scaleAdd(const uint32_t c1, const uint32_t c2, const uint8_t scale) {
|
||||
constexpr uint32_t MASK_RB = 0x00FF00FF; // red and blue mask
|
||||
constexpr uint32_t MASK_G = 0x0000FF00; // green mask
|
||||
// note2: returning CRGBW value is slightly slower as the return value gets written to uint32_t framebuffer
|
||||
__attribute__((optimize("O2"))) static uint32_t fast_color_add(CRGBW c1, const CRGBW c2, const uint8_t scale) {
|
||||
uint32_t r, g, b;
|
||||
r = c1.r + ((c2.r * scale) >> 8);
|
||||
g = c1.g + ((c2.g * scale) >> 8);
|
||||
b = c1.b + ((c2.b * scale) >> 8);
|
||||
|
||||
uint32_t rb = c2 & MASK_RB; // 0x00RR00BB
|
||||
uint32_t g = c2 & MASK_G; // 0x0000GG00
|
||||
// scale second color
|
||||
rb = ((rb * scale) >> 8) & MASK_RB;
|
||||
g = ((g * scale) >> 8) & MASK_G;
|
||||
// add colors
|
||||
rb = (c1 & MASK_RB) + rb;
|
||||
g = ((c1 & MASK_G) + g);
|
||||
// note: this chained comparison is the fastest method for max of 3 values (faster than std:max() or using xor)
|
||||
uint32_t max = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b);
|
||||
if (max <= 255) {
|
||||
c1.r = r; // save result to c1
|
||||
c1.g = g;
|
||||
c1.b = b;
|
||||
} else {
|
||||
uint32_t newscale = (255U << 16) / max;
|
||||
c1.r = (r * newscale) >> 16;
|
||||
c1.g = (g * newscale) >> 16;
|
||||
c1.b = (b * newscale) >> 16;
|
||||
}
|
||||
return c1.color32;
|
||||
}
|
||||
|
||||
// check for overflow by looking at the 9th bit of each channel
|
||||
if ((rb | (g >> 8)) & 0x01000100) {
|
||||
// find max among the three 16-bit values
|
||||
g = g >> 8; // shift to get 0x000000GG
|
||||
uint32_t max_val = (rb >> 16); // red
|
||||
max_val = ((rb & 0xFFFF) > max_val) ? rb & 0xFFFF : max_val; // blue
|
||||
max_val = (g > max_val) ? g : max_val; // green
|
||||
// scale down to avoid saturation
|
||||
uint32_t scale_factor = (255 << 8) / max_val;
|
||||
rb = ((rb * scale_factor) >> 8) & MASK_RB;
|
||||
g = (g * scale_factor) & MASK_G;
|
||||
}
|
||||
return rb | g;
|
||||
// fast CRGBW color scaling ignoring white channel (PS does not handle white)
|
||||
__attribute__((optimize("O2"))) static uint32_t fast_color_scale(CRGBW c, const uint8_t scale) {
|
||||
c.r = ((c.r * scale) >> 8);
|
||||
c.g = ((c.g * scale) >> 8);
|
||||
c.b = ((c.b * scale) >> 8);
|
||||
return c.color32;
|
||||
}
|
||||
|
||||
#endif // !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D))
|
||||
|
||||
+15
-390
@@ -32,36 +32,6 @@ extern char cmDNS[];
|
||||
extern bool cctICused;
|
||||
extern bool useParallelI2S;
|
||||
|
||||
// functions to get/set bits in an array - based on functions created by Brandon for GOL
|
||||
// toDo : make this a class that's completely defined in a header file
|
||||
bool getBitFromArray(const uint8_t* byteArray, size_t position) { // get bit value
|
||||
size_t byteIndex = position / 8;
|
||||
unsigned bitIndex = position % 8;
|
||||
uint8_t byteValue = byteArray[byteIndex];
|
||||
return (byteValue >> bitIndex) & 1;
|
||||
}
|
||||
|
||||
void setBitInArray(uint8_t* byteArray, size_t position, bool value) { // set bit - with error handling for nullptr
|
||||
//if (byteArray == nullptr) return;
|
||||
size_t byteIndex = position / 8;
|
||||
unsigned bitIndex = position % 8;
|
||||
if (value)
|
||||
byteArray[byteIndex] |= (1 << bitIndex);
|
||||
else
|
||||
byteArray[byteIndex] &= ~(1 << bitIndex);
|
||||
}
|
||||
|
||||
size_t getBitArrayBytes(size_t num_bits) { // number of bytes needed for an array with num_bits bits
|
||||
return (num_bits + 7) / 8;
|
||||
}
|
||||
|
||||
void setBitArray(uint8_t* byteArray, size_t numBits, bool value) { // set all bits to same value
|
||||
if (byteArray == nullptr) return;
|
||||
size_t len = getBitArrayBytes(numBits);
|
||||
if (value) memset(byteArray, 0xFF, len);
|
||||
else memset(byteArray, 0x00, len);
|
||||
}
|
||||
|
||||
//colors.cpp
|
||||
uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb);
|
||||
|
||||
@@ -233,7 +203,6 @@ void BusDigital::estimateCurrent() {
|
||||
|
||||
void BusDigital::applyBriLimit(uint8_t newBri) {
|
||||
// a newBri of 0 means calculate per-bus brightness limit
|
||||
_NPBbri = 255; // reset, intermediate value is set below, final value is calculated in bus::show()
|
||||
if (newBri == 0) {
|
||||
if (_milliAmpsLimit == 0 || _milliAmpsTotal == 0) return; // ABL not used for this bus
|
||||
newBri = 255;
|
||||
@@ -251,7 +220,6 @@ void BusDigital::applyBriLimit(uint8_t newBri) {
|
||||
}
|
||||
|
||||
if (newBri < 255) {
|
||||
_NPBbri = newBri; // store value so it can be updated in show() (must be updated even if ABL is not used)
|
||||
uint8_t cctWW = 0, cctCW = 0;
|
||||
unsigned hwLen = _len;
|
||||
if (_type == TYPE_WS2812_1CH_X3) hwLen = NUM_ICS_WS2812_1CH_3X(_len); // only needs a third of "RGB" LEDs for NeoPixelBus
|
||||
@@ -269,7 +237,6 @@ void BusDigital::applyBriLimit(uint8_t newBri) {
|
||||
|
||||
void BusDigital::show() {
|
||||
if (!_valid) return;
|
||||
_NPBbri = (_NPBbri * _bri) / 255; // total applied brightness for use in restoreColorLossy (see applyBriLimit())
|
||||
PolyBus::show(_busPtr, _iType, _skip); // faster if buffer consistency is not important (no skipped LEDs)
|
||||
}
|
||||
|
||||
@@ -332,7 +299,7 @@ uint32_t IRAM_ATTR BusDigital::getPixelColor(unsigned pix) const {
|
||||
if (_reversed) pix = _len - pix -1;
|
||||
pix += _skip;
|
||||
const uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder);
|
||||
uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, (_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix, co),_NPBbri);
|
||||
uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, (_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix, co),_bri);
|
||||
if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs
|
||||
uint8_t r = R(c);
|
||||
uint8_t g = _reversed ? B(c) : G(c); // should G and B be switched if _reversed?
|
||||
@@ -357,7 +324,7 @@ size_t BusDigital::getPins(uint8_t* pinArray) const {
|
||||
}
|
||||
|
||||
size_t BusDigital::getBusSize() const {
|
||||
return sizeof(BusDigital) + (isOk() ? PolyBus::getDataSize(_busPtr, _iType) : 0); // does not include common I2S DMA buffer
|
||||
return sizeof(BusDigital) + (isOk() ? PolyBus::getDataSize(_busPtr, _iType) : 0);
|
||||
}
|
||||
|
||||
void BusDigital::setColorOrder(uint8_t colorOrder) {
|
||||
@@ -787,347 +754,13 @@ void BusNetwork::cleanup() {
|
||||
_valid = false;
|
||||
}
|
||||
|
||||
// ***************************************************************************
|
||||
|
||||
#ifdef WLED_ENABLE_HUB75MATRIX
|
||||
#warning "HUB75 driver enabled (experimental)"
|
||||
#ifdef ESP8266
|
||||
#error ESP8266 does not support HUB75
|
||||
#endif
|
||||
|
||||
BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite) {
|
||||
size_t lastHeap = ESP.getFreeHeap();
|
||||
_valid = false;
|
||||
_hasRgb = true;
|
||||
_hasWhite = false;
|
||||
|
||||
mxconfig.double_buff = false; // Use our own memory-optimised buffer rather than the driver's own double-buffer
|
||||
|
||||
// mxconfig.driver = HUB75_I2S_CFG::ICN2038S; // experimental - use specific shift register driver
|
||||
// mxconfig.driver = HUB75_I2S_CFG::FM6124; // try this driver in case you panel stays dark, or when colors look too pastel
|
||||
|
||||
// mxconfig.latch_blanking = 3;
|
||||
// mxconfig.i2sspeed = HUB75_I2S_CFG::HZ_10M; // experimental - 5MHZ should be enugh, but colours looks slightly better at 10MHz
|
||||
//mxconfig.min_refresh_rate = 90;
|
||||
//mxconfig.min_refresh_rate = 120;
|
||||
mxconfig.clkphase = bc.reversed;
|
||||
|
||||
virtualDisp = nullptr;
|
||||
|
||||
if (bc.type == TYPE_HUB75MATRIX_HS) {
|
||||
mxconfig.mx_width = min((uint8_t) 64, bc.pins[0]);
|
||||
mxconfig.mx_height = min((uint8_t) 64, bc.pins[1]);
|
||||
// Disable chains of panels for now, incomplete UI changes
|
||||
// if(bc.pins[2] > 1 && bc.pins[3] != 0 && bc.pins[4] != 0 && bc.pins[3] != 255 && bc.pins[4] != 255) {
|
||||
// virtualDisp = new VirtualMatrixPanel((*display), bc.pins[3], bc.pins[4], mxconfig.mx_width, mxconfig.mx_height, CHAIN_BOTTOM_LEFT_UP);
|
||||
// }
|
||||
} else if (bc.type == TYPE_HUB75MATRIX_QS) {
|
||||
mxconfig.mx_width = min((uint8_t) 64, bc.pins[0]) * 2;
|
||||
mxconfig.mx_height = min((uint8_t) 64, bc.pins[1]) / 2;
|
||||
virtualDisp = new VirtualMatrixPanel((*display), 1, 1, bc.pins[0], bc.pins[1]);
|
||||
virtualDisp->setRotation(0);
|
||||
switch(bc.pins[1]) {
|
||||
case 16:
|
||||
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_16PX_HIGH);
|
||||
break;
|
||||
case 32:
|
||||
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_32PX_HIGH);
|
||||
break;
|
||||
case 64:
|
||||
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_64PX_HIGH);
|
||||
break;
|
||||
default:
|
||||
DEBUGBUS_PRINTLN("Unsupported height");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
DEBUGBUS_PRINTLN("Unknown type");
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2)// classic esp32, or esp32-s2: reduce bitdepth for large panels
|
||||
if (mxconfig.mx_height >= 64) {
|
||||
if (mxconfig.chain_length * mxconfig.mx_width > 192) mxconfig.setPixelColorDepthBits(3);
|
||||
else if (mxconfig.chain_length * mxconfig.mx_width > 64) mxconfig.setPixelColorDepthBits(4);
|
||||
else mxconfig.setPixelColorDepthBits(8);
|
||||
} else mxconfig.setPixelColorDepthBits(8);
|
||||
#endif
|
||||
|
||||
mxconfig.chain_length = max((uint8_t) 1, min(bc.pins[2], (uint8_t) 4)); // prevent bad data preventing boot due to low memory
|
||||
|
||||
if(mxconfig.mx_height >= 64 && (mxconfig.chain_length > 1)) {
|
||||
DEBUGBUS_PRINTLN("WARNING, only single panel can be used of 64 pixel boards due to memory");
|
||||
mxconfig.chain_length = 1;
|
||||
}
|
||||
|
||||
|
||||
// HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN};
|
||||
|
||||
#if defined(ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3) // MatrixPortal ESP32-S3
|
||||
|
||||
// https://www.adafruit.com/product/5778
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - Matrix Portal S3 config");
|
||||
mxconfig.gpio = { 42, 41, 40, 38, 39, 37, 45, 36, 48, 35, 21, 47, 14, 2 };
|
||||
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)// ESP32-S3 with PSRAM
|
||||
|
||||
#if defined(MOONHUB_S3_PINOUT)
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - T7 S3 with PSRAM, MOONHUB pinout");
|
||||
|
||||
// HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN};
|
||||
mxconfig.gpio = { 1, 5, 6, 7, 13, 9, 16, 48, 47, 21, 38, 8, 4, 18 };
|
||||
|
||||
#else
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - S3 with PSRAM");
|
||||
// HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN};
|
||||
mxconfig.gpio = {1, 2, 42, 41, 40, 39, 45, 48, 47, 21, 38, 8, 3, 18};
|
||||
#endif
|
||||
#elif defined(ESP32_FORUM_PINOUT) // Common format for boards designed for SmartMatrix
|
||||
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - ESP32_FORUM_PINOUT");
|
||||
/*
|
||||
ESP32 with SmartMatrix's default pinout - ESP32_FORUM_PINOUT
|
||||
https://github.com/pixelmatix/SmartMatrix/blob/teensylc/src/MatrixHardware_ESP32_V0.h
|
||||
Can use a board like https://github.com/rorosaurus/esp32-hub75-driver
|
||||
*/
|
||||
|
||||
mxconfig.gpio = { 2, 15, 4, 16, 27, 17, 5, 18, 19, 21, 12, 26, 25, 22 };
|
||||
|
||||
#else
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - Default pins");
|
||||
/*
|
||||
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA?tab=readme-ov-file
|
||||
|
||||
Boards
|
||||
|
||||
https://esp32trinity.com/
|
||||
https://www.electrodragon.com/product/rgb-matrix-panel-drive-interface-board-for-esp32-dma/
|
||||
|
||||
*/
|
||||
mxconfig.gpio = { 25, 26, 27, 14, 12, 13, 23, 19, 5, 17, 18, 4, 15, 16 };
|
||||
|
||||
#endif
|
||||
|
||||
int8_t pins[PIN_COUNT];
|
||||
memcpy(pins, &mxconfig.gpio, sizeof(mxconfig.gpio));
|
||||
if (!PinManager::allocateMultiplePins(pins, PIN_COUNT, PinOwner::HUB75, true)) {
|
||||
DEBUGBUS_PRINTLN("Failed to allocate pins for HUB75");
|
||||
return;
|
||||
}
|
||||
|
||||
if(bc.colorOrder == COL_ORDER_RGB) {
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA = Default color order (RGB)");
|
||||
} else if(bc.colorOrder == COL_ORDER_BGR) {
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA = color order BGR");
|
||||
int8_t tmpPin;
|
||||
tmpPin = mxconfig.gpio.r1;
|
||||
mxconfig.gpio.r1 = mxconfig.gpio.b1;
|
||||
mxconfig.gpio.b1 = tmpPin;
|
||||
tmpPin = mxconfig.gpio.r2;
|
||||
mxconfig.gpio.r2 = mxconfig.gpio.b2;
|
||||
mxconfig.gpio.b2 = tmpPin;
|
||||
}
|
||||
else {
|
||||
DEBUGBUS_PRINTF("MatrixPanel_I2S_DMA = unsupported color order %u\n", bc.colorOrder);
|
||||
}
|
||||
|
||||
DEBUGBUS_PRINTF("MatrixPanel_I2S_DMA config - %ux%u length: %u\n", mxconfig.mx_width, mxconfig.mx_height, mxconfig.chain_length);
|
||||
DEBUGBUS_PRINTF("R1_PIN=%u, G1_PIN=%u, B1_PIN=%u, R2_PIN=%u, G2_PIN=%u, B2_PIN=%u, A_PIN=%u, B_PIN=%u, C_PIN=%u, D_PIN=%u, E_PIN=%u, LAT_PIN=%u, OE_PIN=%u, CLK_PIN=%u\n",
|
||||
mxconfig.gpio.r1, mxconfig.gpio.g1, mxconfig.gpio.b1, mxconfig.gpio.r2, mxconfig.gpio.g2, mxconfig.gpio.b2,
|
||||
mxconfig.gpio.a, mxconfig.gpio.b, mxconfig.gpio.c, mxconfig.gpio.d, mxconfig.gpio.e, mxconfig.gpio.lat, mxconfig.gpio.oe, mxconfig.gpio.clk);
|
||||
|
||||
// OK, now we can create our matrix object
|
||||
display = new MatrixPanel_I2S_DMA(mxconfig);
|
||||
if (display == nullptr) {
|
||||
DEBUGBUS_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! driver allocation failed ***********");
|
||||
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
|
||||
return;
|
||||
}
|
||||
|
||||
this->_len = (display->width() * display->height());
|
||||
DEBUGBUS_PRINTF("Length: %u\n", _len);
|
||||
if(this->_len >= MAX_LEDS) {
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA Too many LEDS - playing safe");
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA created");
|
||||
// let's adjust default brightness
|
||||
display->setBrightness8(25); // range is 0-255, 0 - 0%, 255 - 100%
|
||||
|
||||
delay(24); // experimental
|
||||
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
|
||||
// Allocate memory and start DMA display
|
||||
if( not display->begin() ) {
|
||||
DEBUGBUS_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! I2S memory allocation failed ***********");
|
||||
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
|
||||
return;
|
||||
}
|
||||
else {
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA begin ok");
|
||||
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
|
||||
delay(18); // experiment - give the driver a moment (~ one full frame @ 60hz) to settle
|
||||
_valid = true;
|
||||
display->clearScreen(); // initially clear the screen buffer
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA clear ok");
|
||||
|
||||
if (_ledBuffer) free(_ledBuffer); // should not happen
|
||||
if (_ledsDirty) free(_ledsDirty); // should not happen
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA allocate memory");
|
||||
_ledsDirty = (byte*) malloc(getBitArrayBytes(_len)); // create LEDs dirty bits
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA allocate memory ok");
|
||||
|
||||
if (_ledsDirty == nullptr) {
|
||||
display->stopDMAoutput();
|
||||
delete display; display = nullptr;
|
||||
_valid = false;
|
||||
DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA not started - not enough memory for dirty bits!"));
|
||||
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
|
||||
return; // fail is we cannot get memory for the buffer
|
||||
}
|
||||
setBitArray(_ledsDirty, _len, false); // reset dirty bits
|
||||
|
||||
if (mxconfig.double_buff == false) {
|
||||
_ledBuffer = (CRGB*) calloc(_len, sizeof(CRGB)); // create LEDs buffer (initialized to BLACK)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (_valid) {
|
||||
_panelWidth = virtualDisp ? virtualDisp->width() : display->width(); // cache width - it will never change
|
||||
}
|
||||
|
||||
DEBUGBUS_PRINT(F("MatrixPanel_I2S_DMA "));
|
||||
DEBUGBUS_PRINTF("%sstarted, width=%u, %u pixels.\n", _valid? "":"not ", _panelWidth, _len);
|
||||
|
||||
if (_ledBuffer != nullptr) DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA LEDS buffer enabled."));
|
||||
if (_ledsDirty != nullptr) DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA LEDS dirty bit optimization enabled."));
|
||||
if ((_ledBuffer != nullptr) || (_ledsDirty != nullptr)) {
|
||||
DEBUGBUS_PRINT(F("MatrixPanel_I2S_DMA LEDS buffer uses "));
|
||||
DEBUGBUS_PRINT((_ledBuffer? _len*sizeof(CRGB) :0) + (_ledsDirty? getBitArrayBytes(_len) :0));
|
||||
DEBUGBUS_PRINTLN(F(" bytes."));
|
||||
}
|
||||
}
|
||||
|
||||
void __attribute__((hot)) BusHub75Matrix::setPixelColor(unsigned pix, uint32_t c) {
|
||||
if (!_valid || pix >= _len) return;
|
||||
// if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT
|
||||
|
||||
if (_ledBuffer) {
|
||||
CRGB fastled_col = CRGB(c);
|
||||
if (_ledBuffer[pix] != fastled_col) {
|
||||
_ledBuffer[pix] = fastled_col;
|
||||
setBitInArray(_ledsDirty, pix, true); // flag pixel as "dirty"
|
||||
}
|
||||
}
|
||||
else {
|
||||
if ((c == IS_BLACK) && (getBitFromArray(_ledsDirty, pix) == false)) return; // ignore black if pixel is already black
|
||||
setBitInArray(_ledsDirty, pix, c != IS_BLACK); // dirty = true means "color is not BLACK"
|
||||
|
||||
uint8_t r = R(c);
|
||||
uint8_t g = G(c);
|
||||
uint8_t b = B(c);
|
||||
|
||||
if(virtualDisp != nullptr) {
|
||||
int x = pix % _panelWidth;
|
||||
int y = pix / _panelWidth;
|
||||
virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b);
|
||||
} else {
|
||||
int x = pix % _panelWidth;
|
||||
int y = pix / _panelWidth;
|
||||
display->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t BusHub75Matrix::getPixelColor(unsigned pix) const {
|
||||
if (!_valid || pix >= _len) return IS_BLACK;
|
||||
if (_ledBuffer)
|
||||
return uint32_t(_ledBuffer[pix].scale8(_bri)) & 0x00FFFFFF; // scale8() is needed to mimic NeoPixelBus, which returns scaled-down colours
|
||||
else
|
||||
return getBitFromArray(_ledsDirty, pix) ? IS_DARKGREY: IS_BLACK; // just a hack - we only know if the pixel is black or not
|
||||
}
|
||||
|
||||
void BusHub75Matrix::setBrightness(uint8_t b) {
|
||||
_bri = b;
|
||||
if (display) display->setBrightness(_bri);
|
||||
}
|
||||
|
||||
void BusHub75Matrix::show(void) {
|
||||
if (!_valid) return;
|
||||
display->setBrightness(_bri);
|
||||
|
||||
if (_ledBuffer) {
|
||||
// write out buffered LEDs
|
||||
bool isVirtualDisp = (virtualDisp != nullptr);
|
||||
unsigned height = isVirtualDisp ? virtualDisp->height() : display->height();
|
||||
unsigned width = _panelWidth;
|
||||
|
||||
//while(!previousBufferFree) delay(1); // experimental - Wait before we allow any writing to the buffer. Stop flicker.
|
||||
|
||||
size_t pix = 0; // running pixel index
|
||||
for (int y=0; y<height; y++) for (int x=0; x<width; x++) {
|
||||
if (getBitFromArray(_ledsDirty, pix) == true) { // only repaint the "dirty" pixels
|
||||
uint32_t c = uint32_t(_ledBuffer[pix]) & 0x00FFFFFF; // get RGB color, removing FastLED "alpha" component
|
||||
uint8_t r = R(c);
|
||||
uint8_t g = G(c);
|
||||
uint8_t b = B(c);
|
||||
if (isVirtualDisp) virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b);
|
||||
else display->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b);
|
||||
}
|
||||
pix ++;
|
||||
}
|
||||
setBitArray(_ledsDirty, _len, false); // buffer shown - reset all dirty bits
|
||||
}
|
||||
}
|
||||
|
||||
void BusHub75Matrix::cleanup() {
|
||||
if (display && _valid) display->stopDMAoutput(); // terminate DMA driver (display goes black)
|
||||
_valid = false;
|
||||
_panelWidth = 0;
|
||||
deallocatePins();
|
||||
DEBUGBUS_PRINTLN("HUB75 output ended.");
|
||||
|
||||
//if (virtualDisp != nullptr) delete virtualDisp; // warning: deleting object of polymorphic class type 'VirtualMatrixPanel' which has non-virtual destructor might cause undefined behavior
|
||||
delete display;
|
||||
display = nullptr;
|
||||
virtualDisp = nullptr;
|
||||
if (_ledBuffer != nullptr) free(_ledBuffer); _ledBuffer = nullptr;
|
||||
if (_ledsDirty != nullptr) free(_ledsDirty); _ledsDirty = nullptr;
|
||||
}
|
||||
|
||||
void BusHub75Matrix::deallocatePins() {
|
||||
uint8_t pins[PIN_COUNT];
|
||||
memcpy(pins, &mxconfig.gpio, sizeof(mxconfig.gpio));
|
||||
PinManager::deallocateMultiplePins(pins, PIN_COUNT, PinOwner::HUB75);
|
||||
}
|
||||
|
||||
std::vector<LEDType> BusHub75Matrix::getLEDTypes() {
|
||||
return {
|
||||
{TYPE_HUB75MATRIX_HS, "H", PSTR("HUB75 (Half Scan)")},
|
||||
{TYPE_HUB75MATRIX_QS, "H", PSTR("HUB75 (Quarter Scan)")},
|
||||
};
|
||||
}
|
||||
|
||||
size_t BusHub75Matrix::getPins(uint8_t* pinArray) const {
|
||||
if (pinArray) {
|
||||
pinArray[0] = mxconfig.mx_width;
|
||||
pinArray[1] = mxconfig.mx_height;
|
||||
pinArray[2] = mxconfig.chain_length;
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
|
||||
#endif
|
||||
// ***************************************************************************
|
||||
|
||||
//utility to get the approx. memory usage of a given BusConfig
|
||||
size_t BusConfig::memUsage(unsigned nr) const {
|
||||
if (Bus::isVirtual(type)) {
|
||||
return sizeof(BusNetwork) + (count * Bus::getNumberOfChannels(type));
|
||||
} else if (Bus::isDigital(type)) {
|
||||
// if any of digital buses uses I2S, there is additional common I2S DMA buffer not accounted for here
|
||||
return sizeof(BusDigital) + PolyBus::memUsage(count + skipAmount, PolyBus::getI(type, pins, nr));
|
||||
return sizeof(BusDigital) + PolyBus::memUsage(count + skipAmount, PolyBus::getI(type, pins, nr)) /*+ doubleBuffer * (count + skipAmount) * Bus::getNumberOfChannels(type)*/;
|
||||
} else if (Bus::isOnOff(type)) {
|
||||
return sizeof(BusOnOff);
|
||||
} else {
|
||||
@@ -1143,23 +776,23 @@ size_t BusManager::memUsage() {
|
||||
unsigned maxI2S = 0;
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266)
|
||||
unsigned digitalCount = 0;
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
#define MAX_RMT 4
|
||||
#else
|
||||
#define MAX_RMT 8
|
||||
#endif
|
||||
#endif
|
||||
for (const auto &bus : busses) {
|
||||
size += bus->getBusSize();
|
||||
unsigned busSize = bus->getBusSize();
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266)
|
||||
if (bus->isDigital() && !bus->is2Pin()) {
|
||||
digitalCount++;
|
||||
if ((PolyBus::isParallelI2S1Output() && digitalCount <= 8) || (!PolyBus::isParallelI2S1Output() && digitalCount == 1)) {
|
||||
#ifdef NPB_CONF_4STEP_CADENCE
|
||||
constexpr unsigned stepFactor = 4; // 4 step cadence (4 bits per pixel bit)
|
||||
#else
|
||||
constexpr unsigned stepFactor = 3; // 3 step cadence (3 bits per pixel bit)
|
||||
#endif
|
||||
unsigned i2sCommonSize = stepFactor * bus->getLength() * bus->getNumberOfChannels() * (bus->is16bit()+1);
|
||||
if (i2sCommonSize > maxI2S) maxI2S = i2sCommonSize;
|
||||
}
|
||||
if (bus->isDigital() && !bus->is2Pin()) digitalCount++;
|
||||
if (PolyBus::isParallelI2S1Output() && digitalCount > MAX_RMT) {
|
||||
unsigned i2sCommonSize = 3 * bus->getLength() * bus->getNumberOfChannels() * (bus->is16bit()+1);
|
||||
if (i2sCommonSize > maxI2S) maxI2S = i2sCommonSize;
|
||||
busSize -= i2sCommonSize;
|
||||
}
|
||||
#endif
|
||||
size += busSize;
|
||||
}
|
||||
return size + maxI2S;
|
||||
}
|
||||
@@ -1177,10 +810,6 @@ int BusManager::add(const BusConfig &bc) {
|
||||
if (digital > WLED_MAX_DIGITAL_CHANNELS || analog > WLED_MAX_ANALOG_CHANNELS) return -1;
|
||||
if (Bus::isVirtual(bc.type)) {
|
||||
busses.push_back(make_unique<BusNetwork>(bc));
|
||||
#ifdef WLED_ENABLE_HUB75MATRIX
|
||||
} else if (Bus::isHub75(bc.type)) {
|
||||
busses.push_back(make_unique<BusHub75Matrix>(bc));
|
||||
#endif
|
||||
} else if (Bus::isDigital(bc.type)) {
|
||||
busses.push_back(make_unique<BusDigital>(bc, Bus::is2Pin(bc.type) ? twoPin : digital));
|
||||
} else if (Bus::isOnOff(bc.type)) {
|
||||
@@ -1212,10 +841,6 @@ String BusManager::getLEDTypesJSONString() {
|
||||
json += LEDTypesToJson(BusPwm::getLEDTypes());
|
||||
json += LEDTypesToJson(BusNetwork::getLEDTypes());
|
||||
//json += LEDTypesToJson(BusVirtual::getLEDTypes());
|
||||
#ifdef WLED_ENABLE_HUB75MATRIX
|
||||
json += LEDTypesToJson(BusHub75Matrix::getLEDTypes());
|
||||
#endif
|
||||
|
||||
json.setCharAt(json.length()-1, ']'); // replace last comma with bracket
|
||||
return json;
|
||||
}
|
||||
|
||||
+5
-45
@@ -2,13 +2,6 @@
|
||||
#ifndef BusManager_h
|
||||
#define BusManager_h
|
||||
|
||||
#ifdef WLED_ENABLE_HUB75MATRIX
|
||||
|
||||
#include <ESP32-HUB75-MatrixPanel-I2S-DMA.h>
|
||||
#include <ESP32-VirtualMatrixPanel-I2S-DMA.h>
|
||||
#include <FastLED.h>
|
||||
|
||||
#endif
|
||||
/*
|
||||
* Class for addressing various light types
|
||||
*/
|
||||
@@ -112,7 +105,6 @@ class Bus {
|
||||
Bus(uint8_t type, uint16_t start, uint8_t aw, uint16_t len = 1, bool reversed = false, bool refresh = false)
|
||||
: _type(type)
|
||||
, _bri(255)
|
||||
, _NPBbri(255)
|
||||
, _start(start)
|
||||
, _len(std::max(len,(uint16_t)1))
|
||||
, _reversed(reversed)
|
||||
@@ -166,7 +158,7 @@ class Bus {
|
||||
inline bool containsPixel(uint16_t pix) const { return pix >= _start && pix < _start + _len; }
|
||||
|
||||
static inline std::vector<LEDType> getLEDTypes() { return {{TYPE_NONE, "", PSTR("None")}}; } // not used. just for reference for derived classes
|
||||
static constexpr size_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : isHub75(type) ? 3 : is2Pin(type) + 1; } // credit @PaoloTK
|
||||
static constexpr size_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : is2Pin(type) + 1; } // credit @PaoloTK
|
||||
static constexpr size_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); }
|
||||
static constexpr bool hasRGB(uint8_t type) {
|
||||
return !((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF);
|
||||
@@ -190,7 +182,6 @@ class Bus {
|
||||
static constexpr bool isOnOff(uint8_t type) { return (type == TYPE_ONOFF); }
|
||||
static constexpr bool isPWM(uint8_t type) { return (type >= TYPE_ANALOG_MIN && type <= TYPE_ANALOG_MAX); }
|
||||
static constexpr bool isVirtual(uint8_t type) { return (type >= TYPE_VIRTUAL_MIN && type <= TYPE_VIRTUAL_MAX); }
|
||||
static constexpr bool isHub75(uint8_t type) { return (type >= TYPE_HUB75MATRIX_MIN && type <= TYPE_HUB75MATRIX_MAX); }
|
||||
static constexpr bool is16bit(uint8_t type) { return type == TYPE_UCS8903 || type == TYPE_UCS8904 || type == TYPE_SM16825; }
|
||||
static constexpr bool mustRefresh(uint8_t type) { return type == TYPE_TM1814; }
|
||||
static constexpr int numPWMPins(uint8_t type) { return (type - 40); }
|
||||
@@ -211,9 +202,7 @@ class Bus {
|
||||
|
||||
protected:
|
||||
uint8_t _type;
|
||||
uint8_t _bri; // bus brightness
|
||||
uint8_t _NPBbri; // total brightness applied to colors in NPB buffer (_bri + ABL)
|
||||
uint8_t _autoWhiteMode; // global Auto White Calculation override
|
||||
uint8_t _bri;
|
||||
uint16_t _start;
|
||||
uint16_t _len;
|
||||
//struct { //using bitfield struct adds abour 250 bytes to binary size
|
||||
@@ -224,6 +213,8 @@ class Bus {
|
||||
bool _hasWhite;// : 1;
|
||||
bool _hasCCT;// : 1;
|
||||
//} __attribute__ ((packed));
|
||||
uint8_t _autoWhiteMode;
|
||||
// global Auto White Calculation override
|
||||
static uint8_t _gAWM;
|
||||
// _cct has the following meanings (see calculateCCT() & BusManager::setSegmentCCT()):
|
||||
// -1 means to extract approximate CCT value in K from RGB (in calcualteCCT())
|
||||
@@ -372,37 +363,6 @@ class BusNetwork : public Bus {
|
||||
#endif
|
||||
};
|
||||
|
||||
#ifdef WLED_ENABLE_HUB75MATRIX
|
||||
class BusHub75Matrix : public Bus {
|
||||
public:
|
||||
BusHub75Matrix(const BusConfig &bc);
|
||||
[[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override;
|
||||
[[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override;
|
||||
void show() override;
|
||||
void setBrightness(uint8_t b) override;
|
||||
size_t getPins(uint8_t* pinArray = nullptr) const override;
|
||||
void deallocatePins();
|
||||
void cleanup();
|
||||
|
||||
~BusHub75Matrix() {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
static std::vector<LEDType> getLEDTypes(void);
|
||||
|
||||
private:
|
||||
MatrixPanel_I2S_DMA *display = nullptr;
|
||||
VirtualMatrixPanel *virtualDisp = nullptr;
|
||||
HUB75_I2S_CFG mxconfig;
|
||||
unsigned _panelWidth = 0;
|
||||
CRGB *_ledBuffer = nullptr;
|
||||
byte *_ledsDirty = nullptr;
|
||||
// workaround for missing constants on include path for non-MM
|
||||
uint32_t IS_BLACK = 0x000000;
|
||||
uint32_t IS_DARKGREY = 0x333333;
|
||||
const int PIN_COUNT = 14;
|
||||
};
|
||||
#endif
|
||||
|
||||
//temporary struct for passing bus configuration to bus
|
||||
struct BusConfig {
|
||||
@@ -414,7 +374,7 @@ struct BusConfig {
|
||||
uint8_t skipAmount;
|
||||
bool refreshReq;
|
||||
uint8_t autoWhite;
|
||||
uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255};
|
||||
uint8_t pins[5] = {255, 255, 255, 255, 255};
|
||||
uint16_t frequency;
|
||||
uint8_t milliAmpsPerLed;
|
||||
uint16_t milliAmpsMax;
|
||||
|
||||
+96
-93
@@ -1121,54 +1121,54 @@ class PolyBus {
|
||||
switch (busType) {
|
||||
case I_NONE: break;
|
||||
#ifdef ESP8266
|
||||
case I_8266_U0_NEO_3: size = (static_cast<B_8266_U0_NEO_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_NEO_3: size = (static_cast<B_8266_U1_NEO_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_NEO_3: size = (static_cast<B_8266_U0_NEO_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_NEO_3: size = (static_cast<B_8266_U1_NEO_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_DM_NEO_3: size = (static_cast<B_8266_DM_NEO_3*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_NEO_3: size = (static_cast<B_8266_BB_NEO_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_NEO_4: size = (static_cast<B_8266_U0_NEO_4*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_NEO_4: size = (static_cast<B_8266_U1_NEO_4*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_BB_NEO_3: size = (static_cast<B_8266_BB_NEO_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_NEO_4: size = (static_cast<B_8266_U0_NEO_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_NEO_4: size = (static_cast<B_8266_U1_NEO_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_DM_NEO_4: size = (static_cast<B_8266_DM_NEO_4*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_NEO_4: size = (static_cast<B_8266_BB_NEO_4*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_400_3: size = (static_cast<B_8266_U0_400_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_400_3: size = (static_cast<B_8266_U1_400_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_BB_NEO_4: size = (static_cast<B_8266_BB_NEO_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_400_3: size = (static_cast<B_8266_U0_400_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_400_3: size = (static_cast<B_8266_U1_400_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_DM_400_3: size = (static_cast<B_8266_DM_400_3*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_400_3: size = (static_cast<B_8266_BB_400_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_TM1_4: size = (static_cast<B_8266_U0_TM1_4*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_TM1_4: size = (static_cast<B_8266_U1_TM1_4*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_BB_400_3: size = (static_cast<B_8266_BB_400_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_TM1_4: size = (static_cast<B_8266_U0_TM1_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_TM1_4: size = (static_cast<B_8266_U1_TM1_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_DM_TM1_4: size = (static_cast<B_8266_DM_TM1_4*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_TM1_4: size = (static_cast<B_8266_BB_TM1_4*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_TM2_3: size = (static_cast<B_8266_U0_TM2_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_TM2_3: size = (static_cast<B_8266_U1_TM2_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_BB_TM1_4: size = (static_cast<B_8266_BB_TM1_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_TM2_3: size = (static_cast<B_8266_U0_TM2_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_TM2_3: size = (static_cast<B_8266_U1_TM2_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_DM_TM2_3: size = (static_cast<B_8266_DM_TM2_3*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_TM2_3: size = (static_cast<B_8266_BB_TM2_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_UCS_3: size = (static_cast<B_8266_U0_UCS_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_UCS_3: size = (static_cast<B_8266_U1_UCS_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_BB_TM2_3: size = (static_cast<B_8266_BB_TM2_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_UCS_3: size = (static_cast<B_8266_U0_UCS_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_UCS_3: size = (static_cast<B_8266_U1_UCS_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_DM_UCS_3: size = (static_cast<B_8266_DM_UCS_3*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_UCS_3: size = (static_cast<B_8266_BB_UCS_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_UCS_4: size = (static_cast<B_8266_U0_UCS_4*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_UCS_4: size = (static_cast<B_8266_U1_UCS_4*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_BB_UCS_3: size = (static_cast<B_8266_BB_UCS_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_UCS_4: size = (static_cast<B_8266_U0_UCS_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_UCS_4: size = (static_cast<B_8266_U1_UCS_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_DM_UCS_4: size = (static_cast<B_8266_DM_UCS_4*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_UCS_4: size = (static_cast<B_8266_BB_UCS_4*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_APA106_3: size = (static_cast<B_8266_U0_APA106_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_APA106_3: size = (static_cast<B_8266_U1_APA106_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_BB_UCS_4: size = (static_cast<B_8266_BB_UCS_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_APA106_3: size = (static_cast<B_8266_U0_APA106_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_APA106_3: size = (static_cast<B_8266_U1_APA106_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_DM_APA106_3: size = (static_cast<B_8266_DM_APA106_3*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_APA106_3: size = (static_cast<B_8266_BB_APA106_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_FW6_5: size = (static_cast<B_8266_U0_FW6_5*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_FW6_5: size = (static_cast<B_8266_U1_FW6_5*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_BB_APA106_3: size = (static_cast<B_8266_BB_APA106_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_FW6_5: size = (static_cast<B_8266_U0_FW6_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_FW6_5: size = (static_cast<B_8266_U1_FW6_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_DM_FW6_5: size = (static_cast<B_8266_DM_FW6_5*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_FW6_5: size = (static_cast<B_8266_BB_FW6_5*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_2805_5: size = (static_cast<B_8266_U0_2805_5*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_2805_5: size = (static_cast<B_8266_U1_2805_5*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_BB_FW6_5: size = (static_cast<B_8266_BB_FW6_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_2805_5: size = (static_cast<B_8266_U0_2805_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_2805_5: size = (static_cast<B_8266_U1_2805_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_DM_2805_5: size = (static_cast<B_8266_DM_2805_5*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_2805_5: size = (static_cast<B_8266_BB_2805_5*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_TM1914_3: size = (static_cast<B_8266_U0_TM1914_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_TM1914_3: size = (static_cast<B_8266_U1_TM1914_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_BB_2805_5: size = (static_cast<B_8266_BB_2805_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_TM1914_3: size = (static_cast<B_8266_U0_TM1914_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_TM1914_3: size = (static_cast<B_8266_U1_TM1914_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_DM_TM1914_3: size = (static_cast<B_8266_DM_TM1914_3*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_TM1914_3: size = (static_cast<B_8266_BB_TM1914_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_SM16825_5: size = (static_cast<B_8266_U0_SM16825_5*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_SM16825_5: size = (static_cast<B_8266_U1_SM16825_5*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_BB_TM1914_3: size = (static_cast<B_8266_BB_TM1914_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_SM16825_5: size = (static_cast<B_8266_U0_SM16825_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_SM16825_5: size = (static_cast<B_8266_U1_SM16825_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_DM_SM16825_5: size = (static_cast<B_8266_DM_SM16825_5*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_SM16825_5: size = (static_cast<B_8266_BB_SM16825_5*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_BB_SM16825_5: size = (static_cast<B_8266_BB_SM16825_5*>(busPtr))->PixelsSize()*2; break;
|
||||
#endif
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
// RMT buses (front + back + small system managed RMT)
|
||||
@@ -1220,65 +1220,68 @@ class PolyBus {
|
||||
case I_NONE: size = 0; break;
|
||||
#ifdef ESP8266
|
||||
// UART methods have front + back buffers + small UART
|
||||
case I_8266_U0_NEO_4 : // fallthrough
|
||||
case I_8266_U1_NEO_4 : // fallthrough
|
||||
case I_8266_BB_NEO_4 : // fallthrough
|
||||
case I_8266_U0_TM1_4 : // fallthrough
|
||||
case I_8266_U1_TM1_4 : // fallthrough
|
||||
case I_8266_BB_TM1_4 : size = (size + count); break; // 4 channels
|
||||
case I_8266_U0_UCS_3 : // fallthrough
|
||||
case I_8266_U1_UCS_3 : // fallthrough
|
||||
case I_8266_BB_UCS_3 : size *= 2; break; // 16 bit
|
||||
case I_8266_U0_UCS_4 : // fallthrough
|
||||
case I_8266_U1_UCS_4 : // fallthrough
|
||||
case I_8266_BB_UCS_4 : size = (size + count)*2; break; // 16 bit 4 channels
|
||||
case I_8266_U0_FW6_5 : // fallthrough
|
||||
case I_8266_U1_FW6_5 : // fallthrough
|
||||
case I_8266_BB_FW6_5 : // fallthrough
|
||||
case I_8266_U0_2805_5 : // fallthrough
|
||||
case I_8266_U1_2805_5 : // fallthrough
|
||||
case I_8266_BB_2805_5 : size = (size + 2*count); break; // 5 channels
|
||||
case I_8266_U0_SM16825_5: // fallthrough
|
||||
case I_8266_U1_SM16825_5: // fallthrough
|
||||
case I_8266_BB_SM16825_5: size = (size + 2*count)*2; break; // 16 bit 5 channels
|
||||
// DMA methods have front + DMA buffer = ((1+(3+1)) * channels; exact value is a bit of mistery - needs a dig into NPB)
|
||||
case I_8266_DM_NEO_3 : // fallthrough
|
||||
case I_8266_DM_400_3 : // fallthrough
|
||||
case I_8266_DM_TM2_3 : // fallthrough
|
||||
case I_8266_DM_APA106_3 : // fallthrough
|
||||
case I_8266_DM_TM1914_3 : size *= 5; break;
|
||||
case I_8266_DM_NEO_4 : // fallthrough
|
||||
case I_8266_DM_TM1_4 : size = (size + count)*5; break;
|
||||
case I_8266_DM_UCS_3 : size *= 2*5; break;
|
||||
case I_8266_DM_UCS_4 : size = (size + count)*2*5; break;
|
||||
case I_8266_DM_FW6_5 : // fallthrough
|
||||
case I_8266_DM_2805_5 : size = (size + 2*count)*5; break;
|
||||
case I_8266_U0_NEO_4: size = (size + count)*2; break; // 4 channels
|
||||
case I_8266_U1_NEO_4: size = (size + count)*2; break; // 4 channels
|
||||
case I_8266_BB_NEO_4: size = (size + count)*2; break; // 4 channels
|
||||
case I_8266_U0_TM1_4: size = (size + count)*2; break; // 4 channels
|
||||
case I_8266_U1_TM1_4: size = (size + count)*2; break; // 4 channels
|
||||
case I_8266_BB_TM1_4: size = (size + count)*2; break; // 4 channels
|
||||
case I_8266_U0_UCS_3: size *= 4; break; // 16 bit
|
||||
case I_8266_U1_UCS_3: size *= 4; break; // 16 bit
|
||||
case I_8266_BB_UCS_3: size *= 4; break; // 16 bit
|
||||
case I_8266_U0_UCS_4: size = (size + count)*2*2; break; // 16 bit 4 channels
|
||||
case I_8266_U1_UCS_4: size = (size + count)*2*2; break; // 16 bit 4 channels
|
||||
case I_8266_BB_UCS_4: size = (size + count)*2*2; break; // 16 bit 4 channels
|
||||
case I_8266_U0_FW6_5: size = (size + 2*count)*2; break; // 5 channels
|
||||
case I_8266_U1_FW6_5: size = (size + 2*count)*2; break; // 5channels
|
||||
case I_8266_BB_FW6_5: size = (size + 2*count)*2; break; // 5 channels
|
||||
case I_8266_U0_2805_5: size = (size + 2*count)*2; break; // 5 channels
|
||||
case I_8266_U1_2805_5: size = (size + 2*count)*2; break; // 5 channels
|
||||
case I_8266_BB_2805_5: size = (size + 2*count)*2; break; // 5 channels
|
||||
case I_8266_U0_SM16825_5: size = (size + 2*count)*2*2; break; // 16 bit 5 channels
|
||||
case I_8266_U1_SM16825_5: size = (size + 2*count)*2*2; break; // 16 bit 5 channels
|
||||
case I_8266_BB_SM16825_5: size = (size + 2*count)*2*2; break; // 16 bit 5 channels
|
||||
// DMA methods have front + DMA buffer = ((1+(3+1)) * channels)
|
||||
case I_8266_DM_NEO_3: size *= 5; break;
|
||||
case I_8266_DM_NEO_4: size = (size + count)*5; break;
|
||||
case I_8266_DM_400_3: size *= 5; break;
|
||||
case I_8266_DM_TM1_4: size = (size + count)*5; break;
|
||||
case I_8266_DM_TM2_3: size *= 5; break;
|
||||
case I_8266_DM_UCS_3: size *= 2*5; break;
|
||||
case I_8266_DM_UCS_4: size = (size + count)*2*5; break;
|
||||
case I_8266_DM_APA106_3: size *= 5; break;
|
||||
case I_8266_DM_FW6_5: size = (size + 2*count)*5; break;
|
||||
case I_8266_DM_2805_5: size = (size + 2*count)*5; break;
|
||||
case I_8266_DM_TM1914_3: size *= 5; break;
|
||||
case I_8266_DM_SM16825_5: size = (size + 2*count)*2*5; break;
|
||||
#else
|
||||
// RMT buses (1x front and 1x back buffer, does not include small RMT buffer)
|
||||
case I_32_RN_NEO_4 : // fallthrough
|
||||
case I_32_RN_TM1_4 : size = (size + count)*2; break; // 4 channels
|
||||
case I_32_RN_UCS_3 : size *= 2*2; break; // 16bit
|
||||
case I_32_RN_UCS_4 : size = (size + count)*2*2; break; // 16bit, 4 channels
|
||||
case I_32_RN_FW6_5 : // fallthrough
|
||||
case I_32_RN_2805_5 : size = (size + 2*count)*2; break; // 5 channels
|
||||
case I_32_RN_SM16825_5: size = (size + 2*count)*2*2; break; // 16bit, 5 channels
|
||||
// I2S1 bus or paralell I2S1 buses (1x front, does not include DMA buffer which is front*cadence, a bit(?) more for LCD)
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32C3
|
||||
case I_32_I2_NEO_3 : // fallthrough
|
||||
case I_32_I2_400_3 : // fallthrough
|
||||
case I_32_I2_TM2_3 : // fallthrough
|
||||
case I_32_I2_APA106_3 : break; // do nothing, I2S uses single buffer + DMA buffer
|
||||
case I_32_I2_NEO_4 : // fallthrough
|
||||
case I_32_I2_TM1_4 : size = (size + count); break; // 4 channels
|
||||
case I_32_I2_UCS_3 : size *= 2; break; // 16 bit
|
||||
case I_32_I2_UCS_4 : size = (size + count)*2; break; // 16 bit, 4 channels
|
||||
case I_32_I2_FW6_5 : // fallthrough
|
||||
case I_32_I2_2805_5 : size = (size + 2*count); break; // 5 channels
|
||||
case I_32_I2_SM16825_5: size = (size + 2*count)*2; break; // 16 bit, 5 channels
|
||||
#endif
|
||||
default : size *= 2; break; // everything else uses 2 buffers
|
||||
#endif
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
// RMT buses (1x front and 1x back buffer)
|
||||
case I_32_RN_NEO_4: size = (size + count)*2; break;
|
||||
case I_32_RN_TM1_4: size = (size + count)*2; break;
|
||||
case I_32_RN_UCS_3: size *= 2*2; break;
|
||||
case I_32_RN_UCS_4: size = (size + count)*2*2; break;
|
||||
case I_32_RN_FW6_5: size = (size + 2*count)*2; break;
|
||||
case I_32_RN_2805_5: size = (size + 2*count)*2; break;
|
||||
case I_32_RN_SM16825_5: size = (size + 2*count)*2*2; break;
|
||||
// I2S1 bus or paralell buses (individual 1x front and 1 DMA (3x or 4x pixel count) or common back DMA buffers)
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32C3
|
||||
case I_32_I2_NEO_3: size *= 4; break;
|
||||
case I_32_I2_NEO_4: size = (size + count)*4; break;
|
||||
case I_32_I2_400_3: size *= 4; break;
|
||||
case I_32_I2_TM1_4: size = (size + count)*4; break;
|
||||
case I_32_I2_TM2_3: size *= 4; break;
|
||||
case I_32_I2_UCS_3: size *= 2*4; break;
|
||||
case I_32_I2_UCS_4: size = (size + count)*2*4; break;
|
||||
case I_32_I2_APA106_3: size *= 4; break;
|
||||
case I_32_I2_FW6_5: size = (size + 2*count)*4; break;
|
||||
case I_32_I2_2805_5: size = (size + 2*count)*4; break;
|
||||
case I_32_I2_TM1914_3: size *= 4; break;
|
||||
case I_32_I2_SM16825_5: size = (size + 2*count)*2*4; break;
|
||||
#endif
|
||||
#endif
|
||||
// everything else uses 2 buffers
|
||||
default: size *= 2; break;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
+11
-6
@@ -207,7 +207,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
int s = 0; // bus iterator
|
||||
for (JsonObject elm : ins) {
|
||||
if (s >= WLED_MAX_BUSSES) break; // only counts physical buses
|
||||
uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255};
|
||||
uint8_t pins[5] = {255, 255, 255, 255, 255};
|
||||
JsonArray pinArr = elm["pin"];
|
||||
if (pinArr.size() == 0) continue;
|
||||
//pins[0] = pinArr[0];
|
||||
@@ -256,7 +256,9 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
static_assert(validatePinsAndTypes(defDataTypes, defNumTypes, defNumPins),
|
||||
"The default pin list defined in DATA_PINS does not match the pin requirements for the default buses defined in LED_TYPES");
|
||||
|
||||
unsigned mem = 0;
|
||||
unsigned pinsIndex = 0;
|
||||
unsigned digitalCount = 0;
|
||||
for (unsigned i = 0; i < WLED_MAX_BUSSES; i++) {
|
||||
uint8_t defPin[OUTPUT_MAX_PINS];
|
||||
// if we have less types than requested outputs and they do not align, use last known type to set current type
|
||||
@@ -319,9 +321,16 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
unsigned start = 0;
|
||||
// analog always has length 1
|
||||
if (Bus::isPWM(dataType) || Bus::isOnOff(dataType)) count = 1;
|
||||
busConfigs.emplace_back(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0);
|
||||
BusConfig defCfg = BusConfig(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0);
|
||||
mem += defCfg.memUsage(Bus::isDigital(dataType) && !Bus::is2Pin(dataType) ? digitalCount++ : 0);
|
||||
if (mem > MAX_LED_MEMORY) {
|
||||
DEBUG_PRINTF_P(PSTR("Out of LED memory! Bus %d (%d) #%u not created."), (int)dataType, (int)count, digitalCount);
|
||||
break;
|
||||
}
|
||||
busConfigs.push_back(defCfg); // use push_back for simplification as we needed defCfg to calculate memory usage
|
||||
doInitBusses = true; // finalization done in beginStrip()
|
||||
}
|
||||
DEBUG_PRINTF_P(PSTR("LED buffer size: %uB/%uB\n"), mem, BusManager::memUsage());
|
||||
}
|
||||
if (hw_led["rev"] && BusManager::getNumBusses()) BusManager::getBus(0)->setReversed(true); //set 0.11 global reversed setting for first bus
|
||||
|
||||
@@ -777,10 +786,6 @@ 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() {
|
||||
|
||||
+37
-34
@@ -8,7 +8,7 @@
|
||||
* color blend function, based on FastLED blend function
|
||||
* the calculation for each color is: result = (A*(amountOfA) + A + B*(amountOfB) + B) / 256 with amountOfA = 255 - amountOfB
|
||||
*/
|
||||
uint32_t WLED_O2_ATTR IRAM_ATTR color_blend(uint32_t color1, uint32_t color2, uint8_t blend) {
|
||||
uint32_t IRAM_ATTR color_blend(uint32_t color1, uint32_t color2, uint8_t blend) {
|
||||
// min / max blend checking is omitted: calls with 0 or 255 are rare, checking lowers overall performance
|
||||
const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; // mask for R and B channels or W and G if negated (poorman's SIMD; https://github.com/wled/WLED/pull/4568#discussion_r1986587221)
|
||||
uint32_t rb1 = color1 & TWO_CHANNEL_MASK; // extract R & B channels from color1
|
||||
@@ -25,37 +25,39 @@ uint32_t WLED_O2_ATTR IRAM_ATTR color_blend(uint32_t color1, uint32_t color2, ui
|
||||
* original idea: https://github.com/wled-dev/WLED/pull/2465 by https://github.com/Proto-molecule
|
||||
* speed optimisations by @dedehai
|
||||
*/
|
||||
uint32_t WLED_O2_ATTR color_add(uint32_t c1, uint32_t c2, bool preserveCR) //1212558 | 1212598 | 1212576 | 1212530
|
||||
uint32_t color_add(uint32_t c1, uint32_t c2, bool preserveCR)
|
||||
{
|
||||
if (c1 == BLACK) return c2;
|
||||
if (c2 == BLACK) return c1;
|
||||
const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; // mask for R and B channels or W and G if negated
|
||||
uint32_t rb = ( c1 & TWO_CHANNEL_MASK) + ( c2 & TWO_CHANNEL_MASK); // mask and add two colors at once
|
||||
uint32_t wg = ((c1>>8) & TWO_CHANNEL_MASK) + ((c2>>8) & TWO_CHANNEL_MASK);
|
||||
uint32_t r = rb >> 16; // extract single color values
|
||||
uint32_t b = rb & 0xFFFF;
|
||||
uint32_t w = wg >> 16;
|
||||
uint32_t g = wg & 0xFFFF;
|
||||
|
||||
if (preserveCR) { // preserve color ratios
|
||||
uint32_t overflow = (rb | wg) & 0x01000100; // detect overflow by checking 9th bit
|
||||
if (overflow) {
|
||||
uint32_t r = rb >> 16; // extract single color values
|
||||
uint32_t b = rb & 0xFFFF;
|
||||
uint32_t w = wg >> 16;
|
||||
uint32_t g = wg & 0xFFFF;
|
||||
uint32_t max = std::max(r,g);
|
||||
max = std::max(max,b);
|
||||
max = std::max(max,w);
|
||||
uint32_t max = std::max(r,g); // check for overflow note
|
||||
max = std::max(max,b);
|
||||
max = std::max(max,w);
|
||||
//unsigned max = r; // check for overflow note
|
||||
//max = g > max ? g : max;
|
||||
//max = b > max ? b : max;
|
||||
//max = w > max ? w : max;
|
||||
if (max > 255) {
|
||||
const uint32_t scale = (uint32_t(255)<<8) / max; // division of two 8bit (shifted) values does not work -> use bit shifts and multiplaction instead
|
||||
rb = ((rb * scale) >> 8) & TWO_CHANNEL_MASK;
|
||||
wg = (wg * scale) & ~TWO_CHANNEL_MASK;
|
||||
} else wg <<= 8; //shift white and green back to correct position
|
||||
return rb | wg;
|
||||
} else {
|
||||
// branchless per-channel saturation to 255 (extract 9th bit, subtract 1 if it is set, mask with 0xFF, input is 0xFF+0xFF=0x1EF max)
|
||||
// example with overflow: input: 0x01EF01EF -> (0x0100100 - 0x00010001) = 0x00FF00FF -> input|0x00FF00FF = 0x00FF00FF (saturate)
|
||||
// example without overflow: input: 0x007F007F -> (0x00000000 - 0x00000000) = 0x00000000 -> input|0x00000000 = input (no change)
|
||||
rb |= ((rb & 0x01000100) - ((rb >> 8) & 0x00010001)) & 0x00FF00FF;
|
||||
wg |= ((wg & 0x01000100) - ((wg >> 8) & 0x00010001)) & 0x00FF00FF;
|
||||
wg <<= 8; // restore WG position
|
||||
r = r > 255 ? 255 : r;
|
||||
g = g > 255 ? 255 : g;
|
||||
b = b > 255 ? 255 : b;
|
||||
w = w > 255 ? 255 : w;
|
||||
return RGBW32(r,g,b,w);
|
||||
}
|
||||
return rb | wg;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -72,10 +74,11 @@ uint32_t IRAM_ATTR color_fade(uint32_t c1, uint8_t amount, bool video) {
|
||||
// video scaling: make sure colors do not dim to zero if they started non-zero unless they distort the hue
|
||||
uint8_t r = byte(c1>>16), g = byte(c1>>8), b = byte(c1), w = byte(c1>>24); // extract r, g, b, w channels
|
||||
uint8_t maxc = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); // determine dominant channel for hue preservation
|
||||
addRemains = r && (r<<5) > maxc ? 0x00010000 : 0; // note: setting color preservation threshold too high results in flickering and
|
||||
addRemains |= g && (g<<5) > maxc ? 0x00000100 : 0; // jumping colors in low brightness gradients. Multiplying the color preserves
|
||||
addRemains |= b && (b<<5) > maxc ? 0x00000001 : 0; // better accuracy than dividing the maxc. Shifting by 5 is a good compromise
|
||||
addRemains |= w ? 0x01000000 : 0; // i.e. remove color channel if <13% of max
|
||||
uint8_t quarterMax = maxc >> 2; // note: using half of max results in color artefacts
|
||||
addRemains = r && r > quarterMax ? 0x00010000 : 0;
|
||||
addRemains |= g && g > quarterMax ? 0x00000100 : 0;
|
||||
addRemains |= b && b > quarterMax ? 0x00000001 : 0;
|
||||
addRemains |= w ? 0x01000000 : 0;
|
||||
}
|
||||
const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF;
|
||||
uint32_t rb = (((c1 & TWO_CHANNEL_MASK) * amount) >> 8) & TWO_CHANNEL_MASK; // scale red and blue
|
||||
@@ -89,15 +92,15 @@ uint32_t IRAM_ATTR color_fade(uint32_t c1, uint8_t amount, bool video) {
|
||||
note: inputs are 32bit to speed up the function, useful input value ranges are 0-255
|
||||
*/
|
||||
uint32_t adjust_color(uint32_t rgb, uint32_t hueShift, uint32_t lighten, uint32_t brighten) {
|
||||
if (rgb == 0 || hueShift + lighten + brighten == 0) return rgb; // black or no change
|
||||
CHSV32 hsv;
|
||||
rgb2hsv(rgb, hsv); //convert to HSV
|
||||
hsv.h += (hueShift << 8); // shift hue (hue is 16 bits)
|
||||
hsv.s = max((int32_t)0, (int32_t)hsv.s - (int32_t)lighten); // desaturate
|
||||
hsv.v = min((uint32_t)255, (uint32_t)hsv.v + brighten); // increase brightness
|
||||
uint32_t rgb_adjusted;
|
||||
hsv2rgb(hsv, rgb_adjusted); // convert back to RGB TODO: make this into 16 bit conversion
|
||||
return rgb_adjusted;
|
||||
if (rgb == 0 | hueShift + lighten + brighten == 0) return rgb; // black or no change
|
||||
CHSV32 hsv;
|
||||
rgb2hsv(rgb, hsv); //convert to HSV
|
||||
hsv.h += (hueShift << 8); // shift hue (hue is 16 bits)
|
||||
hsv.s = max((int32_t)0, (int32_t)hsv.s - (int32_t)lighten); // desaturate
|
||||
hsv.v = min((uint32_t)255, (uint32_t)hsv.v + brighten); // increase brightness
|
||||
uint32_t rgb_adjusted;
|
||||
hsv2rgb(hsv, rgb_adjusted); // convert back to RGB TODO: make this into 16 bit conversion
|
||||
return rgb_adjusted;
|
||||
}
|
||||
|
||||
// 1:1 replacement of fastled function optimized for ESP, slightly faster, more accurate and uses less flash (~ -200bytes)
|
||||
@@ -249,7 +252,7 @@ void loadCustomPalettes() {
|
||||
byte tcp[72]; //support gradient palettes with up to 18 entries
|
||||
CRGBPalette16 targetPalette;
|
||||
customPalettes.clear(); // start fresh
|
||||
for (int index = 0; index < WLED_MAX_CUSTOM_PALETTES; index++) {
|
||||
for (int index = 0; index<10; index++) {
|
||||
char fileName[32];
|
||||
sprintf_P(fileName, PSTR("/palette%d.json"), index);
|
||||
|
||||
@@ -594,13 +597,13 @@ void NeoGammaWLEDMethod::calcGammaTable(float gamma)
|
||||
gammaT_inv[0] = 0;
|
||||
}
|
||||
|
||||
uint8_t NeoGammaWLEDMethod::Correct(uint8_t value)
|
||||
uint8_t IRAM_ATTR_YN NeoGammaWLEDMethod::Correct(uint8_t value)
|
||||
{
|
||||
if (!gammaCorrectCol) return value;
|
||||
return gammaT[value];
|
||||
}
|
||||
|
||||
uint32_t NeoGammaWLEDMethod::inverseGamma32(uint32_t color)
|
||||
uint32_t IRAM_ATTR_YN NeoGammaWLEDMethod::inverseGamma32(uint32_t color)
|
||||
{
|
||||
if (!gammaCorrectCol) return color;
|
||||
uint8_t w = W(color);
|
||||
|
||||
+2
-13
@@ -117,14 +117,13 @@ class NeoGammaWLEDMethod {
|
||||
[[gnu::hot, gnu::pure]] uint32_t color_blend(uint32_t c1, uint32_t c2 , uint8_t blend);
|
||||
inline uint32_t color_blend16(uint32_t c1, uint32_t c2, uint16_t b) { return color_blend(c1, c2, b >> 8); };
|
||||
[[gnu::hot, gnu::pure]] uint32_t color_add(uint32_t, uint32_t, bool preserveCR = false);
|
||||
[[gnu::hot, gnu::pure]] uint32_t color_fade(uint32_t c1, uint8_t amount, bool video = false);
|
||||
[[gnu::hot, gnu::pure]] uint32_t adjust_color(uint32_t rgb, uint32_t hueShift, uint32_t lighten, uint32_t brighten);
|
||||
[[gnu::hot, gnu::pure]] uint32_t ColorFromPaletteWLED(const CRGBPalette16 &pal, unsigned index, uint8_t brightness = (uint8_t)255U, TBlendType blendType = LINEARBLEND);
|
||||
CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette);
|
||||
CRGBPalette16 generateRandomPalette();
|
||||
void loadCustomPalettes();
|
||||
extern std::vector<CRGBPalette16> customPalettes;
|
||||
inline size_t getPaletteCount() { return FIXED_PALETTE_COUNT + customPalettes.size(); }
|
||||
inline size_t getPaletteCount() { return 13 + GRADIENT_PALETTE_COUNT + customPalettes.size(); }
|
||||
inline uint32_t colorFromRgbw(byte* rgbw) { return uint32_t((byte(rgbw[3]) << 24) | (byte(rgbw[0]) << 16) | (byte(rgbw[1]) << 8) | (byte(rgbw[2]))); }
|
||||
void hsv2rgb(const CHSV32& hsv, uint32_t& rgb);
|
||||
void colorHStoRGB(uint16_t hue, byte sat, byte* rgb);
|
||||
@@ -140,16 +139,6 @@ uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb);
|
||||
uint16_t approximateKelvinFromRGB(uint32_t rgb);
|
||||
void setRandomColor(byte* rgb);
|
||||
|
||||
// fast scaling function for colors, performs color*scale/256 for all four channels, speed over accuracy
|
||||
// note: inlining uses less code than actual function calls
|
||||
static inline uint32_t fast_color_scale(const uint32_t c, const uint8_t scale) {
|
||||
uint32_t rb = (((c & 0x00FF00FF) * scale) >> 8) & 0x00FF00FF;
|
||||
uint32_t wg = (((c>>8) & 0x00FF00FF) * scale) & ~0x00FF00FF;
|
||||
return rb | wg;
|
||||
}
|
||||
[[gnu::hot, gnu::pure]] uint32_t color_fade(uint32_t c1, uint8_t amount, bool video = false);
|
||||
|
||||
// palettes
|
||||
extern const TProgmemRGBPalette16* const fastledPalettes[];
|
||||
extern const uint8_t* const gGradientPalettes[];
|
||||
#endif
|
||||
|
||||
|
||||
+1
-17
@@ -6,15 +6,7 @@
|
||||
* 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 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
|
||||
#else
|
||||
#define WLED_MAX_CUSTOM_PALETTES 10 // ESP8266: limit custom palettes to 10
|
||||
#endif
|
||||
#define GRADIENT_PALETTE_COUNT 59
|
||||
|
||||
// You can define custom product info from build flags.
|
||||
// This is useful to allow API consumer to identify what type of WLED version
|
||||
@@ -324,12 +316,6 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define TYPE_P9813 53
|
||||
#define TYPE_LPD6803 54
|
||||
#define TYPE_2PIN_MAX 63
|
||||
|
||||
#define TYPE_HUB75MATRIX_MIN 64
|
||||
#define TYPE_HUB75MATRIX_HS 65
|
||||
#define TYPE_HUB75MATRIX_QS 66
|
||||
#define TYPE_HUB75MATRIX_MAX 71
|
||||
|
||||
//Network types (master broadcast) (80-95)
|
||||
#define TYPE_VIRTUAL_MIN 80
|
||||
#define TYPE_NET_DDP_RGB 80 //network DDP RGB bus (master broadcast bus)
|
||||
@@ -682,6 +668,4 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define IRAM_ATTR_YN IRAM_ATTR
|
||||
#endif
|
||||
|
||||
#define WLED_O2_ATTR __attribute__((optimize("O2")))
|
||||
|
||||
#endif
|
||||
|
||||
@@ -116,62 +116,3 @@ function uploadFile(fileObj, name) {
|
||||
fileObj.value = '';
|
||||
return false;
|
||||
}
|
||||
// connect to WebSocket, use parent WS or open new
|
||||
function connectWs(onOpen) {
|
||||
try {
|
||||
if (top.window.ws && top.window.ws.readyState === WebSocket.OPEN) {
|
||||
if (onOpen) onOpen();
|
||||
return top.window.ws;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
getLoc(); // ensure globals (loc, locip, locproto) are up to date
|
||||
let url = loc ? getURL('/ws').replace("http","ws") : "ws://"+window.location.hostname+"/ws";
|
||||
let ws = new WebSocket(url);
|
||||
ws.binaryType = "arraybuffer";
|
||||
if (onOpen) { ws.onopen = onOpen; }
|
||||
try { top.window.ws = ws; } catch (e) {} // store in parent for reuse
|
||||
return ws;
|
||||
}
|
||||
|
||||
// send LED colors to ESP using WebSocket and DDP protocol (RGB)
|
||||
// ws: WebSocket object
|
||||
// start: start pixel index
|
||||
// len: number of pixels to send
|
||||
// colors: Uint8Array with RGB values (3*len bytes)
|
||||
function sendDDP(ws, start, len, colors) {
|
||||
if (!colors || colors.length < len * 3) return false; // not enough color data
|
||||
let maxDDPpx = 472; // must fit into one WebSocket frame of 1428 bytes, DDP header is 10+1 bytes -> 472 RGB pixels
|
||||
//let maxDDPpx = 172; // ESP8266: must fit into one WebSocket frame of 528 bytes -> 172 RGB pixels TODO: add support for ESP8266?
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
|
||||
// send in chunks of maxDDPpx
|
||||
for (let i = 0; i < len; i += maxDDPpx) {
|
||||
let cnt = Math.min(maxDDPpx, len - i);
|
||||
let off = (start + i) * 3; // DDP pixel offset in bytes
|
||||
let dLen = cnt * 3;
|
||||
let cOff = i * 3; // offset in color buffer
|
||||
let pkt = new Uint8Array(11 + dLen); // DDP header is 10 bytes, plus 1 byte for WLED websocket protocol indicator
|
||||
pkt[0] = 0x02; // DDP protocol indicator for WLED websocket. Note: below DDP protocol bytes are offset by 1
|
||||
pkt[1] = 0x40; // flags: 0x40 = no push, 0x41 = push (i.e. render), note: this is DDP protocol byte 0
|
||||
pkt[2] = 0x00; // reserved
|
||||
pkt[3] = 0x01; // 1 = RGB (currently only supported mode)
|
||||
pkt[4] = 0x01; // destination id (not used but 0x01 is default output)
|
||||
pkt[5] = (off >> 24) & 255; // DDP protocol 4-7 is offset
|
||||
pkt[6] = (off >> 16) & 255;
|
||||
pkt[7] = (off >> 8) & 255;
|
||||
pkt[8] = off & 255;
|
||||
pkt[9] = (dLen >> 8) & 255; // DDP protocol 8-9 is data length
|
||||
pkt[10] = dLen & 255;
|
||||
pkt.set(colors.subarray(cOff, cOff + dLen), 11);
|
||||
if(i + cnt >= len) {
|
||||
pkt[1] = 0x41; //if this is last packet, set the "push" flag to render the frame
|
||||
}
|
||||
try {
|
||||
ws.send(pkt.buffer);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+416
-316
@@ -11,8 +11,7 @@
|
||||
function gId(e) {return d.getElementById(e);}
|
||||
function cE(e) {return d.createElement(e);}
|
||||
</script>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
@@ -22,39 +21,39 @@
|
||||
margin: 0 10px;
|
||||
line-height: 0.5;
|
||||
}
|
||||
#pCont {
|
||||
#parent-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
#bCont {
|
||||
#bottomContainer {
|
||||
position: absolute;
|
||||
margin-top: 50px;
|
||||
}
|
||||
#gBox {
|
||||
#gradient-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.cMark, .cPickMark {
|
||||
.color-marker, .color-picker-marker {
|
||||
position: absolute;
|
||||
border-radius: 3px;
|
||||
background-color: rgb(192, 192, 192);
|
||||
border: 2px solid rgba(68, 68, 68, 0.5);
|
||||
z-index: 2;
|
||||
}
|
||||
.cMark {
|
||||
.color-marker {
|
||||
height: 30px;
|
||||
width: 7px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
touch-action: none;
|
||||
}
|
||||
.cPickMark {
|
||||
.color-picker-marker {
|
||||
height: 7px;
|
||||
width: 7px;
|
||||
top: 150%;
|
||||
}
|
||||
.dMark {
|
||||
.delete-marker {
|
||||
position: absolute;
|
||||
height: 5px;
|
||||
width: 5px;
|
||||
@@ -64,7 +63,7 @@
|
||||
top: 220%;
|
||||
z-index: 2;
|
||||
}
|
||||
.cPick {
|
||||
.color-picker {
|
||||
position: absolute;
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
@@ -74,20 +73,21 @@
|
||||
border-color: #111;
|
||||
background-color: #111;
|
||||
}
|
||||
.btnCls {
|
||||
.buttonclass {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
vertical-align: bottom;
|
||||
background-color: #111;
|
||||
}
|
||||
#bCont span {
|
||||
#bottomContainer span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#info {
|
||||
display: "";
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
@@ -104,76 +104,119 @@
|
||||
width: 800px;
|
||||
}
|
||||
}
|
||||
.pal {height: 20px;}
|
||||
.pGrads {flex: 1; height: 20px; border-radius: 3px;}
|
||||
.pMain {margin-top: 50px; width: 100%;}
|
||||
.pTop {height: fit-content; text-align: center; color: #fff; font-size: 14px; line-height: 1;}
|
||||
.pGradPar {display: flex; align-items: center; height: fit-content; margin-top: 10px; text-align: center; color: #fff; font-size: 12px; line-height: 1;}
|
||||
.btnsDiv {display: inline-flex; margin-left: 5px; width: 50px;}
|
||||
.sSpan, .eSpan {cursor: pointer;}
|
||||
h1 {font-size: 1.6rem;}
|
||||
.palette {
|
||||
height: 20px;
|
||||
}
|
||||
.paletteGradients {
|
||||
flex: 1;
|
||||
height: 20px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.palettesMain {
|
||||
margin-top: 50px;
|
||||
width: 100%;
|
||||
}
|
||||
.palTop {
|
||||
height: fit-content;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
line-height: 1;
|
||||
}
|
||||
.palGradientParent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: fit-content;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
line-height: 1;
|
||||
}
|
||||
.buttonsDiv {
|
||||
display: inline-flex;
|
||||
margin-left: 5px;
|
||||
width: 50px;
|
||||
}
|
||||
.sendSpan, .editSpan{
|
||||
cursor: pointer;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrap" class="wrap">
|
||||
<div style="display: flex; justify-content: center;">
|
||||
<h1 style="display: flex; align-items: center;">
|
||||
<svg style="width: 36px; height: 36px; margin-right: 6px;" viewBox="0 0 32 32">
|
||||
<rect style="fill: #03F" x="6" y="22" width="8" height="4"/>
|
||||
<rect style="fill: #03F" x="14" y="14" width="4" height="8"/>
|
||||
<rect style="fill: #03F" x="18" y="10" width="4" height="8"/>
|
||||
<rect style="fill: #03F" x="22" y="6" width="8" height="4"/>
|
||||
<svg style="width:36px;height:36px;margin-right:6px;" viewBox="0 0 32 32">
|
||||
<rect style="fill:#003FFF" x="6" y="22" width="8" height="4"/>
|
||||
<rect style="fill:#003FFF" x="14" y="14" width="4" height="8"/>
|
||||
<rect style="fill:#003FFF" x="18" y="10" width="4" height="8"/>
|
||||
<rect style="fill:#003FFF" x="22" y="6" width="8" height="4"/>
|
||||
</svg>
|
||||
<span id="head">WLED Palette Editor</span>
|
||||
<span id="head">WLED Custom Palette Editor</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div id="pCont"><div id="gBox"></div></div>
|
||||
<div id="parent-container">
|
||||
<div id="gradient-box"></div>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: center;">
|
||||
<div id="pals" class="pMain">
|
||||
<div id="distDiv" class="pTop"></div>
|
||||
<div id="memWarn" class="pTop" style="display:none; color:#ff6600; margin-bottom:8px; font-size:16px;">
|
||||
Warning: Adding many custom palettes might cause stability issues, create <a href="/settings/sec#backup" style="color:#ff9900">backups</a> before proceeding.</div>
|
||||
<div id="pTop" class="pTop">Custom palettes</div>
|
||||
<div id="palettes" class="palettesMain">
|
||||
<div id="distDiv" class="palTop"></div>
|
||||
<div id="palTop" class="palTop">
|
||||
Currently in use custom palettes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: center;">
|
||||
<div id="info">Click gradient to add. Box = color. Red = delete. Arrow = upload. Pencil = edit.</div>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: center;">
|
||||
<div id="sPals" class="pMain">
|
||||
<div id="spTop" class="pTop">Static palettes</div>
|
||||
<div id="info">
|
||||
Click on the gradient editor to add new color slider, then the colored box below the slider to change its color.
|
||||
Click the red box below indicator (and confirm) to delete.
|
||||
Once finished, click the arrow icon to upload into the desired slot.
|
||||
To edit existing palette, click the pencil icon.
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: center;">
|
||||
<div id="staticPalettes" class="palettesMain">
|
||||
<div id="statpalTop" class="palTop">
|
||||
Available static palettes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
<script type="text/javascript">
|
||||
// global vars
|
||||
var gBox = gId('gBox'); // gradientBox
|
||||
var cpalc = -1, cpalm = 10; // current palette count, max custom
|
||||
var pxCol = {}; // pixel color map
|
||||
var tCol = {}; // true color map
|
||||
var rect = gBox.getBoundingClientRect(); // bounding rect of gBox
|
||||
var gLen = rect.width; // gradientLength
|
||||
var mOffs = Math.round((gLen / 256) / 2) - 5; // marker offset
|
||||
var palArr = []; // paletteArray
|
||||
var palNm = []; // paletteName
|
||||
|
||||
//global variables
|
||||
var gradientBox = gId('gradient-box');
|
||||
var cpalc = -1;
|
||||
var pxCol = {};
|
||||
var tCol = {};
|
||||
var rect = gradientBox.getBoundingClientRect();
|
||||
var gradientLength = rect.width;
|
||||
var mOffs = Math.round((gradientLength / 256) / 2) - 5;
|
||||
var paletteArray = []; //Holds the palettes after load.
|
||||
var paletteName = []; // Holds the names of the palettes after load.
|
||||
var svgSave = '<svg style="width:25px;height:25px" viewBox="0 0 24 24"><path fill=#fff d="M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M7,12L12,17V14H16V10H12V7L7,12Z"/></svg>'
|
||||
var svgEdit = '<svg style="width:25px;height:25px" viewBox="0 0 24 24"><path fill=#fff d="M12,2C6.47,2 2,6.47 2,12C2,17.53 6.47,22 12,22C17.53,22 22,17.53 22,12C22,6.47 17.53,2 12,2M15.1,7.07C15.24,7.07 15.38,7.12 15.5,7.23L16.77,8.5C17,8.72 17,9.07 16.77,9.28L15.77,10.28L13.72,8.23L14.72,7.23C14.82,7.12 14.96,7.07 15.1,7.07M13.13,8.81L15.19,10.87L9.13,16.93H7.07V14.87L13.13,8.81Z"/></svg>'
|
||||
var svgDist = '<svg style="width:25px;height:25px" viewBox="0 0 24 24"><path fill=#fff d="M4 22H2V2H4V22M22 2H20V22H22V2M13.5 7H10.5V17H13.5V7Z"/></svg>'
|
||||
var svgTrash = '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="30px" height="30px"><path style="fill:#880000; stroke: #888888; stroke-width: -2px;stroke-dasharray: 0.1, 8;" d="M9,3V4H4V6H5V19A2,2 0 0,0 7,21H17A2,2 0 0,0 19,19V6H20V4H15V3H9M7,6H17V19H7V6M9,8V17H11V8H9M13,8V17H15V8H13Z"/></svg>'
|
||||
|
||||
const distDiv = gId("distDiv");
|
||||
distDiv.addEventListener('click', distrib);
|
||||
distDiv.setAttribute('title', 'Distribute equally');
|
||||
distDiv.addEventListener('click', distribute);
|
||||
distDiv.setAttribute('title', 'Distribute colors equally');
|
||||
distDiv.innerHTML = svgDist;
|
||||
|
||||
function recOf() {
|
||||
rect = gBox.getBoundingClientRect();
|
||||
gLen = rect.width;
|
||||
mOffs = Math.round((gLen / 256) / 2) - 5;
|
||||
rect = gradientBox.getBoundingClientRect();
|
||||
gradientLength = rect.width;
|
||||
mOffs = Math.round((gradientLength / 256) / 2) - 5;
|
||||
}
|
||||
|
||||
//Initiation
|
||||
@@ -181,220 +224,277 @@
|
||||
window.addEventListener('load', chkW);
|
||||
window.addEventListener('resize', chkW);
|
||||
|
||||
gBox.addEventListener("click", clikGrad);
|
||||
|
||||
gradientBox.addEventListener("click", clikOnGradient);
|
||||
|
||||
//Sets start and stop, mandatory
|
||||
addC(0);
|
||||
addC(255);
|
||||
|
||||
updGrad(); // updateGradient at startup
|
||||
updateGradient(); //Sets the gradient at startup
|
||||
|
||||
function clikGrad(e) { // clickOnGradient
|
||||
rmTrash(e); // removeTrashcan
|
||||
addC(Math.round((e.offsetX/gLen)*256));
|
||||
function clikOnGradient(e) {
|
||||
removeTrashcan(e);
|
||||
addC(Math.round((e.offsetX/gradientLength)*256));
|
||||
}
|
||||
|
||||
///////// Add a new color marker
|
||||
function addC(tPos, thisCol = '') {
|
||||
let pos = -1;
|
||||
let exist = false;
|
||||
const cMarks = gBox.querySelectorAll('.cMark'); // color markers
|
||||
///////// Add a new colorMarker
|
||||
function addC(truePos, thisColor = '') {
|
||||
let position = -1;
|
||||
let iExist = false;
|
||||
const colorMarkers = gradientBox.querySelectorAll('.color-marker');
|
||||
|
||||
cMarks.forEach((cm) => {
|
||||
if (cm.getAttribute("data-tpos") == tPos) exist = true;
|
||||
colorMarkers.forEach((colorMarker, i) => {
|
||||
if (colorMarker.getAttribute("data-truepos") == truePos) {
|
||||
iExist = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (cMarks.length > 17) exist = true;
|
||||
if (exist) return;
|
||||
|
||||
if (tPos > 0 && tPos < 255) {
|
||||
for (var i=1; i<=16 && pos<1; i++) {
|
||||
if (!gId("cMark"+i)) pos = i;
|
||||
if (colorMarkers.length > 17) iExist = true;
|
||||
if (iExist) return; // Exit the function early if iExist is true
|
||||
|
||||
if (truePos > 0 && truePos < 255) {
|
||||
//calculate first available > 0
|
||||
for (var i = 1; i <= 16 && position < 1; i++) {
|
||||
if (!gId("colorMarker"+i)) {
|
||||
position = i;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pos = tPos;
|
||||
} else{
|
||||
position = truePos;
|
||||
}
|
||||
if (thisCol == '') {
|
||||
thisCol = `#${(Math.random()*0xFFFFFF<<0).toString(16).padStart(6,'0')}`;
|
||||
if (thisColor == ''){
|
||||
thisColor = `#${(Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0')}`;// set random color as default
|
||||
}
|
||||
|
||||
const colorMarker = cE('span'); // create a marker for the color position
|
||||
colorMarker.className = 'color-marker';
|
||||
colorMarker.id = 'colorMarker' + position.toString();
|
||||
colorMarker.setAttribute("data-truepos", truePos); //Used to always have a true position no matter what screen or percentage we use
|
||||
colorMarker.setAttribute("data-truecol", thisColor); //Used to hold the color of the position in the gradient connected to a true position
|
||||
colorMarker.setAttribute("data-offset", mOffs);
|
||||
colorMarker.addEventListener('click', stopFurtherProp); //Added to prevent the gradient click to fire when covered by a marker
|
||||
colorMarker.style.left = `${Math.round((gradientLength / 256) * truePos)+mOffs}px`;
|
||||
|
||||
const colorPicker = cE('input');
|
||||
colorPicker.type = 'color';
|
||||
colorPicker.value = thisColor;
|
||||
colorPicker.className = 'color-picker';
|
||||
colorPicker.id = 'colorPicker' + position.toString();
|
||||
colorPicker.addEventListener('input', updateGradient);
|
||||
colorPicker.addEventListener('click',cpClk)
|
||||
|
||||
const colorPickerMarker = cE('span'); // create a marker for the color position
|
||||
colorPickerMarker.className = 'color-picker-marker';
|
||||
colorPickerMarker.id = 'colorPickerMarker' + position.toString();
|
||||
colorPickerMarker.addEventListener('click', colClk);
|
||||
colorPickerMarker.style.left = colorMarker.style.left;
|
||||
colorPicker.style.left = colorMarker.style.left;
|
||||
|
||||
const deleteMarker = cE('span'); // create a delete marker for the color position
|
||||
if (position > 0 && position < 255) {
|
||||
deleteMarker.className = 'delete-marker';
|
||||
deleteMarker.id = 'deleteMarker' + position.toString();
|
||||
deleteMarker.addEventListener('click', (e) => {
|
||||
deleteColor(e);
|
||||
});
|
||||
deleteMarker.style.left = colorMarker.style.left
|
||||
}
|
||||
|
||||
const cMark = cE('span'); // color marker
|
||||
cMark.className = 'cMark';
|
||||
cMark.id = 'cMark' + pos;
|
||||
cMark.setAttribute("data-tpos", tPos);
|
||||
cMark.setAttribute("data-tcol", thisCol);
|
||||
cMark.setAttribute("data-offset", mOffs);
|
||||
cMark.addEventListener('click', stopProp);
|
||||
cMark.style.left = `${Math.round((gLen/256)*tPos)+mOffs}px`;
|
||||
colorMarker.style.backgroundColor = colorPicker.value; // set marker color to match color picker
|
||||
colorPickerMarker.style.backgroundColor = colorPicker.value;
|
||||
|
||||
const cPick = cE('input'); // colorPicker
|
||||
cPick.type = 'color';
|
||||
cPick.value = thisCol;
|
||||
cPick.className = 'cPick';
|
||||
cPick.id = 'cPick' + pos;
|
||||
cPick.addEventListener('input', updGrad);
|
||||
cPick.addEventListener('click', cpClk);
|
||||
gradientBox.appendChild(colorPicker);
|
||||
gradientBox.appendChild(colorMarker);
|
||||
gradientBox.appendChild(colorPickerMarker);
|
||||
if (position != 0 && position != 255) gradientBox.appendChild(deleteMarker); // append the marker if not start or end
|
||||
//make markers slidable IF they are not the first or last slider
|
||||
if (position > 0 && position < 255) makeMeDrag(gId(colorMarker.id));
|
||||
|
||||
const cPM = cE('span'); // colorPickerMarker
|
||||
cPM.className = 'cPickMark';
|
||||
cPM.id = 'cPM' + pos;
|
||||
cPM.addEventListener('click', colClk);
|
||||
cPM.style.left = cMark.style.left;
|
||||
cPick.style.left = cMark.style.left;
|
||||
setTooltipMarker(gId(colorMarker.id));
|
||||
|
||||
if (pos > 0 && pos < 255) {
|
||||
const dMark = cE('span'); // deleteMarker
|
||||
dMark.className = 'dMark';
|
||||
dMark.id = 'dMark' + pos;
|
||||
dMark.addEventListener('click', (e) => { delCol(e); });
|
||||
dMark.style.left = cMark.style.left;
|
||||
gBox.appendChild(dMark);
|
||||
}
|
||||
|
||||
cMark.style.backgroundColor = cPick.value;
|
||||
cPM.style.backgroundColor = cPick.value;
|
||||
|
||||
gBox.appendChild(cPick);
|
||||
gBox.appendChild(cMark);
|
||||
gBox.appendChild(cPM);
|
||||
if (pos > 0 && pos < 255) mkDrag(gId(cMark.id)); // makeMeDrag
|
||||
|
||||
setTip(gId(cMark.id)); // setTooltipMarker
|
||||
updGrad();
|
||||
updateGradient();
|
||||
}
|
||||
|
||||
///////// Update Gradient
|
||||
function updGrad() { // updateGradient
|
||||
const cMarks = gBox.querySelectorAll('.cMark');
|
||||
function updateGradient() {
|
||||
const colorMarkers = gradientBox.querySelectorAll('.color-marker');
|
||||
pxCol = {};
|
||||
tCol = {};
|
||||
cMarks.forEach((cm) => {
|
||||
const cp = gId(cm.id.replace('cMark','cPick'));
|
||||
const col = cp.value;
|
||||
gId(cm.id.replace('cMark','cPM')).style.backgroundColor = col;
|
||||
cm.style.backgroundColor = col;
|
||||
cm.setAttribute("data-tcol", col);
|
||||
const tPos = cm.getAttribute("data-tpos");
|
||||
const gPos = Math.round((gLen/256)*tPos);
|
||||
pxCol[gPos] = col;
|
||||
tCol[tPos] = col;
|
||||
tCol = {}
|
||||
colorMarkers.forEach((colorMarker, index) => {
|
||||
const thisColorPicker = gId(colorMarkers[index].id.replace('colorMarker', 'colorPicker'));
|
||||
const colorToSet = thisColorPicker.value;
|
||||
gId(colorMarkers[index].id.replace('colorMarker', 'colorPickerMarker')).style.backgroundColor = colorToSet;
|
||||
colorMarkers[index].style.backgroundColor = colorToSet;
|
||||
colorMarkers[index].setAttribute("data-truecol", colorToSet);
|
||||
const tPos = colorMarkers[index].getAttribute("data-truepos");
|
||||
const gradientPos = Math.round((gradientLength / 256)*tPos);
|
||||
pxCol[gradientPos] = colorToSet;
|
||||
tCol[tPos] = colorToSet;
|
||||
});
|
||||
let gStr = 'linear-gradient(to right';
|
||||
Object.entries(pxCol).forEach(([p,c]) => {
|
||||
gStr += `, ${c} ${p}px`;
|
||||
gradientString = 'linear-gradient(to right';
|
||||
Object.entries(pxCol).forEach(([p, c]) => {
|
||||
gradientString += `, ${c} ${p}px`;
|
||||
});
|
||||
gStr += ')';
|
||||
gBox.style.background = gStr;
|
||||
gradientString += ')';
|
||||
gradientBox.style.background = gradientString;
|
||||
//gId("jsonstring").innerHTML = calcJSON();
|
||||
}
|
||||
|
||||
function stopProp(e) { e.stopPropagation(); }
|
||||
|
||||
function colClk(e) {
|
||||
rmTrash(e);
|
||||
function stopFurtherProp(e) {
|
||||
e.stopPropagation();
|
||||
const src = e.target || e.srcElement;
|
||||
let cp = gId(src.id.replace("cPM","cPick"));
|
||||
}
|
||||
|
||||
function colClk(e){
|
||||
removeTrashcan(e)
|
||||
e.stopPropagation();
|
||||
let cp = gId(e.srcElement.id.replace("Marker",""));
|
||||
cp.click();
|
||||
}
|
||||
|
||||
function cpClk(e) {
|
||||
rmTrash(e);
|
||||
removeTrashcan(event)
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// make element draggable
|
||||
function mkDrag(el) { // makeMeDrag
|
||||
var posNew=0, mPos=0;
|
||||
var rect=gBox.getBoundingClientRect();
|
||||
var maxX=rect.right, minX=rect.left, gLen=maxX-minX+1;
|
||||
//This neat little function makes any element draggable on the X-axis.
|
||||
//Just call: makeMeDrag(myElement); And you are good to go.
|
||||
function makeMeDrag(elmnt) {
|
||||
var posNew = 0, mousePos = 0, mouseOffset = 0
|
||||
|
||||
//Set these to whatever you want to limit your movement to
|
||||
var rect = gradientBox.getBoundingClientRect();
|
||||
var maxX = rect.right; // maximum X coordinate
|
||||
var minX = rect.left; // minimum X coordinate i.e. also offset from left of screen
|
||||
var gradientLength = maxX - minX + 1;
|
||||
|
||||
el.onmousedown=dragStart;
|
||||
el.ontouchstart=dragStart;
|
||||
elmnt.onmousedown = dragMouseDown;
|
||||
elmnt.ontouchstart = dragMouseDown;
|
||||
|
||||
function dragStart(e) {
|
||||
rmTrash(e);
|
||||
var isT=e.type.startsWith('touch');
|
||||
if (!isT) e.preventDefault();
|
||||
mPos=isT?e.touches[0].clientX:e.clientX;
|
||||
d.onmouseup=dragEnd; d.ontouchend=dragEnd; d.ontouchcancel=dragEnd;
|
||||
d.onmousemove=dragMove; d.ontouchmove=dragMove;
|
||||
function dragMouseDown(e) {
|
||||
removeTrashcan(event)
|
||||
e = e || window.event;
|
||||
var isTouch = e.type.startsWith('touch');
|
||||
if (!isTouch) e.preventDefault();
|
||||
// get the mouse cursor position at startup:
|
||||
mousePos = isTouch ? e.touches[0].clientX : e.clientX;
|
||||
d.onmouseup = closeDragElement;
|
||||
d.ontouchcancel = closeDragElement;
|
||||
d.ontouchend = closeDragElement;
|
||||
// call a function whenever the cursor moves:
|
||||
d.onmousemove = elementDrag;
|
||||
d.ontouchmove = elementDrag;
|
||||
}
|
||||
|
||||
function dragMove(e) {
|
||||
var isT=e.type.startsWith('touch');
|
||||
if (!isT) e.preventDefault();
|
||||
var cX=isT?e.touches[0].clientX:e.clientX;
|
||||
posNew=mPos-cX; mPos=cX;
|
||||
var mInG=mPos-(minX+1);
|
||||
var tPos=Math.round((mInG/gLen)*256);
|
||||
var old=el.getAttribute("data-tpos");
|
||||
if (tPos>0 && tPos<255 && old!=tPos) {
|
||||
el.style.left=(Math.round((gLen/256)*tPos)+mOffs)+"px";
|
||||
gId(el.id.replace('cMark','cPM')).style.left=el.style.left;
|
||||
gId(el.id.replace('cMark','dMark')).style.left=el.style.left;
|
||||
gId(el.id.replace('cMark','cPick')).style.left=el.style.left;
|
||||
el.setAttribute("data-tpos",tPos);
|
||||
setTip(el);
|
||||
updGrad();
|
||||
function elementDrag(e) {
|
||||
e = e || window.event;
|
||||
var isTouch = e.type.startsWith('touch');
|
||||
if (!isTouch) e.preventDefault();
|
||||
// calculate the new cursor position:
|
||||
var clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||||
posNew = mousePos - clientX;
|
||||
mousePos = clientX;
|
||||
mousePosInGradient = mousePos - (minX + 1)
|
||||
|
||||
truePos = Math.round((mousePosInGradient/gradientLength)*256);
|
||||
oldTruePos = elmnt.getAttribute("data-truepos");
|
||||
// set the element's new position if new position within min/max limits:
|
||||
if (truePos > 0 && truePos < 255 && oldTruePos != truePos) {
|
||||
if (truePos < 64) {
|
||||
thisOffset = 0;
|
||||
} else if (truePos > 192) {
|
||||
thisOffset = 7;
|
||||
} else {
|
||||
thisOffset=3;
|
||||
}
|
||||
elmnt.style.left = (Math.round((gradientLength/256)*truePos)+mOffs) + "px";
|
||||
gId(elmnt.id.replace('colorMarker', 'colorPickerMarker')).style.left = elmnt.style.left;
|
||||
gId(elmnt.id.replace('colorMarker', 'deleteMarker')).style.left = elmnt.style.left;
|
||||
gId(elmnt.id.replace('colorMarker', 'colorPicker')).style.left = elmnt.style.left;
|
||||
elmnt.setAttribute("data-truepos", truePos);
|
||||
setTooltipMarker(elmnt);
|
||||
updateGradient();
|
||||
}
|
||||
}
|
||||
|
||||
function dragEnd() {
|
||||
d.onmouseup=null; d.ontouchend=null; d.ontouchcancel=null;
|
||||
d.onmousemove=null; d.ontouchmove=null;
|
||||
function closeDragElement() {
|
||||
/* stop moving when mouse button is released:*/
|
||||
d.onmouseup = null;
|
||||
d.ontouchcancel = null;
|
||||
d.ontouchend = null;
|
||||
d.onmousemove = null;
|
||||
d.ontouchmove = null;
|
||||
}
|
||||
}
|
||||
|
||||
function setTip(el) { // setTooltipMarker
|
||||
el.setAttribute('title', `${el.getAttribute("data-tpos")} : ${el.getAttribute("data-tcol")}`);
|
||||
function setTooltipMarker(elmnt) {
|
||||
elmnt.setAttribute('title', `${elmnt.getAttribute("data-truepos")} : ${elmnt.getAttribute("data-truecol")}`)
|
||||
}
|
||||
|
||||
function delCol(e) { // deleteColor
|
||||
var trash=cE("div");
|
||||
var dM=e.target || e.srcElement;
|
||||
var cM=gId(dM.id.replace("d","c"));
|
||||
var cPM=gId(dM.id.replace("dMark","cPM"));
|
||||
var cP=gId(dM.id.replace("dMark","cPick"));
|
||||
var rX=dM.getBoundingClientRect().x-10;
|
||||
var rY=dM.getBoundingClientRect().y+13;
|
||||
|
||||
trash.id="trash";
|
||||
trash.innerHTML=svgTrash;
|
||||
trash.style.position="absolute";
|
||||
trash.style.left=rX+"px";
|
||||
trash.style.top=rY+"px";
|
||||
function deleteColor(e) {
|
||||
var trash = cE("div");
|
||||
thisDeleteMarker = e.srcElement;
|
||||
thisColorMarker = gId(thisDeleteMarker.id.replace("delete", "color"));
|
||||
thisColorPickerMarker = gId(thisDeleteMarker.id.replace("delete", "colorPicker"));
|
||||
thisColorPicker = gId(thisDeleteMarker.id.replace("deleteMarker", "colorPicker"));
|
||||
renderOffsetX = 15 - 5;
|
||||
renderX = e.srcElement.getBoundingClientRect().x - renderOffsetX;
|
||||
renderY = e.srcElement.getBoundingClientRect().y + 13;
|
||||
|
||||
trash.id = "trash";
|
||||
trash.innerHTML = svgTrash;
|
||||
trash.style.position = "absolute";
|
||||
trash.style.left = (renderX) + "px";
|
||||
trash.style.top = (renderY) + "px";
|
||||
d.body.appendChild(trash);
|
||||
|
||||
trash.addEventListener("click",()=>{
|
||||
trash.remove(); cM.remove(); cPM.remove(); cP.remove(); dM.remove();
|
||||
updGrad();
|
||||
|
||||
trash.addEventListener("click", (e)=>{
|
||||
trash.parentNode.removeChild(trash);
|
||||
thisDeleteMarker.parentNode.removeChild(thisDeleteMarker);
|
||||
thisColorPickerMarker.parentNode.removeChild(thisColorPickerMarker);
|
||||
thisColorMarker.parentNode.removeChild(thisColorMarker);
|
||||
thisColorPicker.parentNode.removeChild(thisColorPicker);
|
||||
updateGradient();
|
||||
});
|
||||
e.stopPropagation();
|
||||
d.addEventListener("click", rmTrash);
|
||||
// Add event listener to close the trashcan on outside click
|
||||
d.addEventListener("click", removeTrashcan);
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function rmTrash(e) { // removeTrashcan
|
||||
var t=gId("trash");
|
||||
if (t && e.target!=t) { t.remove(); d.removeEventListener("click", rmTrash);}
|
||||
function removeTrashcan(event) {
|
||||
trash = gId("trash");
|
||||
if (event.target != trash && trash) {
|
||||
trash.parentNode.removeChild(trash);
|
||||
d.removeEventListener("click", removeTrashcan);
|
||||
}
|
||||
}
|
||||
|
||||
function chkW() {
|
||||
const wrap=gId('wrap'); const head=gId('head');
|
||||
head.style.display=(wrap.offsetWidth<600)?'none':'inline';
|
||||
//Possibly add more code that recalculates the gradient... Massive job ;)
|
||||
const wrap = gId('wrap');
|
||||
const head = gId('head');
|
||||
if (wrap.offsetWidth < 600) {
|
||||
head.style.display = 'none';
|
||||
} else {
|
||||
head.style.display = 'inline';
|
||||
}
|
||||
}
|
||||
|
||||
function calcJSON() {
|
||||
let rStr='{"palette":[';
|
||||
Object.entries(tCol).forEach(([p,c],i)=>{
|
||||
if (i>0) rStr+=',';
|
||||
rStr+=`${p},"${c.slice(1)}"`;
|
||||
let rStr = '{"palette":['
|
||||
Object.entries(tCol).forEach(([p, c]) => {
|
||||
if (p > 0) rStr += ',';
|
||||
rStr += `${p},"${c.slice(1)}"`; // store in hex notation
|
||||
//rStr += `${p},${parseInt(c.slice(1, 3), 16)},${parseInt(c.slice(3, 5), 16)},${parseInt(c.slice(5, 7), 16)}`;
|
||||
});
|
||||
rStr+=']}';
|
||||
rStr += ']}';
|
||||
return rStr;
|
||||
}
|
||||
|
||||
function initUpload(i) {
|
||||
uploadJSON(calcJSON(), `/palette${i}.json`);
|
||||
function initiateUpload(idx) {
|
||||
const data = calcJSON();
|
||||
const fileName = `/palette${idx}.json`;
|
||||
uploadJSON(data, fileName);
|
||||
}
|
||||
|
||||
function uploadJSON(jsonString, fileName) {
|
||||
@@ -425,38 +525,41 @@
|
||||
}
|
||||
|
||||
async function getInfo() {
|
||||
getLoc();
|
||||
try {
|
||||
var arr = [];
|
||||
const resInfo = await fetch(getURL('/json/info')); // fetch info (includes cpalcount and cpalmax)
|
||||
const resPals = await fetch(getURL('/json/pal')); // fetch palette names
|
||||
const json = await resInfo.json();
|
||||
palNm = await resPals.json();
|
||||
cpalc = json.cpalcount;
|
||||
cpalm = json.cpalmax;
|
||||
fetchPals(cpalc-1);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
hst = location.host;
|
||||
if (hst.length > 0 ) {
|
||||
try {
|
||||
var arr = [];
|
||||
const responseInfo = await fetch('http://'+hst+'/json/info');
|
||||
const responsePalettes = await fetch('http://'+hst+'/json/palettes');
|
||||
const json = await responseInfo.json();
|
||||
paletteName = await responsePalettes.json();
|
||||
cpalc = json.cpalcount;
|
||||
fetchPalettes(cpalc-1);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
console.error('cannot identify host');
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPals(lastPal) {
|
||||
palArr.length = 0;
|
||||
async function fetchPalettes(lastPal) {
|
||||
paletteArray.length = 0;
|
||||
for (let i = 0; i <= lastPal; i++) {
|
||||
const url = getURL(`/palette${i}.json`);
|
||||
const url = `http://${hst}/palette${i}.json`;
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const json = await response.json();
|
||||
palArr.push(json);
|
||||
paletteArray.push(json);
|
||||
} catch (error) {
|
||||
cpalc--; //remove audio/dynamically generated palettes
|
||||
console.error(`Error fetching JSON from ${url}: `, error);
|
||||
}
|
||||
}
|
||||
//If there is room for more custom palettes, add an empty, gray slot
|
||||
if (palArr.length < cpalm) {
|
||||
if (paletteArray.length < 10) {
|
||||
//Room for one more :)
|
||||
palArr.push({"palette":[0,70,70,70,255,70,70,70]});
|
||||
paletteArray.push({"palette":[0,70,70,70,255,70,70,70]});
|
||||
}
|
||||
|
||||
//Get static palettes from localStorage and do some magic to reformat them into the same format as the palette JSONs
|
||||
@@ -466,12 +569,12 @@
|
||||
|
||||
const wledPalx = JSON.parse(localStorage.getItem('wledPalx'));
|
||||
if (!wledPalx) {
|
||||
alert("Palette cache missing from browser. Return to main page first.","Missing cache!")
|
||||
alert("The cache of palettes are missig from your browser. You should probably return to the main page and let it load properly for the palettes cache to regenerate before returning here.","Missing cached palettes!")
|
||||
} else {
|
||||
for (const key in wledPalx.p) {
|
||||
wledPalx.p[key].name = palNm[key];
|
||||
if (key > 255-cpalm) {
|
||||
delete wledPalx.p[key]; // remove custom palettes
|
||||
wledPalx.p[key].name = paletteName[key];
|
||||
if (key > 245) {
|
||||
delete wledPalx.p[key];
|
||||
continue;
|
||||
}
|
||||
const arr = wledPalx.p[key];
|
||||
@@ -507,133 +610,130 @@
|
||||
// Sort pArray by name
|
||||
pArray.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
palArr.push( ...pArray);
|
||||
paletteArray.push( ...pArray);
|
||||
}
|
||||
genPalDivs();
|
||||
generatePaletteDivs();
|
||||
}
|
||||
|
||||
function genPalDivs() {
|
||||
const palsDiv = gId("pals");
|
||||
const sPalsDiv = gId("sPals");
|
||||
const memWarn = gId("memWarn");
|
||||
const palDivs = Array.from(palsDiv.children).filter((child) => {
|
||||
return /^pal\d+$/.test(child.id); // match ids "pal" followed by one or more digits
|
||||
function generatePaletteDivs() {
|
||||
const palettesDiv = gId("palettes");
|
||||
const staticPalettesDiv = gId("staticPalettes");
|
||||
const paletteDivs = Array.from(palettesDiv.children).filter((child) => {
|
||||
return child.id.match(/^palette\d$/); // match only elements with id starting with "palette" followed by a single digit
|
||||
});
|
||||
|
||||
for (const div of palDivs) {
|
||||
palsDiv.removeChild(div); // remove each div that matches the above selector
|
||||
for (const div of paletteDivs) {
|
||||
palettesDiv.removeChild(div); // remove each div that matches the above selector
|
||||
}
|
||||
|
||||
memWarn.style.display = (cpalc >= 10) ? 'block' : 'none'; // Show/hide memory warning based on custom palette count
|
||||
for (let i = 0; i < paletteArray.length; i++) {
|
||||
const palette = paletteArray[i];
|
||||
const paletteDiv = cE("div");
|
||||
paletteDiv.id = `palette${i}`;
|
||||
paletteDiv.classList.add("palette");
|
||||
const thisKey = Object.keys(palette)[0];
|
||||
paletteDiv.dataset.colarray = JSON.stringify(palette[thisKey]);
|
||||
|
||||
for (let i = 0; i < palArr.length; i++) {
|
||||
const pal = palArr[i];
|
||||
const palDiv = cE("div");
|
||||
palDiv.id = `pal${i}`;
|
||||
palDiv.classList.add("pal");
|
||||
const thisKey = Object.keys(pal)[0];
|
||||
palDiv.dataset.colarray = JSON.stringify(pal[thisKey]);
|
||||
const gradientDiv = cE("div");
|
||||
gradientDiv.id = `paletteGradient${i}`
|
||||
const buttonsDiv = cE("div");
|
||||
buttonsDiv.id = `buttonsDiv${i}`;
|
||||
buttonsDiv.classList.add("buttonsDiv")
|
||||
|
||||
const gradDiv = cE("div");
|
||||
gradDiv.id = `pGrad${i}`
|
||||
const btnsDiv = cE("div");
|
||||
btnsDiv.id = `btns${i}`;
|
||||
btnsDiv.classList.add("btnsDiv")
|
||||
|
||||
const sSpan = cE("span");
|
||||
sSpan.id = `s${i}`;
|
||||
sSpan.onclick = function() {initUpload(i)};
|
||||
sSpan.setAttribute('title', `Send current editor to slot ${i}`); // perhaps Save instead of Send?
|
||||
sSpan.innerHTML = svgSave;
|
||||
sSpan.classList.add("sSpan")
|
||||
const eSpan = cE("span");
|
||||
eSpan.id = `e${i}`;
|
||||
eSpan.onclick = function() {loadEdit(i)};
|
||||
eSpan.setAttribute('title', `Copy slot ${i} to editor`);
|
||||
if (palArr[i].name) {
|
||||
eSpan.setAttribute('title', `Copy ${palArr[i].name} to editor`);
|
||||
const sendSpan = cE("span");
|
||||
sendSpan.id = `sendSpan${i}`;
|
||||
sendSpan.onclick = function() {initiateUpload(i)};
|
||||
sendSpan.setAttribute('title', `Send current editor to slot ${i}`); // perhaps Save instead of Send?
|
||||
sendSpan.innerHTML = svgSave;
|
||||
sendSpan.classList.add("sendSpan")
|
||||
const editSpan = cE("span");
|
||||
editSpan.id = `editSpan${i}`;
|
||||
editSpan.onclick = function() {loadForEdit(i)};
|
||||
editSpan.setAttribute('title', `Copy slot ${i} palette to editor`);
|
||||
if (paletteArray[i].name) {
|
||||
editSpan.setAttribute('title', `Copy ${paletteArray[i].name} palette to editor`);
|
||||
}
|
||||
eSpan.innerHTML = svgEdit;
|
||||
eSpan.classList.add("eSpan")
|
||||
editSpan.innerHTML = svgEdit;
|
||||
editSpan.classList.add("editSpan")
|
||||
|
||||
gradDiv.classList.add("pGrads");
|
||||
let gCols = "";
|
||||
gradientDiv.classList.add("paletteGradients");
|
||||
let gradientColors = "";
|
||||
|
||||
for (let j = 0; j < pal[thisKey].length; j += 2) {
|
||||
const pos = pal[thisKey][j];
|
||||
if (typeof(pal[thisKey][j+1]) === "string") {
|
||||
gCols += `#${pal[thisKey][j+1]} ${pos/255*100}%, `;
|
||||
for (let j = 0; j < palette[thisKey].length; j += 2) {
|
||||
const position = palette[thisKey][j];
|
||||
if (typeof(palette[thisKey][j+1]) === "string") {
|
||||
gradientColors += `#${palette[thisKey][j+1]} ${position/255*100}%, `;
|
||||
} else {
|
||||
const r = pal[thisKey][j + 1];
|
||||
const g = pal[thisKey][j + 2];
|
||||
const b = pal[thisKey][j + 3];
|
||||
gCols += `rgba(${r}, ${g}, ${b}, 1) ${pos/255*100}%, `;
|
||||
const red = palette[thisKey][j + 1];
|
||||
const green = palette[thisKey][j + 2];
|
||||
const blue = palette[thisKey][j + 3];
|
||||
gradientColors += `rgba(${red}, ${green}, ${blue}, 1) ${position/255*100}%, `;
|
||||
j += 2;
|
||||
}
|
||||
}
|
||||
|
||||
gCols = gCols.slice(0, -2); // remove the last comma and space
|
||||
gradDiv.style.backgroundImage = `linear-gradient(to right, ${gCols})`;
|
||||
palDiv.className = "pGradPar";
|
||||
gradientColors = gradientColors.slice(0, -2); // remove the last comma and space
|
||||
gradientDiv.style.backgroundImage = `linear-gradient(to right, ${gradientColors})`;
|
||||
paletteDiv.className = "palGradientParent";
|
||||
if (thisKey == "palette") {
|
||||
btnsDiv.appendChild(sSpan); //Only offer to send to custom palettes
|
||||
buttonsDiv.appendChild(sendSpan); //Only offer to send to custom palettes
|
||||
} else{
|
||||
eSpan.style.marginLeft = "25px";
|
||||
editSpan.style.marginLeft = "25px";
|
||||
}
|
||||
if (i!=cpalc) {
|
||||
btnsDiv.appendChild(eSpan); //Dont offer to edit the empty spot
|
||||
buttonsDiv.appendChild(editSpan); //Dont offer to edit the empty spot
|
||||
}
|
||||
palDiv.appendChild(gradDiv);
|
||||
palDiv.appendChild(btnsDiv);
|
||||
paletteDiv.appendChild(gradientDiv);
|
||||
paletteDiv.appendChild(buttonsDiv);
|
||||
if (thisKey == "palette") {
|
||||
palsDiv.appendChild(palDiv);
|
||||
palettesDiv.appendChild(paletteDiv);
|
||||
} else {
|
||||
sPalsDiv.appendChild(palDiv);
|
||||
staticPalettesDiv.appendChild(paletteDiv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadEdit(i) {
|
||||
d.querySelectorAll('input[id^="cPick"]').forEach((input) => {
|
||||
function loadForEdit(i) {
|
||||
d.querySelectorAll('input[id^="colorPicker"]').forEach((input) => {
|
||||
input.parentNode.removeChild(input);
|
||||
});
|
||||
d.querySelectorAll('span[id^="cMark"], span[id^="cPM"], span[id^="dMark"]').forEach((span) => {
|
||||
d.querySelectorAll('span[id^="colorMarker"], span[id^="colorPickerMarker"], span[id^="deleteMarker"]').forEach((span) => {
|
||||
span.parentNode.removeChild(span);
|
||||
});
|
||||
|
||||
let colArr = JSON.parse(gId(`pal${i}`).getAttribute("data-colarray"));
|
||||
|
||||
for (let j = 0; j < colArr.length; j += 2) {
|
||||
const pos = colArr[j];
|
||||
|
||||
let colArray = JSON.parse(gId(`palette${i}`).getAttribute("data-colarray"));
|
||||
|
||||
for (let j = 0; j < colArray.length; j += 2) {
|
||||
const position = colArray[j];
|
||||
let hex;
|
||||
if (typeof(colArr[j+1]) === "string") {
|
||||
hex = `#${colArr[j+1]}`;
|
||||
if (typeof(colArray[j+1]) === "string") {
|
||||
hex = `#${colArray[j+1]}`;
|
||||
} else {
|
||||
const r = colArr[j + 1];
|
||||
const g = colArr[j + 2];
|
||||
const b = colArr[j + 3];
|
||||
hex = rgbToHex(r, g, b);
|
||||
const red = colArray[j + 1];
|
||||
const green = colArray[j + 2];
|
||||
const blue = colArray[j + 3];
|
||||
hex = rgbToHex(red, green, blue);
|
||||
j += 2;
|
||||
}
|
||||
addC(pos, hex);
|
||||
addC(position, hex);
|
||||
window.scroll(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function distrib() {
|
||||
let cMarks = [...gBox.querySelectorAll('.cMark')];
|
||||
cMarks.sort((a, b) => a.getAttribute('data-tpos') - b.getAttribute('data-tpos'));
|
||||
cMarks = cMarks.slice(1, -1);
|
||||
const spacing = Math.round(256 / (cMarks.length + 1));
|
||||
function distribute() {
|
||||
let colorMarkers = [...gradientBox.querySelectorAll('.color-marker')];
|
||||
colorMarkers.sort((a, b) => a.getAttribute('data-truepos') - b.getAttribute('data-truepos'));
|
||||
colorMarkers = colorMarkers.slice(1, -1);
|
||||
const spacing = Math.round(256 / (colorMarkers.length + 1));
|
||||
|
||||
cMarks.forEach((e, i) => {
|
||||
const mId = e.id.match(/\d+/)[0];
|
||||
const tCol = e.getAttribute("data-tcol");
|
||||
gBox.removeChild(e);
|
||||
gBox.removeChild(gId(`cPick${mId}`));
|
||||
gBox.removeChild(gId(`cPM${mId}`));
|
||||
gBox.removeChild(gId(`dMark${mId}`));
|
||||
addC(spacing * (i + 1), tCol);
|
||||
colorMarkers.forEach((e, i) => {
|
||||
const markerId = e.id.match(/\d+/)[0];
|
||||
const trueCol = e.getAttribute("data-truecol");
|
||||
gradientBox.removeChild(e);
|
||||
gradientBox.removeChild(gId(`colorPicker${markerId}`));
|
||||
gradientBox.removeChild(gId(`colorPickerMarker${markerId}`));
|
||||
gradientBox.removeChild(gId(`deleteMarker${markerId}`));
|
||||
addC(spacing * (i + 1), trueCol);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+530
-535
File diff suppressed because it is too large
Load Diff
+11
-5
@@ -672,6 +672,7 @@ 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;
|
||||
@@ -1041,7 +1042,8 @@ function genPalPrevCss(id)
|
||||
}
|
||||
|
||||
var gradient = [];
|
||||
paletteData.forEach((e,j) => {
|
||||
for (let j = 0; j < paletteData.length; j++) {
|
||||
const e = paletteData[j];
|
||||
let r, g, b;
|
||||
let index = false;
|
||||
if (Array.isArray(e)) {
|
||||
@@ -1063,8 +1065,9 @@ function genPalPrevCss(id)
|
||||
if (index === false) {
|
||||
index = Math.round(j / paletteData.length * 100);
|
||||
}
|
||||
|
||||
gradient.push(`rgb(${r},${g},${b}) ${index}%`);
|
||||
});
|
||||
}
|
||||
|
||||
return `background: linear-gradient(to right,${gradient.join()});`;
|
||||
}
|
||||
@@ -3085,9 +3088,11 @@ function unify(e) { return e.changedTouches ? e.changedTouches[0] : e; }
|
||||
|
||||
function hasIroClass(classList)
|
||||
{
|
||||
let found = false;
|
||||
classList.forEach((e)=>{ if (e.startsWith('Iro')) found = true; });
|
||||
return found;
|
||||
for (var i = 0; i < classList.length; i++) {
|
||||
var element = classList[i];
|
||||
if (element.startsWith('Iro')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
//required by rangetouch.js
|
||||
function lock(e)
|
||||
@@ -3150,6 +3155,7 @@ 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%';
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
<script src="common.js"></script>
|
||||
<script>
|
||||
var d = document;
|
||||
var ws;
|
||||
var tmout = null;
|
||||
var c;
|
||||
@@ -62,14 +62,32 @@
|
||||
if (window.location.href.indexOf("?ws") == -1) {update(); return;}
|
||||
|
||||
// Initialize WebSocket connection
|
||||
ws = connectWs(function () {
|
||||
//console.info("Peek WS open");
|
||||
ws.send('{"lv":true}');
|
||||
});
|
||||
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.addEventListener('message', (e) => {
|
||||
try {
|
||||
if (toString.call(e.data) === '[object ArrayBuffer]') {
|
||||
let leds = new Uint8Array(e.data);
|
||||
let leds = new Uint8Array(event.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]})`);
|
||||
@@ -84,4 +102,4 @@
|
||||
<body onload="S()">
|
||||
<canvas id="canv"></canvas>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<script src="common.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canv"></canvas>
|
||||
@@ -27,13 +26,30 @@
|
||||
var ctx = c.getContext('2d');
|
||||
if (ctx) { // Access the rendering context
|
||||
// use parent WS or open new
|
||||
var ws = connectWs(()=>{
|
||||
ws.send('{"lv":true}');
|
||||
});
|
||||
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(e.data);
|
||||
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
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<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 src="common.js" async type="text/javascript"></script>
|
||||
<script>
|
||||
function S() {
|
||||
getLoc();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<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>
|
||||
<script src="common.js" async type="text/javascript"></script>
|
||||
<script>
|
||||
var maxPanels=64;
|
||||
var ctx = null;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<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>
|
||||
<script src="common.js" async type="text/javascript"></script>
|
||||
<script>
|
||||
function HW(){window.open("https://kno.wled.ge/interfaces/dmx-output/");}
|
||||
function GCH(num) {
|
||||
|
||||
+70
-103
@@ -4,7 +4,7 @@
|
||||
<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>
|
||||
<script src="common.js" async type="text/javascript"></script>
|
||||
<script>
|
||||
var maxB=1,maxD=1,maxA=1,maxV=0,maxM=4000,maxPB=2048,maxL=1664,maxCO=5; //maximum bytes for LED allocation: 4kB for 8266, 32kB for 32
|
||||
var customStarts=false,startsDirty=[];
|
||||
@@ -17,7 +17,6 @@
|
||||
function isD2P(t) { return gT(t).t === "2P"; } // is digital 2 pin type
|
||||
function isNet(t) { return gT(t).t === "N"; } // is network type
|
||||
function isVir(t) { return gT(t).t === "V" || isNet(t); } // is virtual type
|
||||
function isHub75(t){ return gT(t).t === "H"; } // is HUB75 type
|
||||
function hasRGB(t) { return !!(gT(t).c & 0x01); } // has RGB
|
||||
function hasW(t) { return !!(gT(t).c & 0x02); } // has white channel
|
||||
function hasCCT(t) { return !!(gT(t).c & 0x04); } // is white CCT enabled
|
||||
@@ -25,7 +24,6 @@
|
||||
function mustR(t) { return !!(gT(t).c & 0x20); } // Off refresh is mandatory
|
||||
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)
|
||||
function S() {
|
||||
getLoc();
|
||||
loadJS(getURL('/settings/s.js?p=2'), false, ()=>{
|
||||
@@ -45,13 +43,13 @@
|
||||
}
|
||||
function bLimits(b,v,p,m,l,o=5,d=2,a=6) {
|
||||
maxB = b; // maxB - max physical (analog + digital) buses: 32 - ESP32, 14 - S3/S2, 6 - C3, 4 - 8266
|
||||
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
|
||||
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
|
||||
maxM = m; // maxM - max LED memory
|
||||
maxL = l; // maxL - max LEDs (will serve to determine ESP >1664 == ESP32)
|
||||
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
|
||||
}
|
||||
function is8266() { return maxA == 5 && maxD == 3; } // NOTE: see const.h
|
||||
function is32() { return maxA == 16 && maxD == 16; } // NOTE: see const.h
|
||||
@@ -63,44 +61,45 @@
|
||||
var nList = d.Sf.querySelectorAll("#mLC input[name^=L]");
|
||||
nList.forEach((LC,i)=>{
|
||||
if (!ok) return; // prevent iteration after conflict
|
||||
let nm = LC.name.substring(0,2); // field name : /L./
|
||||
if (nm.search(/^L[0-4]/) < 0) return; // not pin fields
|
||||
let n = LC.name.substring(2,3); // bus number (0-Z)
|
||||
let nm = LC.name.substring(0,2);
|
||||
let n = LC.name.substring(2);
|
||||
let t = parseInt(d.Sf["LT"+n].value, 10); // LED type SELECT
|
||||
if(isHub75(t)) {
|
||||
return;
|
||||
}
|
||||
// ignore IP address
|
||||
if (isNet(t)) return;
|
||||
if (nm=="L0" || nm=="L1" || nm=="L2" || nm=="L3") {
|
||||
if (isNet(t)) return;
|
||||
}
|
||||
//check for pin conflicts
|
||||
if (LC.value!="" && LC.value!="-1") {
|
||||
let p = d.rsvd.concat(d.um_p); // used pin array
|
||||
d.Sf.querySelectorAll("select.pin").forEach((e)=>{if(e.value>-1)p.push(parseInt(e.value));}) // buttons, IR & relay
|
||||
if (p.some((e)=>e==parseInt(LC.value))) {
|
||||
alert(`Sorry, pins ${JSON.stringify(p)} can't be used.`);
|
||||
LC.value="";
|
||||
LC.focus();
|
||||
ok = false;
|
||||
return;
|
||||
} else if (d.ro_gpio.some((e)=>e==parseInt(LC.value))) {
|
||||
alert(`Sorry, pins ${JSON.stringify(d.ro_gpio)} are input only.`);
|
||||
LC.value="";
|
||||
LC.focus();
|
||||
ok = false;
|
||||
return;
|
||||
}
|
||||
for (j=i+1; j<nList.length; j++) {
|
||||
let n2 = nList[j].name.substring(0,2); // field name /L./
|
||||
if (n2.search(/^L[0-4]/) == 0) { // pin fields
|
||||
let m = nList[j].name.substring(2,3); // bus number (0-Z)
|
||||
let t2 = parseInt(gN("LT"+m).value, 10);
|
||||
if (isVir(t2)) continue;
|
||||
if (nList[j].value!="" && nList[i].value==nList[j].value) {
|
||||
alert(`Pin conflict between ${LC.name}/${nList[j].name}!`);
|
||||
nList[j].value="";
|
||||
nList[j].focus();
|
||||
ok = false;
|
||||
return;
|
||||
if (nm=="L0" || nm=="L1" || nm=="L2" || nm=="L3" || nm=="L4")
|
||||
if (LC.value!="" && LC.value!="-1") {
|
||||
let p = d.rsvd.concat(d.um_p); // used pin array
|
||||
d.Sf.querySelectorAll("select.pin").forEach((e)=>{if(e.value>-1)p.push(parseInt(e.value));}) // buttons, IR & relay
|
||||
if (p.some((e)=>e==parseInt(LC.value))) {
|
||||
alert(`Sorry, pins ${JSON.stringify(p)} can't be used.`);
|
||||
LC.value="";
|
||||
LC.focus();
|
||||
ok = false;
|
||||
return;
|
||||
} else if (d.ro_gpio.some((e)=>e==parseInt(LC.value))) {
|
||||
alert(`Sorry, pins ${JSON.stringify(d.ro_gpio)} are input only.`);
|
||||
LC.value="";
|
||||
LC.focus();
|
||||
ok = false;
|
||||
return;
|
||||
}
|
||||
for (j=i+1; j<nList.length; j++) {
|
||||
let n2 = nList[j].name.substring(0,2);
|
||||
if (n2=="L0" || n2=="L1" || n2=="L2" || n2=="L3" || n2=="L4") {
|
||||
if (n2.substring(0,1)==="L") {
|
||||
var m = nList[j].name.substring(2);
|
||||
var t2 = parseInt(d.Sf["LT"+m].value, 10);
|
||||
if (t2>=80) continue;
|
||||
}
|
||||
if (nList[j].value!="" && nList[i].value==nList[j].value) {
|
||||
alert(`Pin conflict between ${LC.name}/${nList[j].name}!`);
|
||||
nList[j].value="";
|
||||
nList[j].focus();
|
||||
ok = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,15 +111,11 @@
|
||||
d.Sf.data.value = '';
|
||||
e.preventDefault();
|
||||
if (!pinsOK()) {e.stopPropagation();return false;} // Prevent form submission and contact with server
|
||||
if (bquot > 200) {var msg = "Too many LEDs! Can't handle that!"; alert(msg); e.stopPropagation(); return false;}
|
||||
else {
|
||||
if (bquot > 80) {var msg = "Memory usage is high, reboot recommended!\n\rSet transitions to 0 to save memory.";
|
||||
if (bquot > 100) msg += "\n\rToo many LEDs for me to handle properly!"; if (maxM < 10000) msg += "\n\rConsider using an ESP32."; alert(msg);}
|
||||
if (!d.Sf.ABL.checked || d.Sf.PPL.checked) d.Sf.MA.value = 0; // submit 0 as ABL (PPL will handle it)
|
||||
if (d.Sf.checkValidity()) {
|
||||
d.Sf.querySelectorAll("#mLC select[name^=LT]").forEach((s)=>{s.disabled=false;}); // just in case
|
||||
d.Sf.submit(); //https://stackoverflow.com/q/37323914
|
||||
}
|
||||
if (bquot > 100) {var msg = "Too many LEDs for me to handle!"; if (maxM < 10000) msg += "\n\rConsider using an ESP32."; alert(msg);}
|
||||
if (!d.Sf.ABL.checked || d.Sf.PPL.checked) d.Sf.MA.value = 0; // submit 0 as ABL (PPL will handle it)
|
||||
if (d.Sf.checkValidity()) {
|
||||
d.Sf.querySelectorAll("#mLC select[name^=LT]").forEach((s)=>{s.disabled=false;}); // just in case
|
||||
d.Sf.submit(); //https://stackoverflow.com/q/37323914
|
||||
}
|
||||
}
|
||||
function enABL()
|
||||
@@ -198,27 +193,20 @@
|
||||
//returns mem usage
|
||||
function getMem(t, n) {
|
||||
if (isAna(t)) return 5; // analog
|
||||
let len = parseInt(d.Sf["LC"+n].value);
|
||||
len += parseInt(d.Sf["SL"+n].value); // skipped LEDs are allocated too
|
||||
let dbl = 0;
|
||||
let pbfr = len * 8; // pixel buffers: global buffer + segment buffer (at least one segment buffer is required)
|
||||
let len = parseInt(d.getElementsByName("LC"+n)[0].value);
|
||||
len += parseInt(d.getElementsByName("SL"+n)[0].value); // skipped LEDs are allocated too
|
||||
let ch = 3*hasRGB(t) + hasW(t) + hasCCT(t);
|
||||
let mul = 1;
|
||||
if (isDig(t)) {
|
||||
if (is16b(t)) len *= 2; // 16 bit LEDs
|
||||
if (is8266() && d.Sf["L0"+n].value == 3) { //8266 DMA uses 5x the mem
|
||||
if (maxM < 10000 && d.getElementsByName("L0"+n)[0].value == 3) { //8266 DMA uses 5x the mem
|
||||
mul = 5;
|
||||
}
|
||||
let parallelI2S = d.Sf.PR.checked && (is32() || isS2() || isS3()) && !isD2P(t);
|
||||
if (isC3() || (isS3() && !parallelI2S)) {
|
||||
mul = 2; // ESP32 RMT uses double buffer
|
||||
} else if ((is32() || isS2() || isS3()) && toNum(n) > (parallelI2S ? 7 : 0)) {
|
||||
mul = 2; // ESP32 RMT uses double buffer
|
||||
} else if ((parallelI2S && toNum(n) < 8) || (n == 0 && is32())) { // I2S uses extra DMA buffer
|
||||
dbl = len * ch * 3; // DMA buffer for parallel I2S (TODO: ony the bus with largst LED count should be used)
|
||||
if (maxM >= 10000) { //ESP32 RMT uses double buffer?
|
||||
mul = 2;
|
||||
}
|
||||
}
|
||||
return len * ch * mul + dbl + pbfr;
|
||||
return len * ch * mul + len * 4; // add 4 bytes per LED for segment buffer (TODO: how to account for global buffer?)
|
||||
}
|
||||
|
||||
function UI(change=false)
|
||||
@@ -248,15 +236,12 @@
|
||||
case 'V': // virtual/non-GPIO based
|
||||
p0d = "Config:"
|
||||
break;
|
||||
case 'H': // HUB75
|
||||
p0d = "Panel size (width x height), Panel count:"
|
||||
break;
|
||||
}
|
||||
gId("p0d"+n).innerText = p0d;
|
||||
gId("p1d"+n).innerText = p1d;
|
||||
gId("off"+n).innerText = off;
|
||||
// secondary pins show/hide (type string length is equivalent to number of pins used; except for network and on/off)
|
||||
let pins = Math.max(gT(t).t.length,1) + 3*isNet(t) + 2*isHub75(t); // fixes network pins to 4
|
||||
let pins = Math.max(gT(t).t.length,1) + 3*isNet(t); // fixes network pins to 4
|
||||
for (let p=1; p<5; p++) {
|
||||
var LK = d.Sf["L"+p+n];
|
||||
if (!LK) continue;
|
||||
@@ -272,7 +257,7 @@
|
||||
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 n = s.name.substring(2);
|
||||
var t = parseInt(s.value);
|
||||
memu += getMem(t, n); // calc memory
|
||||
dC += (isDig(t) && !isD2P(t));
|
||||
@@ -286,13 +271,13 @@
|
||||
}
|
||||
gId("rf"+n).onclick = mustR(t) ? (()=>{return false}) : (()=>{}); // prevent change change of "Refresh" checkmark when mandatory
|
||||
gRGBW |= hasW(t); // RGBW checkbox
|
||||
gId("co"+n).style.display = (isVir(t) || isAna(t) || isHub75(t)) ? "none":"inline"; // hide color order for PWM
|
||||
gId("co"+n).style.display = (isVir(t) || isAna(t)) ? "none":"inline"; // hide color order for PWM
|
||||
gId("dig"+n+"w").style.display = (isDig(t) && hasW(t)) ? "inline":"none"; // show swap channels dropdown
|
||||
gId("dig"+n+"w").querySelector("[data-opt=CCT]").disabled = !hasCCT(t); // disable WW/CW swapping
|
||||
if (!(isDig(t) && hasW(t))) d.Sf["WO"+n].value = 0; // reset swapping
|
||||
gId("dig"+n+"c").style.display = (isAna(t) || isHub75(t)) ? "none":"inline"; // hide count for analog
|
||||
gId("dig"+n+"c").style.display = (isAna(t)) ? "none":"inline"; // hide count for analog
|
||||
gId("dig"+n+"r").style.display = (isVir(t)) ? "none":"inline"; // hide reversed for virtual
|
||||
gId("dig"+n+"s").style.display = (isVir(t) || isAna(t) || isHub75(t)) ? "none":"inline"; // hide skip 1st for virtual & analog
|
||||
gId("dig"+n+"s").style.display = (isVir(t) || isAna(t)) ? "none":"inline"; // hide skip 1st for virtual & analog
|
||||
gId("dig"+n+"f").style.display = (isDig(t) || (isPWM(t) && maxL>2048)) ? "inline":"none"; // hide refresh (PWM hijacks reffresh for dithering on ESP32)
|
||||
gId("dig"+n+"a").style.display = (hasW(t)) ? "inline":"none"; // auto calculate white
|
||||
gId("dig"+n+"l").style.display = (isD2P(t) || isPWM(t)) ? "inline":"none"; // bus clock speed / PWM speed (relative) (not On/Off)
|
||||
@@ -311,8 +296,8 @@
|
||||
let sameType = 0;
|
||||
var nList = d.Sf.querySelectorAll("#mLC input[name^=L]");
|
||||
nList.forEach((LC,i)=>{
|
||||
let nm = LC.name.substring(0,2); // field name : /L./
|
||||
let n = LC.name.substring(2,3); // bus number (0-Z)
|
||||
let nm = LC.name.substring(0,2); // field name
|
||||
let n = LC.name.substring(2); // bus number
|
||||
let t = parseInt(d.Sf["LT"+n].value); // LED type SELECT
|
||||
if (isDig(t)) {
|
||||
if (sameType == 0) sameType = t; // first bus type
|
||||
@@ -321,7 +306,7 @@
|
||||
// do we have a led count field
|
||||
if (nm=="LC") {
|
||||
let c = parseInt(LC.value,10); //get LED count
|
||||
if (!customStarts || !startsDirty[toNum(n)]) gId("ls"+n).value = sLC; //update start value
|
||||
if (!customStarts || !startsDirty[n]) gId("ls"+n).value = sLC; //update start value
|
||||
gId("ls"+n).disabled = !customStarts; //enable/disable field editing
|
||||
if (c) {
|
||||
let s = parseInt(gId("ls"+n).value); //start value
|
||||
@@ -339,16 +324,10 @@
|
||||
}
|
||||
// do we have led pins for digital leds
|
||||
if (nm=="L0" || nm=="L1") {
|
||||
if (!isHub75(t)) {
|
||||
d.Sf["LC"+n].max = maxPB; // update max led count value
|
||||
}
|
||||
else {
|
||||
d.Sf["LC"+n].min = undefined;
|
||||
d.Sf["LC"+n].max = undefined;
|
||||
}
|
||||
d.Sf["LC"+n].max = maxPB; // update max led count value
|
||||
}
|
||||
// ignore IP address (stored in pins for virtual busses)
|
||||
if (nm.search(/^L[0-3]/) == 0) { // pin fields
|
||||
if (nm=="L0" || nm=="L1" || nm=="L2" || nm=="L3") {
|
||||
if (isVir(t)) {
|
||||
LC.max = 255;
|
||||
LC.min = 0;
|
||||
@@ -359,39 +338,27 @@
|
||||
LC.min = -1;
|
||||
}
|
||||
}
|
||||
if (isHub75(t) && (nm=="L0" || nm=="L1")) {
|
||||
// Matrix width and height
|
||||
LC.max = 128;
|
||||
LC.min = 16;
|
||||
LC.style.color="#fff";
|
||||
return; // do not check conflicts
|
||||
}
|
||||
else if (isHub75(t) && nm=="L2") {
|
||||
// Chain length aka Panel Count
|
||||
LC.max = 4;
|
||||
LC.min = 1;
|
||||
LC.style.color="#fff";
|
||||
return; // do not check conflicts
|
||||
}
|
||||
// check for pin conflicts & color fields
|
||||
if (nm.search(/^L[0-4]/) == 0) // pin fields
|
||||
if (nm=="L0" || nm=="L1" || nm=="L2" || nm=="L3" || nm=="L4")
|
||||
if (LC.value!="" && LC.value!="-1") {
|
||||
let p = d.rsvd.concat(d.um_p); // used pin array
|
||||
d.Sf.querySelectorAll("select.pin").forEach((e)=>{if(e.value>-1)p.push(parseInt(e.value));}) // buttons, IR & relay
|
||||
for (j=0; j<nList.length; j++) {
|
||||
if (i==j) continue;
|
||||
let n2 = nList[j].name.substring(0,2); // field name : /L./
|
||||
if (n2.search(/^L[0-4]/) == 0) { // pin fields
|
||||
let m = nList[j].name.substring(2,3); // bus number (0-Z)
|
||||
let t2 = parseInt(gN("LT"+m).value, 10);
|
||||
if (isVir(t2)) continue;
|
||||
let n2 = nList[j].name.substring(0,2);
|
||||
if (n2=="L0" || n2=="L1" || n2=="L2" || n2=="L3" || n2=="L4") {
|
||||
if (n2.substring(0,1)==="L") {
|
||||
let m = nList[j].name.substring(2);
|
||||
let t2 = parseInt(d.Sf["LT"+m].value, 10);
|
||||
if (isVir(t2)) continue;
|
||||
}
|
||||
if (nList[j].value!="" && nList[j].value!="-1") p.push(parseInt(nList[j].value,10)); // add current pin
|
||||
}
|
||||
}
|
||||
// now check for conflicts
|
||||
if (p.some((e)=>e==parseInt(LC.value))) LC.style.color = "red";
|
||||
else LC.style.color = d.ro_gpio.some((e)=>e==parseInt(LC.value)) ? "orange" : "#fff";
|
||||
} else LC.style.color = "#fff";
|
||||
}
|
||||
});
|
||||
if (is32() || isS2() || isS3()) {
|
||||
if (maxLC > 600 || dC < 2 || sameType <= 0) {
|
||||
@@ -413,7 +380,7 @@
|
||||
gId('dbar').style.background = `linear-gradient(90deg, ${bquot > 60 ? (bquot > 90 ? "red":"orange"):"#ccc"} 0 ${bquot}%, #444 ${bquot}% 100%)`;
|
||||
gId('ledwarning').style.display = (maxLC > Math.min(maxPB,800) || bquot > 80) ? 'inline':'none';
|
||||
gId('ledwarning').style.color = (maxLC > Math.max(maxPB,800) || bquot > 100) ? 'red':'orange';
|
||||
gId('wreason').innerHTML = (bquot > 80) ? "80% of max LED memory" +(bquot>100 ? ` (<b>WARNING: using over ${maxM}B!</b>)` : "") : "800 LEDs per output";
|
||||
gId('wreason').innerHTML = (bquot > 80) ? "80% of max. LED memory" +(bquot>100 ? ` (<b>ERROR: Using over ${maxM}B!</b>)` : "") : "800 LEDs per output";
|
||||
// calculate power
|
||||
gId('ampwarning').style.display = (parseInt(d.Sf.MA.value,10) > 7200) ? 'inline':'none';
|
||||
var val = Math.ceil((100 + busMA)/500)/2;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<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>
|
||||
<script src="common.js" async type="text/javascript"></script>
|
||||
<script>
|
||||
function U() { window.open(getURL("/update"),"_self"); }
|
||||
function checkNum(o) {
|
||||
@@ -17,6 +17,73 @@
|
||||
function setBckFilename(x) {
|
||||
x.setAttribute("download","wled_" + x.getAttribute("download") + (sd=="WLED"?"":("_" +sd)));
|
||||
}
|
||||
function userBackup(type) {
|
||||
if (!confirm("Create internal backup for " + type + "? This will overwrite any existing backup.")) return;
|
||||
var btn = gId("ubk" + type.charAt(0).toUpperCase() + type.slice(1));
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = "Creating...";
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', getURL('/backup/' + type), true);
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = "Create " + type.charAt(0).toUpperCase() + type.slice(1) + " Backup";
|
||||
if (xhr.status === 200) {
|
||||
showToast(xhr.responseText, false);
|
||||
updateBackupButtons();
|
||||
} else {
|
||||
showToast("Backup failed: " + xhr.responseText, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
function userRestore(type) {
|
||||
var message = "Restore " + type + " from internal backup? This will overwrite current " + type + ".";
|
||||
if (type === 'config') message += " Device will reboot after restore.";
|
||||
if (!confirm(message)) return;
|
||||
var btn = gId("ures" + type.charAt(0).toUpperCase() + type.slice(1));
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = "Restoring...";
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', getURL('/restore/' + type), true);
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status === 200) {
|
||||
showToast(xhr.responseText, false);
|
||||
if (type === 'config') {
|
||||
setTimeout(function() { window.location.href = "/"; }, 3000);
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = "Restore " + type.charAt(0).toUpperCase() + type.slice(1);
|
||||
}
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = "Restore " + type.charAt(0).toUpperCase() + type.slice(1);
|
||||
showToast("Restore failed: " + xhr.responseText, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
function updateBackupButtons() {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', getURL('/backup/status'), true);
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
try {
|
||||
var status = JSON.parse(xhr.responseText);
|
||||
gId("uresCfg").style.display = status.config ? "inline-block" : "none";
|
||||
gId("uresPresets").style.display = status.presets ? "inline-block" : "none";
|
||||
gId("uresPalettes").style.display = status.palettes ? "inline-block" : "none";
|
||||
gId("uresMappings").style.display = status.mappings ? "inline-block" : "none";
|
||||
} catch(e) {
|
||||
console.error("Failed to parse backup status:", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
function S() {
|
||||
getLoc();
|
||||
if (loc) {
|
||||
@@ -26,6 +93,7 @@
|
||||
loadJS(getURL('/settings/s.js?p=6'), false, undefined, ()=>{
|
||||
setBckFilename(gId("bckcfg"));
|
||||
setBckFilename(gId("bckpresets"));
|
||||
updateBackupButtons();
|
||||
}); // If we set async false, file is loaded and executed, then next statement is processed
|
||||
if (loc) d.Sf.action = getURL('/settings/sec');
|
||||
}
|
||||
@@ -48,7 +116,7 @@
|
||||
To enable OTA, for security reasons you need to also enter the correct password!<br>
|
||||
The password should be changed when OTA is enabled.<br>
|
||||
<b>Disable OTA when not in use, otherwise an attacker can reflash device software!</b><br>
|
||||
<i>Settings on this page are only changeable if OTA lock is disabled!</i><br>
|
||||
<i>Settings on this page are only changable if OTA lock is disabled!</i><br>
|
||||
Deny access to WiFi settings if locked: <input type="checkbox" name="OW"><br><br>
|
||||
Factory reset: <input type="checkbox" name="RS"><br>
|
||||
All settings and presets will be erased.<br><br>
|
||||
@@ -70,6 +138,22 @@
|
||||
<a class="btn lnk" id="bckpresets" href="/cfg.json" download="cfg">Backup configuration</a><br>
|
||||
<div>Restore configuration<br><input type="file" name="data2" accept=".json"> <button type="button" onclick="uploadFile(d.Sf.data2,'/cfg.json');">Upload</button><br></div>
|
||||
<hr>
|
||||
<h3>Internal User Backup</h3>
|
||||
<div class="warn">⚠ Internal backups are stored on the device filesystem and will be lost if the device is reset or reflashed.<br>
|
||||
User backups will OVERWRITE existing backups of the same type.</div>
|
||||
<h4>Configuration</h4>
|
||||
<button type="button" id="ubkCfg" onclick="userBackup('config')">Create Config Backup</button>
|
||||
<button type="button" id="uresCfg" onclick="userRestore('config')" style="display:none;">Restore Config</button><br><br>
|
||||
<h4>Presets</h4>
|
||||
<button type="button" id="ubkPresets" onclick="userBackup('presets')">Create Presets Backup</button>
|
||||
<button type="button" id="uresPresets" onclick="userRestore('presets')" style="display:none;">Restore Presets</button><br><br>
|
||||
<h4>Custom Palettes</h4>
|
||||
<button type="button" id="ubkPalettes" onclick="userBackup('palettes')">Create Palettes Backup</button>
|
||||
<button type="button" id="uresPalettes" onclick="userRestore('palettes')" style="display:none;">Restore Palettes</button><br><br>
|
||||
<h4>Custom Mappings</h4>
|
||||
<button type="button" id="ubkMappings" onclick="userBackup('mappings')">Create Mappings Backup</button>
|
||||
<button type="button" id="uresMappings" onclick="userRestore('mappings')" style="display:none;">Restore Mappings</button><br><br>
|
||||
<hr>
|
||||
<h3>About</h3>
|
||||
<a href="https://github.com/wled-dev/WLED/" target="_blank">WLED</a> version ##VERSION##<!-- Autoreplaced from package.json --><br><br>
|
||||
<a href="https://kno.wled.ge/about/contributors/" target="_blank">Contributors, dependencies and special thanks</a><br>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<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>
|
||||
<script src="common.js" async type="text/javascript"></script>
|
||||
<script>
|
||||
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;} }
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<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>
|
||||
<script src="common.js" async type="text/javascript"></script>
|
||||
<script>
|
||||
var el=false;
|
||||
var ms=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<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>
|
||||
<script src="common.js" async type="text/javascript"></script>
|
||||
<script>
|
||||
var initial_ds, initial_st, initial_su, oldUrl;
|
||||
var sett = null;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<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>
|
||||
<script src="common.js" async type="text/javascript"></script>
|
||||
<script>
|
||||
var umCfg = {};
|
||||
var pins = [], pinO = [], owner;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<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>
|
||||
<script src="common.js" async type="text/javascript"></script>
|
||||
<script>
|
||||
var scanLoops = 0, preScanSSID = "";
|
||||
var maxNetworks = 3;
|
||||
|
||||
+5
-44
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta content='width=device-width' name='viewport'>
|
||||
<title>WLED Update</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<script src="common.js" async type="text/javascript"></script>
|
||||
<script>
|
||||
function B() { window.history.back(); }
|
||||
var cnfr = false;
|
||||
@@ -17,65 +17,26 @@
|
||||
}
|
||||
window.open(getURL("/update?revert"),"_self");
|
||||
}
|
||||
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';
|
||||
});
|
||||
}
|
||||
function GetV() {/*injected values here*/}
|
||||
</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 installed-version">Loading...</span><br>
|
||||
Release: <span class="sip release-name">Loading...</span><br>
|
||||
Installed version: <span class="sip">WLED ##VERSION##</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 dmxWledVersionString = "WLED_V" + std::to_string(VERSION);
|
||||
strncpy(config.software_version_label, dmxWledVersionString.c_str(), 32);
|
||||
const std::string versionString = "WLED_V" + std::to_string(VERSION);
|
||||
strncpy(config.software_version_label, versionString.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";
|
||||
|
||||
+2
-10
@@ -30,19 +30,11 @@ void handleDDPPacket(e131_packet_t* p) {
|
||||
|
||||
uint32_t start = htonl(p->channelOffset) / ddpChannelsPerLed;
|
||||
start += DMXAddress / ddpChannelsPerLed;
|
||||
uint16_t dataLen = htons(p->dataLen);
|
||||
unsigned stop = start + dataLen / ddpChannelsPerLed;
|
||||
unsigned stop = start + htons(p->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);
|
||||
|
||||
@@ -422,7 +414,7 @@ void prepareArtnetPollReply(ArtPollReply *reply) {
|
||||
|
||||
reply->reply_port = ARTNET_DEFAULT_PORT;
|
||||
|
||||
char * numberEnd = (char*) versionString; // strtol promises not to try to edit this.
|
||||
char * numberEnd = versionString;
|
||||
reply->reply_version_h = (uint8_t)strtol(numberEnd, &numberEnd, 10);
|
||||
numberEnd++;
|
||||
reply->reply_version_l = (uint8_t)strtol(numberEnd, &numberEnd, 10);
|
||||
|
||||
+16
-4
@@ -27,7 +27,6 @@ void IRAM_ATTR touchButtonISR();
|
||||
bool backupConfig();
|
||||
bool restoreConfig();
|
||||
bool verifyConfig();
|
||||
bool configBackupExists();
|
||||
void resetConfig();
|
||||
bool deserializeConfig(JsonObject doc, bool fromFS = false);
|
||||
bool deserializeConfigFromFS();
|
||||
@@ -104,7 +103,21 @@ 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 userBackupFile(const char* filename);
|
||||
bool userRestoreFile(const char* filename);
|
||||
bool userBackupExists(const char* filename);
|
||||
bool userBackupConfig();
|
||||
bool userRestoreConfig();
|
||||
bool userBackupConfigExists();
|
||||
bool userBackupPresets();
|
||||
bool userRestorePresets();
|
||||
bool userBackupPresetsExists();
|
||||
int userBackupPalettes();
|
||||
int userRestorePalettes();
|
||||
bool userBackupPalettesExist();
|
||||
int userBackupMappings();
|
||||
int userRestoreMappings();
|
||||
bool userBackupMappingsExist();
|
||||
bool validateJsonFile(const char* filename);
|
||||
void dumpFilesToSerial();
|
||||
|
||||
@@ -401,8 +414,6 @@ 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);
|
||||
@@ -543,6 +554,7 @@ 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);
|
||||
|
||||
+145
-6
@@ -516,6 +516,7 @@ bool compareFiles(const char* path1, const char* path2) {
|
||||
}
|
||||
|
||||
static const char s_backup_fmt[] PROGMEM = "/bkp.%s";
|
||||
static const char s_user_backup_fmt[] PROGMEM = "/bku.%s";
|
||||
|
||||
bool backupFile(const char* filename) {
|
||||
DEBUG_PRINTF("backup %s \n", filename);
|
||||
@@ -557,12 +558,6 @@ 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");
|
||||
@@ -578,6 +573,150 @@ bool validateJsonFile(const char* filename) {
|
||||
return result;
|
||||
}
|
||||
|
||||
bool userBackupFile(const char* filename) {
|
||||
DEBUG_PRINTF("user backup %s \n", filename);
|
||||
if (!validateJsonFile(filename)) {
|
||||
DEBUG_PRINTLN(F("broken file"));
|
||||
return false;
|
||||
}
|
||||
char backupname[32];
|
||||
snprintf_P(backupname, sizeof(backupname), s_user_backup_fmt, filename + 1); // skip leading '/' in filename
|
||||
|
||||
if (copyFile(filename, backupname)) {
|
||||
DEBUG_PRINTLN(F("user backup ok"));
|
||||
return true;
|
||||
}
|
||||
DEBUG_PRINTLN(F("user backup failed"));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool userRestoreFile(const char* filename) {
|
||||
DEBUG_PRINTF("user restore %s \n", filename);
|
||||
char backupname[32];
|
||||
snprintf_P(backupname, sizeof(backupname), s_user_backup_fmt, filename + 1); // skip leading '/' in filename
|
||||
|
||||
if (!WLED_FS.exists(backupname)) {
|
||||
DEBUG_PRINTLN(F("no user backup found"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validateJsonFile(backupname)) {
|
||||
DEBUG_PRINTLN(F("broken user backup"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (copyFile(backupname, filename)) {
|
||||
DEBUG_PRINTLN(F("user restore ok"));
|
||||
return true;
|
||||
}
|
||||
DEBUG_PRINTLN(F("user restore failed"));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool userBackupExists(const char* filename) {
|
||||
char backupname[32];
|
||||
snprintf_P(backupname, sizeof(backupname), s_user_backup_fmt, filename + 1); // skip leading '/' in filename
|
||||
return WLED_FS.exists(backupname);
|
||||
}
|
||||
|
||||
// User backup functions for different file types
|
||||
bool userBackupConfig() {
|
||||
return userBackupFile("/cfg.json");
|
||||
}
|
||||
|
||||
bool userRestoreConfig() {
|
||||
return userRestoreFile("/cfg.json");
|
||||
}
|
||||
|
||||
bool userBackupConfigExists() {
|
||||
return userBackupExists("/cfg.json");
|
||||
}
|
||||
|
||||
bool userBackupPresets() {
|
||||
return userBackupFile("/presets.json");
|
||||
}
|
||||
|
||||
bool userRestorePresets() {
|
||||
return userRestoreFile("/presets.json");
|
||||
}
|
||||
|
||||
bool userBackupPresetsExists() {
|
||||
return userBackupExists("/presets.json");
|
||||
}
|
||||
|
||||
int userBackupPalettes() {
|
||||
int count = 0;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
char filename[32];
|
||||
sprintf_P(filename, PSTR("/palette%d.json"), i);
|
||||
if (WLED_FS.exists(filename)) {
|
||||
if (userBackupFile(filename)) count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int userRestorePalettes() {
|
||||
int count = 0;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
char filename[32];
|
||||
sprintf_P(filename, PSTR("/palette%d.json"), i);
|
||||
if (userRestoreFile(filename)) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool userBackupPalettesExist() {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
char filename[32];
|
||||
sprintf_P(filename, PSTR("/palette%d.json"), i);
|
||||
if (userBackupExists(filename)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int userBackupMappings() {
|
||||
int count = 0;
|
||||
// Backup ledmap files
|
||||
for (int i = 1; i < WLED_MAX_LEDMAPS; i++) {
|
||||
char filename[32];
|
||||
sprintf_P(filename, PSTR("/ledmap%d.json"), i);
|
||||
if (WLED_FS.exists(filename)) {
|
||||
if (userBackupFile(filename)) count++;
|
||||
}
|
||||
}
|
||||
// Backup 2D gaps file if it exists
|
||||
if (WLED_FS.exists("/2d-gaps.json")) {
|
||||
if (userBackupFile("/2d-gaps.json")) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int userRestoreMappings() {
|
||||
int count = 0;
|
||||
// Restore ledmap files
|
||||
for (int i = 1; i < WLED_MAX_LEDMAPS; i++) {
|
||||
char filename[32];
|
||||
sprintf_P(filename, PSTR("/ledmap%d.json"), i);
|
||||
if (userRestoreFile(filename)) count++;
|
||||
}
|
||||
// Restore 2D gaps file if backup exists
|
||||
if (userRestoreFile("/2d-gaps.json")) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
bool userBackupMappingsExist() {
|
||||
// Check ledmap files
|
||||
for (int i = 1; i < WLED_MAX_LEDMAPS; i++) {
|
||||
char filename[32];
|
||||
sprintf_P(filename, PSTR("/ledmap%d.json"), i);
|
||||
if (userBackupExists(filename)) return true;
|
||||
}
|
||||
// Check 2D gaps file
|
||||
if (userBackupExists("/2d-gaps.json")) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// print contents of all files in root dir to Serial except wsec files
|
||||
void dumpFilesToSerial() {
|
||||
File rootdir = WLED_FS.open("/", "r");
|
||||
|
||||
+28
-121
@@ -9,11 +9,11 @@
|
||||
* Functions to render images from filesystem to segments, used by the "Image" effect
|
||||
*/
|
||||
|
||||
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;
|
||||
File file;
|
||||
char lastFilename[34] = "/";
|
||||
GifDecoder<320,320,12,true> decoder;
|
||||
bool gifDecodeFailed = false;
|
||||
unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0;
|
||||
|
||||
bool fileSeekCallback(unsigned long position) {
|
||||
return file.seek(position);
|
||||
@@ -35,62 +35,29 @@ int fileSizeCallback(void) {
|
||||
return file.size();
|
||||
}
|
||||
|
||||
bool openGif(const char *filename) { // side-effect: updates "file"
|
||||
bool openGif(const char *filename) {
|
||||
file = WLED_FS.open(filename, "r");
|
||||
DEBUG_PRINTF_P(PSTR("opening GIF file %s\n"), filename);
|
||||
|
||||
if (!file) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
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
|
||||
Segment* activeSeg;
|
||||
uint16_t gifWidth, gifHeight;
|
||||
|
||||
void screenClearCallback(void) {
|
||||
activeSeg->fill(0);
|
||||
}
|
||||
|
||||
// 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 updateScreenCallback(void) {}
|
||||
|
||||
// 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
|
||||
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;
|
||||
// set multiple pixels if upscaling
|
||||
for (int i = 0; i < perPixelX; i++) {
|
||||
for (int j = 0; j < perPixelY; j++) {
|
||||
for (int16_t i = 0; i < (activeSeg->width()+(gifWidth-1)) / gifWidth; i++) {
|
||||
for (int16_t j = 0; j < (activeSeg->height()+(gifHeight-1)) / gifHeight; j++) {
|
||||
activeSeg->setPixelColorXY(outX + i, outY + j, red, green, blue);
|
||||
}
|
||||
}
|
||||
@@ -112,88 +79,31 @@ 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) { // 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;
|
||||
}
|
||||
|
||||
if (activeSeg && activeSeg != &seg) return IMAGE_ERROR_SEG_LIMIT; // only one segment at a time
|
||||
activeSeg = &seg;
|
||||
|
||||
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
|
||||
if (strncmp(lastFilename +1, seg.name, 32) != 0) { // segment name changed, load new image
|
||||
strncpy(lastFilename +1, seg.name, 32);
|
||||
gifDecodeFailed = false;
|
||||
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
|
||||
if (strcmp(lastFilename + strlen(lastFilename) - 4, ".gif") != 0) {
|
||||
gifDecodeFailed = true;
|
||||
DEBUG_PRINTF_P(PSTR("GIF decoder unsupported file: %s\n"), lastFilename);
|
||||
return IMAGE_ERROR_UNSUPPORTED_FORMAT;
|
||||
}
|
||||
if (file) file.close();
|
||||
if (!openGif(lastFilename)) {
|
||||
gifDecodeFailed = true;
|
||||
DEBUG_PRINTF_P(PSTR("GIF file not found: %s\n"), lastFilename);
|
||||
return IMAGE_ERROR_FILE_MISSING;
|
||||
}
|
||||
lastCoordinate = -1;
|
||||
openGif(lastFilename);
|
||||
if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; }
|
||||
decoder.setScreenClearCallback(screenClearCallback);
|
||||
decoder.setUpdateScreenCallback(updateScreenCallback);
|
||||
decoder.setDrawPixelCallback(drawPixelCallbackNoScale); // default: use "fast path" callback without scaling
|
||||
decoder.setDrawPixelCallback(drawPixelCallback);
|
||||
decoder.setFileSeekCallback(fileSeekCallback);
|
||||
decoder.setFilePositionCallback(filePositionCallback);
|
||||
decoder.setFileReadCallback(fileReadCallback);
|
||||
decoder.setFileReadBlockCallback(fileReadBlockCallback);
|
||||
decoder.setFileSizeCallback(fileSizeCallback);
|
||||
#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
|
||||
decoder.alloc();
|
||||
DEBUG_PRINTLN(F("Starting decoding"));
|
||||
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;
|
||||
}
|
||||
if(decoder.startDecoding() < 0) { 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;
|
||||
@@ -207,12 +117,10 @@ 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) {
|
||||
DEBUG_PRINTF_P(PSTR("GIF Decoding error %d in decodeFrame().\n"), result);
|
||||
gifDecodeFailed = true;
|
||||
return IMAGE_ERROR_FRAME_DECODE;
|
||||
}
|
||||
if (result < 0) { 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
|
||||
@@ -229,8 +137,7 @@ void endImagePlayback(Segment *seg) {
|
||||
decoder.dealloc();
|
||||
gifDecodeFailed = false;
|
||||
activeSeg = nullptr;
|
||||
strcpy(lastFilename, "/"); // reset filename
|
||||
gifWidth = gifHeight = 0; // reset dimensions
|
||||
lastFilename[1] = '\0';
|
||||
DEBUG_PRINTLN(F("Image playback ended"));
|
||||
}
|
||||
|
||||
|
||||
+7
-16
@@ -1,5 +1,6 @@
|
||||
#include "wled.h"
|
||||
|
||||
#include "palettes.h"
|
||||
|
||||
#define JSON_PATH_STATE 1
|
||||
#define JSON_PATH_INFO 2
|
||||
@@ -52,9 +53,6 @@ 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;
|
||||
|
||||
@@ -691,15 +689,12 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void serializeInfo(JsonObject root)
|
||||
{
|
||||
root[F("ver")] = versionString;
|
||||
root[F("vid")] = VERSION;
|
||||
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();
|
||||
@@ -775,8 +770,7 @@ void serializeInfo(JsonObject root)
|
||||
|
||||
root[F("fxcount")] = strip.getModeCount();
|
||||
root[F("palcount")] = getPaletteCount();
|
||||
root[F("cpalcount")] = customPalettes.size(); // number of custom palettes
|
||||
root[F("cpalmax")] = WLED_MAX_CUSTOM_PALETTES; // maximum number of custom palettes
|
||||
root[F("cpalcount")] = customPalettes.size(); //number of custom palettes
|
||||
|
||||
JsonArray ledmaps = root.createNestedArray(F("maps"));
|
||||
for (size_t i=0; i<WLED_MAX_LEDMAPS; i++) {
|
||||
@@ -823,9 +817,6 @@ 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();
|
||||
@@ -943,7 +934,7 @@ void serializePalettes(JsonObject root, int page)
|
||||
#endif
|
||||
|
||||
int customPalettesCount = customPalettes.size();
|
||||
int palettesCount = getPaletteCount() - customPalettesCount; // palettesCount is number of palettes, not palette index
|
||||
int palettesCount = getPaletteCount() - customPalettesCount;
|
||||
|
||||
int maxPage = (palettesCount + customPalettesCount -1) / itemPerPage;
|
||||
if (page > maxPage) page = maxPage;
|
||||
@@ -955,8 +946,8 @@ void serializePalettes(JsonObject root, int page)
|
||||
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);
|
||||
@@ -985,8 +976,8 @@ void serializePalettes(JsonObject root, int page)
|
||||
curPalette.add("c1");
|
||||
break;
|
||||
default:
|
||||
if (i > palettesCount)
|
||||
setPaletteColors(curPalette, customPalettes[i - (palettesCount + 1)]);
|
||||
if (i >= palettesCount)
|
||||
setPaletteColors(curPalette, customPalettes[i - palettesCount]);
|
||||
else if (i < 13) // palette 6 - 12, fastled palettes
|
||||
setPaletteColors(curPalette, *fastledPalettes[i-6]);
|
||||
else {
|
||||
|
||||
@@ -1,741 +0,0 @@
|
||||
#include "ota_update.h"
|
||||
#include "wled.h"
|
||||
|
||||
#ifdef ESP32
|
||||
#include <esp_app_format.h>
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_flash.h>
|
||||
#include <mbedtls/sha256.h>
|
||||
#endif
|
||||
|
||||
// Platform-specific metadata locations
|
||||
#ifdef ESP32
|
||||
constexpr size_t METADATA_OFFSET = 256; // ESP32: metadata appears after Espressif metadata
|
||||
#define UPDATE_ERROR errorString
|
||||
const size_t BOOTLOADER_OFFSET = 0x1000;
|
||||
#elif defined(ESP8266)
|
||||
constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4KB offset
|
||||
#define UPDATE_ERROR getErrorString
|
||||
#endif
|
||||
constexpr size_t METADATA_SEARCH_RANGE = 512; // bytes
|
||||
|
||||
|
||||
/**
|
||||
* Check if OTA should be allowed based on release compatibility using custom description
|
||||
* @param binaryData Pointer to binary file data (not modified)
|
||||
* @param dataSize Size of binary data in bytes
|
||||
* @param errorMessage Buffer to store error message if validation fails
|
||||
* @param errorMessageLen Maximum length of error message buffer
|
||||
* @return true if OTA should proceed, false if it should be blocked
|
||||
*/
|
||||
|
||||
static bool validateOTA(const uint8_t* binaryData, size_t dataSize, char* errorMessage, size_t errorMessageLen) {
|
||||
// Clear error message
|
||||
if (errorMessage && errorMessageLen > 0) {
|
||||
errorMessage[0] = '\0';
|
||||
}
|
||||
|
||||
// Try to extract WLED structure directly from binary data
|
||||
wled_metadata_t extractedDesc;
|
||||
bool hasDesc = findWledMetadata(binaryData, dataSize, &extractedDesc);
|
||||
|
||||
if (hasDesc) {
|
||||
return shouldAllowOTA(extractedDesc, errorMessage, errorMessageLen);
|
||||
} else {
|
||||
// No custom description - this could be a legacy binary
|
||||
if (errorMessage && errorMessageLen > 0) {
|
||||
strncpy_P(errorMessage, PSTR("This firmware file is missing compatibility metadata."), errorMessageLen - 1);
|
||||
errorMessage[errorMessageLen - 1] = '\0';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
struct UpdateContext {
|
||||
// State flags
|
||||
// FUTURE: the flags could be replaced by a state machine
|
||||
bool replySent = false;
|
||||
bool needsRestart = false;
|
||||
bool updateStarted = false;
|
||||
bool uploadComplete = false;
|
||||
bool releaseCheckPassed = false;
|
||||
String errorMessage;
|
||||
|
||||
// Buffer to hold block data across posts, if needed
|
||||
std::vector<uint8_t> releaseMetadataBuffer;
|
||||
};
|
||||
|
||||
|
||||
static void endOTA(AsyncWebServerRequest *request) {
|
||||
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
|
||||
request->_tempObject = nullptr;
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("EndOTA %x --> %x (%d)\n"), (uintptr_t)request,(uintptr_t) context, context ? context->uploadComplete : 0);
|
||||
if (context) {
|
||||
if (context->updateStarted) { // We initialized the update
|
||||
// We use Update.end() because not all forms of Update() support an abort.
|
||||
// If the upload is incomplete, Update.end(false) should error out.
|
||||
if (Update.end(context->uploadComplete)) {
|
||||
// Update successful!
|
||||
#ifndef ESP8266
|
||||
bootloopCheckOTA(); // let the bootloop-checker know there was an OTA update
|
||||
#endif
|
||||
doReboot = true;
|
||||
context->needsRestart = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (context->needsRestart) {
|
||||
strip.resume();
|
||||
UsermodManager::onUpdateBegin(false);
|
||||
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||
WLED::instance().enableWatchdog();
|
||||
#endif
|
||||
}
|
||||
delete context;
|
||||
}
|
||||
};
|
||||
|
||||
static bool beginOTA(AsyncWebServerRequest *request, UpdateContext* context)
|
||||
{
|
||||
#ifdef ESP8266
|
||||
Update.runAsync(true);
|
||||
#endif
|
||||
|
||||
if (Update.isRunning()) {
|
||||
request->send(503);
|
||||
setOTAReplied(request);
|
||||
return false;
|
||||
}
|
||||
|
||||
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||
WLED::instance().disableWatchdog();
|
||||
#endif
|
||||
UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init)
|
||||
|
||||
strip.suspend();
|
||||
backupConfig(); // backup current config in case the update ends badly
|
||||
strip.resetSegments(); // free as much memory as you can
|
||||
context->needsRestart = true;
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("OTA Update Start, %x --> %x\n"), (uintptr_t)request,(uintptr_t) context);
|
||||
|
||||
auto skipValidationParam = request->getParam("skipValidation", true);
|
||||
if (skipValidationParam && (skipValidationParam->value() == "1")) {
|
||||
context->releaseCheckPassed = true;
|
||||
DEBUG_PRINTLN(F("OTA validation skipped by user"));
|
||||
}
|
||||
|
||||
// Begin update with the firmware size from content length
|
||||
size_t updateSize = request->contentLength() > 0 ? request->contentLength() : ((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
|
||||
if (!Update.begin(updateSize)) {
|
||||
context->errorMessage = Update.UPDATE_ERROR();
|
||||
DEBUG_PRINTF_P(PSTR("OTA Failed to begin: %s\n"), context->errorMessage.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
context->updateStarted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create an OTA context object on an AsyncWebServerRequest
|
||||
// Returns true if successful, false on failure.
|
||||
bool initOTA(AsyncWebServerRequest *request) {
|
||||
// Allocate update context
|
||||
UpdateContext* context = new (std::nothrow) UpdateContext {};
|
||||
if (context) {
|
||||
request->_tempObject = context;
|
||||
request->onDisconnect([=]() { endOTA(request); }); // ensures we restart on failure
|
||||
};
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("OTA Update init, %x --> %x\n"), (uintptr_t)request,(uintptr_t) context);
|
||||
return (context != nullptr);
|
||||
}
|
||||
|
||||
void setOTAReplied(AsyncWebServerRequest *request) {
|
||||
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
|
||||
if (!context) return;
|
||||
context->replySent = true;
|
||||
};
|
||||
|
||||
// Returns pointer to error message, or nullptr if OTA was successful.
|
||||
std::pair<bool, String> getOTAResult(AsyncWebServerRequest* request) {
|
||||
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
|
||||
if (!context) return { true, F("OTA context unexpectedly missing") };
|
||||
if (context->replySent) return { false, {} };
|
||||
if (context->errorMessage.length()) return { true, context->errorMessage };
|
||||
|
||||
if (context->updateStarted) {
|
||||
// Release the OTA context now.
|
||||
endOTA(request);
|
||||
if (Update.hasError()) {
|
||||
return { true, Update.UPDATE_ERROR() };
|
||||
} else {
|
||||
return { true, {} };
|
||||
}
|
||||
}
|
||||
|
||||
// Should never happen
|
||||
return { true, F("Internal software failure") };
|
||||
}
|
||||
|
||||
|
||||
|
||||
void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal)
|
||||
{
|
||||
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
|
||||
if (!context) return;
|
||||
|
||||
//DEBUG_PRINTF_P(PSTR("HandleOTAData: %d %d %d\n"), index, len, isFinal);
|
||||
|
||||
if (context->replySent || (context->errorMessage.length())) return;
|
||||
|
||||
if (index == 0) {
|
||||
if (!beginOTA(request, context)) return;
|
||||
}
|
||||
|
||||
// Perform validation if we haven't done it yet and we have reached the metadata offset
|
||||
if (!context->releaseCheckPassed && (index+len) > METADATA_OFFSET) {
|
||||
// Current chunk contains the metadata offset
|
||||
size_t availableDataAfterOffset = (index + len) - METADATA_OFFSET;
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("OTA metadata check: %d in buffer, %d received, %d available\n"), context->releaseMetadataBuffer.size(), len, availableDataAfterOffset);
|
||||
|
||||
if (availableDataAfterOffset >= METADATA_SEARCH_RANGE) {
|
||||
// We have enough data to validate, one way or another
|
||||
const uint8_t* search_data = data;
|
||||
size_t search_len = len;
|
||||
|
||||
// If we have saved data, use that instead
|
||||
if (context->releaseMetadataBuffer.size()) {
|
||||
// Add this data
|
||||
context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len);
|
||||
search_data = context->releaseMetadataBuffer.data();
|
||||
search_len = context->releaseMetadataBuffer.size();
|
||||
}
|
||||
|
||||
// Do the checking
|
||||
char errorMessage[128];
|
||||
bool OTA_ok = validateOTA(search_data, search_len, errorMessage, sizeof(errorMessage));
|
||||
|
||||
// Release buffer if there was one
|
||||
context->releaseMetadataBuffer = decltype(context->releaseMetadataBuffer){};
|
||||
|
||||
if (!OTA_ok) {
|
||||
DEBUG_PRINTF_P(PSTR("OTA declined: %s\n"), errorMessage);
|
||||
context->errorMessage = errorMessage;
|
||||
context->errorMessage += F(" Enable 'Ignore firmware validation' to proceed anyway.");
|
||||
return;
|
||||
} else {
|
||||
DEBUG_PRINTLN(F("OTA allowed: Release compatibility check passed"));
|
||||
context->releaseCheckPassed = true;
|
||||
}
|
||||
} else {
|
||||
// Store the data we just got for next pass
|
||||
context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if validation was still pending (shouldn't happen normally)
|
||||
// This is done before writing the last chunk, so endOTA can abort
|
||||
if (isFinal && !context->releaseCheckPassed) {
|
||||
DEBUG_PRINTLN(F("OTA failed: Validation never completed"));
|
||||
// Don't write the last chunk to the updater: this will trip an error later
|
||||
context->errorMessage = F("Release check data never arrived?");
|
||||
return;
|
||||
}
|
||||
|
||||
// Write chunk data to OTA update (only if release check passed or still pending)
|
||||
if (!Update.hasError()) {
|
||||
if (Update.write(data, len) != len) {
|
||||
DEBUG_PRINTF_P(PSTR("OTA write failed on chunk %zu: %s\n"), index, Update.UPDATE_ERROR());
|
||||
}
|
||||
}
|
||||
|
||||
if(isFinal) {
|
||||
DEBUG_PRINTLN(F("OTA Update End"));
|
||||
// Upload complete
|
||||
context->uploadComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
void markOTAvalid() {
|
||||
#ifndef ESP8266
|
||||
const esp_partition_t* running = esp_ota_get_running_partition();
|
||||
esp_ota_img_states_t ota_state;
|
||||
if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) {
|
||||
if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) {
|
||||
esp_ota_mark_app_valid_cancel_rollback(); // only needs to be called once, it marks the ota_state as ESP_OTA_IMG_VALID
|
||||
DEBUG_PRINTLN(F("Current firmware validated"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
|
||||
// Cache for bootloader SHA256 digest as hex string
|
||||
static String bootloaderSHA256HexCache = "";
|
||||
|
||||
// Calculate and cache the bootloader SHA256 digest as hex string
|
||||
void calculateBootloaderSHA256() {
|
||||
if (!bootloaderSHA256HexCache.isEmpty()) return;
|
||||
|
||||
// Bootloader is at fixed offset 0x1000 (4KB) and is typically 32KB
|
||||
const uint32_t bootloaderSize = 0x8000; // 32KB, typical bootloader size
|
||||
|
||||
// Calculate SHA256
|
||||
uint8_t sha256[32];
|
||||
mbedtls_sha256_context ctx;
|
||||
mbedtls_sha256_init(&ctx);
|
||||
mbedtls_sha256_starts(&ctx, 0); // 0 = SHA256 (not SHA224)
|
||||
|
||||
const size_t chunkSize = 256;
|
||||
uint8_t buffer[chunkSize];
|
||||
|
||||
for (uint32_t offset = 0; offset < bootloaderSize; offset += chunkSize) {
|
||||
size_t readSize = min((size_t)(bootloaderSize - offset), chunkSize);
|
||||
if (esp_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) {
|
||||
mbedtls_sha256_update(&ctx, buffer, readSize);
|
||||
}
|
||||
}
|
||||
|
||||
mbedtls_sha256_finish(&ctx, sha256);
|
||||
mbedtls_sha256_free(&ctx);
|
||||
|
||||
// Convert to hex string and cache it
|
||||
char hex[65];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
sprintf(hex + (i * 2), "%02x", sha256[i]);
|
||||
}
|
||||
hex[64] = '\0';
|
||||
bootloaderSHA256HexCache = String(hex);
|
||||
}
|
||||
|
||||
// Get bootloader SHA256 as hex string
|
||||
String getBootloaderSHA256Hex() {
|
||||
calculateBootloaderSHA256();
|
||||
return bootloaderSHA256HexCache;
|
||||
}
|
||||
|
||||
// Invalidate cached bootloader SHA256 (call after bootloader update)
|
||||
void invalidateBootloaderSHA256Cache() {
|
||||
bootloaderSHA256HexCache = "";
|
||||
}
|
||||
|
||||
// Verify complete buffered bootloader using ESP-IDF validation approach
|
||||
// This matches the key validation steps from esp_image_verify() in ESP-IDF
|
||||
// Returns the actual bootloader data pointer and length via the buffer and len parameters
|
||||
bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String* bootloaderErrorMsg) {
|
||||
size_t availableLen = len;
|
||||
if (!bootloaderErrorMsg) {
|
||||
DEBUG_PRINTLN(F("bootloaderErrorMsg is null"));
|
||||
return false;
|
||||
}
|
||||
// ESP32 image header structure (based on esp_image_format.h)
|
||||
// Offset 0: magic (0xE9)
|
||||
// Offset 1: segment_count
|
||||
// Offset 2: spi_mode
|
||||
// Offset 3: spi_speed (4 bits) + spi_size (4 bits)
|
||||
// Offset 4-7: entry_addr (uint32_t)
|
||||
// Offset 8: wp_pin
|
||||
// Offset 9-11: spi_pin_drv[3]
|
||||
// Offset 12-13: chip_id (uint16_t, little-endian)
|
||||
// Offset 14: min_chip_rev
|
||||
// Offset 15-22: reserved[8]
|
||||
// Offset 23: hash_appended
|
||||
|
||||
const size_t MIN_IMAGE_HEADER_SIZE = 24;
|
||||
|
||||
// 1. Validate minimum size for header
|
||||
if (len < MIN_IMAGE_HEADER_SIZE) {
|
||||
*bootloaderErrorMsg = "Bootloader too small - invalid header";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the bootloader starts at offset 0x1000 (common in partition table dumps)
|
||||
// This happens when someone uploads a complete flash dump instead of just the bootloader
|
||||
if (len > BOOTLOADER_OFFSET + MIN_IMAGE_HEADER_SIZE &&
|
||||
buffer[BOOTLOADER_OFFSET] == 0xE9 &&
|
||||
buffer[0] != 0xE9) {
|
||||
DEBUG_PRINTF_P(PSTR("Bootloader magic byte detected at offset 0x%04X - adjusting buffer\n"), BOOTLOADER_OFFSET);
|
||||
// Adjust buffer pointer to start at the actual bootloader
|
||||
buffer = buffer + BOOTLOADER_OFFSET;
|
||||
len = len - BOOTLOADER_OFFSET;
|
||||
|
||||
// Re-validate size after adjustment
|
||||
if (len < MIN_IMAGE_HEADER_SIZE) {
|
||||
*bootloaderErrorMsg = "Bootloader at offset 0x1000 too small - invalid header";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Magic byte check (matches esp_image_verify step 1)
|
||||
if (buffer[0] != 0xE9) {
|
||||
*bootloaderErrorMsg = "Invalid bootloader magic byte (expected 0xE9, got 0x" + String(buffer[0], HEX) + ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. Segment count validation (matches esp_image_verify step 2)
|
||||
uint8_t segmentCount = buffer[1];
|
||||
if (segmentCount == 0 || segmentCount > 16) {
|
||||
*bootloaderErrorMsg = "Invalid segment count: " + String(segmentCount);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. SPI mode validation (basic sanity check)
|
||||
uint8_t spiMode = buffer[2];
|
||||
if (spiMode > 3) { // Valid modes are 0-3 (QIO, QOUT, DIO, DOUT)
|
||||
*bootloaderErrorMsg = "Invalid SPI mode: " + String(spiMode);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. Chip ID validation (matches esp_image_verify step 3)
|
||||
uint16_t chipId = buffer[12] | (buffer[13] << 8); // Little-endian
|
||||
|
||||
// Known ESP32 chip IDs from ESP-IDF:
|
||||
// 0x0000 = ESP32
|
||||
// 0x0002 = ESP32-S2
|
||||
// 0x0005 = ESP32-C3
|
||||
// 0x0009 = ESP32-S3
|
||||
// 0x000C = ESP32-C2
|
||||
// 0x000D = ESP32-C6
|
||||
// 0x0010 = ESP32-H2
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32)
|
||||
if (chipId != 0x0000) {
|
||||
*bootloaderErrorMsg = "Chip ID mismatch - expected ESP32 (0x0000), got 0x" + String(chipId, HEX);
|
||||
return false;
|
||||
}
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
if (chipId != 0x0002) {
|
||||
*bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-S2 (0x0002), got 0x" + String(chipId, HEX);
|
||||
return false;
|
||||
}
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
if (chipId != 0x0005) {
|
||||
*bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-C3 (0x0005), got 0x" + String(chipId, HEX);
|
||||
return false;
|
||||
}
|
||||
*bootloaderErrorMsg = "ESP32-C3 update not supported yet";
|
||||
return false;
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
if (chipId != 0x0009) {
|
||||
*bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-S3 (0x0009), got 0x" + String(chipId, HEX);
|
||||
return false;
|
||||
}
|
||||
*bootloaderErrorMsg = "ESP32-S3 update not supported yet";
|
||||
return false;
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
if (chipId != 0x000D) {
|
||||
*bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-C6 (0x000D), got 0x" + String(chipId, HEX);
|
||||
return false;
|
||||
}
|
||||
*bootloaderErrorMsg = "ESP32-C6 update not supported yet";
|
||||
return false;
|
||||
#else
|
||||
// Generic validation - chip ID should be valid
|
||||
if (chipId > 0x00FF) {
|
||||
*bootloaderErrorMsg = "Invalid chip ID: 0x" + String(chipId, HEX);
|
||||
return false;
|
||||
}
|
||||
*bootloaderErrorMsg = "Unknown ESP32 target - bootloader update not supported";
|
||||
return false;
|
||||
#endif
|
||||
|
||||
// 6. Entry point validation (should be in valid memory range)
|
||||
uint32_t entryAddr = buffer[4] | (buffer[5] << 8) | (buffer[6] << 16) | (buffer[7] << 24);
|
||||
// ESP32 bootloader entry points are typically in IRAM range (0x40000000 - 0x40400000)
|
||||
// or ROM range (0x40000000 and above)
|
||||
if (entryAddr < 0x40000000 || entryAddr > 0x50000000) {
|
||||
*bootloaderErrorMsg = "Invalid entry address: 0x" + String(entryAddr, HEX);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 7. Basic segment structure validation
|
||||
// Each segment has a header: load_addr (4 bytes) + data_len (4 bytes)
|
||||
size_t offset = MIN_IMAGE_HEADER_SIZE;
|
||||
size_t actualBootloaderSize = MIN_IMAGE_HEADER_SIZE;
|
||||
|
||||
for (uint8_t i = 0; i < segmentCount && offset + 8 <= len; i++) {
|
||||
uint32_t segmentSize = buffer[offset + 4] | (buffer[offset + 5] << 8) |
|
||||
(buffer[offset + 6] << 16) | (buffer[offset + 7] << 24);
|
||||
|
||||
// Segment size sanity check
|
||||
// ESP32 classic bootloader segments can be larger, C3 are smaller
|
||||
if (segmentSize > 0x20000) { // 128KB max per segment (very generous)
|
||||
*bootloaderErrorMsg = "Segment " + String(i) + " too large: " + String(segmentSize) + " bytes";
|
||||
return false;
|
||||
}
|
||||
|
||||
offset += 8 + segmentSize; // Skip segment header and data
|
||||
}
|
||||
|
||||
actualBootloaderSize = offset;
|
||||
|
||||
// 8. Check for appended SHA256 hash (byte 23 in header)
|
||||
// If hash_appended != 0, there's a 32-byte SHA256 hash after the segments
|
||||
uint8_t hashAppended = buffer[23];
|
||||
if (hashAppended != 0) {
|
||||
actualBootloaderSize += 32;
|
||||
if (actualBootloaderSize > availableLen) {
|
||||
*bootloaderErrorMsg = "Bootloader missing SHA256 trailer";
|
||||
return false;
|
||||
}
|
||||
DEBUG_PRINTF_P(PSTR("Bootloader has appended SHA256 hash\n"));
|
||||
}
|
||||
|
||||
// 9. The image may also have a 1-byte checksum after segments/hash
|
||||
// Check if there's at least one more byte available
|
||||
if (actualBootloaderSize + 1 <= availableLen) {
|
||||
// There's likely a checksum byte
|
||||
actualBootloaderSize += 1;
|
||||
} else if (actualBootloaderSize > availableLen) {
|
||||
*bootloaderErrorMsg = "Bootloader truncated before checksum";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 10. Align to 16 bytes (ESP32 requirement for flash writes)
|
||||
// The bootloader image must be 16-byte aligned
|
||||
if (actualBootloaderSize % 16 != 0) {
|
||||
size_t alignedSize = ((actualBootloaderSize + 15) / 16) * 16;
|
||||
// Make sure we don't exceed available data
|
||||
if (alignedSize <= len) {
|
||||
actualBootloaderSize = alignedSize;
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("Bootloader validation: %d segments, actual size %d bytes (buffer size %d bytes, hash_appended=%d)\n"),
|
||||
segmentCount, actualBootloaderSize, len, hashAppended);
|
||||
|
||||
// 11. Verify we have enough data for all segments + hash + checksum
|
||||
if (actualBootloaderSize > availableLen) {
|
||||
*bootloaderErrorMsg = "Bootloader truncated - expected at least " + String(actualBootloaderSize) + " bytes, have " + String(availableLen) + " bytes";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (offset > availableLen) {
|
||||
*bootloaderErrorMsg = "Bootloader truncated - expected at least " + String(offset) + " bytes, have " + String(len) + " bytes";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update len to reflect actual bootloader size (including hash and checksum, with alignment)
|
||||
// This is critical - we must write the complete image including checksums
|
||||
len = actualBootloaderSize;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bootloader OTA context structure
|
||||
struct BootloaderUpdateContext {
|
||||
// State flags
|
||||
bool replySent = false;
|
||||
bool uploadComplete = false;
|
||||
String errorMessage;
|
||||
|
||||
// Buffer to hold bootloader data
|
||||
uint8_t* buffer = nullptr;
|
||||
size_t bytesBuffered = 0;
|
||||
const uint32_t bootloaderOffset = 0x1000;
|
||||
const uint32_t maxBootloaderSize = 0x10000; // 64KB buffer size
|
||||
};
|
||||
|
||||
// Cleanup bootloader OTA context
|
||||
static void endBootloaderOTA(AsyncWebServerRequest *request) {
|
||||
BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);
|
||||
request->_tempObject = nullptr;
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("EndBootloaderOTA %x --> %x\n"), (uintptr_t)request, (uintptr_t)context);
|
||||
if (context) {
|
||||
if (context->buffer) {
|
||||
free(context->buffer);
|
||||
context->buffer = nullptr;
|
||||
}
|
||||
|
||||
// If update failed, restore system state
|
||||
if (!context->uploadComplete || !context->errorMessage.isEmpty()) {
|
||||
strip.resume();
|
||||
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||
WLED::instance().enableWatchdog();
|
||||
#endif
|
||||
}
|
||||
|
||||
delete context;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize bootloader OTA context
|
||||
bool initBootloaderOTA(AsyncWebServerRequest *request) {
|
||||
if (request->_tempObject) {
|
||||
return true; // Already initialized
|
||||
}
|
||||
|
||||
BootloaderUpdateContext* context = new BootloaderUpdateContext();
|
||||
if (!context) {
|
||||
DEBUG_PRINTLN(F("Failed to allocate bootloader OTA context"));
|
||||
return false;
|
||||
}
|
||||
|
||||
request->_tempObject = context;
|
||||
request->onDisconnect([=]() { endBootloaderOTA(request); }); // ensures cleanup on disconnect
|
||||
|
||||
DEBUG_PRINTLN(F("Bootloader Update Start - initializing buffer"));
|
||||
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||
WLED::instance().disableWatchdog();
|
||||
#endif
|
||||
lastEditTime = millis(); // make sure PIN does not lock during update
|
||||
strip.suspend();
|
||||
strip.resetSegments();
|
||||
|
||||
// Check available heap before attempting allocation
|
||||
size_t freeHeap = getFreeHeapSize();
|
||||
DEBUG_PRINTF_P(PSTR("Free heap before bootloader buffer allocation: %d bytes (need %d bytes)\n"), freeHeap, context->maxBootloaderSize);
|
||||
|
||||
context->buffer = (uint8_t*)malloc(context->maxBootloaderSize);
|
||||
if (!context->buffer) {
|
||||
size_t freeHeapNow = getFreeHeapSize();
|
||||
DEBUG_PRINTF_P(PSTR("Failed to allocate %d byte bootloader buffer! Free heap: %d bytes\n"), context->maxBootloaderSize, freeHeapNow);
|
||||
context->errorMessage = "Out of memory! Free heap: " + String(freeHeapNow) + " bytes, need: " + String(context->maxBootloaderSize) + " bytes";
|
||||
strip.resume();
|
||||
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||
WLED::instance().enableWatchdog();
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
context->bytesBuffered = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Set bootloader OTA replied flag
|
||||
void setBootloaderOTAReplied(AsyncWebServerRequest *request) {
|
||||
BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);
|
||||
if (context) {
|
||||
context->replySent = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Get bootloader OTA result
|
||||
std::pair<bool, String> getBootloaderOTAResult(AsyncWebServerRequest *request) {
|
||||
BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);
|
||||
|
||||
if (!context) {
|
||||
return std::make_pair(true, String(F("Internal error: No bootloader OTA context")));
|
||||
}
|
||||
|
||||
bool needsReply = !context->replySent;
|
||||
String errorMsg = context->errorMessage;
|
||||
|
||||
// If upload was successful, return empty string and trigger reboot
|
||||
if (context->uploadComplete && errorMsg.isEmpty()) {
|
||||
doReboot = true;
|
||||
endBootloaderOTA(request);
|
||||
return std::make_pair(needsReply, String());
|
||||
}
|
||||
|
||||
// If there was an error, return it
|
||||
if (!errorMsg.isEmpty()) {
|
||||
endBootloaderOTA(request);
|
||||
return std::make_pair(needsReply, errorMsg);
|
||||
}
|
||||
|
||||
// Should never happen
|
||||
return std::make_pair(true, String(F("Internal software failure")));
|
||||
}
|
||||
|
||||
// Handle bootloader OTA data
|
||||
void handleBootloaderOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal) {
|
||||
BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);
|
||||
|
||||
if (!context) {
|
||||
DEBUG_PRINTLN(F("No bootloader OTA context - ignoring data"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context->errorMessage.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Buffer the incoming data
|
||||
if (context->buffer && context->bytesBuffered + len <= context->maxBootloaderSize) {
|
||||
memcpy(context->buffer + context->bytesBuffered, data, len);
|
||||
context->bytesBuffered += len;
|
||||
DEBUG_PRINTF_P(PSTR("Bootloader buffer progress: %d / %d bytes\n"), context->bytesBuffered, context->maxBootloaderSize);
|
||||
} else if (!context->buffer) {
|
||||
DEBUG_PRINTLN(F("Bootloader buffer not allocated!"));
|
||||
context->errorMessage = "Internal error: Bootloader buffer not allocated";
|
||||
return;
|
||||
} else {
|
||||
size_t totalSize = context->bytesBuffered + len;
|
||||
DEBUG_PRINTLN(F("Bootloader size exceeds maximum!"));
|
||||
context->errorMessage = "Bootloader file too large: " + String(totalSize) + " bytes (max: " + String(context->maxBootloaderSize) + " bytes)";
|
||||
return;
|
||||
}
|
||||
|
||||
// Only write to flash when upload is complete
|
||||
if (isFinal) {
|
||||
DEBUG_PRINTLN(F("Bootloader Upload Complete - validating and flashing"));
|
||||
|
||||
if (context->buffer && context->bytesBuffered > 0) {
|
||||
// Prepare pointers for verification (may be adjusted if bootloader at offset)
|
||||
const uint8_t* bootloaderData = context->buffer;
|
||||
size_t bootloaderSize = context->bytesBuffered;
|
||||
|
||||
// Verify the complete bootloader image before flashing
|
||||
// Note: verifyBootloaderImage may adjust bootloaderData pointer and bootloaderSize
|
||||
// for validation purposes only
|
||||
if (!verifyBootloaderImage(bootloaderData, bootloaderSize, &context->errorMessage)) {
|
||||
DEBUG_PRINTLN(F("Bootloader validation failed!"));
|
||||
// Error message already set by verifyBootloaderImage
|
||||
} else {
|
||||
// Calculate offset to write to flash
|
||||
// If bootloaderData was adjusted (partition table detected), we need to skip it in flash too
|
||||
size_t flashOffset = context->bootloaderOffset;
|
||||
const uint8_t* dataToWrite = context->buffer;
|
||||
size_t bytesToWrite = context->bytesBuffered;
|
||||
|
||||
// If validation adjusted the pointer, it means we have a partition table at the start
|
||||
// In this case, we should skip writing the partition table and write bootloader at 0x1000
|
||||
if (bootloaderData != context->buffer) {
|
||||
// bootloaderData was adjusted - skip partition table in our data
|
||||
size_t partitionTableSize = bootloaderData - context->buffer;
|
||||
dataToWrite = bootloaderData;
|
||||
bytesToWrite = bootloaderSize;
|
||||
DEBUG_PRINTF_P(PSTR("Skipping %d bytes of partition table data\n"), partitionTableSize);
|
||||
}
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("Bootloader validation passed - writing %d bytes to flash at 0x%04X\n"),
|
||||
bytesToWrite, flashOffset);
|
||||
|
||||
// Calculate erase size (must be multiple of 4KB)
|
||||
size_t eraseSize = ((bytesToWrite + 0xFFF) / 0x1000) * 0x1000;
|
||||
if (eraseSize > context->maxBootloaderSize) {
|
||||
eraseSize = context->maxBootloaderSize;
|
||||
}
|
||||
|
||||
// Erase bootloader region
|
||||
DEBUG_PRINTF_P(PSTR("Erasing %d bytes at 0x%04X...\n"), eraseSize, flashOffset);
|
||||
esp_err_t err = esp_flash_erase_region(NULL, flashOffset, eraseSize);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_PRINTF_P(PSTR("Bootloader erase error: %d\n"), err);
|
||||
context->errorMessage = "Flash erase failed (error code: " + String(err) + ")";
|
||||
} else {
|
||||
// Write the validated bootloader data to flash
|
||||
err = esp_flash_write(NULL, dataToWrite, flashOffset, bytesToWrite);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_PRINTF_P(PSTR("Bootloader flash write error: %d\n"), err);
|
||||
context->errorMessage = "Flash write failed (error code: " + String(err) + ")";
|
||||
} else {
|
||||
DEBUG_PRINTF_P(PSTR("Bootloader Update Success - %d bytes written to 0x%04X\n"),
|
||||
bytesToWrite, flashOffset);
|
||||
// Invalidate cached bootloader hash
|
||||
invalidateBootloaderSHA256Cache();
|
||||
context->uploadComplete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (context->bytesBuffered == 0) {
|
||||
context->errorMessage = "No bootloader data received";
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,120 +0,0 @@
|
||||
// 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
|
||||
|
||||
Regular → Executable
+4
-1
@@ -1,4 +1,5 @@
|
||||
#include "wled.h"
|
||||
#ifndef PalettesWLED_h
|
||||
#define PalettesWLED_h
|
||||
|
||||
/*
|
||||
* WLED Color palettes
|
||||
@@ -767,3 +768,5 @@ const uint8_t* const gGradientPalettes[] PROGMEM = {
|
||||
candy2_gp, //70-57 Candy2
|
||||
trafficlight_gp //71-58 Traffic Light
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -139,12 +139,6 @@ bool PinManager::allocateMultiplePins(const managed_pin_type * mptArray, byte ar
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PinManager::allocateMultiplePins(const int8_t * mptArray, byte arrayElementCount, PinOwner tag, boolean output) {
|
||||
PinManagerPinType pins[arrayElementCount];
|
||||
for (int i=0; i<arrayElementCount; i++) pins[i] = {mptArray[i], output};
|
||||
return allocateMultiplePins(pins, arrayElementCount, tag);
|
||||
}
|
||||
|
||||
bool PinManager::allocatePin(byte gpio, bool output, PinOwner tag)
|
||||
{
|
||||
// HW I2C & SPI pins have to be allocated using allocateMultiplePins variant since there is always SCL/SDA pair
|
||||
|
||||
@@ -40,7 +40,6 @@ enum struct PinOwner : uint8_t {
|
||||
HW_I2C = 0x8B, // 'I2C' == hardware I2C pins (4&5 on ESP8266, 21&22 on ESP32)
|
||||
HW_SPI = 0x8C, // 'SPI' == hardware (V)SPI pins (13,14&15 on ESP8266, 5,18&23 on ESP32)
|
||||
DMX_INPUT = 0x8D, // 'DMX_INPUT' == DMX input via serial
|
||||
HUB75 = 0x8E, // 'Hub75' == Hub75 driver
|
||||
// Use UserMod IDs from const.h here
|
||||
UM_Unspecified = USERMOD_ID_UNSPECIFIED, // 0x01
|
||||
UM_Example = USERMOD_ID_EXAMPLE, // 0x02 // Usermod "usermod_v2_example.h"
|
||||
@@ -87,7 +86,6 @@ namespace PinManager {
|
||||
// using more than one pin, such as I2C, SPI, rotary encoders,
|
||||
// ethernet, etc..
|
||||
bool allocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag );
|
||||
bool allocateMultiplePins(const int8_t * mptArray, byte arrayElementCount, PinOwner tag, boolean output);
|
||||
|
||||
[[deprecated("Replaced by three-parameter allocatePin(gpio, output, ownerTag), for improved debugging")]]
|
||||
inline bool allocatePin(byte gpio, bool output = true) { return allocatePin(gpio, output, PinOwner::None); }
|
||||
@@ -98,7 +96,7 @@ namespace PinManager {
|
||||
bool isPinAllocated(byte gpio, PinOwner tag = PinOwner::None);
|
||||
// will return false for reserved pins
|
||||
bool isPinOk(byte gpio, bool output = true);
|
||||
|
||||
|
||||
bool isReadOnlyPin(byte gpio);
|
||||
|
||||
PinOwner getPinOwner(byte gpio);
|
||||
|
||||
+2
-6
@@ -140,7 +140,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
|
||||
|
||||
unsigned colorOrder, type, skip, awmode, channelSwap, maPerLed;
|
||||
unsigned length, start, maMax;
|
||||
uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255};
|
||||
uint8_t pins[5] = {255, 255, 255, 255, 255};
|
||||
String text;
|
||||
|
||||
// this will set global ABL max current used when per-port ABL is not used
|
||||
@@ -613,6 +613,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
|
||||
#ifndef WLED_DISABLE_OTA
|
||||
aOtaEnabled = request->hasArg(F("AO"));
|
||||
#endif
|
||||
//createEditHandler(correctPIN && !otaLock);
|
||||
otaSameSubnet = request->hasArg(F("SU"));
|
||||
}
|
||||
}
|
||||
@@ -814,13 +815,8 @@ 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
|
||||
|
||||
|
||||
+6
-104
@@ -3,7 +3,6 @@
|
||||
#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)
|
||||
@@ -11,8 +10,6 @@
|
||||
#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
|
||||
|
||||
|
||||
@@ -233,7 +230,7 @@ uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLe
|
||||
} else return 0;
|
||||
}
|
||||
|
||||
if (src == JSON_palette_names && mode > 255-customPalettes.size()) {
|
||||
if (src == JSON_palette_names && mode > (GRADIENT_PALETTE_COUNT + 13)) {
|
||||
snprintf_P(dest, maxLen, PSTR("~ Custom %d ~"), 255-mode);
|
||||
dest[maxLen-1] = '\0';
|
||||
return strlen(dest);
|
||||
@@ -372,6 +369,7 @@ 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();
|
||||
}
|
||||
|
||||
@@ -636,12 +634,10 @@ 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
|
||||
#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
|
||||
// BOARD_HAS_PSRAM also means that compiler flag "-mfix-esp32-psram-cache-issue" has to be used
|
||||
#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.")
|
||||
@@ -652,8 +648,7 @@ 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
|
||||
// note: ESP826 needs very little contiguous heap for webserver, checking total free heap works better
|
||||
if (getFreeHeapSize() < MIN_HEAP_SIZE) {
|
||||
if (getContiguousFreeHeap() < MIN_HEAP_SIZE) {
|
||||
free(buffer);
|
||||
return nullptr;
|
||||
}
|
||||
@@ -901,8 +896,7 @@ static bool detectBootLoop() {
|
||||
bl_crashcounter++;
|
||||
if (bl_crashcounter >= BOOTLOOP_THRESHOLD) {
|
||||
DEBUG_PRINTLN(F("!BOOTLOOP DETECTED!"));
|
||||
bl_crashcounter = 0;
|
||||
if(bl_actiontracker > BOOTLOOP_ACTION_DUMP) bl_actiontracker = BOOTLOOP_ACTION_RESTORE; // reset action tracker if out of bounds
|
||||
bl_crashcounter = 0;
|
||||
result = true;
|
||||
}
|
||||
} else {
|
||||
@@ -1130,96 +1124,4 @@ 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
|
||||
static String dump_raw_block(esp_efuse_block_t block)
|
||||
{
|
||||
const int WORDS = 8; // ESP32: 8×32-bit words per block i.e. 256bits
|
||||
uint32_t buf[WORDS] = {0};
|
||||
|
||||
const esp_efuse_desc_t d = {
|
||||
.efuse_block = block,
|
||||
.bit_start = 0,
|
||||
.bit_count = WORDS * 32
|
||||
};
|
||||
const esp_efuse_desc_t* field[2] = { &d, NULL };
|
||||
|
||||
esp_err_t err = esp_efuse_read_field_blob(field, buf, WORDS * 32);
|
||||
if (err != ESP_OK) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String result = "";
|
||||
for (const unsigned int i : buf) {
|
||||
char line[32];
|
||||
sprintf(line, "0x%08X", i);
|
||||
result += line;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
// Generate a device ID based on SHA1 hash of MAC address salted with "WLED"
|
||||
// Returns: original SHA1 + last 2 chars of double-hashed SHA1 (42 chars total)
|
||||
String getDeviceId() {
|
||||
static String cachedDeviceId = "";
|
||||
if (cachedDeviceId.length() > 0) return cachedDeviceId;
|
||||
|
||||
uint8_t mac[6];
|
||||
WiFi.macAddress(mac);
|
||||
char macStr[18];
|
||||
sprintf(macStr, "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
|
||||
// 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
|
||||
#ifdef ESP8266
|
||||
String deviceString = String(macStr) + "WLED" + ESP.getFlashChipId();
|
||||
#else
|
||||
String deviceString = String(macStr) + "WLED" + ESP.getChipModel() + ESP.getChipRevision();
|
||||
deviceString += dump_raw_block(EFUSE_BLK0);
|
||||
deviceString += dump_raw_block(EFUSE_BLK1);
|
||||
deviceString += dump_raw_block(EFUSE_BLK2);
|
||||
deviceString += dump_raw_block(EFUSE_BLK3);
|
||||
#endif
|
||||
String firstHash = computeSHA1(deviceString);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
}
|
||||
+4
-5
@@ -1,7 +1,6 @@
|
||||
#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>
|
||||
@@ -167,15 +166,16 @@ 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);
|
||||
DEBUG_PRINTF_P(PSTR("Heap too low! %u\n"), heap);
|
||||
forceReconnect = true;
|
||||
strip.resetSegments(); // remove all but one segments from memory
|
||||
if (!Update.isRunning()) forceReconnect = true;
|
||||
} else if (heap < MIN_HEAP_SIZE) {
|
||||
DEBUG_PRINTLN(F("Heap low, purging segments."));
|
||||
strip.purgeSegments();
|
||||
@@ -474,7 +474,7 @@ void WLED::setup()
|
||||
|
||||
if (needsCfgSave) serializeConfigToFS(); // usermods required new parameters; need to wait for strip to be initialised #4752
|
||||
|
||||
if (strcmp(multiWiFi[0].clientSSID, DEFAULT_CLIENT_SSID) == 0 && !configBackupExists())
|
||||
if (strcmp(multiWiFi[0].clientSSID, DEFAULT_CLIENT_SSID) == 0)
|
||||
showWelcomePage = true;
|
||||
WiFi.persistent(false);
|
||||
WiFi.onEvent(WiFiEvent);
|
||||
@@ -555,7 +555,6 @@ 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()
|
||||
|
||||
+11
-5
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
// version code in format yymmddb (b = daily build)
|
||||
#define VERSION 2506160
|
||||
#define VERSION 2412040
|
||||
|
||||
//uncomment this if you have a "my_config.h" file you'd like to use
|
||||
//#define WLED_USE_MY_CONFIG
|
||||
@@ -189,15 +189,11 @@ 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
|
||||
@@ -274,6 +270,16 @@ 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
|
||||
|
||||
// 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\"
|
||||
#define WLED_CODENAME "Niji"
|
||||
|
||||
// AP and OTA default passwords (for maximum security change them!)
|
||||
|
||||
@@ -96,7 +96,7 @@ void loadSettingsFromEEPROM()
|
||||
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 pins[5] = {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;
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
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);
|
||||
+189
-197
@@ -1,7 +1,11 @@
|
||||
#include "wled.h"
|
||||
|
||||
#ifndef WLED_DISABLE_OTA
|
||||
#include "ota_update.h"
|
||||
#ifdef ESP8266
|
||||
#include <Updater.h>
|
||||
#else
|
||||
#include <Update.h>
|
||||
#endif
|
||||
#endif
|
||||
#include "html_ui.h"
|
||||
#include "html_settings.h"
|
||||
@@ -13,8 +17,6 @@
|
||||
#include "html_pxmagic.h"
|
||||
#endif
|
||||
#include "html_cpal.h"
|
||||
#include "html_edit.h"
|
||||
|
||||
|
||||
// define flash strings once (saves flash memory)
|
||||
static const char s_redirecting[] PROGMEM = "Redirecting...";
|
||||
@@ -24,16 +26,8 @@ static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN co
|
||||
static const char s_rebooting [] PROGMEM = "Rebooting now...";
|
||||
static const char s_notimplemented[] PROGMEM = "Not implemented";
|
||||
static const char s_accessdenied[] PROGMEM = "Access Denied";
|
||||
static const char s_not_found[] PROGMEM = "Not found";
|
||||
static const char s_wsec[] PROGMEM = "wsec.json";
|
||||
static const char s_func[] PROGMEM = "func";
|
||||
static const char s_path[] PROGMEM = "path";
|
||||
static const char s_cache_control[] PROGMEM = "Cache-Control";
|
||||
static const char s_no_store[] PROGMEM = "no-store";
|
||||
static const char s_expires[] PROGMEM = "Expires";
|
||||
static const char _common_js[] PROGMEM = "/common.js";
|
||||
|
||||
|
||||
//Is this an IP?
|
||||
static bool isIp(const String &str) {
|
||||
for (size_t i = 0; i < str.length(); i++) {
|
||||
@@ -77,9 +71,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(FPSTR(s_cache_control), F("no-cache"));
|
||||
response->addHeader(F("Cache-Control"), F("no-cache"));
|
||||
#else
|
||||
response->addHeader(FPSTR(s_cache_control), F("no-store,max-age=0")); // prevent caching if debug build
|
||||
response->addHeader(F("Cache-Control"), F("no-store,max-age=0")); // prevent caching if debug build
|
||||
#endif
|
||||
char etag[32];
|
||||
generateEtag(etag, eTagSuffix);
|
||||
@@ -182,7 +176,6 @@ 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));
|
||||
@@ -205,7 +198,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("Config restore ok.\nRebooting..."));
|
||||
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Configuration restore successful.\nRebooting..."));
|
||||
} else {
|
||||
if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) loadCustomPalettes();
|
||||
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File Uploaded!"));
|
||||
@@ -214,94 +207,25 @@ static void handleUpload(AsyncWebServerRequest *request, const String& filename,
|
||||
}
|
||||
}
|
||||
|
||||
static const char _edit_htm[] PROGMEM = "/edit.htm";
|
||||
|
||||
void createEditHandler() {
|
||||
void createEditHandler(bool enable) {
|
||||
if (editHandler != nullptr) server.removeHandler(editHandler);
|
||||
|
||||
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) {
|
||||
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){
|
||||
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
|
||||
return;
|
||||
}
|
||||
const String& func = request->arg(FPSTR(s_func));
|
||||
|
||||
if(func.length() == 0) {
|
||||
// default: serve the editor page
|
||||
handleStaticContent(request, FPSTR(_edit_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_edit, PAGE_edit_length);
|
||||
return;
|
||||
}
|
||||
|
||||
if (func == "list") {
|
||||
bool first = true;
|
||||
AsyncResponseStream* response = request->beginResponseStream(FPSTR(CONTENT_TYPE_JSON));
|
||||
response->addHeader(FPSTR(s_cache_control), FPSTR(s_no_store));
|
||||
response->addHeader(FPSTR(s_expires), F("0"));
|
||||
response->write('[');
|
||||
|
||||
File rootdir = WLED_FS.open("/", "r");
|
||||
File rootfile = rootdir.openNextFile();
|
||||
while (rootfile) {
|
||||
String name = rootfile.name();
|
||||
if (name.indexOf(FPSTR(s_wsec)) >= 0) {
|
||||
rootfile = rootdir.openNextFile(); // skip wsec.json
|
||||
continue;
|
||||
}
|
||||
if (!first) response->write(',');
|
||||
first = false;
|
||||
response->printf_P(PSTR("{\"name\":\"%s\",\"type\":\"file\",\"size\":%u}"), name.c_str(), rootfile.size());
|
||||
rootfile = rootdir.openNextFile();
|
||||
}
|
||||
rootfile.close();
|
||||
rootdir.close();
|
||||
response->write(']');
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
String path = request->arg(FPSTR(s_path)); // remaining functions expect a path
|
||||
|
||||
if (path.length() == 0) {
|
||||
request->send(400, FPSTR(CONTENT_TYPE_PLAIN), F("Missing path"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.charAt(0) != '/') {
|
||||
path = '/' + path; // prepend slash if missing
|
||||
}
|
||||
|
||||
if (!WLED_FS.exists(path)) {
|
||||
request->send(404, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_not_found));
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.indexOf(FPSTR(s_wsec)) >= 0) {
|
||||
request->send(403, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_accessdenied)); // skip wsec.json
|
||||
return;
|
||||
}
|
||||
|
||||
if (func == "edit") {
|
||||
request->send(WLED_FS, path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (func == "download") {
|
||||
request->send(WLED_FS, path, String(), true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (func == "delete") {
|
||||
if (!WLED_FS.remove(path))
|
||||
request->send(500, FPSTR(CONTENT_TYPE_PLAIN), F("Delete failed"));
|
||||
else
|
||||
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File deleted"));
|
||||
return;
|
||||
}
|
||||
|
||||
// unrecognized func
|
||||
request->send(400, FPSTR(CONTENT_TYPE_PLAIN), F("Invalid function"));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static bool captivePortal(AsyncWebServerRequest *request)
|
||||
@@ -452,6 +376,110 @@ void initServer()
|
||||
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), (String)getFreeHeapSize());
|
||||
});
|
||||
|
||||
// User backup endpoints
|
||||
server.on(F("/backup/config"), HTTP_POST, [](AsyncWebServerRequest *request){
|
||||
if (!correctPIN) {
|
||||
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
|
||||
return;
|
||||
}
|
||||
bool success = userBackupConfig();
|
||||
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), success ? F("Config backup created") : F("Config backup failed"));
|
||||
});
|
||||
|
||||
server.on(F("/restore/config"), HTTP_POST, [](AsyncWebServerRequest *request){
|
||||
if (!correctPIN) {
|
||||
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
|
||||
return;
|
||||
}
|
||||
bool success = userRestoreConfig();
|
||||
if (success) {
|
||||
serveMessage(request, 200, F("Configuration restored."), F("Rebooting..."), 131);
|
||||
doReboot = true;
|
||||
} else {
|
||||
request->send(400, FPSTR(CONTENT_TYPE_PLAIN), F("Config restore failed or no backup found"));
|
||||
}
|
||||
});
|
||||
|
||||
server.on(F("/backup/presets"), HTTP_POST, [](AsyncWebServerRequest *request){
|
||||
if (!correctPIN) {
|
||||
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
|
||||
return;
|
||||
}
|
||||
bool success = userBackupPresets();
|
||||
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), success ? F("Presets backup created") : F("Presets backup failed"));
|
||||
});
|
||||
|
||||
server.on(F("/restore/presets"), HTTP_POST, [](AsyncWebServerRequest *request){
|
||||
if (!correctPIN) {
|
||||
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
|
||||
return;
|
||||
}
|
||||
bool success = userRestorePresets();
|
||||
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), success ? F("Presets restored") : F("Presets restore failed or no backup found"));
|
||||
});
|
||||
|
||||
server.on(F("/backup/palettes"), HTTP_POST, [](AsyncWebServerRequest *request){
|
||||
if (!correctPIN) {
|
||||
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
|
||||
return;
|
||||
}
|
||||
int count = userBackupPalettes();
|
||||
String response = F("Palettes backup created: ");
|
||||
response += count;
|
||||
response += F(" files");
|
||||
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), response);
|
||||
});
|
||||
|
||||
server.on(F("/restore/palettes"), HTTP_POST, [](AsyncWebServerRequest *request){
|
||||
if (!correctPIN) {
|
||||
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
|
||||
return;
|
||||
}
|
||||
int count = userRestorePalettes();
|
||||
String response = F("Palettes restored: ");
|
||||
response += count;
|
||||
response += F(" files");
|
||||
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), response);
|
||||
});
|
||||
|
||||
server.on(F("/backup/mappings"), HTTP_POST, [](AsyncWebServerRequest *request){
|
||||
if (!correctPIN) {
|
||||
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
|
||||
return;
|
||||
}
|
||||
int count = userBackupMappings();
|
||||
String response = F("Mappings backup created: ");
|
||||
response += count;
|
||||
response += F(" files");
|
||||
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), response);
|
||||
});
|
||||
|
||||
server.on(F("/restore/mappings"), HTTP_POST, [](AsyncWebServerRequest *request){
|
||||
if (!correctPIN) {
|
||||
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
|
||||
return;
|
||||
}
|
||||
int count = userRestoreMappings();
|
||||
String response = F("Mappings restored: ");
|
||||
response += count;
|
||||
response += F(" files");
|
||||
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), response);
|
||||
});
|
||||
|
||||
// Check backup status endpoint
|
||||
server.on(F("/backup/status"), HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
String response = F("{\"config\":");
|
||||
response += userBackupConfigExists() ? F("true") : F("false");
|
||||
response += F(",\"presets\":");
|
||||
response += userBackupPresetsExists() ? F("true") : F("false");
|
||||
response += F(",\"palettes\":");
|
||||
response += userBackupPalettesExist() ? F("true") : F("false");
|
||||
response += F(",\"mappings\":");
|
||||
response += userBackupMappingsExist() ? F("true") : F("false");
|
||||
response += F("}");
|
||||
request->send(200, F("application/json"), response);
|
||||
});
|
||||
|
||||
#ifdef WLED_ENABLE_USERMOD_PAGE
|
||||
server.on("/u", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
handleStaticContent(request, "", 200, FPSTR(CONTENT_TYPE_HTML), PAGE_usermod, PAGE_usermod_length);
|
||||
@@ -467,7 +495,7 @@ void initServer()
|
||||
size_t len, bool isFinal) {handleUpload(request, filename, index, data, len, isFinal);}
|
||||
);
|
||||
|
||||
createEditHandler(); // initialize "/edit" handler, access is protected by "correctPIN"
|
||||
createEditHandler(correctPIN);
|
||||
|
||||
static const char _update[] PROGMEM = "/update";
|
||||
#ifndef WLED_DISABLE_OTA
|
||||
@@ -480,47 +508,59 @@ void initServer()
|
||||
});
|
||||
|
||||
server.on(_update, HTTP_POST, [](AsyncWebServerRequest *request){
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
} else {
|
||||
// No context structure - something's gone horribly wrong
|
||||
serveMessage(request, 500, F("Update failed!"), F("Internal server fault"), 254);
|
||||
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;
|
||||
}
|
||||
},[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){
|
||||
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;
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
handleOTAData(request, index, data, len, isFinal);
|
||||
});
|
||||
#else
|
||||
const auto notSupported = [](AsyncWebServerRequest *request){
|
||||
@@ -530,53 +570,6 @@ 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);
|
||||
@@ -586,31 +579,29 @@ void initServer()
|
||||
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
if (captivePortal(request)) return;
|
||||
if (!showWelcomePage || request->hasArg(F("sliders"))) {
|
||||
handleStaticContent(request, F("/index.htm"), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_index, PAGE_index_length);
|
||||
handleStaticContent(request, F("/index.htm"), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_index, PAGE_index_L);
|
||||
} else {
|
||||
serveSettings(request);
|
||||
}
|
||||
});
|
||||
|
||||
#ifndef WLED_DISABLE_2D
|
||||
#ifdef WLED_ENABLE_PIXART
|
||||
#ifdef WLED_ENABLE_PIXART
|
||||
static const char _pixart_htm[] PROGMEM = "/pixart.htm";
|
||||
server.on(_pixart_htm, HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
handleStaticContent(request, FPSTR(_pixart_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pixart, PAGE_pixart_length);
|
||||
handleStaticContent(request, FPSTR(_pixart_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pixart, PAGE_pixart_L);
|
||||
});
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef WLED_DISABLE_PXMAGIC
|
||||
#ifndef WLED_DISABLE_PXMAGIC
|
||||
static const char _pxmagic_htm[] PROGMEM = "/pxmagic.htm";
|
||||
server.on(_pxmagic_htm, HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
handleStaticContent(request, FPSTR(_pxmagic_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pxmagic, PAGE_pxmagic_length);
|
||||
handleStaticContent(request, FPSTR(_pxmagic_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pxmagic, PAGE_pxmagic_L);
|
||||
});
|
||||
#endif
|
||||
#endif
|
||||
|
||||
static const char _cpal_htm[] PROGMEM = "/cpal.htm";
|
||||
server.on(_cpal_htm, HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
handleStaticContent(request, FPSTR(_cpal_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_cpal, PAGE_cpal_length);
|
||||
handleStaticContent(request, FPSTR(_cpal_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_cpal, PAGE_cpal_L);
|
||||
});
|
||||
|
||||
#ifdef WLED_ENABLE_WEBSOCKETS
|
||||
@@ -680,8 +671,8 @@ void serveSettingsJS(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
AsyncResponseStream *response = request->beginResponseStream(FPSTR(CONTENT_TYPE_JAVASCRIPT));
|
||||
response->addHeader(FPSTR(s_cache_control), FPSTR(s_no_store));
|
||||
response->addHeader(FPSTR(s_expires), F("0"));
|
||||
response->addHeader(F("Cache-Control"), F("no-store"));
|
||||
response->addHeader(F("Expires"), F("0"));
|
||||
|
||||
response->print(F("function GetV(){var d=document;"));
|
||||
getSettingsJS(subPage, *response);
|
||||
@@ -805,6 +796,7 @@ 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;
|
||||
}
|
||||
|
||||
+1
-28
@@ -5,12 +5,6 @@
|
||||
*/
|
||||
#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;
|
||||
@@ -31,7 +25,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. 1428 bytes / ESP8266: 528 bytes)
|
||||
// the whole message is in a single frame and we got all of its data (max. 1450 bytes)
|
||||
if(info->opcode == WS_TEXT)
|
||||
{
|
||||
if (len > 0 && len < 10 && data[0] == 'p') {
|
||||
@@ -77,29 +71,8 @@ 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];
|
||||
|
||||
+14
-4
@@ -331,11 +331,11 @@ void getSettingsJS(byte subPage, Print& settingsScript)
|
||||
char ma[4] = "MA"; ma[2] = offset+s; ma[3] = 0; //max per-port PSU current
|
||||
char hs[4] = "HS"; hs[2] = offset+s; hs[3] = 0; //hostname (for network types, custom text for others)
|
||||
settingsScript.print(F("addLEDs(1);"));
|
||||
uint8_t pins[OUTPUT_MAX_PINS];
|
||||
uint8_t pins[5];
|
||||
int nPins = bus->getPins(pins);
|
||||
for (int i = 0; i < nPins; i++) {
|
||||
lp[1] = '0'+i;
|
||||
if (PinManager::isPinOk(pins[i]) || bus->isVirtual() || Bus::isHub75(bus->getType())) printSetFormValue(settingsScript,lp,pins[i]);
|
||||
if (PinManager::isPinOk(pins[i]) || bus->isVirtual()) printSetFormValue(settingsScript,lp,pins[i]);
|
||||
}
|
||||
printSetFormValue(settingsScript,lc,bus->getLength());
|
||||
printSetFormValue(settingsScript,lt,bus->getType());
|
||||
@@ -457,8 +457,8 @@ void getSettingsJS(byte subPage, Print& settingsScript)
|
||||
printSetFormCheckbox(settingsScript,PSTR("EM"),e131Multicast);
|
||||
printSetFormValue(settingsScript,PSTR("EU"),e131Universe);
|
||||
#ifdef WLED_ENABLE_DMX
|
||||
settingsScript.print(SET_F("hideNoDMX();")); // hide "not compiled in" message
|
||||
#endif
|
||||
settingsScript.print(SET_F("hideNoDMX();")); // hide "not compiled in" message
|
||||
#endif
|
||||
#ifndef WLED_ENABLE_DMX_INPUT
|
||||
settingsScript.print(SET_F("hideDMXInput();")); // hide "dmx input" settings
|
||||
#else
|
||||
@@ -671,6 +671,16 @@ 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