Compare commits

..

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 9672b9bee0 Final improvements: enhanced UI feedback and cleanup
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-09-18 08:01:52 +00:00
copilot-swe-agent[bot] 6d0921bdd7 Implement user backup functionality with web API and UI
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-09-18 07:59:15 +00:00
copilot-swe-agent[bot] ea4661e907 Initial plan 2025-09-18 07:47:38 +00:00
71 changed files with 2206 additions and 4975 deletions
+9 -43
View File
@@ -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.
+1 -1
View File
@@ -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
View File
@@ -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
-66
View File
@@ -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"
}
-47
View File
@@ -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"
}
+1 -3
View File
@@ -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}")
-116
View 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"
)
+8
View File
@@ -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
View File
@@ -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
View File
@@ -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
-7
View File
@@ -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
1 # Name, Type, SubType, Offset, Size, Flags
2 nvs, data, nvs, 0x9000, 0x5000,
3 otadata, data, ota, 0xe000, 0x2000,
4 app0, app, ota_0, 0x10000, 0x300000,
5 app1, app, ota_1, 0x310000,0x300000,
6 spiffs, data, spiffs, 0x610000,0x19E0000,
7 coredump, data, coredump,,64K
+9 -31
View File
@@ -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",
-10
View File
@@ -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 # ESP-IDF Partition Table
2 # Name, Type, SubType, Offset, Size, Flags
3 # bootloader.bin,, 0x1000, 32K
4 # partition table,, 0x8000, 4K
5 nvs, data, nvs, 0x9000, 20K,
6 otadata, data, ota, 0xe000, 8K,
7 ota_0, app, ota_0, 0x10000, 2048K,
8 ota_1, app, ota_1, 0x210000, 2048K,
9 uf2, app, factory,0x410000, 256K,
10 spiffs, data, spiffs, 0x450000, 11968K,
-11
View File
@@ -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 # ESP-IDF Partition Table
2 # Name, Type, SubType, Offset, Size, Flags
3 # bootloader.bin,, 0x1000, 32K
4 # partition table, 0x8000, 4K
5 nvs, data, nvs, 0x9000, 20K,
6 otadata, data, ota, 0xe000, 8K,
7 ota_0, 0, ota_0, 0x10000, 1408K,
8 ota_1, 0, ota_1, 0x170000, 1408K,
9 uf2, app, factory,0x2d0000, 256K,
10 spiffs, data, spiffs, 0x310000, 960K,
-10
View File
@@ -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,
1 # ESP-IDF Partition Table
2 # Name, Type, SubType, Offset, Size, Flags
3 # bootloader.bin,, 0x1000, 32K
4 # partition table,, 0x8000, 4K
5 nvs, data, nvs, 0x9000, 20K,
6 otadata, data, ota, 0xe000, 8K,
7 ota_0, app, ota_0, 0x10000, 2048K,
8 ota_1, app, ota_1, 0x210000, 2048K,
9 uf2, app, factory,0x410000, 256K,
10 spiffs, data, spiffs, 0x450000, 3776K,
+35 -78
View File
@@ -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
+3 -17
View File
@@ -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
View File
@@ -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 1D; `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 0255 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 0255) 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. (Its 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 (0255 range, usually exposed via sliders) to define the diffusion rate, mapped to 0100.
* This controls how much "heat" spreads to neighboring pixels — more diffusion = smoother flame spread.
* Next we assign `SEGMENT.intensity` (user input 0255) 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 cant 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 variablelength arrays (VLAs) is nonstandard 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. Its 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 0255 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` (0255).
* 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 theres a pixel to the left, add its heat to the total.
* If theres 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 doesnt 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, lets 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, theres 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 FastLEDs 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 doesnt 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.
+10 -11
View File
@@ -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
View File
@@ -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
-4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
-59
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+11 -5
View File
@@ -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%';
+25 -7
View File
@@ -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>
+21 -5
View File
@@ -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
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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
View File
@@ -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;
+86 -2
View File
@@ -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">&#9888; 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>&#32;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>
+1 -1
View File
@@ -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;} }
+1 -1
View File
@@ -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"];
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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
View File
@@ -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>
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
-741
View File
@@ -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
-120
View File
@@ -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
+4 -1
View File
@@ -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
-6
View File
@@ -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
+1 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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!)
+1 -1
View File
@@ -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;
-164
View File
@@ -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;
}
-61
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);