Compare commits

..

6 Commits

Author SHA1 Message Date
Blaž Kristan
72fc016d7e Bugfix 2025-11-03 14:14:37 +01:00
Blaž Kristan
454c73009a Merge branch 'main' into multibutton 2025-10-12 15:32:40 +02:00
Blaž Kristan
cbc5dec0af Merge branch 'main' into multibutton 2025-08-04 08:04:17 +02:00
Blaž Kristan
6c3f4a7c33 Have at least one button defined 2025-07-04 18:48:36 +02:00
Blaž Kristan
e670b26cd0 Remove second pin defaults 2025-07-04 18:46:54 +02:00
Blaž Kristan
5c0ec6750a Variable button count (up to 32)
- adds ability to configure variable number of buttons during runtime
- fixes #4692
2025-07-04 13:56:45 +02:00
47 changed files with 1279 additions and 3089 deletions

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"
}

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}")

View File

@@ -1,6 +1,5 @@
Import('env')
import subprocess
import json
import re
def get_github_repo():
@@ -43,7 +42,7 @@ def get_github_repo():
# Check if it's a GitHub URL
if 'github.com' not in remote_url.lower():
return None
return 'unknown'
# Parse GitHub URL patterns:
# https://github.com/owner/repo.git
@@ -64,53 +63,17 @@ def get_github_repo():
if ssh_match:
return ssh_match.group(1)
return None
return 'unknown'
except FileNotFoundError:
# Git CLI is not installed or not in PATH
return None
return 'unknown'
except subprocess.CalledProcessError:
# Git command failed (e.g., not a git repo, no remote, etc.)
return None
return 'unknown'
except Exception:
# Any other unexpected error
return None
return 'unknown'
# 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"
)
repo = get_github_repo()
env.Append(BUILD_FLAGS=[f'-DWLED_REPO=\\"{repo}\\"'])

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}"])

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,8 @@ ldscript_4m1m = eagle.flash.4m1m.ld
[scripts_defaults]
extra_scripts =
pre:pio-scripts/set_metadata.py
pre:pio-scripts/set_version.py
pre:pio-scripts/set_repo.py
post:pio-scripts/output_bins.py
post:pio-scripts/strip-floats.py
pre:pio-scripts/user_config_copy.py
@@ -284,14 +265,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 +285,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 +303,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 +321,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,31 +429,21 @@ 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}
@@ -489,11 +455,9 @@ board_build.flash_mode = dio
[env:esp32dev_16M]
board = esp32dev
platform = ${esp32_idf_V4.platform}
platform_packages = ${esp32_idf_V4.platform_packages}
custom_usermods = audioreactive
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_16M\" #-D WLED_DISABLE_BROWNOUT_DET
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
lib_deps = ${esp32_idf_V4.lib_deps}
monitor_filters = esp32_exception_decoder
board_build.partitions = ${esp32.extreme_partitions}
@@ -505,12 +469,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 +487,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 +494,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 +505,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 +532,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 +550,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 +571,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 +591,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 +617,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

View File

