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