Merge pull request #293 from MoonModules/pixelforge_backport

Pixelforge backport, UI stability improvements, speedup for UDP real-time

* Backport of the new PixelForge tool by @DedeHai, some adaptations to make it work with 14.x API's
* Upgraded HTML build system
* Several bugfixes to prevent presets.json corruption (prevents parallel file writing, better protection of buffers)

* Improved stability when several tasks try to draw/show LEDs in parallel
* JSON de-serializer: interlock added to prevent segment updates while ``strip.service()`` runs
* JSON de-serializer: reduced delay when updating presets (``strip.service()`` lock released before writing to presets.json)
* UDP real time streaming: small speedup, improved stability and prevent broken frames.
This commit is contained in:
Frank
2026-01-01 20:51:10 +01:00
committed by GitHub
30 changed files with 5836 additions and 3893 deletions

3919
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "wled",
"version": "14.7.0-mdev",
"description": "Tools for WLED project",
"description": "Tools for WLED-MM project",
"main": "tools/cdata.js",
"directories": {
"lib": "lib",
@@ -9,6 +9,7 @@
},
"scripts": {
"build": "node tools/cdata.js",
"test": "node --test",
"dev": "nodemon -e js,html,htm,css,png,jpg,gif,ico,js -w tools/ -w wled00/data/ -x node tools/cdata.js"
},
"repository": {
@@ -22,10 +23,12 @@
},
"homepage": "https://github.com/MoonModules/WLED-MM#readme",
"dependencies": {
"clean-css": "^4.2.3",
"html-minifier-terser": "^5.1.1",
"inliner": "^1.13.1",
"nodemon": "^2.0.20",
"zlib": "^1.0.5"
"clean-css": "^5.3.3",
"html-minifier-terser": "^7.2.0",
"web-resource-inliner": "^7.0.0",
"nodemon": "^3.1.9"
},
"engines": {
"node": ">=20.0.0"
}
}

21
pio-scripts/build_ui.py Normal file
View File

@@ -0,0 +1,21 @@
Import("env")
import shutil
node_ex = shutil.which("node")
# Check if Node.js is installed and present in PATH if it failed, abort the build
if node_ex is None:
print('\x1b[0;31;43m' + 'Node.js is not installed or missing from PATH html css js will not be processed check https://kno.wled.ge/advanced/compiling-wled/' + '\x1b[0m')
exitCode = env.Execute("null")
exit(exitCode)
else:
# Install the necessary node packages for the pre-build asset bundling script
print('\x1b[6;33;42m' + 'Installing node packages' + '\x1b[0m')
env.Execute("npm ci")
# Call the bundling script
exitCode = env.Execute("npm run build")
# If it failed, abort the build
if (exitCode):
print('\x1b[0;31;43m' + 'npm run build fails check https://kno.wled.ge/advanced/compiling-wled/' + '\x1b[0m')
exit(exitCode)

116
pio-scripts/set_metadata.py Normal file
View File

@@ -0,0 +1,116 @@
Import('env')
import subprocess
import json
import re
def get_github_repo():
"""Extract GitHub repository name from git remote URL.
Uses the remote that the current branch tracks, falling back to 'origin'.
This handles cases where repositories have multiple remotes or where the
main remote is not named 'origin'.
Returns:
str: Repository name in 'owner/repo' format for GitHub repos,
'unknown' for non-GitHub repos, missing git CLI, or any errors.
"""
try:
remote_name = 'origin' # Default fallback
# Try to get the remote for the current branch
try:
# Get current branch name
branch_result = subprocess.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
capture_output=True, text=True, check=True)
current_branch = branch_result.stdout.strip()
# Get the remote for the current branch
remote_result = subprocess.run(['git', 'config', f'branch.{current_branch}.remote'],
capture_output=True, text=True, check=True)
tracked_remote = remote_result.stdout.strip()
# Use the tracked remote if we found one
if tracked_remote:
remote_name = tracked_remote
except subprocess.CalledProcessError:
# If branch config lookup fails, continue with 'origin' as fallback
pass
# Get the remote URL for the determined remote
result = subprocess.run(['git', 'remote', 'get-url', remote_name],
capture_output=True, text=True, check=True)
remote_url = result.stdout.strip()
# Check if it's a GitHub URL
if 'github.com' not in remote_url.lower():
return None
# Parse GitHub URL patterns:
# https://github.com/owner/repo.git
# git@github.com:owner/repo.git
# https://github.com/owner/repo
# Remove .git suffix if present
if remote_url.endswith('.git'):
remote_url = remote_url[:-4]
# Handle HTTPS URLs
https_match = re.search(r'github\.com/([^/]+/[^/]+)', remote_url, re.IGNORECASE)
if https_match:
return https_match.group(1)
# Handle SSH URLs
ssh_match = re.search(r'github\.com:([^/]+/[^/]+)', remote_url, re.IGNORECASE)
if ssh_match:
return ssh_match.group(1)
return None
except FileNotFoundError:
# Git CLI is not installed or not in PATH
return None
except subprocess.CalledProcessError:
# Git command failed (e.g., not a git repo, no remote, etc.)
return None
except Exception:
# Any other unexpected error
return None
# WLED version is managed by package.json; this is picked up in several places
# - It's integrated in to the UI code
# - Here, for wled_metadata.cpp
# - The output_bins script
# We always take it from package.json to ensure consistency
with open("package.json", "r") as package:
WLED_VERSION = json.load(package)["version"]
def has_def(cppdefs, name):
""" Returns true if a given name is set in a CPPDEFINES collection """
for f in cppdefs:
if isinstance(f, tuple):
f = f[0]
if f == name:
return True
return False
def add_wled_metadata_flags(env, node):
cdefs = env["CPPDEFINES"].copy()
if not has_def(cdefs, "WLED_REPO"):
repo = get_github_repo()
if repo:
cdefs.append(("WLED_REPO", f"\\\"{repo}\\\""))
cdefs.append(("WLED_VERSION", WLED_VERSION))
# This transforms the node in to a Builder; it cannot be modified again
return env.Object(
node,
CPPDEFINES=cdefs
)
env.AddBuildMiddleware(
add_wled_metadata_flags,
"*/wled_metadata.cpp"
)

View File

@@ -0,0 +1,80 @@
import re
from pathlib import Path # For OS-agnostic path manipulation
from typing import Iterable
from click import secho
from SCons.Script import Action, Exit
from platformio.builder.tools.piolib import LibBuilderBase
def is_wled_module(env, dep: LibBuilderBase) -> bool:
"""Returns true if the specified library is a wled module
"""
usermod_dir = Path(env["PROJECT_DIR"]).resolve() / "usermods"
return usermod_dir in Path(dep.src_dir).parents or str(dep.name).startswith("wled-")
def read_lines(p: Path):
""" Read in the contents of a file for analysis """
with p.open("r", encoding="utf-8", errors="ignore") as f:
return f.readlines()
def check_map_file_objects(map_file: list[str], dirs: Iterable[str]) -> set[str]:
""" Identify which dirs contributed to the final build
Returns the (sub)set of dirs that are found in the output ELF
"""
# Pattern to match symbols in object directories
# Join directories into alternation
usermod_dir_regex = "|".join([re.escape(dir) for dir in dirs])
# Matches nonzero address, any size, and any path in a matching directory
object_path_regex = re.compile(r"0x0*[1-9a-f][0-9a-f]*\s+0x[0-9a-f]+\s+\S+[/\\](" + usermod_dir_regex + r")[/\\]\S+\.o")
found = set()
for line in map_file:
matches = object_path_regex.findall(line)
for m in matches:
found.add(m)
return found
def count_usermod_objects(map_file: list[str]) -> int:
""" Returns the number of usermod objects in the usermod list """
# Count the number of entries in the usermods table section
return len([x for x in map_file if ".dtors.tbl.usermods.1" in x])
def validate_map_file(source, target, env):
""" Validate that all modules appear in the output build """
build_dir = Path(env.subst("$BUILD_DIR"))
map_file_path = build_dir / env.subst("${PROGNAME}.map")
if not map_file_path.exists():
secho(f"ERROR: Map file not found: {map_file_path}", fg="red", err=True)
Exit(1)
# Identify the WLED module builders, set by load_usermods.py
module_lib_builders = env['WLED_MODULES']
# Extract the values we care about
modules = {Path(builder.build_dir).name: builder.name for builder in module_lib_builders}
secho(f"INFO: {len(modules)} libraries linked as WLED optional/user modules")
# Now parse the map file
map_file_contents = read_lines(map_file_path)
usermod_object_count = count_usermod_objects(map_file_contents)
secho(f"INFO: {usermod_object_count} usermod object entries")
confirmed_modules = check_map_file_objects(map_file_contents, modules.keys())
missing_modules = [modname for mdir, modname in modules.items() if mdir not in confirmed_modules]
if missing_modules:
secho(
f"ERROR: No object files from {missing_modules} found in linked output!",
fg="red",
err=True)
Exit(1)
return None
Import("env")
env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")])
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked optional modules (usermods) in map file'))

View File