@@ -191,22 +191,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
@@ -557,15 +541,12 @@ build_flags = ${common.build_flags}
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
-D WLED_DEBUG_BUS
; -D WLED_DEBUG
-D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
lib_deps = ${esp32_idf_V4.lib_deps}
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#3.0.11
monitor_filters = esp32_exception_decoder
board_build.partitions = ${esp32.default_partitions}
board_build.flash_mode = dio
custom_usermods = audioreactive
[env:esp32dev_hub75_forum_pinout]
extends = env:esp32dev_hub75
@@ -574,22 +555,19 @@ build_flags = ${common.build_flags}
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
-D ESP32_FORUM_PINOUT ;; enable for SmartMatrix default pins
-D WLED_DEBUG_BUS
-D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
; -D WLED_DEBUG
[env:adafruit_matrixportal_esp32s3]
; ESP32-S3 processor, 8 MB flash, 2 MB of PSRAM, dedicated driver pins for HUB75
board = adafruit_matrixportal_esp32s3
;; adafruit recommends to use arduino-esp32 2.0.14
;;platform = espressif32@ ~6.5.0
;;platform_packages = platformio/framework-arduinoespressif32 @ 3.20014.231204 ;; arduino-esp32 2.0.14
platform = ${esp32s3.platform}
platform_packages =
upload_speed = 921600
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8M_qspi\"
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_4M_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
@@ -597,30 +575,25 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=
-D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips
-D ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3
-D WLED_DEBUG_BUS
-D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
lib_deps = ${esp32s3.lib_deps}
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix
board_build.partitions = ${esp32.large_partitions} ;; standard bootloader and 8MB Flash partitions
;; board_build.partitions = tools/partitions-8MB_spiffs-tinyuf2.csv ;; supports adafruit UF2 bootloader
board_build.partitions = ${esp32.default_partitions}
board_build.f_flash = 80000000L
board_build.flash_mode = qio
monitor_filters = esp32_exception_decoder
custom_usermods = audioreactive
[env:esp32S3_PSRAM_HUB75]
;; MOONHUB HUB75 adapter board (lilygo T7-S3 with 16MB flash and PSRAM)
;; MOONHUB HUB75 adapter board
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")
-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
@@ -628,15 +601,11 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=
-D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips
-D MOONHUB_S3_PINOUT ;; HUB75 pinout
-D WLED_DEBUG_BUS
-D LEDPIN=14 -D BTNPIN=0 -D RLYPIN=15 -D IRPIN=-1 -D AUDIOPIN=-1 ;; defaults that avoid pin conflicts with HUB75
-D SR_DMTYPE=1 -D I2S_SDPIN=10 -D I2S_CKPIN=11 -D I2S_WSPIN=12 -D MCLK_PIN=-1 ;; I2S mic
lib_deps = ${esp32s3.lib_deps}
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix
;;board_build.partitions = ${esp32.large_partitions} ;; for 8MB flash
board_build.partitions = ${esp32.extreme_partitions} ;; for 16MB flash
board_build.partitions = ${esp32.default_partitions}
board_build.f_flash = 80000000L
board_build.flash_mode = qio
monitor_filters = esp32_exception_decoder
custom_usermods = audioreactive

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

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 = `
@@ -246,21 +246,6 @@ writeHtmlGzipped("wled00/data/index.htm", "wled00/html_ui.h", 'index');
writeHtmlGzipped("wled00/data/pixart/pixart.htm", "wled00/html_pixart.h", 'pixart');
//writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal');
writeHtmlGzipped("wled00/data/pxmagic/pxmagic.htm", "wled00/html_pxmagic.h", 'pxmagic');
//writeHtmlGzipped("wled00/data/edit.htm", "wled00/html_edit.h", 'edit');
writeChunks(
"wled00/data",
[
{
file: "edit.htm",
name: "PAGE_edit",
method: "gzip",
filter: "html-minify"
}
],
"wled00/html_edit.h"
);
writeChunks(
"wled00/data/cpal",
@@ -403,6 +388,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",

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,

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,

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,

View File

@@ -313,11 +313,11 @@ class MyExampleUsermod : public Usermod {
yield();
// ignore certain button types as they may have other consequences
if (!enabled
|| buttonType[b] == BTN_TYPE_NONE
|| buttonType[b] == BTN_TYPE_RESERVED
|| buttonType[b] == BTN_TYPE_PIR_SENSOR
|| buttonType[b] == BTN_TYPE_ANALOG
|| buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|| buttons[b].type == BTN_TYPE_NONE
|| buttons[b].type == BTN_TYPE_RESERVED
|| buttons[b].type == BTN_TYPE_PIR_SENSOR
|| buttons[b].type == BTN_TYPE_ANALOG
|| buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
return false;
}

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
@@ -1544,7 +1530,7 @@ class AudioReactive : public Usermod {
// better would be for AudioSource to implement getType()
if (enabled
&& dmType == 0 && audioPin>=0
&& (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED)
&& (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED)
) {
return true;
}

View File

@@ -562,11 +562,11 @@ void MultiRelay::loop() {
bool MultiRelay::handleButton(uint8_t b) {
yield();
if (!enabled
|| buttonType[b] == BTN_TYPE_NONE
|| buttonType[b] == BTN_TYPE_RESERVED
|| buttonType[b] == BTN_TYPE_PIR_SENSOR
|| buttonType[b] == BTN_TYPE_ANALOG
|| buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|| buttons[b].type == BTN_TYPE_NONE
|| buttons[b].type == BTN_TYPE_RESERVED
|| buttons[b].type == BTN_TYPE_PIR_SENSOR
|| buttons[b].type == BTN_TYPE_ANALOG
|| buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
return false;
}
@@ -581,20 +581,20 @@ bool MultiRelay::handleButton(uint8_t b) {
unsigned long now = millis();
//button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0)
if (buttonType[b] == BTN_TYPE_SWITCH) {
if (buttons[b].type == BTN_TYPE_SWITCH) {
//handleSwitch(b);
if (buttonPressedBefore[b] != isButtonPressed(b)) {
buttonPressedTime[b] = now;
buttonPressedBefore[b] = !buttonPressedBefore[b];
if (buttons[b].pressedBefore != isButtonPressed(b)) {
buttons[b].pressedTime = now;
buttons[b].pressedBefore = !buttons[b].pressedBefore;
}
if (buttonLongPressed[b] == buttonPressedBefore[b]) return handled;
if (buttons[b].longPressed == buttons[b].pressedBefore) return handled;
if (now - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
if (now - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
if (_relay[i].button == b) {
switchRelay(i, buttonPressedBefore[b]);
buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state
switchRelay(i, buttons[b].pressedBefore);
buttons[b].longPressed = buttons[b].pressedBefore; //save the last "long term" switch state
}
}
}
@@ -604,40 +604,40 @@ bool MultiRelay::handleButton(uint8_t b) {
//momentary button logic
if (isButtonPressed(b)) { //pressed
if (!buttonPressedBefore[b]) buttonPressedTime[b] = now;
buttonPressedBefore[b] = true;
if (!buttons[b].pressedBefore) buttons[b].pressedTime = now;
buttons[b].pressedBefore = true;
if (now - buttonPressedTime[b] > 600) { //long press
if (now - buttons[b].pressedTime > 600) { //long press
//longPressAction(b); //not exposed
//handled = false; //use if you want to pass to default behaviour
buttonLongPressed[b] = true;
buttons[b].longPressed = true;
}
} else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released
} else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released
long dur = now - buttonPressedTime[b];
long dur = now - buttons[b].pressedTime;
if (dur < WLED_DEBOUNCE_THRESHOLD) {
buttonPressedBefore[b] = false;
buttons[b].pressedBefore = false;
return handled;
} //too short "press", debounce
bool doublePress = buttonWaitTime[b]; //did we have short press before?
buttonWaitTime[b] = 0;
bool doublePress = buttons[b].waitTime; //did we have short press before?
buttons[b].waitTime = 0;
if (!buttonLongPressed[b]) { //short press
if (!buttons[b].longPressed) { //short press
// if this is second release within 350ms it is a double press (buttonWaitTime!=0)
if (doublePress) {
//doublePressAction(b); //not exposed
//handled = false; //use if you want to pass to default behaviour
} else {
buttonWaitTime[b] = now;
buttons[b].waitTime = now;
}
}
buttonPressedBefore[b] = false;
buttonLongPressed[b] = false;
buttons[b].pressedBefore = false;
buttons[b].longPressed = false;
}
// if 350ms elapsed since last press/release it is a short press
if (buttonWaitTime[b] && now - buttonWaitTime[b] > 350 && !buttonPressedBefore[b]) {
buttonWaitTime[b] = 0;
if (buttons[b].waitTime && now - buttons[b].waitTime > 350 && !buttons[b].pressedBefore) {
buttons[b].waitTime = 0;
//shortPressAction(b); //not exposed
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
if (_relay[i].button == b) {

View File

@@ -461,11 +461,11 @@ class PixelsDiceTrayUsermod : public Usermod {
#if USING_TFT_DISPLAY
bool handleButton(uint8_t b) override {
if (!enabled || b > 1 // buttons 0,1 only
|| buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_NONE ||
buttonType[b] == BTN_TYPE_RESERVED ||
buttonType[b] == BTN_TYPE_PIR_SENSOR ||
buttonType[b] == BTN_TYPE_ANALOG ||
buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|| buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_NONE ||
buttons[b].type == BTN_TYPE_RESERVED ||
buttons[b].type == BTN_TYPE_PIR_SENSOR ||
buttons[b].type == BTN_TYPE_ANALOG ||
buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
return false;
}
@@ -476,43 +476,43 @@ class PixelsDiceTrayUsermod : public Usermod {
static unsigned long buttonWaitTime[2] = {0};
//momentary button logic
if (!buttonLongPressed[b] && isButtonPressed(b)) { //pressed
if (!buttonPressedBefore[b]) {
buttonPressedTime[b] = now;
if (!buttons[b].longPressed && isButtonPressed(b)) { //pressed
if (!buttons[b].pressedBefore) {
buttons[b].pressedTime = now;
}
buttonPressedBefore[b] = true;
buttons[b].pressedBefore = true;
if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press
if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press
menu_ctrl.HandleButton(ButtonType::LONG, b);
buttonLongPressed[b] = true;
buttons[b].longPressed = true;
return true;
}
} else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released
} else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released
long dur = now - buttonPressedTime[b];
long dur = now - buttons[b].pressedTime;
if (dur < WLED_DEBOUNCE_THRESHOLD) {
buttonPressedBefore[b] = false;
buttons[b].pressedBefore = false;
return true;
} //too short "press", debounce
bool doublePress = buttonWaitTime[b]; //did we have short press before?
buttonWaitTime[b] = 0;
bool doublePress = buttons[b].waitTime; //did we have short press before?
buttons[b].waitTime = 0;
if (!buttonLongPressed[b]) { //short press
if (!buttons[b].longPressed) { //short press
// if this is second release within 350ms it is a double press (buttonWaitTime!=0)
if (doublePress) {
menu_ctrl.HandleButton(ButtonType::DOUBLE, b);
} else {
buttonWaitTime[b] = now;
buttons[b].waitTime = now;
}
}
buttonPressedBefore[b] = false;
buttonLongPressed[b] = false;
buttons[b].pressedBefore = false;
buttons[b].longPressed = false;
}
// if 350ms elapsed since last press/release it is a short press
if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS &&
!buttonPressedBefore[b]) {
buttonWaitTime[b] = 0;
if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS &&
!buttons[b].pressedBefore) {
buttons[b].waitTime = 0;
menu_ctrl.HandleButton(ButtonType::SINGLE, b);
}

View File

@@ -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)
{

View File

@@ -749,12 +749,12 @@ bool FourLineDisplayUsermod::handleButton(uint8_t b) {
yield();
if (!enabled
|| b // button 0 only
|| buttonType[b] == BTN_TYPE_SWITCH
|| buttonType[b] == BTN_TYPE_NONE
|| buttonType[b] == BTN_TYPE_RESERVED
|| buttonType[b] == BTN_TYPE_PIR_SENSOR
|| buttonType[b] == BTN_TYPE_ANALOG
|| buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|| buttons[b].type == BTN_TYPE_SWITCH
|| buttons[b].type == BTN_TYPE_NONE
|| buttons[b].type == BTN_TYPE_RESERVED
|| buttons[b].type == BTN_TYPE_PIR_SENSOR
|| buttons[b].type == BTN_TYPE_ANALOG
|| buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
return false;
}

View File

@@ -678,7 +678,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++)
{
@@ -4509,7 +4509,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 +4669,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 +4681,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
@@ -5195,162 +5196,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";
/////////////////////////
@@ -9677,11 +9628,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 +9642,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 +9661,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 +9692,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;

View File

@@ -17,7 +17,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 +45,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 +113,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:"));

View File

@@ -1681,17 +1681,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 +1810,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 +1818,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 +1889,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 +1951,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 +1980,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 +2012,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 +2024,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 +2040,8 @@ bool WS2812FX::deserializeMap(unsigned n) {
DEBUG_PRINTLN(F("ERROR LED map allocation error."));
}
resume();
releaseJSONBufferLock();
return (customMappingSize > 0);
}

View File

@@ -17,13 +17,13 @@ static bool buttonBriDirection = false; // true: increase brightness, false: dec
void shortPressAction(uint8_t b)
{
if (!macroButton[b]) {
if (!buttons[b].macroButton) {
switch (b) {
case 0: toggleOnOff(); stateUpdated(CALL_MODE_BUTTON); break;
case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break;
}
} else {
applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET);
applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET);
}
#ifndef WLED_DISABLE_MQTT
@@ -38,7 +38,7 @@ void shortPressAction(uint8_t b)
void longPressAction(uint8_t b)
{
if (!macroLongPress[b]) {
if (!buttons[b].macroLongPress) {
switch (b) {
case 0: setRandomColor(colPri); colorUpdated(CALL_MODE_BUTTON); break;
case 1:
@@ -52,11 +52,11 @@ void longPressAction(uint8_t b)
else bri -= WLED_LONG_BRI_STEPS;
}
stateUpdated(CALL_MODE_BUTTON);
buttonPressedTime[b] = millis();
buttons[b].pressedTime = millis();
break; // repeatable action
}
} else {
applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET);
applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET);
}
#ifndef WLED_DISABLE_MQTT
@@ -71,13 +71,13 @@ void longPressAction(uint8_t b)
void doublePressAction(uint8_t b)
{
if (!macroDoublePress[b]) {
if (!buttons[b].macroDoublePress) {
switch (b) {
//case 0: toggleOnOff(); colorUpdated(CALL_MODE_BUTTON); break; //instant short press on button 0 if no macro set
case 1: ++effectPalette %= getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break;
}
} else {
applyPreset(macroDoublePress[b], CALL_MODE_BUTTON_PRESET);
applyPreset(buttons[b].macroDoublePress, CALL_MODE_BUTTON_PRESET);
}
#ifndef WLED_DISABLE_MQTT
@@ -92,10 +92,10 @@ void doublePressAction(uint8_t b)
bool isButtonPressed(uint8_t b)
{
if (btnPin[b]<0) return false;
unsigned pin = btnPin[b];
if (buttons[b].pin < 0) return false;
unsigned pin = buttons[b].pin;
switch (buttonType[b]) {
switch (buttons[b].type) {
case BTN_TYPE_NONE:
case BTN_TYPE_RESERVED:
break;
@@ -113,7 +113,7 @@ bool isButtonPressed(uint8_t b)
#ifdef SOC_TOUCH_VERSION_2 //ESP32 S2 and S3 provide a function to check touch state (state is updated in interrupt)
if (touchInterruptGetLastStatus(pin)) return true;
#else
if (digitalPinToTouchChannel(btnPin[b]) >= 0 && touchRead(pin) <= touchThreshold) return true;
if (digitalPinToTouchChannel(pin) >= 0 && touchRead(pin) <= touchThreshold) return true;
#endif
#endif
break;
@@ -124,25 +124,25 @@ bool isButtonPressed(uint8_t b)
void handleSwitch(uint8_t b)
{
// isButtonPressed() handles inverted/noninverted logic
if (buttonPressedBefore[b] != isButtonPressed(b)) {
if (buttons[b].pressedBefore != isButtonPressed(b)) {
DEBUG_PRINTF_P(PSTR("Switch: State changed %u\n"), b);
buttonPressedTime[b] = millis();
buttonPressedBefore[b] = !buttonPressedBefore[b];
buttons[b].pressedTime = millis();
buttons[b].pressedBefore = !buttons[b].pressedBefore; // toggle pressed state
}
if (buttonLongPressed[b] == buttonPressedBefore[b]) return;
if (buttons[b].longPressed == buttons[b].pressedBefore) return;
if (millis() - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
if (millis() - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
DEBUG_PRINTF_P(PSTR("Switch: Activating %u\n"), b);
if (!buttonPressedBefore[b]) { // on -> off
if (!buttons[b].pressedBefore) { // on -> off
DEBUG_PRINTF_P(PSTR("Switch: On -> Off (%u)\n"), b);
if (macroButton[b]) applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET);
if (buttons[b].macroButton) applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET);
else { //turn on
if (!bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);}
}
} else { // off -> on
DEBUG_PRINTF_P(PSTR("Switch: Off -> On (%u)\n"), b);
if (macroLongPress[b]) applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET);
if (buttons[b].macroLongPress) applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET);
else { //turn off
if (bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);}
}
@@ -152,13 +152,13 @@ void handleSwitch(uint8_t b)
// publish MQTT message
if (buttonPublishMqtt && WLED_MQTT_CONNECTED) {
char subuf[MQTT_MAX_TOPIC_LEN + 32];
if (buttonType[b] == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b);
if (buttons[b].type == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b);
else sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b);
mqtt->publish(subuf, 0, false, !buttonPressedBefore[b] ? "off" : "on");
mqtt->publish(subuf, 0, false, !buttons[b].pressedBefore ? "off" : "on");
}
#endif
buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state
buttons[b].longPressed = buttons[b].pressedBefore; //save the last "long term" switch state
}
}
@@ -178,17 +178,17 @@ void handleAnalog(uint8_t b)
#ifdef ESP8266
rawReading = analogRead(A0) << 2; // convert 10bit read to 12bit
#else
if ((btnPin[b] < 0) /*|| (digitalPinToAnalogChannel(btnPin[b]) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise
rawReading = analogRead(btnPin[b]); // collect at full 12bit resolution
if ((buttons[b].pin < 0) /*|| (digitalPinToAnalogChannel(buttons[b].pin) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise
rawReading = analogRead(buttons[b].pin); // collect at full 12bit resolution
#endif
yield(); // keep WiFi task running - analog read may take several millis on ESP8266
filteredReading[b] += POT_SMOOTHING * ((float(rawReading) / 16.0f) - filteredReading[b]); // filter raw input, and scale to [0..255]
unsigned aRead = max(min(int(filteredReading[b]), 255), 0); // squash into 8bit
if(aRead <= POT_SENSITIVITY) aRead = 0; // make sure that 0 and 255 are used
if(aRead >= 255-POT_SENSITIVITY) aRead = 255;
if (aRead <= POT_SENSITIVITY) aRead = 0; // make sure that 0 and 255 are used
if (aRead >= 255-POT_SENSITIVITY) aRead = 255;
if (buttonType[b] == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead;
if (buttons[b].type == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead;
// remove noise & reduce frequency of UI updates
if (abs(int(aRead) - int(oldRead[b])) <= POT_SENSITIVITY) return; // no significant change in reading
@@ -206,10 +206,10 @@ void handleAnalog(uint8_t b)
oldRead[b] = aRead;
// if no macro for "short press" and "long press" is defined use brightness control
if (!macroButton[b] && !macroLongPress[b]) {
DEBUG_PRINTF_P(PSTR("Analog: Action = %u\n"), macroDoublePress[b]);
if (!buttons[b].macroButton && !buttons[b].macroLongPress) {
DEBUG_PRINTF_P(PSTR("Analog: Action = %u\n"), buttons[b].macroDoublePress);
// if "double press" macro defines which option to change
if (macroDoublePress[b] >= 250) {
if (buttons[b].macroDoublePress >= 250) {
// global brightness
if (aRead == 0) {
briLast = bri;
@@ -218,27 +218,30 @@ void handleAnalog(uint8_t b)
if (bri == 0) strip.restartRuntime();
bri = aRead;
}
} else if (macroDoublePress[b] == 249) {
} else if (buttons[b].macroDoublePress == 249) {
// effect speed
effectSpeed = aRead;
} else if (macroDoublePress[b] == 248) {
} else if (buttons[b].macroDoublePress == 248) {
// effect intensity
effectIntensity = aRead;
} else if (macroDoublePress[b] == 247) {
} else if (buttons[b].macroDoublePress == 247) {
// selected palette
effectPalette = map(aRead, 0, 252, 0, getPaletteCount()-1);
effectPalette = constrain(effectPalette, 0, getPaletteCount()-1); // map is allowed to "overshoot", so we need to contrain the result
} else if (macroDoublePress[b] == 200) {
} else if (buttons[b].macroDoublePress == 200) {
// primary color, hue, full saturation
colorHStoRGB(aRead*256,255,colPri);
colorHStoRGB(aRead*256, 255, colPri);
} else {
// otherwise use "double press" for segment selection
Segment& seg = strip.getSegment(macroDoublePress[b]);
Segment& seg = strip.getSegment(buttons[b].macroDoublePress);
if (aRead == 0) {
seg.setOption(SEG_OPTION_ON, false); // off (use transition)
seg.on = false; // do not use transition
//seg.setOption(SEG_OPTION_ON, false); // off (use transition)
} else {
seg.setOpacity(aRead);
seg.setOption(SEG_OPTION_ON, true); // on (use transition)
seg.opacity = aRead; // set brightness (opacity) of segment
seg.on = true;
//seg.setOpacity(aRead);
//seg.setOption(SEG_OPTION_ON, true); // on (use transition)
}
// this will notify clients of update (websockets,mqtt,etc)
updateInterfaces(CALL_MODE_BUTTON);
@@ -261,16 +264,16 @@ void handleButton()
if (strip.isUpdating() && (now - lastRun < ANALOG_BTN_READ_CYCLE+1)) return; // don't interfere with strip update (unless strip is updating continuously, e.g. very long strips)
lastRun = now;
for (unsigned b=0; b<WLED_MAX_BUTTONS; b++) {
for (unsigned b = 0; b < buttons.size(); b++) {
#ifdef ESP8266
if ((btnPin[b]<0 && !(buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED)) || buttonType[b] == BTN_TYPE_NONE) continue;
if ((buttons[b].pin < 0 && !(buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED)) || buttons[b].type == BTN_TYPE_NONE) continue;
#else
if (btnPin[b]<0 || buttonType[b] == BTN_TYPE_NONE) continue;
if (buttons[b].pin < 0 || buttons[b].type == BTN_TYPE_NONE) continue;
#endif
if (UsermodManager::handleButton(b)) continue; // did usermod handle buttons
if (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer
if (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer
if (now - lastAnalogRead > ANALOG_BTN_READ_CYCLE) {
handleAnalog(b);
}
@@ -278,7 +281,7 @@ void handleButton()
}
// button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0)
if (buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_TOUCH_SWITCH || buttonType[b] == BTN_TYPE_PIR_SENSOR) {
if (buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_TOUCH_SWITCH || buttons[b].type == BTN_TYPE_PIR_SENSOR) {
handleSwitch(b);
continue;
}
@@ -287,40 +290,39 @@ void handleButton()
if (isButtonPressed(b)) { // pressed
// if all macros are the same, fire action immediately on rising edge
if (macroButton[b] && macroButton[b] == macroLongPress[b] && macroButton[b] == macroDoublePress[b]) {
if (!buttonPressedBefore[b])
shortPressAction(b);
buttonPressedBefore[b] = true;
buttonPressedTime[b] = now; // continually update (for debouncing to work in release handler)
if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) {
if (!buttons[b].pressedBefore) shortPressAction(b);
buttons[b].pressedBefore = true;
buttons[b].pressedTime = now; // continually update (for debouncing to work in release handler)
continue;
}
if (!buttonPressedBefore[b]) buttonPressedTime[b] = now;
buttonPressedBefore[b] = true;
if (!buttons[b].pressedBefore) buttons[b].pressedTime = now;
buttons[b].pressedBefore = true;
if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press
if (!buttonLongPressed[b]) {
if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press
if (!buttons[b].longPressed) {
buttonBriDirection = !buttonBriDirection; //toggle brightness direction on long press
longPressAction(b);
} else if (b) { //repeatable action (~5 times per s) on button > 0
longPressAction(b);
buttonPressedTime[b] = now - WLED_LONG_REPEATED_ACTION; //200ms
buttons[b].pressedTime = now - WLED_LONG_REPEATED_ACTION; //200ms
}
buttonLongPressed[b] = true;
buttons[b].longPressed = true;
}
} else if (buttonPressedBefore[b]) { //released
long dur = now - buttonPressedTime[b];
} else if (buttons[b].pressedBefore) { //released
long dur = now - buttons[b].pressedTime;
// released after rising-edge short press action
if (macroButton[b] && macroButton[b] == macroLongPress[b] && macroButton[b] == macroDoublePress[b]) {
if (dur > WLED_DEBOUNCE_THRESHOLD) buttonPressedBefore[b] = false; // debounce, blocks button for 50 ms once it has been released
if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) {
if (dur > WLED_DEBOUNCE_THRESHOLD) buttons[b].pressedBefore = false; // debounce, blocks button for 50 ms once it has been released
continue;
}
if (dur < WLED_DEBOUNCE_THRESHOLD) {buttonPressedBefore[b] = false; continue;} // too short "press", debounce
bool doublePress = buttonWaitTime[b]; //did we have a short press before?
buttonWaitTime[b] = 0;
if (dur < WLED_DEBOUNCE_THRESHOLD) {buttons[b].pressedBefore = false; continue;} // too short "press", debounce
bool doublePress = buttons[b].waitTime; //did we have a short press before?
buttons[b].waitTime = 0;
if (b == 0 && dur > WLED_LONG_AP) { // long press on button 0 (when released)
if (dur > WLED_LONG_FACTORY_RESET) { // factory reset if pressed > 10 seconds
@@ -332,25 +334,25 @@ void handleButton()
} else {
WLED::instance().initAP(true);
}
} else if (!buttonLongPressed[b]) { //short press
} else if (!buttons[b].longPressed) { //short press
//NOTE: this interferes with double click handling in usermods so usermod needs to implement full button handling
if (b != 1 && !macroDoublePress[b]) { //don't wait for double press on buttons without a default action if no double press macro set
if (b != 1 && !buttons[b].macroDoublePress) { //don't wait for double press on buttons without a default action if no double press macro set
shortPressAction(b);
} else { //double press if less than 350 ms between current press and previous short press release (buttonWaitTime!=0)
if (doublePress) {
doublePressAction(b);
} else {
buttonWaitTime[b] = now;
buttons[b].waitTime = now;
}
}
}
buttonPressedBefore[b] = false;
buttonLongPressed[b] = false;
buttons[b].pressedBefore = false;
buttons[b].longPressed = false;
}
//if 350ms elapsed since last short press release it is a short press
if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS && !buttonPressedBefore[b]) {
buttonWaitTime[b] = 0;
if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS && !buttons[b].pressedBefore) {
buttons[b].waitTime = 0;
shortPressAction(b);
}
}

View File

@@ -345,97 +345,91 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
JsonArray hw_btn_ins = btn_obj["ins"];
if (!hw_btn_ins.isNull()) {
// deallocate existing button pins
for (unsigned b = 0; b < WLED_MAX_BUTTONS; b++) PinManager::deallocatePin(btnPin[b], PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button
for (const auto &button : buttons) PinManager::deallocatePin(button.pin, PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button
buttons.clear(); // clear existing buttons
unsigned s = 0;
for (JsonObject btn : hw_btn_ins) {
CJSON(buttonType[s], btn["type"]);
int8_t pin = btn["pin"][0] | -1;
uint8_t type = btn["type"] | BTN_TYPE_NONE;
int8_t pin = btn["pin"][0] | -1;
if (pin > -1 && PinManager::allocatePin(pin, false, PinOwner::Button)) {
btnPin[s] = pin;
#ifdef ARDUINO_ARCH_ESP32
#ifdef ARDUINO_ARCH_ESP32
// ESP32 only: check that analog button pin is a valid ADC gpio
if ((buttonType[s] == BTN_TYPE_ANALOG) || (buttonType[s] == BTN_TYPE_ANALOG_INVERTED)) {
if (digitalPinToAnalogChannel(btnPin[s]) < 0) {
if ((type == BTN_TYPE_ANALOG) || (type == BTN_TYPE_ANALOG_INVERTED)) {
if (digitalPinToAnalogChannel(pin) < 0) {
// not an ADC analog pin
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[s], s);
btnPin[s] = -1;
PinManager::deallocatePin(pin,PinOwner::Button);
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), pin, s);
PinManager::deallocatePin(pin, PinOwner::Button);
pin = -1;
continue;
} else {
analogReadResolution(12); // see #4040
}
}
else if ((buttonType[s] == BTN_TYPE_TOUCH || buttonType[s] == BTN_TYPE_TOUCH_SWITCH))
{
if (digitalPinToTouchChannel(btnPin[s]) < 0) {
} else if ((type == BTN_TYPE_TOUCH || type == BTN_TYPE_TOUCH_SWITCH)) {
if (digitalPinToTouchChannel(pin) < 0) {
// not a touch pin
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\n"), btnPin[s], s);
btnPin[s] = -1;
PinManager::deallocatePin(pin,PinOwner::Button);
}
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\n"), pin, s);
PinManager::deallocatePin(pin, PinOwner::Button);
pin = -1;
continue;
}
//if touch pin, enable the touch interrupt on ESP32 S2 & S3
#ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state but need to attach an interrupt to do so
else
{
touchAttachInterrupt(btnPin[s], touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
}
else touchAttachInterrupt(pin, touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
#endif
}
else
#endif
} else
#endif
{
// regular buttons and switches
if (disablePullUp) {
pinMode(btnPin[s], INPUT);
pinMode(pin, INPUT);
} else {
#ifdef ESP32
pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
pinMode(pin, type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
#else
pinMode(btnPin[s], INPUT_PULLUP);
pinMode(pin, INPUT_PULLUP);
#endif
}
}
} else {
btnPin[s] = -1;
JsonArray hw_btn_ins_0_macros = btn["macros"];
uint8_t press = hw_btn_ins_0_macros[0] | 0;
uint8_t longPress = hw_btn_ins_0_macros[1] | 0;
uint8_t doublePress = hw_btn_ins_0_macros[2] | 0;
buttons.emplace_back(pin, type, press, longPress, doublePress); // add button to vector
}
JsonArray hw_btn_ins_0_macros = btn["macros"];
CJSON(macroButton[s], hw_btn_ins_0_macros[0]);
CJSON(macroLongPress[s],hw_btn_ins_0_macros[1]);
CJSON(macroDoublePress[s], hw_btn_ins_0_macros[2]);
if (++s >= WLED_MAX_BUTTONS) break; // max buttons reached
}
// clear remaining buttons
for (; s<WLED_MAX_BUTTONS; s++) {
btnPin[s] = -1;
buttonType[s] = BTN_TYPE_NONE;
macroButton[s] = 0;
macroLongPress[s] = 0;
macroDoublePress[s] = 0;
}
} else if (fromFS) {
// new install/missing configuration (button 0 has defaults)
// relies upon only being called once with fromFS == true, which is currently true.
for (size_t s = 0; s < WLED_MAX_BUTTONS; s++) {
if (buttonType[s] == BTN_TYPE_NONE || btnPin[s] < 0 || !PinManager::allocatePin(btnPin[s], false, PinOwner::Button)) {
btnPin[s] = -1;
buttonType[s] = BTN_TYPE_NONE;
constexpr uint8_t defTypes[] = {BTNTYPE};
constexpr int8_t defPins[] = {BTNPIN};
constexpr unsigned numTypes = (sizeof(defTypes) / sizeof(defTypes[0]));
constexpr unsigned numPins = (sizeof(defPins) / sizeof(defPins[0]));
// check if the number of pins and types are valid; count of pins must be greater than or equal to types
static_assert(numTypes <= numPins, "The default button pins defined in BTNPIN do not match the button types defined in BTNTYPE");
uint8_t type = BTN_TYPE_NONE;
buttons.clear(); // clear existing buttons (just in case)
for (size_t s = 0; s < WLED_MAX_BUTTONS && s < numPins; s++) {
type = defTypes[s < numTypes ? s : numTypes - 1]; // use last known type to set current type if types less than pins
if (type == BTN_TYPE_NONE || defPins[s] < 0 || !PinManager::allocatePin(defPins[s], false, PinOwner::Button)) {
if (buttons.size() == 0) buttons.emplace_back(-1, BTN_TYPE_NONE); // add disabled button to vector (so we have at least one button defined)
continue; // pin not available or invalid, skip configuring this GPIO
}
if (btnPin[s] >= 0) {
if (disablePullUp) {
pinMode(btnPin[s], INPUT);
} else {
#ifdef ESP32
pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
#else
pinMode(btnPin[s], INPUT_PULLUP);
#endif
}
if (disablePullUp) {
pinMode(defPins[s], INPUT);
} else {
#ifdef ESP32
pinMode(defPins[s], type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
#else
pinMode(defPins[s], INPUT_PULLUP);
#endif
}
macroButton[s] = 0;
macroLongPress[s] = 0;
macroDoublePress[s] = 0;
buttons.emplace_back(defPins[s], type); // add button to vector
}
}
CJSON(buttonPublishMqtt,btn_obj["mqtt"]);
CJSON(buttonPublishMqtt, btn_obj["mqtt"]);
#ifndef WLED_DISABLE_INFRARED
int hw_ir_pin = hw["ir"]["pin"] | -2; // 4
@@ -777,10 +771,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() {
@@ -1016,15 +1006,15 @@ void serializeConfig(JsonObject root) {
JsonArray hw_btn_ins = hw_btn.createNestedArray("ins");
// configuration for all buttons
for (int i = 0; i < WLED_MAX_BUTTONS; i++) {
for (const auto &button : buttons) {
JsonObject hw_btn_ins_0 = hw_btn_ins.createNestedObject();
hw_btn_ins_0["type"] = buttonType[i];
hw_btn_ins_0["type"] = button.type;
JsonArray hw_btn_ins_0_pin = hw_btn_ins_0.createNestedArray("pin");
hw_btn_ins_0_pin.add(btnPin[i]);
hw_btn_ins_0_pin.add(button.pin);
JsonArray hw_btn_ins_0_macros = hw_btn_ins_0.createNestedArray("macros");
hw_btn_ins_0_macros.add(macroButton[i]);
hw_btn_ins_0_macros.add(macroLongPress[i]);
hw_btn_ins_0_macros.add(macroDoublePress[i]);
hw_btn_ins_0_macros.add(button.macroButton);
hw_btn_ins_0_macros.add(button.macroLongPress);
hw_btn_ins_0_macros.add(button.macroDoublePress);
}
hw_btn[F("tt")] = touchThreshold;

View File

@@ -102,9 +102,9 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#ifndef WLED_MAX_BUTTONS
#ifdef ESP8266
#define WLED_MAX_BUTTONS 2
#define WLED_MAX_BUTTONS 10
#else
#define WLED_MAX_BUTTONS 4
#define WLED_MAX_BUTTONS 32
#endif
#else
#if WLED_MAX_BUTTONS < 2

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;
}

File diff suppressed because it is too large Load Diff

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;
@@ -3150,6 +3151,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%';

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>

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

View File

@@ -6,7 +6,7 @@
<title>LED Settings</title>
<script src="common.js" type="text/javascript"></script>
<script>
var maxB=1,maxD=1,maxA=1,maxV=0,maxM=4000,maxPB=2048,maxL=1664,maxCO=5; //maximum bytes for LED allocation: 4kB for 8266, 32kB for 32
var maxB=1,maxD=1,maxA=1,maxV=0,maxM=4000,maxPB=2048,maxL=1664,maxCO=5,maxBT=4; //maximum bytes for LED allocation: 4kB for 8266, 32kB for 32
var customStarts=false,startsDirty=[];
function off(n) { gN(n).value = -1;}
// these functions correspond to C macros found in const.h
@@ -43,7 +43,7 @@
}); // If we set async false, file is loaded and executed, then next statement is processed
if (loc) d.Sf.action = getURL('/settings/leds');
}
function bLimits(b,v,p,m,l,o=5,d=2,a=6) {
function bLimits(b,v,p,m,l,o=5,d=2,a=6,n=4) {
maxB = b; // maxB - max physical (analog + digital) buses: 32 - ESP32, 14 - S3/S2, 6 - C3, 4 - 8266
maxV = v; // maxV - min virtual buses: 6 - ESP32/S3, 4 - S2/C3, 3 - ESP8266 (only used to distinguish S2/S3)
maxPB = p; // maxPB - max LEDs per bus
@@ -52,6 +52,7 @@
maxCO = o; // maxCO - max Color Order mappings
maxD = d; // maxD - max digital channels (can be changed if using ESP32 parallel I2S): 16 - ESP32, 12 - S3/S2, 2 - C3, 3 - 8266
maxA = a; // maxA - max analog channels: 16 - ESP32, 8 - S3/S2, 6 - C3, 5 - 8266
maxBT = n; // maxBT - max buttons
}
function is8266() { return maxA == 5 && maxD == 3; } // NOTE: see const.h
function is32() { return maxA == 16 && maxD == 16; } // NOTE: see const.h
@@ -600,9 +601,9 @@ Swap: <select id="xw${s}" name="XW${s}">
}
function addBtn(i,p,t) {
var c = gId("btns").innerHTML;
var b = gId("btns");
var s = chrID(i);
c += `Button ${i} GPIO: <input type="number" name="BT${s}" onchange="UI()" class="xs" value="${p}">`;
var c = `<div id="btn${i}">#${i} GPIO: <input type="number" name="BT${s}" onchange="UI()" min="-1" max="${d.max_gpio}" class="xs" value="${p}">`;
c += `&nbsp;<select name="BE${s}">`
c += `<option value="0" ${t==0?"selected":""}>Disabled</option>`;
c += `<option value="2" ${t==2?"selected":""}>Pushbutton</option>`;
@@ -614,8 +615,24 @@ Swap: <select id="xw${s}" name="XW${s}">
c += `<option value="8" ${t==8?"selected":""}>Analog inverted</option>`;
c += `<option value="9" ${t==9?"selected":""}>Touch (switch)</option>`;
c += `</select>`;
c += `<span style="cursor: pointer;" onclick="off('BT${s}')">&nbsp;&#x2715;</span><br>`;
gId("btns").innerHTML = c;
c += `<span style="cursor: pointer;" onclick="off('BT${s}')">&nbsp;&#x2715;</span><br></div>`;
b.insertAdjacentHTML("beforeend", c);
btnBtn();
pinDropdowns();
UI();
}
function remBtn() {
var b = gId("btns");
if (b.children.length <= 1) return;
b.lastElementChild.remove();
btnBtn();
pinDropdowns();
UI();
}
function btnBtn() {
var b = gId("btns");
gId("btn_rem").style.display = (b.children.length > 1) ? "inline" : "none";
gId("btn_add").style.display = (b.children.length < maxBT) ? "inline" : "none";
}
function tglSi(cs) {
customStarts = cs;
@@ -867,10 +884,16 @@ Swap: <select id="xw${s}" name="XW${s}">
<div id="com_entries"></div>
<hr class="sml">
<button type="button" id="com_add" onclick="addCOM()">+</button>
<button type="button" id="com_rem" onclick="remCOM()">-</button><br>
<button type="button" id="com_rem" onclick="remCOM()">-</button>
</div>
<hr class="sml">
<div id="btns"></div>
<div id="btn_wrap">
Buttons:
<div id="btns"></div>
<hr class="sml">
<button type="button" id="btn_add" onclick="addBtn(gId('btns').children.length,-1,0)">+</button>
<button type="button" id="btn_rem" onclick="remBtn()">-</button>
</div>
Disable internal pull-up/down: <input type="checkbox" name="IP"><br>
Touch threshold: <input type="number" class="s" min="0" max="100" name="TT" required><br>
<hr class="sml">

View File

@@ -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>

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";

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);

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,6 @@ inline bool readObjectFromFile(const String &file, const char* key, JsonDocument
bool copyFile(const char* src_path, const char* dst_path);
bool backupFile(const char* filename);
bool restoreFile(const char* filename);
bool checkBackupExists(const char* filename);
bool validateJsonFile(const char* filename);
void dumpFilesToSerial();
@@ -401,8 +399,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 +539,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);