@@ -220,7 +220,7 @@ ldscript_16m14m = eagle.flash.16m14m.ld
[scripts_defaults]
extra_scripts =
pre:pio-scripts/set_version.py
pre:pio-scripts/build-html.py
pre:pio-scripts/build_ui.py
pre:pio-scripts/conditional_usb_mode.py
post:pio-scripts/output_bins.py
post:pio-scripts/strip-floats.py
@@ -299,6 +299,7 @@ build_flags =
-D USERMOD_AUDIOREACTIVE
-D NON32XFER_HANDLER ;; ask forgiveness for PROGMEM misuse
-D WLED_DISABLE_PARTICLESYSTEM2D
-D WLED_DISABLE_PIXELFORGE ;; not enought space in flash
;; special library dependencies for 8266 (workaround for upsteam #5136) - replaces env.lib_deps
lib8266_deps =
@@ -367,6 +368,8 @@ default_partitions = ${esp32.default_partitions} ;; backwards compatibi
board_build.f_flash = 80000000L
board_build.flash_mode = dout ;; avoid dio/quot/qio - these are broken in arduino-esp32 1.0.6.x
;;board_build.flash_mode = dio
; RAM: [== ] 24.2% (used 79284 bytes from 327680 bytes)
; Flash: [========= ] 93.1% (used 1464961 bytes from 1572864 bytes)
;; standard platform for esp32
[esp32]
@@ -669,6 +672,9 @@ platform_packages = ${esp32_legacy.platform_packages}
build_unflags = ${esp32_legacy.build_unflags}
build_flags = ${common.build_flags} ${esp32_legacy.build_flags} -D WLED_RELEASE_NAME=ESP32_compat #-D WLED_DISABLE_BROWNOUT_DET
${esp32.AR_build_flags}
;;-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE -DWLED_ENABLE_GIF ;; 30KB flash
;;-D WLED_ENABLE_FULL_FONTS ;; 10KB flash
lib_deps = ${esp32_legacy.lib_deps}
${esp32.AR_lib_deps}
board_build.partitions = ${esp32_legacy.default_partitions}
@@ -720,8 +726,12 @@ platform_packages = ${esp32_legacy.platform_packages}
upload_speed = 921600
build_unflags = ${esp32_legacy.build_unflags}
build_flags = ${common.build_flags} ${esp32_legacy.build_flags} -D WLED_RELEASE_NAME=ESP32_Ethernet_compat -D RLYPIN=-1
-D WLED_USE_ETHERNET -D BTNPIN=-1
-D WLED_USE_ETHERNET -D BTNPIN=-1 -D RLYPIN=-1 -D IRPIN=-1
-D LEDPIN=4
-D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only
;;-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE -DWLED_ENABLE_GIF ;; 30KB flash
;;-D WLED_ENABLE_FULL_FONTS ;; 10KB flash
${esp32.AR_build_flags}
lib_deps = ${esp32_legacy.lib_deps}
${esp32.AR_lib_deps}
@@ -1216,6 +1226,9 @@ build_flags_M =
-D USERMOD_ROTARY_ENCODER_UI
-D USERMOD_AUTO_SAVE
-D WLED_ENABLE_FULL_FONTS ;; enables (limited) unicode support in scrolling text - warning: increases firmware size by 6848 bytes
;;-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
${common_mm.animartrix_build_flags}
${common_mm.NetDebug_build_flags}
@@ -1437,7 +1450,8 @@ board_build.partitions = ${esp32.extended_partitions} ;; 1.65MB firmware, 700KB
build_flags = ${esp32_4MB_M_base.build_flags}
-D WLED_RELEASE_NAME=esp32_4MB_M_eth
-D WLED_USE_ETHERNET
-D RLYPIN=-1 -D BTNPIN=-1 ;; Prevent clash
-D RLYPIN=-1 -D BTNPIN=-1 -D IRPIN=-1 ;; Prevent clash
-D LEDPIN=4 ;; safe default
-D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only
-D WLEDMM_SAVE_FLASH
;-D WLED_DISABLE_ALEXA
@@ -1478,6 +1492,9 @@ build_flags = ${esp32_4MB_S_base.build_flags}
${Speed_Flags.build_flags} ;; optimize for speed instead of size
-D WLEDMM_FASTPATH ;; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions.
${common_mm.animartrix_build_flags}
-D WLED_ENABLE_GIF ;; 28KB Flash
-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
lib_deps = ${esp32_4MB_S_base.lib_deps}
${common_mm.animartrix_lib_deps}
; lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation
@@ -1489,6 +1506,8 @@ lib_deps = ${esp32_4MB_S_base.lib_deps}
extends = esp32_4MB_M_base
build_flags = ${esp32_4MB_M_base.build_flags}
-D WLED_RELEASE_NAME=esp32_16MB_M
-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
board = esp32_16MB
;board_build.partitions = tools/WLED_ESP32_16MB.csv ;; WLED standard for 16MB flash: 2MB firmware, 12 MB filesystem
board_build.partitions = ${esp32.extreme_partitions} ;; WLED extended for 16MB flash: 3.2MB firmware, 9 MB filesystem
@@ -1545,8 +1564,12 @@ board_build.partitions = ${esp32.extreme_partitions} ;; WLED extended for 16MB f
board = esp32_16MB-poe ;; needed for ethernet boards (selects "esp32-poe" as variant)
build_flags = ${esp32_4MB_M_base.build_flags}
-D WLED_RELEASE_NAME=esp32_16MB_M_eth ; This will be included in the firmware.bin filename
-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
-D WLED_USE_ETHERNET
-D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only
-D BTNPIN=-1 -D RLYPIN=-1 -D IRPIN=-1
-D LEDPIN=4
; RAM: [== ] 24.2% (used 79388 bytes from 327680 bytes)
; Flash: [======= ] 73.8% (used 1548525 bytes from 2097152 bytes)
@@ -1827,6 +1850,8 @@ build_flags = ${esp32_4MB_V4_S_base.esp32_build_flags}
-D SR_DMTYPE=254 ;; HUB75 driver needs the I2S unit - set AR default mode to 'Network Receive Only' to prevent driver conflicts.
-D WLED_USE_ETHERNET
-D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only
-D BTNPIN=-1 -D RLYPIN=-1 -D IRPIN=-1
-D LEDPIN=4
lib_deps = ${esp32_4MB_V4_S_base.esp32_lib_deps}
${common_mm.HUB75_lib_deps}
lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation
@@ -1938,6 +1963,9 @@ build_flags = ${esp32_4MB_V4_S_base.esp32_build_flags}
${common_mm.HUB75_build_flags}
-D SR_DMTYPE=254 ;; HUB75 driver needs the I2S unit - set AR default mode to 'Network Receive Only' to prevent driver conflicts.
${common_mm.animartrix_build_flags}
-D WLED_ENABLE_GIF ;; 28KB Flash
-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
lib_deps = ${esp32_4MB_V4_S_base.esp32_lib_deps}
${common_mm.HUB75_lib_deps}
${common_mm.animartrix_lib_deps}
@@ -1965,6 +1993,9 @@ build_flags = ${esp32_4MB_V4_M_base.esp32_build_flags}
${common_mm.HUB75_build_flags}
-D SR_DMTYPE=254 ;; HUB75 driver needs the I2S unit - set AR default mode to 'Network Receive Only' to prevent driver conflicts.
${common_mm.animartrix_build_flags}
-D WLED_ENABLE_GIF ;; 28KB Flash
-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
lib_deps = ${esp32_4MB_V4_M_base.esp32_lib_deps}
${common_mm.HUB75_lib_deps}
${common_mm.animartrix_lib_deps}
@@ -2163,6 +2194,8 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-inden
; -D WLED_DISABLE_MQTT ; RAM 216 bytes; FLASH 16496 bytes
; -D WLED_DISABLE_HUESYNC ;RAM 122 bytes; FLASH 6308 bytes
; -D WLED_DISABLE_INFRARED ;RAM 136 bytes; FLASH 24492 bytes
-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
;;-D WLEDMM_FASTPATH ; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions.
-D LEDPIN=4
;-D STATUSLED=39
@@ -2217,6 +2250,8 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-inden
; -D WLED_DISABLE_MQTT ; RAM 216 bytes; FLASH 16496 bytes
; -D WLED_DISABLE_HUESYNC ;RAM 122 bytes; FLASH 6308 bytes
; -D WLED_DISABLE_INFRARED ;RAM 136 bytes; FLASH 24492 bytes
-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
-D LEDPIN=21
;;-D DATA_PINS=21,48,3 -D PIXEL_COUNTS=30,1,144 ;; just an example: my board has a builtin neopixel on gpio48
; -D STATUSLED=2
@@ -2256,6 +2291,9 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-inden
-D WLEDMM_FASTPATH ; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions.
${common_mm.HUB75_build_flags}
${common_mm.animartrix_build_flags}
-D WLED_ENABLE_GIF ;; 28KB Flash
-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
-D WLED_WATCHDOG_TIMEOUT=0 -D CONFIG_ASYNC_TCP_USE_WDT=0
-D WLED_DISABLE_LOXONE ; FLASH 1272 bytes
-D WLED_DISABLE_ALEXA ; RAM 116 bytes; FLASH 13524 bytes
@@ -2307,6 +2345,9 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-inden
${common_mm.HUB75_build_flags}
-D MOONHUB_S3_PINOUT ;; HUB75 pinout
${common_mm.animartrix_build_flags}
-D WLED_ENABLE_GIF ;; 28KB Flash
-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
-D WLED_RELEASE_NAME=esp32S3_16MB_PSRAM_M_HUB75
-D WLEDMM_FASTPATH
-D WLED_DISABLE_BROWNOUT_DET
@@ -2361,6 +2402,8 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-inden
${Speed_Flags.build_flags_V4} ;; optimize for speed
${common_mm.HUB75_build_flags}
${common_mm.animartrix_build_flags}
-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
-D WLED_RELEASE_NAME=esp32S3_WROOM-2_M
-D WLEDMM_FASTPATH
-DBOARD_HAS_PSRAM ;; -D WLED_USE_PSRAM ;; your board supports PSRAM
@@ -2413,6 +2456,8 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-inden
-D WLEDMM_FASTPATH ;; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions.
;; -D WLEDMM_SAVE_FLASH
${common_mm.animartrix_build_flags}
-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
-D WLED_WATCHDOG_TIMEOUT=0 -D CONFIG_ASYNC_TCP_USE_WDT=0
-D WLED_DISABLE_LOXONE ; FLASH 1272 bytes
;; -D WLED_DISABLE_ALEXA ; RAM 116 bytes; FLASH 13524 bytes
@@ -2445,7 +2490,7 @@ board = lolin_s3_mini ;; -S3 mini: 4MB flash 2MB PSRAM
board_build.partitions = ${esp32.extended_partitions} ;; 1.65MB firmware, 700KB filesystem
build_unflags = ${common.build_unflags}
-D WLED_ENABLE_HUB75MATRIX ;; board does not have enough pins for HUB75
-D USERMOD_ANIMARTRIX ;; not enough flash
;;-D USERMOD_ANIMARTRIX ;; not enough flash
build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-indentation -Wno-format-truncation
${common_mm.build_flags_S} ${common_mm.build_flags_M}
-D WLED_RELEASE_NAME=esp32S3_4MB_PSRAM_M
@@ -2461,12 +2506,14 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-inden
-D BTNPIN=-1 -D RLYPIN=-1 -D IRPIN=-1 -D AUDIOPIN=-1
-D HW_PIN_SDA=12 -D HW_PIN_SCL=13
-D SR_DMTYPE=1 -D I2S_SDPIN=5 -D I2S_WSPIN=6 -D I2S_CKPIN=4 -D MCLK_PIN=7
-D WLED_DISABLE_LOXONE ; FLASH 1272 bytes - disabled to stay below 100%
-D WLED_DISABLE_HUESYNC ; RAM 122 bytes; FLASH 6308 bytes - disabled to stay below 100%
-D WLED_DISABLE_INFRARED ; RAM 136 bytes; FLASH 24492 bytes - disabled to stay below 100%
;;-D WLED_DISABLE_LOXONE ; FLASH 1272 bytes - disabled to stay below 100%
;;-D WLED_DISABLE_HUESYNC ; RAM 122 bytes; FLASH 6308 bytes - disabled to stay below 100%
;;-D WLED_DISABLE_INFRARED ; RAM 136 bytes; FLASH 24492 bytes - disabled to stay below 100%
;; -D WLED_DISABLE_ALEXA ; RAM 116 bytes; FLASH 13524 bytes
;; -D WLED_DISABLE_MQTT ; RAM 216 bytes; FLASH 16496 bytes
;; -D WLEDMM_SAVE_FLASH
-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
; -D WLED_DEBUG
; -D SR_DEBUG
; -D MIC_LOGGER
@@ -2474,12 +2521,12 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-inden
;; -D WLED_DISABLE_PARTICLESYSTEM2D ;; exceeds flash size limit
lib_deps = ${esp32s3.lib_deps} ${common_mm.lib_deps_S} ${common_mm.lib_deps_V4_M}
lib_ignore =
IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation
;; IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation
${common_mm.HUB75_lib_ignore}
${common_mm.DMXin_lib_ignore}
${common_mm.animartrix_lib_ignore}
; RAM: [== ] 16.1% (used 52744 bytes from 327680 bytes)
; Flash: [======== ] 83.5% (used 1312937 bytes from 1572864 bytes)
;; ${common_mm.animartrix_lib_ignore}
; RAM: [== ] 16.7% (used 54676 bytes from 327680 bytes)
; Flash: [======== ] 83.2% (used 1416997 bytes from 1703936 bytes)
# ------------------------------------------------------------------------------
# esp32-S2 environments
@@ -2580,10 +2627,12 @@ build_flags = ${common.build_flags} ${esp32s2.build_flags}
-DARDUINO_USB_CDC_ON_BOOT=0 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=1
-D WLED_DISABLE_ADALIGHT ;; disables serial protocols, as the board only has CDC USB
-D WLED_DISABLE_INFRARED ;; save flash space
-D WLED_DISABLE_ALEXA ;; save flash space
-D WLED_DISABLE_HUESYNC ;; save flash space
-D WLED_DISABLE_LOXONE ;; save flash space
-D WLEDMM_SAVE_FLASH
;;-D WLED_DISABLE_ALEXA ;; save flash space
;;-D WLED_DISABLE_HUESYNC ;; save flash space
;;-D WLED_DISABLE_LOXONE ;; save flash space
-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
;;-D WLEDMM_SAVE_FLASH
-D AUDIOPIN=-1
-D BTNPIN=-1 -D IRPIN=-1
-D LEDPIN=16 ;; second led pin = 18
@@ -2601,8 +2650,8 @@ lib_ignore =
${common_mm.HUB75_lib_ignore}
${common_mm.DMXin_lib_ignore}
monitor_filters = esp32_exception_decoder
; RAM: [== ] 18.2% (used 59640 bytes from 327680 bytes)
; Flash: [======== ] 80.3% (used 1368130 bytes from 1703936 bytes)
; RAM: [== ] 18.5% (used 60660 bytes from 327680 bytes)
; Flash: [======== ] 83.6% (used 1424518 bytes from 1703936 bytes)
[env:esp32s2_PSRAM_S]
extends = env:esp32s2_PSRAM_M
@@ -2677,6 +2726,8 @@ build_flags = ${common.build_flags} ${esp32c3.build_flags}
; -D WLED_USE_MY_CONFIG
;; -D WLED_DISABLE_PARTICLESYSTEM1D ;; exceeds flash size limit
;; -D WLED_DISABLE_PARTICLESYSTEM2D ;; exceeds flash size limit
-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
lib_deps = ${esp32c3.lib_deps} ${common_mm.lib_deps_S} ${common_mm.lib_deps_V4_M}
lib_ignore =
;IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation
@@ -2944,7 +2995,9 @@ build_flags = ${esp32_4MB_M_base.build_flags}
; -D PIR_SENSOR_PIN=-1
; -D PWM_PIN=-1
; -D WLED_USE_MY_CONFIG
-D WLEDMM_SAVE_FLASH
; -D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
lib_ignore = ${esp32_4MB_M_base.lib_ignore}
${common_mm.DMXin_lib_ignore}
; RAM: [=== ] 26.3% (used 86204 bytes from 327680 bytes)
@@ -2962,6 +3015,8 @@ build_flags = ${esp32_4MB_S_base.build_flags}
-D WLED_ETH_DEFAULT=9 ; ABC! WLED V43 & compatible
-D RLYPIN=-1 -D BTNPIN=-1 ;; Prevent clash
-D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only
-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
-D AUDIOPIN=-1
-D FLD_PIN_SCL=-1 -D FLD_PIN_SDA=-1 ; use global!
; -D WLED_USE_MY_CONFIG
@@ -3006,6 +3061,8 @@ build_flags = ${esp32_4MB_M_base.build_flags}
-D WLEDMM_SAVE_FLASH
-D WLED_DISABLE_ESPNOW ;; might help in case of WiFi connectivity problems
-D WLED_DISABLE_PARTICLESYSTEM1D ;; exceeds flash size limit
; -D WLED_ENABLE_PIXART ;; 8KB Flash
; -D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
;; -D WLED_DISABLE_PARTICLESYSTEM2D
; RAM: [== ] 24.2% (used 79424 bytes from 327680 bytes)
; Flash: [==========] 99.9% (used 1571177 bytes from 1572864 bytes)
@@ -3042,6 +3099,8 @@ build_flags = ${esp32_4MB_V4_S_base.esp32_build_flags}
-D WLED_DISABLE_ESPNOW ;; might help in case of WiFi connectivity problems
;;-D WLED_DISABLE_PARTICLESYSTEM1D ;; exceeds flash size limit (default_partitions only)
;;-D WLED_DISABLE_PARTICLESYSTEM2D ;; exceeds flash size limit (default_partitions only)
-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
lib_deps = ${esp32_4MB_V4_S_base.esp32_lib_deps}
lib_ignore =
IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation
@@ -3072,8 +3131,10 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-inden
; Serial debug enabled -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=1 ;; for Hardware-CDC USB mode
-D ARDUINO_USB_CDC_ON_BOOT=0
-D WLED_DISABLE_ADALIGHT ;; disables serial protocols - recommended for Hardware-CDC USB (Serial RX will receive junk commands when RX pin is unconnected, unless its pulled down by resistor)
; ${common_mm.animartrix_build_flags}
${common_mm.animartrix_build_flags}
${common_mm.build_disable_sync_interfaces}
-D WLED_ENABLE_PIXART ;; 8KB Flash
-D WLED_ENABLE_PIXELFORGE ;; 12KB Flash
-D LOLIN_WIFI_FIX ;; try this in case Wifi does not work
-D WLED_WATCHDOG_TIMEOUT=0 -D CONFIG_ASYNC_TCP_USE_WDT=0
-D WLED_USE_PSRAM -DBOARD_HAS_PSRAM ; tells WLED that PSRAM shall be used
@@ -3082,7 +3143,7 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-inden
${common_mm.HUB75_build_flags}
-D DEFAULT_LED_TYPE=101
lib_deps = ${esp32s3.lib_deps} ${common_mm.lib_deps_S} ;; ;; do not include ${esp32.lib_depsV4} !!!!
; ${common_mm.animartrix_lib_deps}
${common_mm.animartrix_lib_deps}
${common_mm.HUB75_lib_deps}
lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation

212
tools/cdata-test.js Normal file
View File

@@ -0,0 +1,212 @@
'use strict';
const assert = require('node:assert');
const { describe, it, before, after } = require('node:test');
const fs = require('fs');
const path = require('path');
const child_process = require('child_process');
const util = require('util');
const execPromise = util.promisify(child_process.exec);
process.env.NODE_ENV = 'test'; // Set the environment to testing
const cdata = require('./cdata.js');
describe('Function', () => {
const testFolderPath = path.join(__dirname, 'testFolder');
const oldFilePath = path.join(testFolderPath, 'oldFile.txt');
const newFilePath = path.join(testFolderPath, 'newFile.txt');
// Create a temporary file before the test
before(() => {
// Create test folder
if (!fs.existsSync(testFolderPath)) {
fs.mkdirSync(testFolderPath);
}
// Create an old file
fs.writeFileSync(oldFilePath, 'This is an old file.');
// Modify the 'mtime' to simulate an old file
const oldTime = new Date();
oldTime.setFullYear(oldTime.getFullYear() - 1);
fs.utimesSync(oldFilePath, oldTime, oldTime);
// Create a new file
fs.writeFileSync(newFilePath, 'This is a new file.');
});
// delete the temporary files after the test
after(() => {
fs.rmSync(testFolderPath, { recursive: true });
});
describe('isFileNewerThan', async () => {
it('should return true if the file is newer than the provided time', async () => {
const pastTime = Date.now() - 10000; // 10 seconds ago
assert.strictEqual(cdata.isFileNewerThan(newFilePath, pastTime), true);
});
it('should return false if the file is older than the provided time', async () => {
assert.strictEqual(cdata.isFileNewerThan(oldFilePath, Date.now()), false);
});
it('should throw an exception if the file does not exist', async () => {
assert.throws(() => {
cdata.isFileNewerThan('nonexistent.txt', Date.now());
});
});
});
describe('isAnyFileInFolderNewerThan', async () => {
it('should return true if a file in the folder is newer than the given time', async () => {
const time = fs.statSync(path.join(testFolderPath, 'oldFile.txt')).mtime;
assert.strictEqual(cdata.isAnyFileInFolderNewerThan(testFolderPath, time), true);
});
it('should return false if no files in the folder are newer than the given time', async () => {
assert.strictEqual(cdata.isAnyFileInFolderNewerThan(testFolderPath, new Date()), false);
});
it('should throw an exception if the folder does not exist', async () => {
assert.throws(() => {
cdata.isAnyFileInFolderNewerThan('nonexistent', new Date());
});
});
});
});
describe('Script', () => {
const folderPath = 'wled00';
const dataPath = path.join(folderPath, 'data');
before(() => {
process.env.NODE_ENV = 'production';
// Backup files
fs.cpSync("wled00/data", "wled00Backup", { recursive: true });
fs.cpSync("tools/cdata.js", "cdata.bak.js");
fs.cpSync("package.json", "package.bak.json");
});
after(() => {
// Restore backup
fs.rmSync("wled00/data", { recursive: true });
fs.renameSync("wled00Backup", "wled00/data");
fs.rmSync("tools/cdata.js");
fs.renameSync("cdata.bak.js", "tools/cdata.js");
fs.rmSync("package.json");
fs.renameSync("package.bak.json", "package.json");
});
// delete all html_*.h files
async function deleteBuiltFiles() {
const files = await fs.promises.readdir(folderPath);
await Promise.all(files.map(file => {
if (file.startsWith('html_') && path.extname(file) === '.h') {
return fs.promises.unlink(path.join(folderPath, file));
}
}));
}
// check if html_*.h files were created
async function checkIfBuiltFilesExist() {
const files = await fs.promises.readdir(folderPath);
const htmlFiles = files.filter(file => file.startsWith('html_') && path.extname(file) === '.h');
assert(htmlFiles.length > 0, 'html_*.h files were not created');
}
async function runAndCheckIfBuiltFilesExist() {
await execPromise('node tools/cdata.js');
await checkIfBuiltFilesExist();
}
async function checkIfFileWasNewlyCreated(file) {
const modifiedTime = fs.statSync(file).mtimeMs;
assert(Date.now() - modifiedTime < 500, file + ' was not modified');
}
async function testFileModification(sourceFilePath, resultFile) {
// run cdata.js to ensure html_*.h files are created
await execPromise('node tools/cdata.js');
// modify file
fs.appendFileSync(sourceFilePath, ' ');
// delay for 1 second to ensure the modified time is different
await new Promise(resolve => setTimeout(resolve, 1000));
// run script cdata.js again and wait for it to finish
await execPromise('node tools/cdata.js');
await checkIfFileWasNewlyCreated(path.join(folderPath, resultFile));
}
describe('should build if', () => {
it('html_*.h files are missing', async () => {
await deleteBuiltFiles();
await runAndCheckIfBuiltFilesExist();
});
it('only one html_*.h file is missing', async () => {
// run script cdata.js and wait for it to finish
await execPromise('node tools/cdata.js');
// delete a random html_*.h file
let files = await fs.promises.readdir(folderPath);
let htmlFiles = files.filter(file => file.startsWith('html_') && path.extname(file) === '.h');
const randomFile = htmlFiles[Math.floor(Math.random() * htmlFiles.length)];
await fs.promises.unlink(path.join(folderPath, randomFile));
await runAndCheckIfBuiltFilesExist();
});
it('script was executed with -f or --force', async () => {
await execPromise('node tools/cdata.js');
await new Promise(resolve => setTimeout(resolve, 1000));
await execPromise('node tools/cdata.js --force');
await checkIfFileWasNewlyCreated(path.join(folderPath, 'html_ui.h'));
await new Promise(resolve => setTimeout(resolve, 1000));
await execPromise('node tools/cdata.js -f');
await checkIfFileWasNewlyCreated(path.join(folderPath, 'html_ui.h'));
});
it('a file changes', async () => {
await testFileModification(path.join(dataPath, 'index.htm'), 'html_ui.h');
});
it('a inlined file changes', async () => {
await testFileModification(path.join(dataPath, 'index.js'), 'html_ui.h');
});
it('a settings file changes', async () => {
await testFileModification(path.join(dataPath, 'settings_leds.htm'), 'html_ui.h');
});
it('the favicon changes', async () => {
await testFileModification(path.join(dataPath, 'favicon.ico'), 'html_ui.h');
});
it('cdata.js changes', async () => {
await testFileModification('tools/cdata.js', 'html_ui.h');
});
it('package.json changes', async () => {
await testFileModification('package.json', 'html_ui.h');
});
});
describe('should not build if', () => {
it('the files are already built', async () => {
await deleteBuiltFiles();
// run script cdata.js and wait for it to finish
let startTime = Date.now();
await execPromise('node tools/cdata.js');
const firstRunTime = Date.now() - startTime;
// run script cdata.js and wait for it to finish
startTime = Date.now();
await execPromise('node tools/cdata.js');
const secondRunTime = Date.now() - startTime;
// check if second run was faster than the first (must be at least 2x faster)
assert(secondRunTime < firstRunTime / 2, 'html_*.h files were rebuilt');
});
});
});

View File

@@ -2,7 +2,7 @@
* Writes compressed C arrays of data files (web interface)
* How to use it?
*
* 1) Install Node 11+ and npm
* 1) Install Node 20+ and npm
* 2) npm install
* 3) npm run build
*
@@ -15,26 +15,70 @@
* It uses NodeJS packages to inline, minify and GZIP files. See writeHtmlGzipped and writeChunks invocations at the bottom of the page.
*/
const fs = require("fs");
const inliner = require("inliner");
const zlib = require("zlib");
const fs = require("node:fs");
const path = require("path");
const inline = require("web-resource-inliner");
const zlib = require("node:zlib");
const CleanCSS = require("clean-css");
const MinifyHTML = require("html-minifier-terser").minify;
const minifyHtml = require("html-minifier-terser").minify;
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_pixelforge.h", "wled00/html_settings.h", "wled00/html_other.h"]
const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h", "wled00/html_edit.h", "wled00/html_pxmagic.h", "wled00/html_settings.h", "wled00/html_other.h"]
// \x1b[34m is blue, \x1b[36m is cyan, \x1b[0m is reset
const wledBanner = `
\t\x1b[34m ## ## ## ###### ######
\t\x1b[34m## ## ## ## ## ## ##
\t\x1b[34m## ## ## ## ###### ## ##
\t\x1b[34m## ## ## ## ## ## ##
\t\x1b[34m ## ## ###### ###### ######
\t\t\x1b[36m build script for web UI
\x1b[0m`;
// Generate build timestamp as UNIX timestamp (seconds since epoch)
function generateBuildTime() {
return Math.floor(Date.now() / 1000);
}
const singleHeader = `/*
* Binary array for the Web UI.
* gzip is used for smaller size and improved speeds.
*
* Please see https://mm.kno.wled.ge/advanced/custom-features/#changing-web-ui
* to find out how to easily modify the web UI source!
*/
function hexdump(buffer,isHex=false) {
// Automatically generated build time for cache busting (UNIX timestamp)
#ifdef WEB_BUILD_TIME
#undef WEB_BUILD_TIME
#endif
#define WEB_BUILD_TIME ${generateBuildTime()}
`;
const multiHeader = `/*
* More web UI HTML source arrays.
* This file is auto generated, please don't make any changes manually.
*
* Instead, see https://mm.kno.wled.ge/advanced/custom-features/#changing-web-ui
* to find out how to easily modify the web UI source!
*/
`;
function hexdump(buffer, isHex = false) {
let lines = [];
for (let i = 0; i < buffer.length; i +=(isHex?32:16)) {
for (let i = 0; i < buffer.length; i += (isHex ? 32 : 16)) {
var block;
let hexArray = [];
if (isHex) {
block = buffer.slice(i, i + 32)
for (let j = 0; j < block.length; j +=2 ) {
hexArray.push("0x" + block.slice(j,j+2))
for (let j = 0; j < block.length; j += 2) {
hexArray.push("0x" + block.slice(j, j + 2))
}
} else {
block = buffer.slice(i, i + 16); // cut buffer into blocks of 16
@@ -51,219 +95,203 @@ function hexdump(buffer,isHex=false) {
return lines.join(",\n");
}
function strReplace(str, search, replacement) {
return str.split(search).join(replacement);
}
function adoptVersionAndRepo(html) {
let repoUrl = packageJson.repository ? packageJson.repository.url : undefined;
if (repoUrl) {
repoUrl = repoUrl.replace(/^git\+/, "");
repoUrl = repoUrl.replace(/\.git$/, "");
// Replace we
html = strReplace(html, "https://github.com/atuline/WLED", repoUrl);
html = strReplace(html, "https://github.com/Aircoookie/WLED", repoUrl);
// html = strReplace(html, "https://github.com/wled-dev/WLED", repoUrl); // replacing upstream break "credits"
// html = strReplace(html, "https://github.com/wled/WLED", repoUrl);
// html = strReplace(html, "https://github.com/MoonModules/WLED", repoUrl); //WLEDMM
// html = strReplace(html, "https://github.com/MoonModules/WLED-MM", repoUrl); //WLEDMM - not necessary to replace ourselves ;-)
// html = html.replaceAll("https://github.com/wled-dev/WLED", repoUrl); // WLEDMM replacing upstream break "credits"
html = html.replaceAll("https://github.com/atuline/WLED", repoUrl);
html = html.replaceAll("https://github.com/Aircoookie/WLED", repoUrl);
}
let version = packageJson.version;
if (version) {
html = strReplace(html, "##VERSION##", version);
html = html.replaceAll("##VERSION##", version);
}
return html;
}
function filter(str, type) {
str = adoptVersionAndRepo(str);
if (type === undefined) {
async function minify(str, type = "plain") {
const options = {
collapseWhitespace: true,
conservativeCollapse: true, // preserve spaces in text
collapseBooleanAttributes: true,
collapseInlineTagWhitespace: true,
minifyCSS: true,
minifyJS: true,
removeAttributeQuotes: true,
removeComments: true,
sortAttributes: true,
sortClassName: true,
};
if (type == "plain") {
return str;
} else if (type == "css-minify") {
return new CleanCSS({}).minify(str).styles;
} else if (type == "js-minify") {
return MinifyHTML('<script>' + str + '</script>', {
collapseWhitespace: true,
minifyJS: true,
continueOnParseError: false,
removeComments: true,
}).replace(/<[\/]*script>/g,'');
let js = await minifyHtml('<script>' + str + '</script>', options);
return js.replace(/<[\/]*script>/g, '');
} else if (type == "html-minify") {
return MinifyHTML(str, {
collapseWhitespace: true,
maxLineLength: 80,
minifyCSS: true,
minifyJS: true,
continueOnParseError: false,
removeComments: true,
});
} else if (type == "html-minify-ui") {
return MinifyHTML(str, {
collapseWhitespace: true,
conservativeCollapse: true,
maxLineLength: 80,
minifyCSS: true,
minifyJS: true,
continueOnParseError: false,
removeComments: true,
});
} else {
console.warn("Unknown filter: " + type);
return str;
return await minifyHtml(str, options);
}
throw new Error("Unknown filter: " + type);
}
// Generate build timestamp as UNIX timestamp (seconds since epoch)
function generateBuildTime() {
return Math.floor(Date.now() / 1000);
}
function writeHtmlGzipped(sourceFile, resultFile, page) {
async function writeHtmlGzipped(sourceFile, resultFile, page, inlineCss = true) {
console.info("Reading " + sourceFile);
new inliner(sourceFile, function (error, html) {
console.info("Inlined " + html.length + " characters");
html = filter(html, "html-minify-ui");
console.info("Minified to " + html.length + " characters");
inline.html({
fileContent: fs.readFileSync(sourceFile, "utf8"),
relativeTo: path.dirname(sourceFile),
strict: inlineCss, // when not inlining css, ignore errors (enables linking style.css from subfolder htm files)
stylesheets: inlineCss // when true (default), css is inlined
},
async function (error, html) {
if (error) throw error;
if (error) {
console.warn(error);
throw error;
}
html = adoptVersionAndRepo(html);
zlib.gzip(html, { level: zlib.constants.Z_BEST_COMPRESSION }, function (error, result) {
if (error) {
console.warn(error);
throw error;
}
console.info("Compressed " + result.length + " bytes");
html = adoptVersionAndRepo(html);
const originalLength = html.length;
html = await minify(html, "html-minify");
const result = zlib.gzipSync(html, { level: zlib.constants.Z_BEST_COMPRESSION });
console.info("Minified and compressed " + sourceFile + " from " + originalLength + " to " + result.length + " bytes");
const array = hexdump(result);
const src = `/*
* Binary array for the Web UI.
* gzip is used for smaller size and improved speeds.
*
* Please see https://mm.kno.wled.ge/advanced/custom-features/#changing-web-ui
* to find out how to easily modify the web UI source!
*/
// Automatically generated build time for cache busting (UNIX timestamp)
#ifdef WEB_BUILD_TIME // avoid duplicate defintions
#undef WEB_BUILD_TIME
#endif
#define WEB_BUILD_TIME ${generateBuildTime()}
// Autogenerated from ${sourceFile}, do not edit!!
const uint16_t PAGE_${page}_L = ${result.length};
const uint8_t PAGE_${page}[] PROGMEM = {
${array}
};
`;
let src = singleHeader;
src += `const uint16_t PAGE_${page}_L = ${result.length};\n`;
src += `const uint8_t PAGE_${page}[] PROGMEM = {\n${array}\n};\n\n`;
console.info("Writing " + resultFile);
fs.writeFileSync(resultFile, src);
});
});
}
function specToChunk(srcDir, s) {
if (s.method == "plaintext") {
const buf = fs.readFileSync(srcDir + "/" + s.file);
const str = buf.toString("utf-8");
const chunk = `
// Autogenerated from ${srcDir}/${s.file}, do not edit!!
const char ${s.name}[] PROGMEM = R"${s.prepend || ""}${filter(str, s.filter)}${
s.append || ""
}";
async function specToChunk(srcDir, s) {
const buf = fs.readFileSync(srcDir + "/" + s.file);
let chunk = `\n// Autogenerated from ${srcDir}/${s.file}, do not edit!!\n`
`;
return s.mangle ? s.mangle(chunk) : chunk;
} else if (s.method == "gzip") {
const buf = fs.readFileSync(srcDir + "/" + s.file);
var str = buf.toString('utf-8');
if (s.mangle) str = s.mangle(str);
const zip = zlib.gzipSync(filter(str, s.filter), { level: zlib.constants.Z_BEST_COMPRESSION });
const result = hexdump(zip.toString('hex'), true);
const chunk = `
// Autogenerated from ${srcDir}/${s.file}, do not edit!!
const uint16_t ${s.name}_length = ${zip.length};
const uint8_t ${s.name}[] PROGMEM = {
${result}
};
`;
return chunk;
} else if (s.method == "binary") {
const buf = fs.readFileSync(srcDir + "/" + s.file);
const result = hexdump(buf);
const chunk = `
// Autogenerated from ${srcDir}/${s.file}, do not edit!!
const uint16_t ${s.name}_length = ${result.length};
const uint8_t ${s.name}[] PROGMEM = {
${result}
};
`;
return chunk;
} else {
console.warn("Unknown method: " + s.method);
return undefined;
}
}
function writeChunks(srcDir, specs, resultFile) {
let src = `/*
* More web UI HTML source arrays.
* This file is auto generated, please don't make any changes manually.
* Instead, see https://mm.kno.wled.ge/advanced/custom-features/#changing-web-ui
* to find out how to easily modify the web UI source!
*/
`;
specs.forEach((s) => {
try {
console.info("Reading " + srcDir + "/" + s.file + " as " + s.name);
src += specToChunk(srcDir, s);
} catch (e) {
console.warn(
"Failed " + s.name + " from " + srcDir + "/" + s.file,
e.message.length > 60 ? e.message.substring(0, 60) : e.message
);
if (s.method == "plaintext" || s.method == "gzip") {
let str = buf.toString("utf-8");
str = adoptVersionAndRepo(str);
const originalLength = str.length;
if (s.method == "gzip") {
if (s.mangle) str = s.mangle(str);
const zip = zlib.gzipSync(await minify(str, s.filter), { level: zlib.constants.Z_BEST_COMPRESSION });
console.info("Minified and compressed " + s.file + " from " + originalLength + " to " + zip.length + " bytes");
const result = hexdump(zip);
chunk += `const uint16_t ${s.name}_length = ${zip.length};\n`;
chunk += `const uint8_t ${s.name}[] PROGMEM = {\n${result}\n};\n\n`;
return chunk;
} else {
const minified = await minify(str, s.filter);
console.info("Minified " + s.file + " from " + originalLength + " to " + minified.length + " bytes");
chunk += `const char ${s.name}[] PROGMEM = R"${s.prepend || ""}${minified}${s.append || ""}";\n\n`;
return s.mangle ? s.mangle(chunk) : chunk;
}
});
} else if (s.method == "binary") {
const result = hexdump(buf);
chunk += `const uint16_t ${s.name}_length = ${buf.length};\n`;
chunk += `const uint8_t ${s.name}[] PROGMEM = {\n${result}\n};\n\n`;
return chunk;
}
throw new Error("Unknown method: " + s.method);
}
async function writeChunks(srcDir, specs, resultFile) {
let src = multiHeader;
for (const s of specs) {
console.info("Reading " + srcDir + "/" + s.file + " as " + s.name);
src += await specToChunk(srcDir, s);
}
console.info("Writing " + src.length + " characters into " + resultFile);
fs.writeFileSync(resultFile, src);
}
writeHtmlGzipped("wled00/data/index.htm", "wled00/html_ui.h", 'index');
// Check if a file is newer than a given time
function isFileNewerThan(filePath, time) {
const stats = fs.statSync(filePath);
return stats.mtimeMs > time;
}
// Check if any file in a folder (or its subfolders) is newer than a given time
function isAnyFileInFolderNewerThan(folderPath, time) {
const files = fs.readdirSync(folderPath, { withFileTypes: true });
for (const file of files) {
const filePath = path.join(folderPath, file.name);
if (isFileNewerThan(filePath, time)) {
return true;
}
if (file.isDirectory() && isAnyFileInFolderNewerThan(filePath, time)) {
return true;
}
}
return false;
}
// Check if the web UI is already built
function isAlreadyBuilt(webUIPath, packageJsonPath = "package.json") {
let lastBuildTime = Infinity;
for (const file of output) {
try {
lastBuildTime = Math.min(lastBuildTime, fs.statSync(file).mtimeMs);
} catch (e) {
if (e.code !== 'ENOENT') throw e;
console.info("File " + file + " does not exist. Rebuilding...");
return false;
}
}
return !isAnyFileInFolderNewerThan(webUIPath, lastBuildTime) && !isFileNewerThan(packageJsonPath, lastBuildTime) && !isFileNewerThan(__filename, lastBuildTime);
}
// Don't run this script if we're in a test environment
if (process.env.NODE_ENV === 'test') {
return;
}
console.info(wledBanner);
if (isAlreadyBuilt("wled00/data") && process.argv[2] !== '--force' && process.argv[2] !== '-f') {
console.info("Web UI is already built");
return;
}
writeHtmlGzipped("wled00/data/index.htm", "wled00/html_ui.h", 'index', false);
writeHtmlGzipped("wled00/data/simple.htm", "wled00/html_simple.h", 'simple');
writeHtmlGzipped("wled00/data/pixart/pixart.htm", "wled00/html_pixart.h", 'pixart');
writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal');
writeHtmlGzipped("wled00/data/pxmagic/pxmagic.htm", "wled00/html_pxmagic.h", 'pxmagic');
/*
writeHtmlGzipped("wled00/data/pixelforge/pixelforge.htm", "wled00/html_pixelforge.h", 'pixelforge', false); // do not inline css
writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal');
//writeHtmlGzipped("wled00/data/edit.htm", "wled00/html_edit.h", 'edit');
writeChunks(
"wled00/data",
[
{
file: "simple.css",
name: "PAGE_simpleCss",
file: "edit.htm",
name: "PAGE_edit",
method: "gzip",
filter: "css-minify",
},
{
file: "simple.js",
name: "PAGE_simpleJs",
method: "gzip",
filter: "js-minify",
},
{
file: "simple.htm",
name: "PAGE_simple",
method: "gzip",
filter: "html-minify-ui",
filter: "html-minify"
}
],
"wled00/html_simplex.h"
"wled00/html_edit.h"
);
/*
writeChunks(
"wled00/data/cpal",
[
{
file: "cpal.htm",
name: "PAGE_cpal",
method: "gzip",
filter: "html-minify"
}
],
"wled00/html_cpal.h"
);
*/
writeChunks(
"wled00/data",
[
@@ -274,7 +302,13 @@ writeChunks(
filter: "css-minify",
mangle: (str) =>
str
.replace("%%","%")
.replace("%%", "%")
},
{
file: "common.js",
name: "JS_common",
method: "gzip",
filter: "js-minify",
},
{
file: "settings.htm",
@@ -430,6 +464,7 @@ const char PAGE_dmxmap[] PROGMEM = R"=====()=====";
method: "gzip",
filter: "html-minify",
},
//WLEDMM
{
file: "404mini.htm",
name: "PAGE_404_mini",
@@ -444,12 +479,14 @@ const char PAGE_dmxmap[] PROGMEM = R"=====()=====";
{
file: "iro.js",
name: "iroJs",
method: "gzip"
method: "gzip",
filter: "js-minify",
},
{
file: "rangetouch.js",
name: "rangetouchJs",
method: "gzip"
method: "gzip",
filter: "js-minify"
}
],
"wled00/html_other.h"

View File

@@ -984,7 +984,7 @@ class WS2812FX { // 96 bytes
printSize(),
#endif
finalizeInit(),
waitUntilIdle(void), // WLEDMM
waitUntilIdle(unsigned timeout = 0), // WLEDMM
service(void),
setMode(uint8_t segid, uint8_t m),
setColor(uint8_t slot, uint32_t c),
@@ -1037,8 +1037,8 @@ class WS2812FX { // 96 bytes
getActiveSegmentsNum(void) const,
__attribute__((pure)) getFirstSelectedSegId(void),
getLastActiveSegmentId(void) const,
__attribute__((pure)) getActiveSegsLightCapabilities(bool selectedOnly = false),
setPixelSegment(uint8_t n);
__attribute__((pure)) getActiveSegsLightCapabilities(bool selectedOnly = false);
//setPixelSegment(uint8_t n);
inline uint8_t getBrightness(void) const { return _brightness; }
inline uint8_t getSegmentsNum(void) const { return _segments.size(); } // returns currently present segments
@@ -1248,11 +1248,13 @@ class WS2812FX { // 96 bytes
// will require only 1 byte
struct {
bool _isServicing : 1;
bool _isServicing : 1; // can stay inside the bitfield - not critical any more since we have a mutex
bool _isOffRefreshRequired : 1; //periodic refresh is required for the strip to remain off.
bool _hasWhiteChannel : 1;
bool _triggered : 1;
//bool _triggered : 1;
bool unusedBit : 1;
};
volatile bool _triggered; // WLEDMM moved out of struct, so the flag can be updated in one atomic access
uint8_t _modeCount;
std::vector<mode_ptr> _mode; // SRAM footprint: 4 bytes per element

View File

@@ -11,7 +11,7 @@
#include <esp_timer.h> // WLEDMM to get esp_timer_get_time()
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
static portMUX_TYPE s_wled_strip_mux = portMUX_INITIALIZER_UNLOCKED; // to protect deleting Segment::_globalLeds
WLED_create_spinlock(ledsrgb_mux); // to protect deleting Segment::_globalLeds and Segment::ledsrgb
#endif
/*
@@ -116,8 +116,21 @@ void Segment::allocLeds() {
}
if ((size > 0) && (!ledsrgb || size > ledsrgbSize)) { //softhack dont allocate zero bytes
DEBUG_PRINTF("allocLeds (%d,%d to %d,%d), %u from %u\n", start, startY, stop, stopY, size, ledsrgb?ledsrgbSize:0);
if (ledsrgb) free(ledsrgb); // we need a bigger buffer, so free the old one first
ledsrgb = (CRGB*)calloc(size, 1);
// DONG - Valkyrie needs food, badly [Gauntlet, 1985]
// WLEDMM this looks a bit over-compilicated, but it makes the re-allocation step an atomic and threadsafe operation
CRGB* oldLedsRgb = ledsrgb;
portENTER_CRITICAL(&ledsrgb_mux);
ledsrgb = nullptr;
portEXIT_CRITICAL(&ledsrgb_mux);
if (oldLedsRgb) free(oldLedsRgb); // we need a bigger buffer, so free the old one first
CRGB* newLedsRgb = (CRGB*)calloc(1, size); // WLEDMM This is an OS call, so we should not wrap it in portEnterCRITICAL
portENTER_CRITICAL(&ledsrgb_mux);
ledsrgb = newLedsRgb;
portEXIT_CRITICAL(&ledsrgb_mux);
ledsrgbSize = ledsrgb?size:0;
if (ledsrgb == nullptr) {
USER_PRINTLN("allocLeds failed!!");
@@ -558,21 +571,29 @@ void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t
markForReset();
return;
}
if (i1 < Segment::maxWidth || (i1 >= Segment::maxWidth*Segment::maxHeight && i1 < strip.getLengthTotal())) start = i1; // Segment::maxWidth equals strip.getLengthTotal() for 1D
stop = i2 > Segment::maxWidth*Segment::maxHeight ? min(i2,strip.getLengthTotal()) : (i2 > Segment::maxWidth ? Segment::maxWidth : max((uint16_t)1,i2)); // WLEDMM: use native min/max
startY = 0;
stopY = 1;
#ifndef WLED_DISABLE_2D
if (Segment::maxHeight>1) { // 2D
if (i1Y < Segment::maxHeight) startY = i1Y;
stopY = i2Y > Segment::maxHeight ? Segment::maxHeight : max((uint16_t)1,i2Y); // WLEDMM: use native min/max
if (esp32SemTake(segmentMux, 2100) == pdTRUE) { // wait long, but don't wait forever
// WLEDMM acquire lock before doing critical changes to segment
if (i1 < Segment::maxWidth || (i1 >= Segment::maxWidth*Segment::maxHeight && i1 < strip.getLengthTotal())) start = i1; // Segment::maxWidth equals strip.getLengthTotal() for 1D
stop = i2 > Segment::maxWidth*Segment::maxHeight ? min(i2,strip.getLengthTotal()) : (i2 > Segment::maxWidth ? Segment::maxWidth : max((uint16_t)1,i2)); // WLEDMM: use native min/max
startY = 0;
stopY = 1;
#ifndef WLED_DISABLE_2D
if (Segment::maxHeight>1) { // 2D
if (i1Y < Segment::maxHeight) startY = i1Y;
stopY = i2Y > Segment::maxHeight ? Segment::maxHeight : max((uint16_t)1,i2Y); // WLEDMM: use native min/max
}
#endif
if (grp) {
grouping = grp;
spacing = spc;
}
if (ofs < UINT16_MAX) offset = ofs;
esp32SemGive(segmentMux);
} else {
DEBUG_PRINTLN(F("Segment::setUp: Failed to acquire segmentMux, skipping bounds update."));
boundsUnchanged = true;
}
#endif
if (grp) {
grouping = grp;
spacing = spc;
}
if (ofs < UINT16_MAX) offset = ofs;
if (!boundsUnchanged) {
markForReset();
@@ -1443,8 +1464,9 @@ void Segment::refreshLightCapabilities() {
void __attribute__((hot)) Segment::fill(uint32_t c) {
if (!isActive()) return; // not active
const uint_fast16_t cols = is2D() ? virtualWidth() : virtualLength(); // WLEDMM use fast int types
const uint_fast16_t rows = virtualHeight(); // will be 1 for 1D
// WLEDMM use "calc_" functions because fill() is also called from json.cpp without previous seg.startFrame
const uint_fast16_t cols = is2D() ? calc_virtualWidth() : calc_virtualLength(); // WLEDMM use fast int types
const uint_fast16_t rows = calc_virtualHeight(); // will be 1 for 1D
if (is2D()) {
// pre-calculate scaled color
@@ -1867,19 +1889,16 @@ void WS2812FX::finalizeInit(void)
//initialize leds array. TBD: realloc if nr of leds change
if (Segment::_globalLeds) {
// DONG - Valkyrie is about to die
// DONG - Valkyrie is about to die [Gauntlet, 1985]
// this is a critical section that will be removed with PR #278 which removes _globalLeds
// problem: suspendStripService provides interlocking, but theres a window before service() observes it,
// and ESP32 is dual-core. A critical section closes that window so the pointer swap is atomic across cores.
#if defined(ARDUINO_ARCH_ESP32)
portENTER_CRITICAL(&s_wled_strip_mux);
#endif
free(Segment::_globalLeds);
CRGB* oldGLeds = Segment::_globalLeds;
portENTER_CRITICAL(&ledsrgb_mux);
Segment::_globalLeds = nullptr;
portEXIT_CRITICAL(&ledsrgb_mux);
free(oldGLeds);
purgeSegments(true); // WLEDMM moved here, because it seems to improve stability.
#if defined(ARDUINO_ARCH_ESP32)
portEXIT_CRITICAL(&s_wled_strip_mux);
#endif
}
if (useLedsArray && getLengthTotal()>0) { // WLEDMM avoid malloc(0)
size_t arrSize = sizeof(CRGB) * getLengthTotal();
@@ -1907,14 +1926,15 @@ void WS2812FX::finalizeInit(void)
// on 8266 this function does nothing, because we can only do "busy waiting" on ESP32
//#define MAX_IDLE_WAIT_MS 50 // seems to work in most cases
#define MAX_IDLE_WAIT_MS 120 // better safe than sorry - similar to the timeout used by upstream WLED
void WS2812FX::waitUntilIdle(void) {
void WS2812FX::waitUntilIdle(unsigned timeout) {
#if defined(ARDUINO_ARCH_ESP32) && defined(WLEDMM_PROTECT_SERVICE)
if (timeout < MAX_IDLE_WAIT_MS) timeout = MAX_IDLE_WAIT_MS;
if (isServicing()) {
unsigned long waitStarted = millis();
do {
delay(2); // Suspending for 1 tick (or more) gives other tasks a chance to run.
//yield(); // seems to be a no-op on esp32
} while (isServicing() && (millis() - waitStarted < MAX_IDLE_WAIT_MS));
} while (isServicing() && (millis() - waitStarted < timeout));
DEBUG_PRINTF("strip.waitUntilIdle(): strip %sidle after %d ms. (task %s with prio=%d)\n", isServicing()?"not ":"", int(millis() - waitStarted), pcTaskGetTaskName(NULL), uxTaskPriorityGet(NULL));
if (isServicing()) USER_PRINTF("strip.waitUntilIdle(): strip NOT idle after %d ms - overriding access. (task %s with prio=%d)\n", int(millis() - waitStarted), pcTaskGetTaskName(NULL), uxTaskPriorityGet(NULL));
}
@@ -1927,6 +1947,10 @@ void WS2812FX::waitUntilIdle(void) {
void WS2812FX::service() {
unsigned long nowUp = millis(); // Be aware, millis() rolls over every 49 days // WLEDMM avoid losing precision
if (OTAisRunning) return; // WLEDMM avoid flickering during OTA
//#ifdef ARDUINO_ARCH_ESP32
//if ((_isServicing == true) && (strncmp(pcTaskGetTaskName(NULL), "loopTask", 8) != 0)) return; // WLEDMM experimental: not in looptask context - avoid self-blocking (DDP over webSockets)
//#endif
now = nowUp + timebase;
unsigned long elapsed = nowUp - _lastServiceShow;
@@ -1934,11 +1958,7 @@ void WS2812FX::service() {
//if (_suspend) return;
if (elapsed < 2) return; // keep wifi alive
if ( !_triggered && (_targetFps != FPS_UNLIMITED) && (_targetFps != FPS_UNLIMITED_AC)) {
#if 0
if (elapsed < MIN_SHOW_DELAY) return; // WLEDMM too early for service - delivers higher fps
#else
if ((elapsed+1) < _frametime) return; // code from upstream - stricter on FPS
#endif
}
#else // legacy
if (elapsed < _frametime) return;
@@ -1949,6 +1969,7 @@ void WS2812FX::service() {
_isServicing = true;
_segment_index = 0;
if (esp32SemTake(segmentMux, 250) == pdTRUE) { // WLEDMM prevent changes to segments while servicing
for (segment &seg : _segments) {
#ifdef WLEDMM_FASTPATH
_currentSeg = &seg;
@@ -1975,14 +1996,15 @@ void WS2812FX::service() {
if (!cctFromRgb || correctWB) busses.setSegmentCCT(seg.currentBri(seg.cct, true), correctWB);
for (uint8_t c = 0; c < NUM_COLORS; c++) _colors_t[c] = gamma32(_colors_t[c]);
#if 0 // WARNING this would kill _supersync_
now = millis() + timebase;
#endif
seg.startFrame(); // WLEDMM
if (!_triggered && (seg.currentBri(seg.opacity) == 0) && (seg.lastBri == 0)) continue; // WLEDMM skip totally black segments
// effect blending (execute previous effect)
// actual code may be a bit more involved as effects have runtime data including allocated memory
//if (seg.transitional && seg._modeP) (*_mode[seg._modeP])(progress());
// WLEDMM protect against parallel drawing
if (esp32SemTake(busDrawMux, 200) != pdTRUE) { delay(1); continue;} // WLEDMM first acquire draw mutex
frameDelay = (*_mode[seg.currentMode(seg.mode)])();
if (frameDelay < speedLimit) frameDelay = FRAMETIME; // WLEDMM limit effects that want to go faster than target FPS
@@ -1993,26 +2015,24 @@ void WS2812FX::service() {
seg.lastBri = seg.currentBri(seg.on ? seg.opacity:0); // WLEDMM remember for next time
seg.handleTransition();
esp32SemGive(busDrawMux); // WLEDMM unlock mutex
}
seg.next_time = nowUp + frameDelay;
}
_segment_index++;
}
if (_triggered) doShow = true; // WLEDMM "triggered" always means "show"
esp32SemGive(segmentMux);
} // end of critical section
#ifdef WLEDMM_FASTPATH
_currentSeg = & strip.getMainSegment(); // WLEDMM safe default
#endif
_virtualSegmentLength = 0;
busses.setSegmentCCT(-1);
if(doShow) {
#if 0 && defined(ARDUINO_ARCH_ESP32) // EXPERIMENTAL - enabled this to enforce stricter frametime limits
static unsigned long lastTimeShow = 0;
long tdelta = millis() - lastTimeShow;
if ((lastTimeShow > 0) && (tdelta > 1) && (tdelta < _frametime)) // too early - release CPU to slow down
vTaskDelay((tdelta-1) / portTICK_PERIOD_MS); // "-1" because vTaskDelay() may actually delay longer than requested
lastTimeShow = millis();
#else
if(doShow || _triggered) {
yield();
#endif
show();
_lastServiceShow = nowUp; // WLEDMM use correct timestamp
}
@@ -2132,7 +2152,11 @@ void WS2812FX::show(void) {
// some buses send asynchronously and this method will return before
// all of the data has been sent.
// See https://github.com/Makuna/NeoPixelBus/wiki/ESP32-NeoMethods#neoesp32rmt-methods
// WLEDMM protect against parallel access
if (esp32SemTake(busDrawMux, 200) != pdTRUE) { delay(1); return;} // WLEDMM first acquire drawing permission (mutex), wait max 200ms
busses.show();
esp32SemGive(busDrawMux); // WLEDMM return permissions
unsigned long diff = showNow - _lastShow;
uint16_t fpsCurr = 200;
@@ -2331,15 +2355,19 @@ void WS2812FX::purgeSegments(bool force) {
// remove all inactive segments (from the back)
int deleted = 0;
if (_segments.size() <= 1) return;
for (size_t i = _segments.size()-1; i > 0; i--)
// WLEDMM protect against parallel access while drawing
if (esp32SemTake(segmentMux, 300) != pdTRUE) return;
for (size_t i = _segments.size()-1; i > 0; i--) {
if (_segments[i].stop == 0 || force) {
deleted++;
_segments.erase(_segments.begin() + i);
}
} }
if (deleted) {
_segments.shrink_to_fit();
/*if (_mainSegment >= _segments.size())*/ setMainSegmentId(0);
}
esp32SemGive(segmentMux);
}
Segment& WS2812FX::getSegment(uint8_t id) {
@@ -2367,6 +2395,9 @@ void WS2812FX::restartRuntime(bool doReset) {
void WS2812FX::resetSegments(bool boundsOnly) { //WLEDMM add boundsonly
DEBUG_PRINTF("resetSegments %d %dx%d\n", boundsOnly, Segment::maxWidth, Segment::maxHeight);
if (!boundsOnly) {
// WLEDMM protect against parallel access while drawing
if (esp32SemTake(segmentMux, 2100) != pdTRUE) return; // wait long, but don't wait forever
_segments.clear(); // destructs all Segment as part of clearing
#ifndef WLED_DISABLE_2D
segment seg = isMatrix ? Segment(0, Segment::maxWidth, 0, Segment::maxHeight) : Segment(0, _length);
@@ -2375,7 +2406,10 @@ void WS2812FX::resetSegments(bool boundsOnly) { //WLEDMM add boundsonly
#endif
_segments.push_back(seg);
_mainSegment = 0;
esp32SemGive(segmentMux);
} else { //WLEDMM boundsonly
// WLEDMM protect against parallel access while drawing
if (esp32SemTake(segmentMux, 2100) != pdTRUE) return; // wait long, but don't wait forever
for (segment &seg : _segments) {
#ifndef WLED_DISABLE_2D
seg.start = 0;
@@ -2388,11 +2422,15 @@ void WS2812FX::resetSegments(bool boundsOnly) { //WLEDMM add boundsonly
#endif
seg.allocLeds();
}
esp32SemGive(segmentMux);
}
}
void WS2812FX::makeAutoSegments(bool forceReset) {
if (autoSegments) { //make one segment per bus
// WLEDMM protect against parallel access while drawing
if (esp32SemTake(segmentMux, 2100) != pdTRUE) return; // wait long, but don't wait forever
uint16_t segStarts[MAX_NUM_SEGMENTS] = {0};
uint16_t segStops [MAX_NUM_SEGMENTS] = {0};
size_t s = 0;
@@ -2441,7 +2479,7 @@ void WS2812FX::makeAutoSegments(bool forceReset) {
for (size_t i = 1; i < s; i++) {
_segments.push_back(Segment(segStarts[i], segStops[i]));
}
esp32SemGive(segmentMux);
} else {
if (forceReset || getSegmentsNum() == 0) resetSegments();
@@ -2467,6 +2505,9 @@ void WS2812FX::makeAutoSegments(bool forceReset) {
}
void WS2812FX::fixInvalidSegments() {
// WLEDMM protect against parallel access while drawing
if (esp32SemTake(segmentMux, 2100) != pdTRUE) return; // wait long, but don't wait forever
//make sure no segment is longer than total (sanity check)
for (size_t i = getSegmentsNum()-1; i > 0; i--) {
if (isMatrix) {
@@ -2486,6 +2527,8 @@ void WS2812FX::fixInvalidSegments() {
if (_segments[i].stop > _length) _segments[i].stop = _length;
}
}
esp32SemGive(segmentMux); // give back the lock now, so purgeSegments can acquire it again
// if any segments were deleted free memory
purgeSegments();
// this is always called as the last step after finalizeInit(), update covered bus types
@@ -2508,6 +2551,7 @@ bool WS2812FX::checkSegmentAlignment() {
return true;
}
#if 0 // WLEDMM dead code
//After this function is called, setPixelColor() will use that segment (offsets, grouping, ... will apply)
//Note: If called in an interrupt (e.g. JSON API), original segment must be restored,
//otherwise it can lead to a crash on ESP32 because _segment_index is modified while in use by the main thread
@@ -2519,6 +2563,7 @@ uint8_t WS2812FX::setPixelSegment(uint8_t n) {
}
return prevSegId;
}
#endif
void WS2812FX::setRange(uint16_t i, uint16_t i2, uint32_t col) {
if (i2 >= i)

210
wled00/data/common.js Normal file
View File

@@ -0,0 +1,210 @@
var d=document;
var loc = false, locip, locproto = "http:";
function H(pg="") { window.open("https://mm.kno.wled.ge/"+pg); }
function GH() { window.open("https://github.com/MoonModules/WLED-MM"); }
function gId(c) { return d.getElementById(c); } // getElementById
function cE(e) { return d.createElement(e); } // createElement
function gEBCN(c) { return d.getElementsByClassName(c); } // getElementsByClassName
function gN(s) { return d.getElementsByName(s)[0]; } // getElementsByName
function isE(o) { return Object.keys(o).length === 0; } // isEmpty
function isO(i) { return (i && typeof i === 'object' && !Array.isArray(i)); } // isObject
function isN(n) { return !isNaN(parseFloat(n)) && isFinite(n); } // isNumber
// https://stackoverflow.com/questions/3885817/how-do-i-check-that-a-number-is-float-or-integer
function isF(n) { return n === +n && n !== (n|0); } // isFloat
function isI(n) { return n === +n && n === (n|0); } // isInteger
function toggle(el) { gId(el).classList.toggle("hide"); let n = gId('No'+el); if (n) n.classList.toggle("hide"); }
function tooltip(cont=null) {
d.querySelectorAll((cont?cont+" ":"")+"[title]").forEach((element)=>{
element.addEventListener("pointerover", ()=>{
// save title
element.setAttribute("data-title", element.getAttribute("title"));
const tooltip = d.createElement("span");
tooltip.className = "tooltip";
tooltip.textContent = element.getAttribute("title");
// prevent default title popup
element.removeAttribute("title");
let { top, left, width } = element.getBoundingClientRect();
d.body.appendChild(tooltip);
const { offsetHeight, offsetWidth } = tooltip;
const offset = element.classList.contains("sliderwrap") ? 4 : 10;
top -= offsetHeight + offset;
left += (width - offsetWidth) / 2;
tooltip.style.top = top + "px";
tooltip.style.left = left + "px";
tooltip.classList.add("visible");
});
element.addEventListener("pointerout", ()=>{
d.querySelectorAll('.tooltip').forEach((tooltip)=>{
tooltip.classList.remove("visible");
d.body.removeChild(tooltip);
});
// restore title
element.setAttribute("title", element.getAttribute("data-title"));
});
});
};
// sequential loading of external resources (JS or CSS) with retry, calls init() when done
function loadResources(files, init) {
let i = 0;
const loadNext = () => {
if (i >= files.length) {
if (init) {
d.documentElement.style.visibility = 'visible'; // make page visible after all files are loaded if it was hidden (prevent ugly display)
d.readyState === 'complete' ? init() : window.addEventListener('load', init);
}
return;
}
const file = files[i++];
const isCSS = file.endsWith('.css');
const el = d.createElement(isCSS ? 'link' : 'script');
if (isCSS) {
el.rel = 'stylesheet';
el.href = file;
const st = d.head.querySelector('style');
if (st) d.head.insertBefore(el, st); // insert before any <style> to allow overrides
else d.head.appendChild(el);
} else {
el.src = file;
d.head.appendChild(el);
}
el.onload = () => { loadNext(); };
el.onerror = () => {
i--; // load this file again
setTimeout(loadNext, 100);
};
};
loadNext();
}
// https://www.educative.io/edpresso/how-to-dynamically-load-a-js-file-in-javascript
function loadJS(FILE_URL, async = true, preGetV = undefined, postGetV = undefined) {
let scE = d.createElement("script");
scE.setAttribute("src", FILE_URL);
scE.setAttribute("type", "text/javascript");
scE.setAttribute("async", async);
d.body.appendChild(scE);
// success event
scE.addEventListener("load", () => {
//console.log("File loaded");
if (preGetV) preGetV();
GetV();
if (postGetV) postGetV();
});
// error event
scE.addEventListener("error", (ev) => {
console.log("Error on loading file", ev);
alert("Loading of configuration script failed.\nIncomplete page data!");
});
}
function getLoc() {
let l = window.location;
if (l.protocol == "file:") {
loc = true;
locip = localStorage.getItem('locIp');
if (!locip) {
locip = prompt("File Mode. Please enter WLED-MM IP!");
localStorage.setItem('locIp', locip);
}
} else {
// detect reverse proxy
let path = l.pathname;
let paths = path.slice(1,path.endsWith('/')?-1:undefined).split("/");
if (paths.length > 1) paths.pop(); // remove subpage (or "settings")
if (paths.length > 0 && paths[paths.length-1]=="settings") paths.pop(); // remove "settings"
if (paths.length > 1) {
locproto = l.protocol;
loc = true;
locip = l.hostname + (l.port ? ":" + l.port : "") + "/" + paths.join('/');
}
}
}
function getURL(path) { return (loc ? locproto + "//" + locip : "") + path; }
function B() { window.open(getURL("/settings"),"_self"); }
var timeout;
function showToast(text, error = false) {
var x = gId("toast");
if (!x) return;
x.innerHTML = text;
x.className = error ? "error":"show";
clearTimeout(timeout);
x.style.animation = 'none';
timeout = setTimeout(function(){ x.className = x.className.replace("show", ""); }, 2900);
}
function uploadFile(fileObj, name) {
var req = new XMLHttpRequest();
req.addEventListener('load', function(){showToast(this.responseText,this.status >= 400)});
req.addEventListener('error', function(e){showToast(e.stack,true);});
req.open("POST", "/upload");
var formData = new FormData();
formData.append("data", fileObj.files[0], name);
req.send(formData);
fileObj.value = '';
return false;
}
// connect to WebSocket, use parent WS or open new, callback function gets passed the new WS object
function connectWs(onOpen) {
let ws;
try { ws = top.window.ws;} catch (e) {}
// reuse if open
if (ws && ws.readyState === WebSocket.OPEN) {
if (onOpen) onOpen(ws);
} else {
// create new ws connection
getLoc(); // ensure globals are up to date
let url = loc ? getURL('/ws').replace("http", "ws")
: "ws://" + window.location.hostname + "/ws";
ws = new WebSocket(url);
ws.binaryType = "arraybuffer";
if (onOpen) ws.onopen = () => onOpen(ws);
}
return ws;
}
// send LED colors to ESP using WebSocket and DDP protocol (RGB)
// ws: WebSocket object
// start: start pixel index
// len: number of pixels to send
// colors: Uint8Array with RGB values (3*len bytes)
function sendDDP(ws, start, len, colors) {
if (!colors || colors.length < len * 3) return false; // not enough color data
let maxDDPpx = 472; // must fit into one WebSocket frame of 1428 bytes, DDP header is 10+1 bytes -> 472 RGB pixels
//let maxDDPpx = 172; // ESP8266: must fit into one WebSocket frame of 528 bytes -> 172 RGB pixels TODO: add support for ESP8266?
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
// send in chunks of maxDDPpx
for (let i = 0; i < len; i += maxDDPpx) {
let cnt = Math.min(maxDDPpx, len - i);
let off = (start + i) * 3; // DDP pixel offset in bytes
let dLen = cnt * 3;
let cOff = i * 3; // offset in color buffer
let pkt = new Uint8Array(11 + dLen); // DDP header is 10 bytes, plus 1 byte for WLED websocket protocol indicator
pkt[0] = 0x02; // DDP protocol indicator for WLED websocket. Note: below DDP protocol bytes are offset by 1
pkt[1] = 0x40; // flags: 0x40 = no push, 0x41 = push (i.e. render), note: this is DDP protocol byte 0
pkt[2] = 0x00; // reserved
pkt[3] = 0x01; // 1 = RGB (currently only supported mode)
pkt[4] = 0x01; // destination id (not used but 0x01 is default output)
pkt[5] = (off >> 24) & 255; // DDP protocol 4-7 is offset
pkt[6] = (off >> 16) & 255;
pkt[7] = (off >> 8) & 255;
pkt[8] = off & 255;
pkt[9] = (dLen >> 8) & 255; // DDP protocol 8-9 is data length
pkt[10] = dLen & 255;
pkt.set(colors.subarray(cOff, cOff + dLen), 11);
if(i + cnt >= len) {
pkt[1] = 0x41; //if this is last packet, set the "push" flag to render the frame
}
try {
ws.send(pkt.buffer);
} catch (e) {
console.error(e);
return false;
}
}
return true;
}

View File

@@ -204,8 +204,9 @@
</div>
</div>
<div style="padding-bottom: 10px;">
<button class="btn btn-xs" type="button" onclick="window.location.href=(loc?'http://'+locip:'')+'/cpal.htm'"><i class="icons btn-icon">&#xe18a;</i></button>
<button class="btn btn-xs" type="button" onclick="palettesData=null;localStorage.removeItem('wledPalx');requestJson({rmcpal:true});setTimeout(loadPalettes,250,loadPalettesData);"><i class="icons btn-icon">&#xe037;</i></button>
<button class="btn btn-xs" title="Add custom palette" type="button" onclick="window.location.href=(loc?'http://'+locip:'')+'/cpal.htm'"><i class="icons btn-icon">&#xe18a;</i></button>
<button class="btn btn-xs" title="Remove last custom palette" type="button" onclick="palettesData=null;localStorage.removeItem('wledPalx');requestJson({rmcpal:true});setTimeout(loadPalettes,250,loadPalettesData);"><i class="icons btn-icon">&#xe037;</i></button>
<br><button class="btn btn-xs" title="PixelForge" type="button" onclick="window.location.href=(loc?'http://'+locip:'')+'/pixelforge.htm'"><i class="icons btn-icon">&#xe410;</i></button>
</div>
</div>
</div>

View File

@@ -288,7 +288,7 @@ function generateSegmentOptions(array) {
// Get segments from device
async function getSegments() {
cv = gurl.value;
const cv = gurl.value;
if (cv.length > 0 ){
try {
var arr = [];

View File

@@ -0,0 +1,809 @@
// (c) Dean McNamee <dean@gmail.com>, 2013.
//
// https://github.com/deanm/omggif
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//
// omggif is a JavaScript implementation of a GIF 89a encoder and decoder,
// including animation and compression. It does not rely on any specific
// underlying system, so should run in the browser, Node, or Plask.
"use strict";
function GifWriter(buf, width, height, gopts) {
var p = 0;
var gopts = gopts === undefined ? { } : gopts;
var loop_count = gopts.loop === undefined ? null : gopts.loop;
var global_palette = gopts.palette === undefined ? null : gopts.palette;
if (width <= 0 || height <= 0 || width > 65535 || height > 65535)
throw new Error("Width/Height invalid.");
function check_palette_and_num_colors(palette) {
var num_colors = palette.length;
if (num_colors < 2 || num_colors > 256 || num_colors & (num_colors-1)) {
throw new Error(
"Invalid code/color length, must be power of 2 and 2 .. 256.");
}
return num_colors;
}
// - Header.
buf[p++] = 0x47; buf[p++] = 0x49; buf[p++] = 0x46; // GIF
buf[p++] = 0x38; buf[p++] = 0x39; buf[p++] = 0x61; // 89a
// Handling of Global Color Table (palette) and background index.
var gp_num_colors_pow2 = 0;
var background = 0;
if (global_palette !== null) {
var gp_num_colors = check_palette_and_num_colors(global_palette);
while (gp_num_colors >>= 1) ++gp_num_colors_pow2;
gp_num_colors = 1 << gp_num_colors_pow2;
--gp_num_colors_pow2;
if (gopts.background !== undefined) {
background = gopts.background;
if (background >= gp_num_colors)
throw new Error("Background index out of range.");
// The GIF spec states that a background index of 0 should be ignored, so
// this is probably a mistake and you really want to set it to another
// slot in the palette. But actually in the end most browsers, etc end
// up ignoring this almost completely (including for dispose background).
if (background === 0)
throw new Error("Background index explicitly passed as 0.");
}
}
// - Logical Screen Descriptor.
// NOTE(deanm): w/h apparently ignored by implementations, but set anyway.
buf[p++] = width & 0xff; buf[p++] = width >> 8 & 0xff;
buf[p++] = height & 0xff; buf[p++] = height >> 8 & 0xff;
// NOTE: Indicates 0-bpp original color resolution (unused?).
buf[p++] = (global_palette !== null ? 0x80 : 0) | // Global Color Table Flag.
gp_num_colors_pow2; // NOTE: No sort flag (unused?).
buf[p++] = background; // Background Color Index.
buf[p++] = 0; // Pixel aspect ratio (unused?).
// - Global Color Table
if (global_palette !== null) {
for (var i = 0, il = global_palette.length; i < il; ++i) {
var rgb = global_palette[i];
buf[p++] = rgb >> 16 & 0xff;
buf[p++] = rgb >> 8 & 0xff;
buf[p++] = rgb & 0xff;
}
}
if (loop_count !== null) { // Netscape block for looping.
if (loop_count < 0 || loop_count > 65535)
throw new Error("Loop count invalid.")
// Extension code, label, and length.
buf[p++] = 0x21; buf[p++] = 0xff; buf[p++] = 0x0b;
// NETSCAPE2.0
buf[p++] = 0x4e; buf[p++] = 0x45; buf[p++] = 0x54; buf[p++] = 0x53;
buf[p++] = 0x43; buf[p++] = 0x41; buf[p++] = 0x50; buf[p++] = 0x45;
buf[p++] = 0x32; buf[p++] = 0x2e; buf[p++] = 0x30;
// Sub-block
buf[p++] = 0x03; buf[p++] = 0x01;
buf[p++] = loop_count & 0xff; buf[p++] = loop_count >> 8 & 0xff;
buf[p++] = 0x00; // Terminator.
}
var ended = false;
this.addFrame = function(x, y, w, h, indexed_pixels, opts) {
if (ended === true) { --p; ended = false; } // Un-end.
opts = opts === undefined ? { } : opts;
// TODO(deanm): Bounds check x, y. Do they need to be within the virtual
// canvas width/height, I imagine?
if (x < 0 || y < 0 || x > 65535 || y > 65535)
throw new Error("x/y invalid.")
if (w <= 0 || h <= 0 || w > 65535 || h > 65535)
throw new Error("Width/Height invalid.")
if (indexed_pixels.length < w * h)
throw new Error("Not enough pixels for the frame size.");
var using_local_palette = true;
var palette = opts.palette;
if (palette === undefined || palette === null) {
using_local_palette = false;
palette = global_palette;
}
if (palette === undefined || palette === null)
throw new Error("Must supply either a local or global palette.");
var num_colors = check_palette_and_num_colors(palette);
// Compute the min_code_size (power of 2), destroying num_colors.
var min_code_size = 0;
while (num_colors >>= 1) ++min_code_size;
num_colors = 1 << min_code_size; // Now we can easily get it back.
var delay = opts.delay === undefined ? 0 : opts.delay;
// From the spec:
// 0 - No disposal specified. The decoder is
// not required to take any action.
// 1 - Do not dispose. The graphic is to be left
// in place.
// 2 - Restore to background color. The area used by the
// graphic must be restored to the background color.
// 3 - Restore to previous. The decoder is required to
// restore the area overwritten by the graphic with
// what was there prior to rendering the graphic.
// 4-7 - To be defined.
// NOTE(deanm): Dispose background doesn't really work, apparently most
// browsers ignore the background palette index and clear to transparency.
var disposal = opts.disposal === undefined ? 0 : opts.disposal;
if (disposal < 0 || disposal > 3) // 4-7 is reserved.
throw new Error("Disposal out of range.");
var use_transparency = false;
var transparent_index = 0;
if (opts.transparent !== undefined && opts.transparent !== null) {
use_transparency = true;
transparent_index = opts.transparent;
if (transparent_index < 0 || transparent_index >= num_colors)
throw new Error("Transparent color index.");
}
if (disposal !== 0 || use_transparency || delay !== 0) {
// - Graphics Control Extension
buf[p++] = 0x21; buf[p++] = 0xf9; // Extension / Label.
buf[p++] = 4; // Byte size.
buf[p++] = disposal << 2 | (use_transparency === true ? 1 : 0);
buf[p++] = delay & 0xff; buf[p++] = delay >> 8 & 0xff;
buf[p++] = transparent_index; // Transparent color index.
buf[p++] = 0; // Block Terminator.
}
// - Image Descriptor
buf[p++] = 0x2c; // Image Seperator.
buf[p++] = x & 0xff; buf[p++] = x >> 8 & 0xff; // Left.
buf[p++] = y & 0xff; buf[p++] = y >> 8 & 0xff; // Top.
buf[p++] = w & 0xff; buf[p++] = w >> 8 & 0xff;
buf[p++] = h & 0xff; buf[p++] = h >> 8 & 0xff;
// NOTE: No sort flag (unused?).
// TODO(deanm): Support interlace.
buf[p++] = using_local_palette === true ? (0x80 | (min_code_size-1)) : 0;
// - Local Color Table
if (using_local_palette === true) {
for (var i = 0, il = palette.length; i < il; ++i) {
var rgb = palette[i];
buf[p++] = rgb >> 16 & 0xff;
buf[p++] = rgb >> 8 & 0xff;
buf[p++] = rgb & 0xff;
}
}
p = GifWriterOutputLZWCodeStream(
buf, p, min_code_size < 2 ? 2 : min_code_size, indexed_pixels);
return p;
};
this.end = function() {
if (ended === false) {
buf[p++] = 0x3b; // Trailer.
ended = true;
}
return p;
};
this.getOutputBuffer = function() { return buf; };
this.setOutputBuffer = function(v) { buf = v; };
this.getOutputBufferPosition = function() { return p; };
this.setOutputBufferPosition = function(v) { p = v; };
}
// Main compression routine, palette indexes -> LZW code stream.
// |index_stream| must have at least one entry.
function GifWriterOutputLZWCodeStream(buf, p, min_code_size, index_stream) {
buf[p++] = min_code_size;
var cur_subblock = p++; // Pointing at the length field.
var clear_code = 1 << min_code_size;
var code_mask = clear_code - 1;
var eoi_code = clear_code + 1;
var next_code = eoi_code + 1;
var cur_code_size = min_code_size + 1; // Number of bits per code.
var cur_shift = 0;
// We have at most 12-bit codes, so we should have to hold a max of 19
// bits here (and then we would write out).
var cur = 0;
function emit_bytes_to_buffer(bit_block_size) {
while (cur_shift >= bit_block_size) {
buf[p++] = cur & 0xff;
cur >>= 8; cur_shift -= 8;
if (p === cur_subblock + 256) { // Finished a subblock.
buf[cur_subblock] = 255;
cur_subblock = p++;
}
}
}
function emit_code(c) {
cur |= c << cur_shift;
cur_shift += cur_code_size;
emit_bytes_to_buffer(8);
}
// I am not an expert on the topic, and I don't want to write a thesis.
// However, it is good to outline here the basic algorithm and the few data
// structures and optimizations here that make this implementation fast.
// The basic idea behind LZW is to build a table of previously seen runs
// addressed by a short id (herein called output code). All data is
// referenced by a code, which represents one or more values from the
// original input stream. All input bytes can be referenced as the same
// value as an output code. So if you didn't want any compression, you
// could more or less just output the original bytes as codes (there are
// some details to this, but it is the idea). In order to achieve
// compression, values greater then the input range (codes can be up to
// 12-bit while input only 8-bit) represent a sequence of previously seen
// inputs. The decompressor is able to build the same mapping while
// decoding, so there is always a shared common knowledge between the
// encoding and decoder, which is also important for "timing" aspects like
// how to handle variable bit width code encoding.
//
// One obvious but very important consequence of the table system is there
// is always a unique id (at most 12-bits) to map the runs. 'A' might be
// 4, then 'AA' might be 10, 'AAA' 11, 'AAAA' 12, etc. This relationship
// can be used for an effecient lookup strategy for the code mapping. We
// need to know if a run has been seen before, and be able to map that run
// to the output code. Since we start with known unique ids (input bytes),
// and then from those build more unique ids (table entries), we can
// continue this chain (almost like a linked list) to always have small
// integer values that represent the current byte chains in the encoder.
// This means instead of tracking the input bytes (AAAABCD) to know our
// current state, we can track the table entry for AAAABC (it is guaranteed
// to exist by the nature of the algorithm) and the next character D.
// Therefor the tuple of (table_entry, byte) is guaranteed to also be
// unique. This allows us to create a simple lookup key for mapping input
// sequences to codes (table indices) without having to store or search
// any of the code sequences. So if 'AAAA' has a table entry of 12, the
// tuple of ('AAAA', K) for any input byte K will be unique, and can be our
// key. This leads to a integer value at most 20-bits, which can always
// fit in an SMI value and be used as a fast sparse array / object key.
// Output code for the current contents of the index buffer.
var ib_code = index_stream[0] & code_mask; // Load first input index.
var code_table = { }; // Key'd on our 20-bit "tuple".
emit_code(clear_code); // Spec says first code should be a clear code.
// First index already loaded, process the rest of the stream.
for (var i = 1, il = index_stream.length; i < il; ++i) {
var k = index_stream[i] & code_mask;
var cur_key = ib_code << 8 | k; // (prev, k) unique tuple.
var cur_code = code_table[cur_key]; // buffer + k.
// Check if we have to create a new code table entry.
if (cur_code === undefined) { // We don't have buffer + k.
// Emit index buffer (without k).
// This is an inline version of emit_code, because this is the core
// writing routine of the compressor (and V8 cannot inline emit_code
// because it is a closure here in a different context). Additionally
// we can call emit_byte_to_buffer less often, because we can have
// 30-bits (from our 31-bit signed SMI), and we know our codes will only
// be 12-bits, so can safely have 18-bits there without overflow.
// emit_code(ib_code);
cur |= ib_code << cur_shift;
cur_shift += cur_code_size;
while (cur_shift >= 8) {
buf[p++] = cur & 0xff;
cur >>= 8; cur_shift -= 8;
if (p === cur_subblock + 256) { // Finished a subblock.
buf[cur_subblock] = 255;
cur_subblock = p++;
}
}
if (next_code === 4096) { // Table full, need a clear.
emit_code(clear_code);
next_code = eoi_code + 1;
cur_code_size = min_code_size + 1;
code_table = { };
} else { // Table not full, insert a new entry.
// Increase our variable bit code sizes if necessary. This is a bit
// tricky as it is based on "timing" between the encoding and
// decoder. From the encoders perspective this should happen after
// we've already emitted the index buffer and are about to create the
// first table entry that would overflow our current code bit size.
if (next_code >= (1 << cur_code_size)) ++cur_code_size;
code_table[cur_key] = next_code++; // Insert into code table.
}
ib_code = k; // Index buffer to single input k.
} else {
ib_code = cur_code; // Index buffer to sequence in code table.
}
}
emit_code(ib_code); // There will still be something in the index buffer.
emit_code(eoi_code); // End Of Information.
// Flush / finalize the sub-blocks stream to the buffer.
emit_bytes_to_buffer(1);
// Finish the sub-blocks, writing out any unfinished lengths and
// terminating with a sub-block of length 0. If we have already started
// but not yet used a sub-block it can just become the terminator.
if (cur_subblock + 1 === p) { // Started but unused.
buf[cur_subblock] = 0;
} else { // Started and used, write length and additional terminator block.
buf[cur_subblock] = p - cur_subblock - 1;
buf[p++] = 0;
}
return p;
}
function GifReader(buf) {
var p = 0;
// - Header (GIF87a or GIF89a).
if (buf[p++] !== 0x47 || buf[p++] !== 0x49 || buf[p++] !== 0x46 ||
buf[p++] !== 0x38 || (buf[p++]+1 & 0xfd) !== 0x38 || buf[p++] !== 0x61) {
throw new Error("Invalid GIF 87a/89a header.");
}
// - Logical Screen Descriptor.
var width = buf[p++] | buf[p++] << 8;
var height = buf[p++] | buf[p++] << 8;
var pf0 = buf[p++]; // <Packed Fields>.
var global_palette_flag = pf0 >> 7;
var num_global_colors_pow2 = pf0 & 0x7;
var num_global_colors = 1 << (num_global_colors_pow2 + 1);
var background = buf[p++];
buf[p++]; // Pixel aspect ratio (unused?).
var global_palette_offset = null;
var global_palette_size = null;
if (global_palette_flag) {
global_palette_offset = p;
global_palette_size = num_global_colors;
p += num_global_colors * 3; // Seek past palette.
}
var no_eof = true;
var frames = [ ];
var delay = 0;
var transparent_index = null;
var disposal = 0; // 0 - No disposal specified.
var loop_count = null;
this.width = width;
this.height = height;
while (no_eof && p < buf.length) {
switch (buf[p++]) {
case 0x21: // Graphics Control Extension Block
switch (buf[p++]) {
case 0xff: // Application specific block
// Try if it's a Netscape block (with animation loop counter).
if (buf[p ] !== 0x0b || // 21 FF already read, check block size.
// NETSCAPE2.0
buf[p+1 ] == 0x4e && buf[p+2 ] == 0x45 && buf[p+3 ] == 0x54 &&
buf[p+4 ] == 0x53 && buf[p+5 ] == 0x43 && buf[p+6 ] == 0x41 &&
buf[p+7 ] == 0x50 && buf[p+8 ] == 0x45 && buf[p+9 ] == 0x32 &&
buf[p+10] == 0x2e && buf[p+11] == 0x30 &&
// Sub-block
buf[p+12] == 0x03 && buf[p+13] == 0x01 && buf[p+16] == 0) {
p += 14;
loop_count = buf[p++] | buf[p++] << 8;
p++; // Skip terminator.
} else { // We don't know what it is, just try to get past it.
p += 12;
while (true) { // Seek through subblocks.
var block_size = buf[p++];
// Bad block size (ex: undefined from an out of bounds read).
if (!(block_size >= 0)) throw Error("Invalid block size");
if (block_size === 0) break; // 0 size is terminator
p += block_size;
}
}
break;
case 0xf9: // Graphics Control Extension
if (buf[p++] !== 0x4 || buf[p+4] !== 0)
throw new Error("Invalid graphics extension block.");
var pf1 = buf[p++];
delay = buf[p++] | buf[p++] << 8;
transparent_index = buf[p++];
if ((pf1 & 1) === 0) transparent_index = null;
disposal = pf1 >> 2 & 0x7;
p++; // Skip terminator.
break;
case 0xfe: // Comment Extension.
while (true) { // Seek through subblocks.
var block_size = buf[p++];
// Bad block size (ex: undefined from an out of bounds read).
if (!(block_size >= 0)) throw Error("Invalid block size");
if (block_size === 0) break; // 0 size is terminator
// console.log(buf.slice(p, p+block_size).toString('ascii'));
p += block_size;
}
break;
default:
throw new Error(
"Unknown graphic control label: 0x" + buf[p-1].toString(16));
}
break;
case 0x2c: // Image Descriptor.
var x = buf[p++] | buf[p++] << 8;
var y = buf[p++] | buf[p++] << 8;
var w = buf[p++] | buf[p++] << 8;
var h = buf[p++] | buf[p++] << 8;
var pf2 = buf[p++];
var local_palette_flag = pf2 >> 7;
var interlace_flag = pf2 >> 6 & 1;
var num_local_colors_pow2 = pf2 & 0x7;
var num_local_colors = 1 << (num_local_colors_pow2 + 1);
var palette_offset = global_palette_offset;
var palette_size = global_palette_size;
var has_local_palette = false;
if (local_palette_flag) {
var has_local_palette = true;
palette_offset = p; // Override with local palette.
palette_size = num_local_colors;
p += num_local_colors * 3; // Seek past palette.
}
var data_offset = p;
p++; // codesize
while (true) {
var block_size = buf[p++];
// Bad block size (ex: undefined from an out of bounds read).
if (!(block_size >= 0)) throw Error("Invalid block size");
if (block_size === 0) break; // 0 size is terminator
p += block_size;
}
frames.push({x: x, y: y, width: w, height: h,
has_local_palette: has_local_palette,
palette_offset: palette_offset,
palette_size: palette_size,
data_offset: data_offset,
data_length: p - data_offset,
transparent_index: transparent_index,
interlaced: !!interlace_flag,
delay: delay,
disposal: disposal});
break;
case 0x3b: // Trailer Marker (end of file).
no_eof = false;
break;
default:
throw new Error("Unknown gif block: 0x" + buf[p-1].toString(16));
break;
}
}
this.numFrames = function() {
return frames.length;
};
this.loopCount = function() {
return loop_count;
};
this.frameInfo = function(frame_num) {
if (frame_num < 0 || frame_num >= frames.length)
throw new Error("Frame index out of range.");
return frames[frame_num];
}
this.decodeAndBlitFrameBGRA = function(frame_num, pixels) {
var frame = this.frameInfo(frame_num);
var num_pixels = frame.width * frame.height;
var index_stream = new Uint8Array(num_pixels); // At most 8-bit indices.
GifReaderLZWOutputIndexStream(
buf, frame.data_offset, index_stream, num_pixels);
var palette_offset = frame.palette_offset;
// NOTE(deanm): It seems to be much faster to compare index to 256 than
// to === null. Not sure why, but CompareStub_EQ_STRICT shows up high in
// the profile, not sure if it's related to using a Uint8Array.
var trans = frame.transparent_index;
if (trans === null) trans = 256;
// We are possibly just blitting to a portion of the entire frame.
// That is a subrect within the framerect, so the additional pixels
// must be skipped over after we finished a scanline.
var framewidth = frame.width;
var framestride = width - framewidth;
var xleft = framewidth; // Number of subrect pixels left in scanline.
// Output indicies of the top left and bottom right corners of the subrect.
var opbeg = ((frame.y * width) + frame.x) * 4;
var opend = ((frame.y + frame.height) * width + frame.x) * 4;
var op = opbeg;
var scanstride = framestride * 4;
// Use scanstride to skip past the rows when interlacing. This is skipping
// 7 rows for the first two passes, then 3 then 1.
if (frame.interlaced === true) {
scanstride += width * 4 * 7; // Pass 1.
}
var interlaceskip = 8; // Tracking the row interval in the current pass.
for (var i = 0, il = index_stream.length; i < il; ++i) {
var index = index_stream[i];
if (xleft === 0) { // Beginning of new scan line
op += scanstride;
xleft = framewidth;
if (op >= opend) { // Catch the wrap to switch passes when interlacing.
scanstride = framestride * 4 + width * 4 * (interlaceskip-1);
// interlaceskip / 2 * 4 is interlaceskip << 1.
op = opbeg + (framewidth + framestride) * (interlaceskip << 1);
interlaceskip >>= 1;
}
}
if (index === trans) {
op += 4;
} else {
var r = buf[palette_offset + index * 3];
var g = buf[palette_offset + index * 3 + 1];
var b = buf[palette_offset + index * 3 + 2];
pixels[op++] = b;
pixels[op++] = g;
pixels[op++] = r;
pixels[op++] = 255;
}
--xleft;
}
};
// I will go to copy and paste hell one day...
this.decodeAndBlitFrameRGBA = function(frame_num, pixels) {
var frame = this.frameInfo(frame_num);
var num_pixels = frame.width * frame.height;
var index_stream = new Uint8Array(num_pixels); // At most 8-bit indices.
GifReaderLZWOutputIndexStream(
buf, frame.data_offset, index_stream, num_pixels);
var palette_offset = frame.palette_offset;
// NOTE(deanm): It seems to be much faster to compare index to 256 than
// to === null. Not sure why, but CompareStub_EQ_STRICT shows up high in
// the profile, not sure if it's related to using a Uint8Array.
var trans = frame.transparent_index;
if (trans === null) trans = 256;
// We are possibly just blitting to a portion of the entire frame.
// That is a subrect within the framerect, so the additional pixels
// must be skipped over after we finished a scanline.
var framewidth = frame.width;
var framestride = width - framewidth;
var xleft = framewidth; // Number of subrect pixels left in scanline.
// Output indicies of the top left and bottom right corners of the subrect.
var opbeg = ((frame.y * width) + frame.x) * 4;
var opend = ((frame.y + frame.height) * width + frame.x) * 4;
var op = opbeg;
var scanstride = framestride * 4;
// Use scanstride to skip past the rows when interlacing. This is skipping
// 7 rows for the first two passes, then 3 then 1.
if (frame.interlaced === true) {
scanstride += width * 4 * 7; // Pass 1.
}
var interlaceskip = 8; // Tracking the row interval in the current pass.
for (var i = 0, il = index_stream.length; i < il; ++i) {
var index = index_stream[i];
if (xleft === 0) { // Beginning of new scan line
op += scanstride;
xleft = framewidth;
if (op >= opend) { // Catch the wrap to switch passes when interlacing.
scanstride = framestride * 4 + width * 4 * (interlaceskip-1);
// interlaceskip / 2 * 4 is interlaceskip << 1.
op = opbeg + (framewidth + framestride) * (interlaceskip << 1);
interlaceskip >>= 1;
}
}
if (index === trans) {
op += 4;
} else {
var r = buf[palette_offset + index * 3];
var g = buf[palette_offset + index * 3 + 1];
var b = buf[palette_offset + index * 3 + 2];
pixels[op++] = r;
pixels[op++] = g;
pixels[op++] = b;
pixels[op++] = 255;
}
--xleft;
}
};
}
function GifReaderLZWOutputIndexStream(code_stream, p, output, output_length) {
var min_code_size = code_stream[p++];
var clear_code = 1 << min_code_size;
var eoi_code = clear_code + 1;
var next_code = eoi_code + 1;
var cur_code_size = min_code_size + 1; // Number of bits per code.
// NOTE: This shares the same name as the encoder, but has a different
// meaning here. Here this masks each code coming from the code stream.
var code_mask = (1 << cur_code_size) - 1;
var cur_shift = 0;
var cur = 0;
var op = 0; // Output pointer.
var subblock_size = code_stream[p++];
// TODO(deanm): Would using a TypedArray be any faster? At least it would
// solve the fast mode / backing store uncertainty.
// var code_table = Array(4096);
var code_table = new Int32Array(4096); // Can be signed, we only use 20 bits.
var prev_code = null; // Track code-1.
while (true) {
// Read up to two bytes, making sure we always 12-bits for max sized code.
while (cur_shift < 16) {
if (subblock_size === 0) break; // No more data to be read.
cur |= code_stream[p++] << cur_shift;
cur_shift += 8;
if (subblock_size === 1) { // Never let it get to 0 to hold logic above.
subblock_size = code_stream[p++]; // Next subblock.
} else {
--subblock_size;
}
}
// TODO(deanm): We should never really get here, we should have received
// and EOI.
if (cur_shift < cur_code_size)
break;
var code = cur & code_mask;
cur >>= cur_code_size;
cur_shift -= cur_code_size;
// TODO(deanm): Maybe should check that the first code was a clear code,
// at least this is what you're supposed to do. But actually our encoder
// now doesn't emit a clear code first anyway.
if (code === clear_code) {
// We don't actually have to clear the table. This could be a good idea
// for greater error checking, but we don't really do any anyway. We
// will just track it with next_code and overwrite old entries.
next_code = eoi_code + 1;
cur_code_size = min_code_size + 1;
code_mask = (1 << cur_code_size) - 1;
// Don't update prev_code ?
prev_code = null;
continue;
} else if (code === eoi_code) {
break;
}
// We have a similar situation as the decoder, where we want to store
// variable length entries (code table entries), but we want to do in a
// faster manner than an array of arrays. The code below stores sort of a
// linked list within the code table, and then "chases" through it to
// construct the dictionary entries. When a new entry is created, just the
// last byte is stored, and the rest (prefix) of the entry is only
// referenced by its table entry. Then the code chases through the
// prefixes until it reaches a single byte code. We have to chase twice,
// first to compute the length, and then to actually copy the data to the
// output (backwards, since we know the length). The alternative would be
// storing something in an intermediate stack, but that doesn't make any
// more sense. I implemented an approach where it also stored the length
// in the code table, although it's a bit tricky because you run out of
// bits (12 + 12 + 8), but I didn't measure much improvements (the table
// entries are generally not the long). Even when I created benchmarks for
// very long table entries the complexity did not seem worth it.
// The code table stores the prefix entry in 12 bits and then the suffix
// byte in 8 bits, so each entry is 20 bits.
var chase_code = code < next_code ? code : prev_code;
// Chase what we will output, either {CODE} or {CODE-1}.
var chase_length = 0;
var chase = chase_code;
while (chase > clear_code) {
chase = code_table[chase] >> 8;
++chase_length;
}
var k = chase;
var op_end = op + chase_length + (chase_code !== code ? 1 : 0);
if (op_end > output_length) {
console.log("Warning, gif stream longer than expected.");
return;
}
// Already have the first byte from the chase, might as well write it fast.
output[op++] = k;
op += chase_length;
var b = op; // Track pointer, writing backwards.
if (chase_code !== code) // The case of emitting {CODE-1} + k.
output[op++] = k;
chase = chase_code;
while (chase_length--) {
chase = code_table[chase];
output[--b] = chase & 0xff; // Write backwards.
chase >>= 8; // Pull down to the prefix code.
}
if (prev_code !== null && next_code < 4096) {
code_table[next_code++] = prev_code << 8 | k;
// TODO(deanm): Figure out this clearing vs code growth logic better. I
// have an feeling that it should just happen somewhere else, for now it
// is awkward between when we grow past the max and then hit a clear code.
// For now just check if we hit the max 12-bits (then a clear code should
// follow, also of course encoded in 12-bits).
if (next_code >= code_mask+1 && cur_code_size < 12) {
++cur_code_size;
code_mask = code_mask << 1 | 1;
}
}
prev_code = code;
}
if (op !== output_length) {
console.log("Warning, gif stream shorter than expected.");
}
return output;
}
// CommonJS.
//try { exports.GifWriter = GifWriter; exports.GifReader = GifReader } catch(e) {}
try { exports.GifWriter = GifWriter; } catch(e) {}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -68,9 +68,8 @@
}
</script>
<style>
@import url("style.css");
body {
text-align: center;
background: #222;
height: 100px;
margin: 0;
}
@@ -78,17 +77,13 @@
--h: 9vh;
}
button {
background: #333;
color: #fff;
font-family: Verdana, Helvetica, sans-serif;
display: block;
border: 1px solid #333;
border-radius: var(--h);
font-size: 6vmin;
/* height: var(--h); WLEDMM remove to allow more compact display*/
width: calc(100% - 40px);
margin: 2vh auto 0;
cursor: pointer;
padding: 0;
}
</style>
</head>
@@ -104,6 +99,11 @@
<button type="submit" onclick="window.location='./settings/sync'">Sync Interfaces</button>
<button type="submit" onclick="window.location='./settings/time'">Time &amp; Macros</button>
<button type="submit" onclick="window.location='./settings/sec'">Security &amp; Updates</button>
<br>
<button type="submit" onclick="window.location='./edit'">File System ☾</button> <!--WLEDMM-->
</body>
<button id="pixbtn" style="display:none;" type="submit" onclick="window.location='./pixart.htm'">Pixel Art Converter ☾</button>
<button id="pxmbtn" style="display:none;" type="submit" onclick="window.location='./pxmagic.htm'">Pixel Magic Tool</button>
<button id="forgebtn" style="display:none;" type="submit" onclick="window.location='./pixelforge.htm'">WLED Pixel Forge</button>
<br>
</body>
</html>

View File

@@ -33,6 +33,7 @@ button, .btn {
min-width: 48px;
cursor: pointer;
text-decoration: none;
transition: all 0.3s ease;
}
button.sml {
padding: 8px;
@@ -41,6 +42,11 @@ button.sml {
min-width: 40px;
margin: 0 0 0 10px;
}
button:hover, .btn:hover{
background:#555;
border-color:#555;
}
#scan {
margin-top: -10px;
}

View File

@@ -16,6 +16,7 @@ void handleDDPPacket(e131_packet_t* p) {
int lastPushSeq = e131LastSequenceNumber[0];
//reject late packets belonging to previous frame (assuming 4 packets max. before push)
#if 0 // WLEDMM fixme - we definitely have more than 5-10 packets per frame !!!
if (e131SkipOutOfSequence && lastPushSeq) {
int sn = p->sequenceNum & 0xF;
if (sn) {
@@ -26,9 +27,15 @@ void handleDDPPacket(e131_packet_t* p) {
}
}
}
#endif
uint8_t ddpChannelsPerLed = ((p->dataType & 0b00111000)>>3 == 0b011) ? 4 : 3; // data type 0x1B (formerly 0x1A) is RGBW (type 3, 8 bit/channel)
// WLEDMM for debugging
static unsigned lastPush = millis();
static unsigned packets = 0;
static unsigned pixels = 0;
uint32_t start = htonl(p->channelOffset) / ddpChannelsPerLed;
start += DMXAddress / ddpChannelsPerLed;
uint16_t dataLen = htons(p->dataLen);
@@ -48,15 +55,27 @@ void handleDDPPacket(e131_packet_t* p) {
realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP);
if (!realtimeOverride || (realtimeMode && useMainSegmentOnly)) {
for (uint16_t i = start; i < stop; i++) {
setRealtimePixel(i, data[c], data[c+1], data[c+2], ddpChannelsPerLed >3 ? data[c+3] : 0);
c += ddpChannelsPerLed;
// WLEDMM acquire drawing permission (wait max 200ms) before setting pixels
if (esp32SemTake(busDrawMux, 200) == pdTRUE) {
for (uint16_t i = start; i < stop; i++) {
setRealtimePixel(i, data[c], data[c+1], data[c+2], ddpChannelsPerLed >3 ? data[c+3] : 0);
c += ddpChannelsPerLed;
pixels++;
}
packets ++;
esp32SemGive(busDrawMux); // WLEDMM release drawing permissions
}
}
bool push = p->flags & DDP_PUSH_FLAG;
ddpSeenPush |= push;
if (!ddpSeenPush || push) { // if we've never seen a push, or this is one, render display
#ifdef WLED_DEBUG
if (push) { USER_PRINT("-P-");} else { USER_PRINT("--");}
USER_PRINTF("> %dms (%upck, %upix) ", int(millis() - lastPush), packets, pixels);
lastPush = millis();
pixels = packets = 0;
#endif
e131NewData = true;
byte sn = p->sequenceNum & 0xF;
if (sn) e131LastSequenceNumber[0] = sn;
@@ -70,6 +89,8 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){
uint8_t* e131_data = nullptr;
uint8_t seq = 0, mde = REALTIME_MODE_E131;
if (!receiveDirect) { exitRealtime(); return; } // WLEDMM kill switch
if (protocol == P_ARTNET)
{
if (p->art_opcode == ARTNET_OPCODE_OPPOLL) {
@@ -170,8 +191,11 @@ void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8
if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return;
wChannel = (availDMXLen > 3) ? e131_data[dataOffset+3] : 0;
for (uint16_t i = 0; i < totalLen; i++)
setRealtimePixel(i, e131_data[dataOffset+0], e131_data[dataOffset+1], e131_data[dataOffset+2], wChannel);
if (esp32SemTake(busDrawMux, 200) == pdTRUE) { // WLEDMM acquire drawing permission (wait max 200ms) before setting pixels
for (uint16_t i = 0; i < totalLen; i++)
setRealtimePixel(i, e131_data[dataOffset+0], e131_data[dataOffset+1], e131_data[dataOffset+2], wChannel);
esp32SemGive(busDrawMux);
}
break;
case DMX_MODE_SINGLE_DRGB: // 4 channel: [Dimmer,R,G,B]
@@ -186,9 +210,11 @@ void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8
bri = e131_data[dataOffset+0];
strip.setBrightness(bri, true);
}
for (uint16_t i = 0; i < totalLen; i++)
setRealtimePixel(i, e131_data[dataOffset+1], e131_data[dataOffset+2], e131_data[dataOffset+3], wChannel);
if (esp32SemTake(busDrawMux, 200) == pdTRUE) { // WLEDMM acquire drawing permission (wait max 200ms) before setting pixels
for (uint16_t i = 0; i < totalLen; i++)
setRealtimePixel(i, e131_data[dataOffset+1], e131_data[dataOffset+2], e131_data[dataOffset+3], wChannel);
esp32SemGive(busDrawMux);
}
break;
case DMX_MODE_PRESET: // 2 channel: [Dimmer,Preset]
@@ -332,16 +358,19 @@ void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8
}
}
if (!is4Chan) {
for (uint16_t i = previousLeds; i < ledsTotal; i++) {
setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], 0);
dmxOffset+=3;
}
} else {
for (uint16_t i = previousLeds; i < ledsTotal; i++) {
setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], e131_data[dmxOffset+3]);
dmxOffset+=4;
if (esp32SemTake(busDrawMux, 200) == pdTRUE) { // WLEDMM acquire drawing permission (wait max 200ms) before setting pixels
if (!is4Chan) {
for (uint16_t i = previousLeds; i < ledsTotal; i++) {
setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], 0);
dmxOffset+=3;
}
} else {
for (uint16_t i = previousLeds; i < ledsTotal; i++) {
setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], e131_data[dmxOffset+3]);
dmxOffset+=4;
}
}
esp32SemGive(busDrawMux);
}
break;
}

View File

@@ -439,7 +439,7 @@ void sappend(char stype, const char* key, int val);
void sappends(char stype, const char* key, char* val);
void prepareHostname(char* hostname);
bool isAsterisksOnly(const char* str, byte maxLen) __attribute__((pure));
bool requestJSONBufferLock(uint8_t module=255);
bool requestJSONBufferLock(uint8_t module=255, unsigned timeoutMS = 1800);
void releaseJSONBufferLock();
uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLen);
uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxLen, uint8_t *var = nullptr);

View File

@@ -299,6 +299,11 @@ bool writeObjectToFile(const char* file, const char* key, JsonDocument* content)
s = millis();
#endif
if (doCloseFile) {
if (f) { DEBUG_PRINTLN("writeObjectToFile("+String(file)+"): file f is already open, closing to prevent file corruption."); }
closeFile(); // WLEDMM: Ensure previous file is closed
}
size_t pos = 0;
f = WLED_FS.open(file, "r+");
if (!f && !WLED_FS.exists(file)) { f = WLED_FS.open(file, "w+");

View File

@@ -94,9 +94,16 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId)
// if using vectors use this code to append segment
if (id >= strip.getSegmentsNum()) {
if (stop <= 0) return false; // ignore empty/inactive segments
strip.appendSegment(Segment(0, strip.getLengthTotal()));
id = strip.getSegmentsNum()-1; // segments are added at the end of list
newSeg = true;
if (esp32SemTake(segmentMux, 2100) == pdTRUE) { // wait long, but don't wait forever
// WLEDMM make sure we have exclusive access to the segment list
strip.appendSegment(Segment(0, strip.getLengthTotal()));
id = strip.getSegmentsNum()-1; // segments are added at the end of list
newSeg = true;
esp32SemGive(segmentMux);
} else {
USER_PRINTLN(F("deserializeSegment(): segment not added - failed to acquire segmentMux."));
return false;
}
}
// WLEDMM: before changing segments, make sure our strip is _not_ servicing effects in parallel
@@ -353,8 +360,10 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId)
USER_PRINTLN(F("deserializeSegment() image: strip is still drawing effects."));
strip.waitUntilIdle();
}
// WLEDMM protect against parallel drawing
bool drawSuccess = false;
if (esp32SemTake(busDrawMux, 250) == pdTRUE) { // WLEDMM first acquire draw mutex, start of critical section
seg.startFrame();
// WLEDMM end
// set brightness immediately and disable transition
transitionDelayTemp = 0;
@@ -404,8 +413,13 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId)
set = 0;
}
}
drawSuccess = true;
esp32SemGive(busDrawMux); // release lock
} // end of critical section
seg.map1D2D = oldMap1D2D; // restore mapping
strip.trigger(); // force segment update
if (drawSuccess) strip.trigger(); // force segment update
else USER_PRINTLN(F("deserializeSegment() image drawing failed, could not acquire busDrawMux.")); // log failure messaage
suspendStripService = oldLock; // restore previous lock status
}
// send UDP/WS if segment options changed (except selection; will also deselect current preset)
@@ -479,16 +493,15 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId)
}
}
#ifdef ARDUINO_ARCH_ESP32
delay(2); // WLEDMM experimental - de-serialize takes time, so allow other tasks to run
#endif
// esp32: suspendStripService is deferred until the first segment operation
#ifndef ARDUINO_ARCH_ESP32
// WLEDMM: before changing strip, make sure our strip is _not_ servicing effects in parallel
suspendStripService = true; // temporarily lock out strip updates
if (strip.isServicing()) {
USER_PRINTLN(F("deserializeState(): strip is still drawing effects."));
strip.waitUntilIdle();
}
#endif
// temporary transition (applies only once)
tr = root[F("tt")] | -1;
@@ -524,7 +537,18 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId)
if (root[F("psave")].isNull()) doReboot = root[F("rb")] | doReboot;
#ifdef ARDUINO_ARCH_ESP32
// WLEDMM: Acquire strip lock right before segment operations (deferred for better UX)
suspendStripService = true; // temporarily lock out strip updates
vTaskDelay(pdMS_TO_TICKS(2)); // WLEDMM trigger a short task context switch
if (strip.isServicing()) {
DEBUG_PRINTLN(F("deserializeState(): strip is still drawing effects."));
strip.waitUntilIdle();
}
#endif
// do not allow changing main segment while in realtime mode (may get odd results else)
// esp32: safe to change MainSegment without having segmentMux - strip.service() is already suspended
if (!realtimeMode) strip.setMainSegmentId(root[F("mainseg")] | strip.getMainSegmentId()); // must be before realtimeLock() if "live"
realtimeOverride = root[F("lor")] | realtimeOverride;
@@ -537,7 +561,13 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId)
if (root["live"].as<bool>()) {
transitionDelayTemp = 0;
jsonTransitionOnce = true;
#ifdef WLED_ENABLE_JSONLIVE
// infinite timeout only when JSON LIVE leds preview is enabled
realtimeLock(65000);
#else
// more meaningful timeout : use configurable timeout; *3 for some safety margin without staying "live" forever
realtimeLock(realtimeTimeoutMs *3); // Use configurable timeout like other protocols
#endif
} else {
exitRealtime();
}
@@ -637,10 +667,12 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId)
doAdvancePlaylist = root[F("np")] | doAdvancePlaylist; //advances to next preset in playlist when true
// WLEDMM: Release suspendStripService before stateUpdated() to avoid timeout
if (iAmGroot) suspendStripService = false;
stateUpdated(callMode);
if (presetToRestore) currentPreset = presetToRestore;
if (iAmGroot) suspendStripService = false; // WLEDMM release lock
return stateResponse;
}
@@ -719,9 +751,11 @@ void serializeSegment(JsonObject& root, Segment& seg, byte id, bool forPreset, b
void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segmentBounds, bool selectedSegmentsOnly)
{
//WLEDMM add DEBUG_PRINT (not USER_PRINT)
#ifdef WLED_DEBUG
String temp;
serializeJson(root, temp);
DEBUG_PRINTF("serializeState %d %s\n", forPreset, temp.c_str());
#endif
if (includeBri) {
root["on"] = (bri > 0);

View File

@@ -12,8 +12,10 @@ static volatile byte presetToApply = 0;
static volatile byte callModeToApply = 0;
static volatile byte presetToSave = 0;
static volatile int8_t saveLedmap = -1;
static char quickLoad[12]; // WLEDMM 9->12 to prevent crashing with unicode
static char saveName[33];
#define QLOAD_BUFFER 12 // string needed for quickload // WLEDMM 9->12 to prevent crashing with unicode
#define FNAME_BUFFER 32 // string needed for saveName
static char quickLoad[QLOAD_BUFFER+1] = {'\0'}; // 1 extra byte for '\0'
static char saveName[FNAME_BUFFER+1] = {'\0'}; // 1 extra byte for '\0'
static bool includeBri = true, segBounds = true, selectedOnly = false, playlistSave = false;
static const char *getFileName(bool persist = true) {
@@ -38,7 +40,14 @@ static void doSaveState() {
bool persist = (presetToSave < 251);
const char *filename = getFileName(persist);
if (!requestJSONBufferLock(10)) return; // will set fileDoc
if (!requestJSONBufferLock(10)) return; // will set fileDoc // async write
// WLEDMM Acquire file mutex before writing presets.json or tmp.json
if (esp32SemTake(presetFileMux, 2500) != pdTRUE) {
USER_PRINTLN(F("doSaveState(): preset file busy, cannot write"));
releaseJSONBufferLock();
return;
}
initPresetsFile(); // just in case if someone deleted presets.json using /edit
JsonObject sObj = doc.to<JsonObject>();
@@ -82,6 +91,8 @@ static void doSaveState() {
writeObjectToFileUsingId(filename, presetToSave, fileDoc);
if (persist) presetsModifiedTime = toki.second(); //unix time
esp32SemGive(presetFileMux); // Release file mutex
releaseJSONBufferLock();
updateFSInfo();
@@ -296,13 +307,17 @@ void handlePresets()
void savePreset(byte index, const char* pname, JsonObject sObj)
{
if (index == 0 || (index > 250 && index < 255)) return;
if (pname) strlcpy(saveName, pname, 33);
if (pname) strlcpy(saveName, pname, FNAME_BUFFER+1);
else {
if (sObj["n"].is<const char*>()) strlcpy(saveName, sObj["n"].as<const char*>(), 33);
if (sObj["n"].is<const char*>()) strlcpy(saveName, sObj["n"].as<const char*>(), FNAME_BUFFER+1);
else sprintf_P(saveName, PSTR("Preset %d"), index);
}
DEBUG_PRINT(F("Saving preset (")); DEBUG_PRINT(index); DEBUG_PRINT(F(") ")); DEBUG_PRINTLN(saveName);
auto oldpresetToSave = presetToSave; // for recovery in case that esp32SemTake(presetFileMux) fails
auto oldplaylistSave = playlistSave;
char oldQuickLoad[QLOAD_BUFFER+1];
strlcpy(oldQuickLoad, quickLoad, sizeof(oldQuickLoad));
presetToSave = index;
playlistSave = false;
@@ -316,17 +331,36 @@ void savePreset(byte index, const char* pname, JsonObject sObj)
} else {
// this is a playlist or API call
if (sObj[F("playlist")].isNull()) {
// we will save API call immediately (often causes presets.json corruption)
// we will save API call immediately (often causes presets.json corruption in the past)
// WLEDMM Acquire file mutex before writing presets.json, to prevent presets.json corruption
if (esp32SemTake(presetFileMux, 2500) != pdTRUE) {
USER_PRINTLN(F("savePreset(): preset file busy, cannot write"));
presetToSave = oldpresetToSave;
playlistSave = oldplaylistSave;
strlcpy(quickLoad, oldQuickLoad, sizeof(quickLoad));
return; // early exit, no change
}
presetToSave = 0;
if (index > 250 || !fileDoc) return; // cannot save API calls to temporary preset (255)
if (index > 250 || !fileDoc) {
esp32SemGive(presetFileMux); // Release file mutex
presetToSave = oldpresetToSave; // bugfix: restore previous state on error exit
playlistSave = oldplaylistSave;
strlcpy(quickLoad, oldQuickLoad, sizeof(quickLoad));
return; // cannot save API calls to temporary preset (255)
}
sObj.remove("o");
sObj.remove("v");
sObj.remove("time");
sObj.remove(F("error"));
sObj.remove(F("psave"));
if (sObj["n"].isNull()) sObj["n"] = saveName;
initPresetsFile(); // just in case if someone deleted presets.json using /edit
writeObjectToFileUsingId(getFileName(index<255), index, fileDoc);
esp32SemGive(presetFileMux); // Release file mutex
presetsModifiedTime = toki.second(); //unix time
updateFSInfo();
} else {
@@ -339,8 +373,16 @@ void savePreset(byte index, const char* pname, JsonObject sObj)
}
void deletePreset(byte index) {
// WLEDMM Acquire file mutex before writing presets.json, to prevent presets.json corruption
if (esp32SemTake(presetFileMux, 2500) != pdTRUE) {
USER_PRINTLN(F("deletePreset(): preset file busy, cannot write"));
return; // early exit, no change
}
StaticJsonDocument<24> empty;
writeObjectToFileUsingId(getFileName(), index, &empty);
esp32SemGive(presetFileMux); // Release file mutex
presetsModifiedTime = toki.second(); //unix time
updateFSInfo();
}

View File

@@ -150,6 +150,14 @@ void notify(byte callMode, bool followUp)
notificationCount = followUp ? notificationCount + 1 : 0;
}
// WLEDMM cache current main segment: updated in realtimeLock, reset in exitRealtime, used in setRealTimePixel
static Segment* theMainSeg = nullptr;
static int theMainSegLength = 0;
static int theStripLength = 0;
#ifdef ARDUINO_ARCH_ESP32
static portMUX_TYPE critical_lock = portMUX_INITIALIZER_UNLOCKED; // to make cache clearing an atomic operation
#endif
void realtimeLock(uint32_t timeoutMs, byte md)
{
if (!realtimeMode && !realtimeOverride) {
@@ -162,9 +170,8 @@ void realtimeLock(uint32_t timeoutMs, byte md)
if (strip.isServicing()) {
USER_PRINTLN(F("realtimeLock() entering RTM: strip is still drawing effects."));
strip.waitUntilIdle();
strip.waitUntilIdle(350);
}
strip.service(); // WLEDMM make sure that all segments are properly initialized
busses.invalidateCache(true);
// WLEDMM end
@@ -180,7 +187,10 @@ void realtimeLock(uint32_t timeoutMs, byte md)
stop = strip.getLengthTotal();
}
// clear strip/segment
for (size_t i = start; i < stop; i++) strip.setPixelColor(i,BLACK);
if (esp32SemTake(busDrawMux, 200) == pdTRUE) { // WLEDMM acquire drawing permission (wait max 200ms) before setting pixels
for (size_t i = start; i < stop; i++) strip.setPixelColor(i,BLACK);
esp32SemGive(busDrawMux);
}
// if WLED was off and using main segment only, freeze non-main segments so they stay off
if (useMainSegmentOnly && bri == 0) {
for (size_t s=0; s < strip.getSegmentsNum(); s++) {
@@ -198,6 +208,30 @@ void realtimeLock(uint32_t timeoutMs, byte md)
}
realtimeMode = md;
// WLEDMM cache current "main segment"
if (esp32SemTake(busDrawMux, 1200) == pdTRUE) { // stupid long timeout, but we don't want to wait forever
// WLEDMM protect against parallel cache updates from different tasks
// positive side effect: this also introduces a wait if other bus activities are happening in parallel
Segment& mainSegRef = strip.getMainSegment();
theMainSeg = &mainSegRef; //convert from reference to pointer
if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) {
// prevent drawing during user override
theMainSegLength = 0;
theStripLength = 0;
} else {
theMainSegLength = theMainSeg->length();
theStripLength = strip.getLengthTotal();
}
esp32SemGive(busDrawMux);
} else {
// mutex acquisition failed, log debug message and pretend we are in override mode
DEBUG_PRINTLN(F("realtimeLock: failed to acquire busDrawMux for cache update."));
// clear cache to prevent stale pointer usage
theMainSeg = nullptr;
theMainSegLength = 0;
theStripLength = 0;
}
if (realtimeOverride) return;
if (arlsForceMaxBri) strip.setBrightness(scaledBri(255), true);
if (briT > 0 && md == REALTIME_MODE_GENERIC) strip.show();
@@ -217,6 +251,16 @@ void exitRealtime() {
} else {
strip.show(); // possible fix for #3589
}
// WLEDMM invalidate cached main segment pointer and length
#ifdef ARDUINO_ARCH_ESP32
portENTER_CRITICAL(&critical_lock); // critical section to make cache reset atomic and thread-safe
#endif
theMainSeg = nullptr;
theMainSegLength = 0;
theStripLength = 0;
#ifdef ARDUINO_ARCH_ESP32
portEXIT_CRITICAL(&critical_lock); // end of critical section
#endif
busses.invalidateCache(false); // WLEDMM
USER_PRINTLN(F("exitRealtime() realtime mode ended."));
updateInterfaces(CALL_MODE_WS_SEND);
@@ -303,10 +347,13 @@ void handleNotifications()
#endif
uint16_t id = 0;
uint16_t totalLen = strip.getLengthTotal();
for (int i = 0; i < packetSize -2; i += 3)
{
setRealtimePixel(id, lbuf[i], lbuf[i+1], lbuf[i+2], 0);
id++; if (id >= totalLen) break;
if (esp32SemTake(busDrawMux, 200) == pdTRUE) { // WLEDMM acquire drawing permission (wait max 200ms) before setting pixels
for (int i = 0; i < packetSize -2; i += 3)
{
setRealtimePixel(id, lbuf[i], lbuf[i+1], lbuf[i+2], 0);
id++; if (id >= totalLen) break;
}
esp32SemGive(busDrawMux);
}
if (!(realtimeMode && useMainSegmentOnly)) strip.show();
return;
@@ -550,14 +597,16 @@ void handleNotifications()
uint16_t id = (tpmPayloadFrameSize/3)*(packetNum-1); //start LED
uint16_t totalLen = strip.getLengthTotal();
for (size_t i = 6; i < tpmPayloadFrameSize + 4U; i += 3)
{
if (id < totalLen)
if (esp32SemTake(busDrawMux, 200) == pdTRUE) { // WLEDMM acquire drawing permission (wait max 200ms) before setting pixels
for (size_t i = 6; i < tpmPayloadFrameSize + 4U; i += 3)
{
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0);
id++;
if (id < totalLen) {
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0);
id++;
}
else break;
}
else break;
esp32SemGive(busDrawMux);
}
if (tpmPacketCount == numPackets) //reset packet count and show if all packets were received
{
@@ -567,8 +616,8 @@ void handleNotifications()
return;
}
//UDP realtime: 1 warls 2 drgb 3 drgbw
if (udpIn[0] > 0 && udpIn[0] < 5)
//UDP realtime: 1 warls 2 drgb 3 drgbw 4 dnrgb 5 dnrgbw
if (udpIn[0] > 0 && udpIn[0] < 6)
{
realtimeIP = (isSupp) ? notifier2Udp.remoteIP() : notifierUdp.remoteIP();
DEBUG_PRINTLN(realtimeIP);
@@ -583,49 +632,40 @@ void handleNotifications()
}
if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return;
uint16_t totalLen = strip.getLengthTotal();
if (udpIn[0] == 1 && packetSize > 5) //warls
{
for (int i = 2; i < packetSize -3; i += 4)
{
setRealtimePixel(udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3], 0);
}
} else if (udpIn[0] == 2 && packetSize > 4) //drgb
{
uint16_t id = 0;
for (int i = 2; i < packetSize -2; i += 3)
{
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0);
id++; if (id >= totalLen) break;
}
} else if (udpIn[0] == 3 && packetSize > 6) //drgbw
{
uint16_t id = 0;
for (int i = 2; i < packetSize -3; i += 4)
{
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]);
id++; if (id >= totalLen) break;
}
} else if (udpIn[0] == 4 && packetSize > 7) //dnrgb
{
uint16_t id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00);
for (int i = 4; i < packetSize -2; i += 3)
{
if (id >= totalLen) break;
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0);
id++;
}
} else if (udpIn[0] == 5 && packetSize > 8) //dnrgbw
{
uint16_t id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00);
for (int i = 4; i < packetSize -2; i += 4)
{
if (id >= totalLen) break;
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]);
id++;
if (esp32SemTake(busDrawMux, 250) == pdTRUE) { // WLEDMM acquire drawing permission (wait max 200ms) before setting pixels
uint16_t totalLen = strip.getLengthTotal();
if (udpIn[0] == 1 && packetSize > 5) { //warls
for (int i = 2; i < packetSize -3; i += 4) {
setRealtimePixel(udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3], 0);
}
} else if (udpIn[0] == 2 && packetSize > 4) { //drgb
uint16_t id = 0;
for (int i = 2; i < packetSize -2; i += 3) {
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0);
id++; if (id >= totalLen) break;
}
} else if (udpIn[0] == 3 && packetSize > 6) { //drgbw
uint16_t id = 0;
for (int i = 2; i < packetSize -3; i += 4) {
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]);
id++; if (id >= totalLen) break;
}
} else if (udpIn[0] == 4 && packetSize > 7) { //dnrgb
uint16_t id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00);
for (int i = 4; i < packetSize -2; i += 3) {
if (id >= totalLen) break;
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0);
id++;
}
} else if (udpIn[0] == 5 && packetSize > 8) { //dnrgbw
uint16_t id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00);
for (int i = 4; i < packetSize -3; i += 4) {
if (id >= totalLen) break;
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]);
id++;
}
}
esp32SemGive(busDrawMux); // end of critical section
}
strip.show();
return;
@@ -651,8 +691,8 @@ void handleNotifications()
void setRealtimePixel(uint16_t i, byte r, byte g, byte b, byte w)
{
uint16_t pix = i + arlsOffset;
if (pix < strip.getLengthTotal()) {
int pix = i + arlsOffset;
if (unsigned(pix) < theStripLength) { // WLEDMM use cached length
if (!arlsDisableGammaCorrection && gammaCorrectCol) {
r = gamma8(r);
g = gamma8(g);
@@ -660,8 +700,8 @@ void setRealtimePixel(uint16_t i, byte r, byte g, byte b, byte w)
w = gamma8(w);
}
if (useMainSegmentOnly) {
Segment &seg = strip.getMainSegment();
if (pix<seg.length()) seg.setPixelColor(pix, r, g, b, w);
//Segment &seg = strip.getMainSegment();
if ((theMainSeg) && (unsigned(pix) < theMainSegLength)) theMainSeg->setPixelColor(pix, r, g, b, w); // WLEDMM used cached main segment
} else {
strip.setPixelColor(pix, r, g, b, w);
}

View File

@@ -220,19 +220,34 @@ bool isAsterisksOnly(const char* str, byte maxLen)
//threading/network callback details: https://github.com/Aircoookie/WLED/pull/2336#discussion_r762276994
bool requestJSONBufferLock(uint8_t module)
bool requestJSONBufferLock(uint8_t module, unsigned timeoutMS)
{
unsigned long now = millis();
bool haveLock = false;
#ifdef ARDUINO_ARCH_ESP32
// We use a recursive mutex to prevent parallel JSON writes from parallel tasks.
// This also fixes hanging up for the full timeout interval in cases when the contention is from the same task.
// see https://github.com/wled/WLED/pull/4089 for more details.
if (esp32SemTake(jsonBufferLockMutex, timeoutMS) == pdTRUE) haveLock = true; // WLEDMM must wait longer than suspendStripService timeout = 1500ms
#else
// 8266: only wait in case that can_yield() tells us we can yield and delay
if (can_yield()) {
unsigned long now = millis();
while (jsonBufferLock && millis()-now < timeoutMS) delay(1); // wait for fraction for buffer lock // WLEDMM must wait longer than suspendStripService timeout = 1500ms
if (!jsonBufferLock) haveLock = true;
}
#endif
while (jsonBufferLock && millis()-now < 1100) delay(1); // wait for fraction for buffer lock
if (jsonBufferLock) {
if (jsonBufferLock || !haveLock) {
#ifdef ARDUINO_ARCH_ESP32
if (haveLock) esp32SemGive(jsonBufferLockMutex); // we got the mutex, but jsonBufferLock says the opposite -> give up
#endif
USER_PRINT(F("ERROR: Locking JSON buffer failed! (still locked by "));
USER_PRINT(jsonBufferLock);
USER_PRINTLN(")");
return false; // waiting time-outed
}
// success - we keep holding the mutex until releaseJSONBufferLock()
jsonBufferLock = module ? module : 255;
DEBUG_PRINT(F("JSON buffer locked. ("));
DEBUG_PRINT(jsonBufferLock);
@@ -250,6 +265,9 @@ void releaseJSONBufferLock()
DEBUG_PRINTLN(")");
fileDoc = nullptr;
jsonBufferLock = 0;
#ifdef ARDUINO_ARCH_ESP32
esp32SemGive(jsonBufferLockMutex); // return the mutex
#endif
}

View File

@@ -476,6 +476,20 @@ void WLED::setup()
init_math(); // WLEDMM: pre-calculate some lookup tables
#ifdef ARDUINO_ARCH_ESP32
busDrawMux = xSemaphoreCreateRecursiveMutex(); // WLEDMM prevent concurrent running of strip.show and strip.service
segmentMux = xSemaphoreCreateRecursiveMutex(); // WLEDMM prevent segment changes while effects are running
jsonBufferLockMutex = xSemaphoreCreateRecursiveMutex(); // WLEDMM prevent concurrent JSON buffer writing
presetFileMux = xSemaphoreCreateRecursiveMutex(); // WLEDMM prevent concurrent presets.json file writing
if ((busDrawMux == nullptr) || (segmentMux == nullptr) || (jsonBufferLockMutex == nullptr) || (presetFileMux == nullptr)) {
USER_PRINTLN(F("setup error: xSemaphoreCreateRecursiveMutex failed.")); // should never happen.
}
xSemaphoreGiveRecursive(busDrawMux); // init semaphores to initially allow drawing
xSemaphoreGiveRecursive(segmentMux);
xSemaphoreGiveRecursive(jsonBufferLockMutex);
xSemaphoreGiveRecursive(presetFileMux);
#endif
#ifdef ARDUINO_ARCH_ESP32
#if defined(WLED_DEBUG) && (defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || ARDUINO_USB_CDC_ON_BOOT)
if (!Serial) delay(2500); // WLEDMM allow CDC USB serial to initialise (WLED_DEBUG only)
@@ -1098,16 +1112,16 @@ bool WLED::initEthernet()
void WLED::initConnection()
{
#ifdef WLED_ENABLE_WEBSOCKETS
ws.onEvent(wsEvent);
#endif
#ifdef ARDUINO_ARCH_ESP32
unsigned long t_wait = millis();
while(strip.isUpdating() && (millis() - t_wait < 86)) delay(1); // WLEDMM try to catch a moment when strip is idle
//if (strip.isUpdating()) USER_PRINTLN("WLED::initConnection: strip still updating.");
#endif
#ifdef WLED_ENABLE_WEBSOCKETS
ws.onEvent(wsEvent);
#endif
WiFi.disconnect(true); // close old connections
#ifdef ESP8266
WiFi.setPhyMode(force802_3g ? WIFI_PHY_MODE_11G : WIFI_PHY_MODE_11N);

View File

@@ -7,7 +7,7 @@
*/
// version code in format yymmddb (b = daily build)
#define VERSION 2512301
#define VERSION 2601011
// WLEDMM - you can check for this define in usermods, to only enabled WLEDMM specific code in the "right" fork. Its not defined in AC WLED.
#define _MoonModules_WLED_
@@ -765,6 +765,32 @@ WLED_GLOBAL volatile bool loadLedmap _INIT(false); // WLEDMM use as boo
WLED_GLOBAL volatile uint8_t loadedLedmap _INIT(0); // WLEDMM default 0
WLED_GLOBAL volatile bool suspendStripService _INIT(false); // WLEDMM temporarily prevent running strip.service, when strip or segments are "under update" and inconsistent
WLED_GLOBAL volatile bool OTAisRunning _INIT(false); // WLEDMM temporarily stop led updates during OTA
// WLEDMM prevent concurrent strip.show() and strip.service() -> for DDP over ws, and other background tasks
#ifdef ARDUINO_ARCH_ESP32
WLED_GLOBAL SemaphoreHandle_t busDrawMux _INIT(nullptr);
WLED_GLOBAL SemaphoreHandle_t segmentMux _INIT(nullptr);
WLED_GLOBAL SemaphoreHandle_t jsonBufferLockMutex _INIT(nullptr);
WLED_GLOBAL SemaphoreHandle_t presetFileMux _INIT(nullptr); // Protects presets.json file writes
#define esp32SemTake(mux,timeout) xSemaphoreTakeRecursive(mux, pdMS_TO_TICKS(timeout)) // convenience macro that expands to xSemaphoreTakeRecursive - timeout is in milliseconds
#define esp32SemGive(mux) xSemaphoreGiveRecursive(mux) // convenience macro that expands to xSemaphoreGiveRecursive
#define WLED_create_spinlock(theSname) static portMUX_TYPE theSname = portMUX_INITIALIZER_UNLOCKED
#else
// dummy semaphores for 8266
#ifndef pdTRUE
#define pdTRUE 1
#endif
#ifndef portMAX_DELAY
#define portMAX_DELAY UINT32_MAX
#endif
#define esp32SemTake(mux,timeout) (pdTRUE)
#define esp32SemGive(mux)
// dummy critical section for 8266
#define WLED_create_spinlock(sname)
#define portENTER_CRITICAL(sname)
#define portEXIT_CRITICAL(sname)
#endif
#ifndef ESP8266
WLED_GLOBAL char *ledmapNames[WLED_MAX_LEDMAPS-1] _INIT_N(({nullptr}));
#endif

View File

@@ -6,9 +6,27 @@
#endif
#include "html_settings.h"
#include "html_other.h"
#ifdef WLED_ENABLE_PIXART
#include "html_pixart.h"
#endif
#ifdef WLED_ENABLE_PXMAGIC
//#include "html_pxmagic.h"
#if !defined(WLED_ENABLE_PIXART)
#error "PIXEL MAGIC Tool is not supported in WLED-MM. Please use Pixel Art Converter instead: add -D WLED_ENABLE_PIXART to your build_flags"
// PIXEL MAGIC has known problems when creating image presets for larger images.
// if you still want to use it, upload pxmagic.htm to your device (<WLED-IP>/edit) and then start <WLED-IP>/pxmagic.htm
#endif
#endif
#if defined(WLED_ENABLE_PIXELFORGE) && !defined(WLED_DISABLE_PIXELFORGE) // WLEDMM uses WLED_ENABLE_PIXELFORGE, upstream has WLED_DISABLE_PIXELFORGE
#include "html_pixelforge.h"
static const char _pixelforge_htm[] PROGMEM = "/pixelforge.htm";
static const char _common_js[] PROGMEM = "/common.js";
#if !defined(WLED_ENABLE_GIF)
#error "GIF image support is missing. Please add -D WLED_ENABLE_GIF to your build flags."
#endif
#endif
#include "html_cpal.h"
// define flash strings once (saves flash memory)
@@ -19,6 +37,7 @@ static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN co
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 enc_gzip[] PROGMEM = "gzip";
/*
* Integrated HTTP web server page declarations
@@ -448,6 +467,24 @@ void initServer()
});
#endif
#if defined(WLED_ENABLE_PIXELFORGE) && !defined(WLED_DISABLE_PIXELFORGE)
server.on(_pixelforge_htm, HTTP_GET, [](AsyncWebServerRequest *request) {
//handleStaticContent(request, FPSTR(_pixelforge_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pixelforge, PAGE_pixelforge_length);
if (handleFileRead(request, FPSTR(_pixelforge_htm))) return;
if (handleIfNoneMatchCacheHeader(request)) return;
AsyncWebServerResponse *response = request->beginResponse_P(200, FPSTR(CONTENT_TYPE_HTML), PAGE_pixelforge, PAGE_pixelforge_L);
response->addHeader(FPSTR(s_content_enc),FPSTR(enc_gzip));
setStaticContentCacheHeaders(response);
request->send(response);
});
server.on(_common_js, HTTP_GET, [](AsyncWebServerRequest *request){
AsyncWebServerResponse *response = request->beginResponse_P(200, FPSTR(CONTENT_TYPE_JAVASCRIPT), JS_common, JS_common_length);
response->addHeader(FPSTR(s_content_enc),FPSTR(enc_gzip));
setStaticContentCacheHeaders(response);
request->send(response);
});
#endif
server.on("/cpal.htm", HTTP_GET, [](AsyncWebServerRequest *request){
if (handleFileRead(request, "/cpal.htm")) return;
if (handleIfNoneMatchCacheHeader(request)) return;
@@ -596,6 +633,18 @@ void serveSettingsJS(AsyncWebServerRequest* request)
{
char buf[SETTINGS_STACK_BUF_SIZE+37] = { '\0' }; // WLEDMM ensure buffer is cleared initially
buf[0] = 0;
#if defined(WLED_ENABLE_PIXELFORGE) && !defined(WLED_DISABLE_PIXELFORGE)
// serve common.js if requested by subPage = 254 (.js)
if (request->url().indexOf(FPSTR(_common_js)) > 0) {
AsyncWebServerResponse *response = request->beginResponse_P(200, FPSTR(CONTENT_TYPE_JAVASCRIPT), JS_common, JS_common_length);
response->addHeader(FPSTR(s_content_enc),FPSTR(enc_gzip));
setStaticContentCacheHeaders(response);
request->send(response);
return;
}
#endif
byte subPage = request->arg(F("p")).toInt();
if (subPage > 10) {
strcpy_P(buf, PSTR("alert('Settings for this request are not implemented.');"));

View File

@@ -49,7 +49,7 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp
}
bool verboseResponse = false;
if (!requestJSONBufferLock(11)) {
if (!requestJSONBufferLock(11, 300)) {
client->text(F("{\"error\":3}")); // ERR_NOBUF
return;
}
@@ -138,7 +138,7 @@ void sendDataWs(AsyncWebSocketClient * client)
DEBUG_PRINTF("sendDataWs\n");
if (!ws.count()) return;
if (!requestJSONBufferLock(12)) {
if (!requestJSONBufferLock(12, 300)) {
if (client) {
client->text(F("{\"error\":3}")); // ERR_NOBUF
} else {

View File

@@ -308,6 +308,17 @@ void getSettingsJS(AsyncWebServerRequest* request, byte subPage, char* dest) //W
#ifdef WLED_ENABLE_DMX // include only if DMX is enabled
oappend(PSTR("gId('dmxbtn').style.display='';"));
#endif
#ifdef WLED_ENABLE_PIXART // include only if PixelArt tool is enabled
oappend(PSTR("gId('pixbtn').style.display='';"));
#endif
#if defined(WLED_ENABLE_PXMAGIC) && !defined(WLED_ENABLE_PIXART) // include only if PixelMagic tool is enabled - only when PixelArt is not enabled
oappend(PSTR("gId('pxmbtn').style.display='';"));
#endif
#if defined(WLED_ENABLE_PIXELFORGE) && !defined(WLED_DISABLE_PIXELFORGE) // include only if PixelForge is enabled
oappend(PSTR("gId('forgebtn').style.display='';"));
#endif
}
if (subPage == 1)