View File

@@ -557,12 +557,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");

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"));
}

View File

@@ -1,6 +1,5 @@
#include "wled.h"
#define JSON_PATH_STATE 1
#define JSON_PATH_INFO 2
#define JSON_PATH_STATE_INFO 3
@@ -52,9 +51,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,7 +687,6 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme
}
}
void serializeInfo(JsonObject root)
{
root[F("ver")] = versionString;
@@ -699,7 +694,6 @@ void serializeInfo(JsonObject root)
root[F("cn")] = F(WLED_CODENAME);
root[F("release")] = releaseString;
root[F("repo")] = repoString;
root[F("deviceId")] = getDeviceId();
JsonObject leds = root.createNestedObject(F("leds"));
leds[F("count")] = strip.getLengthTotal();
@@ -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();

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

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

View File

@@ -128,12 +128,12 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
PinManager::deallocatePin(irPin, PinOwner::IR);
}
#endif
for (unsigned s=0; s<WLED_MAX_BUTTONS; s++) {
if (btnPin[s]>=0 && PinManager::isPinAllocated(btnPin[s], PinOwner::Button)) {
PinManager::deallocatePin(btnPin[s], PinOwner::Button);
for (const auto &button : buttons) {
if (button.pin >= 0 && PinManager::isPinAllocated(button.pin, PinOwner::Button)) {
PinManager::deallocatePin(button.pin, PinOwner::Button);
#ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state, detach interrupt
if (digitalPinToTouchChannel(btnPin[s]) >= 0) // if touch capable pin
touchDetachInterrupt(btnPin[s]); // if not assigned previously, this will do nothing
if (digitalPinToTouchChannel(button.pin) >= 0) // if touch capable pin
touchDetachInterrupt(button.pin); // if not assigned previously, this will do nothing
#endif
}
}
@@ -280,54 +280,56 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
char bt[4] = "BT"; bt[2] = offset+i; bt[3] = 0; // button pin (use A,B,C,... if WLED_MAX_BUTTONS>10)
char be[4] = "BE"; be[2] = offset+i; be[3] = 0; // button type (use A,B,C,... if WLED_MAX_BUTTONS>10)
int hw_btn_pin = request->arg(bt).toInt();
if (hw_btn_pin >= 0 && PinManager::allocatePin(hw_btn_pin,false,PinOwner::Button)) {
btnPin[i] = hw_btn_pin;
buttonType[i] = request->arg(be).toInt();
#ifdef ARDUINO_ARCH_ESP32
if (i >= buttons.size()) buttons.emplace_back(hw_btn_pin, request->arg(be).toInt()); // add button to vector
else {
buttons[i].pin = hw_btn_pin;
buttons[i].type = request->arg(be).toInt();
}
if (buttons[i].pin >= 0 && PinManager::allocatePin(buttons[i].pin, false, PinOwner::Button)) {
#ifdef ARDUINO_ARCH_ESP32
// ESP32 only: check that button pin is a valid gpio
if ((buttonType[i] == BTN_TYPE_ANALOG) || (buttonType[i] == BTN_TYPE_ANALOG_INVERTED))
{
if (digitalPinToAnalogChannel(btnPin[i]) < 0) {
if ((buttons[i].type == BTN_TYPE_ANALOG) || (buttons[i].type == BTN_TYPE_ANALOG_INVERTED)) {
if (digitalPinToAnalogChannel(buttons[i].pin) < 0) {
// not an ADC analog pin
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[i], i);
btnPin[i] = -1;
PinManager::deallocatePin(hw_btn_pin,PinOwner::Button);
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), buttons[i].pin, i);
PinManager::deallocatePin(buttons[i].pin, PinOwner::Button);
buttons[i].type = BTN_TYPE_NONE;
} else {
analogReadResolution(12); // see #4040
}
}
else if ((buttonType[i] == BTN_TYPE_TOUCH || buttonType[i] == BTN_TYPE_TOUCH_SWITCH))
{
if (digitalPinToTouchChannel(btnPin[i]) < 0)
{
} else if ((buttons[i].type == BTN_TYPE_TOUCH || buttons[i].type == BTN_TYPE_TOUCH_SWITCH)) {
if (digitalPinToTouchChannel(buttons[i].pin) < 0) {
// not a touch pin
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), btnPin[i], i);
btnPin[i] = -1;
PinManager::deallocatePin(hw_btn_pin,PinOwner::Button);
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), buttons[i].pin, i);
PinManager::deallocatePin(buttons[i].pin, PinOwner::Button);
buttons[i].type = BTN_TYPE_NONE;
}
#ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a fucntion to check touch state but need to attach an interrupt to do so
else
{
touchAttachInterrupt(btnPin[i], touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
}
#endif
}
else
#endif
else touchAttachInterrupt(buttons[i].pin, touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
#endif
} else
#endif
{
// regular buttons and switches
if (disablePullUp) {
pinMode(btnPin[i], INPUT);
pinMode(buttons[i].pin, INPUT);
} else {
#ifdef ESP32
pinMode(btnPin[i], buttonType[i]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
pinMode(buttons[i].pin, buttons[i].type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
#else
pinMode(btnPin[i], INPUT_PULLUP);
pinMode(buttons[i].pin, INPUT_PULLUP);
#endif
}
}
} else {
btnPin[i] = -1;
buttonType[i] = BTN_TYPE_NONE;
buttons[i].pin = -1;
buttons[i].type = BTN_TYPE_NONE;
}
}
// we should remove all unused buttons from the vector
for (int i = buttons.size()-1; i > 0; i--) {
if (buttons[i].pin < 0 && buttons[i].type == BTN_TYPE_NONE) {
buttons.erase(buttons.begin() + i); // remove button from vector
}
}
@@ -531,14 +533,16 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
macroAlexaOff = request->arg(F("A1")).toInt();
macroCountdown = request->arg(F("MC")).toInt();
macroNl = request->arg(F("MN")).toInt();
for (unsigned i=0; i<WLED_MAX_BUTTONS; i++) {
char mp[4] = "MP"; mp[2] = (i<10?48:55)+i; mp[3] = 0; // short
char ml[4] = "ML"; ml[2] = (i<10?48:55)+i; ml[3] = 0; // long
char md[4] = "MD"; md[2] = (i<10?48:55)+i; md[3] = 0; // double
int i = 0;
for (auto &button : buttons) {
char mp[4] = "MP"; mp[2] = (i<10?'0':'A'-10)+i; mp[3] = 0; // short
char ml[4] = "ML"; ml[2] = (i<10?'0':'A'-10)+i; ml[3] = 0; // long
char md[4] = "MD"; md[2] = (i<10?'0':'A'-10)+i; md[3] = 0; // double
//if (!request->hasArg(mp)) break;
macroButton[i] = request->arg(mp).toInt(); // these will default to 0 if not present
macroLongPress[i] = request->arg(ml).toInt();
macroDoublePress[i] = request->arg(md).toInt();
button.macroButton = request->arg(mp).toInt(); // these will default to 0 if not present
button.macroLongPress = request->arg(ml).toInt();
button.macroDoublePress = request->arg(md).toInt();
i++;
}
char k[3]; k[2] = 0;
@@ -613,6 +617,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 +819,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

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
@@ -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;
}
@@ -1130,96 +1125,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;
}
}

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()

View File

@@ -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,20 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument<PSRAM_Allocator>;
#define STRINGIFY(X) #X
#define TOSTRING(X) STRINGIFY(X)
#ifndef WLED_VERSION
#define WLED_VERSION dev
#endif
#ifndef WLED_RELEASE_NAME
#define WLED_RELEASE_NAME "Custom"
#endif
#ifndef WLED_REPO
#define WLED_REPO "unknown"
#endif
// Global Variable definitions
WLED_GLOBAL char versionString[] _INIT(TOSTRING(WLED_VERSION));
WLED_GLOBAL char releaseString[] _INIT(WLED_RELEASE_NAME); // must include the quotes when defining, e.g -D WLED_RELEASE_NAME=\"ESP32_MULTI_USREMODS\"
WLED_GLOBAL char repoString[] _INIT(WLED_REPO);
#define WLED_CODENAME "Niji"
// AP and OTA default passwords (for maximum security change them!)
@@ -286,10 +296,10 @@ WLED_GLOBAL char otaPass[33] _INIT(DEFAULT_OTA_PASS);
// Hardware and pin config
#ifndef BTNPIN
#define BTNPIN 0,-1
#define BTNPIN 0
#endif
#ifndef BTNTYPE
#define BTNTYPE BTN_TYPE_PUSH,BTN_TYPE_NONE
#define BTNTYPE BTN_TYPE_PUSH
#endif
#ifndef RLYPIN
WLED_GLOBAL int8_t rlyPin _INIT(-1);
@@ -571,9 +581,6 @@ WLED_GLOBAL byte countdownMin _INIT(0) , countdownSec _INIT(0);
WLED_GLOBAL byte macroNl _INIT(0); // after nightlight delay over
WLED_GLOBAL byte macroCountdown _INIT(0);
WLED_GLOBAL byte macroAlexaOn _INIT(0), macroAlexaOff _INIT(0);
WLED_GLOBAL byte macroButton[WLED_MAX_BUTTONS] _INIT({0});
WLED_GLOBAL byte macroLongPress[WLED_MAX_BUTTONS] _INIT({0});
WLED_GLOBAL byte macroDoublePress[WLED_MAX_BUTTONS] _INIT({0});
// Security CONFIG
#ifdef WLED_OTA_PASS
@@ -639,13 +646,32 @@ WLED_GLOBAL byte briLast _INIT(128); // brightness before
WLED_GLOBAL byte whiteLast _INIT(128); // white channel before turned off. Used for toggle function in ir.cpp
// button
WLED_GLOBAL int8_t btnPin[WLED_MAX_BUTTONS] _INIT({BTNPIN});
WLED_GLOBAL byte buttonType[WLED_MAX_BUTTONS] _INIT({BTNTYPE});
struct Button {
unsigned long pressedTime; // time button was pressed
unsigned long waitTime; // time to wait for next button press
int8_t pin; // pin number
struct {
uint8_t type : 6; // button type (push, long, double, etc.)
bool pressedBefore : 1; // button was pressed before
bool longPressed : 1; // button was long pressed
};
uint8_t macroButton; // macro/preset to call on button press
uint8_t macroLongPress; // macro/preset to call on long press
uint8_t macroDoublePress; // macro/preset to call on double press
Button(int8_t p, uint8_t t, uint8_t mB = 0, uint8_t mLP = 0, uint8_t mDP = 0)
: pressedTime(0)
, waitTime(0)
, pin(p)
, type(t)
, pressedBefore(false)
, longPressed(false)
, macroButton(mB)
, macroLongPress(mLP)
, macroDoublePress(mDP) {}
};
WLED_GLOBAL std::vector<Button> buttons; // vector of button structs
WLED_GLOBAL bool buttonPublishMqtt _INIT(false);
WLED_GLOBAL bool buttonPressedBefore[WLED_MAX_BUTTONS] _INIT({false});
WLED_GLOBAL bool buttonLongPressed[WLED_MAX_BUTTONS] _INIT({false});
WLED_GLOBAL unsigned long buttonPressedTime[WLED_MAX_BUTTONS] _INIT({0});
WLED_GLOBAL unsigned long buttonWaitTime[WLED_MAX_BUTTONS] _INIT({0});
WLED_GLOBAL bool disablePullUp _INIT(false);
WLED_GLOBAL byte touchThreshold _INIT(TOUCH_THRESHOLD);

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;
}

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);

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)
@@ -467,7 +391,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 +404,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 +466,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);
@@ -680,8 +569,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 +694,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;
}

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];

View File

@@ -291,7 +291,7 @@ void getSettingsJS(byte subPage, Print& settingsScript)
settingsScript.printf_P(PSTR("d.ledTypes=%s;"), BusManager::getLEDTypesJSONString().c_str());
// set limits
settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d);"),
settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d,%d);"),
WLED_MAX_BUSSES,
WLED_MIN_VIRTUAL_BUSSES, // irrelevant, but kept to distinguish S2/S3 in UI
MAX_LEDS_PER_BUS,
@@ -299,7 +299,8 @@ void getSettingsJS(byte subPage, Print& settingsScript)
MAX_LEDS,
WLED_MAX_COLOR_ORDER_MAPPINGS,
WLED_MAX_DIGITAL_CHANNELS,
WLED_MAX_ANALOG_CHANNELS
WLED_MAX_ANALOG_CHANNELS,
WLED_MAX_BUTTONS
);
printSetFormCheckbox(settingsScript,PSTR("MS"),strip.autoSegments);
@@ -403,8 +404,9 @@ void getSettingsJS(byte subPage, Print& settingsScript)
printSetFormValue(settingsScript,PSTR("RL"),rlyPin);
printSetFormCheckbox(settingsScript,PSTR("RM"),rlyMde);
printSetFormCheckbox(settingsScript,PSTR("RO"),rlyOpenDrain);
for (int i = 0; i < WLED_MAX_BUTTONS; i++) {
settingsScript.printf_P(PSTR("addBtn(%d,%d,%d);"), i, btnPin[i], buttonType[i]);
int i = 0;
for (const auto &button : buttons) {
settingsScript.printf_P(PSTR("addBtn(%d,%d,%d);"), i++, button.pin, button.type);
}
printSetFormCheckbox(settingsScript,PSTR("IP"),disablePullUp);
printSetFormValue(settingsScript,PSTR("TT"),touchThreshold);
@@ -578,8 +580,9 @@ void getSettingsJS(byte subPage, Print& settingsScript)
printSetFormValue(settingsScript,PSTR("A1"),macroAlexaOff);
printSetFormValue(settingsScript,PSTR("MC"),macroCountdown);
printSetFormValue(settingsScript,PSTR("MN"),macroNl);
for (unsigned i=0; i<WLED_MAX_BUTTONS; i++) {
settingsScript.printf_P(PSTR("addRow(%d,%d,%d,%d);"), i, macroButton[i], macroLongPress[i], macroDoublePress[i]);
int i = 0;
for (const auto &button : buttons) {
settingsScript.printf_P(PSTR("addRow(%d,%d,%d,%d);"), i++, button.macroButton, button.macroLongPress, button.macroDoublePress);
}
char k[4];
@@ -671,6 +674,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);