Compare commits

..

9 Commits

Author SHA1 Message Date
Damian Schneider
1e42ebf852 mask out flag bits in DDP type for compatibility 2026-04-12 19:50:07 +02:00
Damian Schneider
e4a7effdb0 add search icon back(#5500) 2026-04-12 19:08:05 +02:00
Damian Schneider
ae36f7e203 use memory aligned allocations, fix bug in FFT magnitude (integer path) (#5499)
* use memory aligned allocations, fix bug in FFT magnitude (integer path)
* assign pointer to globals not local copy
2026-04-12 19:06:16 +02:00
Will Miles
4cdc23a48b Merge pull request #5497 from willmmiles/16_x-dynarray-fixes
0.16 - Backport dynarray fixes from V5 WIP
2026-04-12 11:34:41 -04:00
Will Miles
48ab88e11e Fix up usermod validation again
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 10:03:11 -04:00
Will Miles
fd890b3d58 dynarray: Support ESP-IDF 2026-04-12 10:03:05 -04:00
Will Miles
f994c5e995 validate_modules: Improve performance
Use readelf instead of nm for great speed.
2026-04-12 10:02:59 -04:00
Will Miles
c789e3d187 Fix usermod count for LTO 2026-04-12 10:02:53 -04:00
Will Miles
fadc75da55 dynarray: Directly apply linker fix
Rather than append a linker file, we edit the upstream supplied ones to
add our section to the binaries.  Works better on all platforms.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-12 10:02:47 -04:00
15 changed files with 215 additions and 109 deletions

View File

@@ -4,16 +4,77 @@
Import("env")
from pathlib import Path
platform = env.get("PIOPLATFORM")
script_file = Path(f"tools/dynarray_{platform}.ld")
if script_file.is_file():
linker_script = f"-T{script_file}"
if platform == "espressif32":
# For ESP32, the script must be added at the right point in the list
linkflags = env.get("LINKFLAGS", [])
idx = linkflags.index("memory.ld")
linkflags.insert(idx+1, linker_script)
env.Replace(LINKFLAGS=linkflags)
# Linker script fragment injected into the rodata output section of whichever
# platform we're building for. Placed just before the end-of-rodata marker so
# that the dynarray entries land in flash rodata and are correctly sorted.
DYNARRAY_INJECTION = (
"\n /* dynarray: WLED dynamic module arrays */\n"
" . = ALIGN(0x10);\n"
" KEEP(*(SORT_BY_INIT_PRIORITY(.dynarray.*)))\n"
" "
)
def inject_before_marker(path, marker):
"""Patch a linker script file in-place, inserting DYNARRAY_INJECTION before marker."""
original = path.read_text()
path.write_text(original.replace(marker, DYNARRAY_INJECTION + marker, 1))
if env.get("PIOPLATFORM") == "espressif32":
# Find sections.ld on the linker search path (LIBPATH).
sections_ld_path = None
for ld_dir in env.get("LIBPATH", []):
candidate = Path(str(ld_dir)) / "sections.ld"
if candidate.exists():
sections_ld_path = candidate
break
if sections_ld_path is not None:
# Inject inside the existing .flash.rodata output section, just before
# _rodata_end. IDF v5 enforces zero gaps between adjacent output
# sections via ASSERT statements, so INSERT AFTER .flash.rodata would
# fail. Injecting inside the section creates no new output section and
# leaves the ASSERTs satisfied.
build_dir = Path(env.subst("$BUILD_DIR"))
patched_path = build_dir / "dynarray_sections.ld"
import shutil
shutil.copy(sections_ld_path, patched_path)
inject_before_marker(patched_path, "_rodata_end = ABSOLUTE(.);")
# Replace "sections.ld" in LINKFLAGS with an absolute path to our
# patched copy. The flag may appear as a bare token, combined as
# "-Tsections.ld", or split across two tokens ("-T", "sections.ld").
patched_str = str(patched_path)
new_flags = []
skip_next = False
for flag in env.get("LINKFLAGS", []):
if skip_next:
new_flags.append(patched_str if flag == "sections.ld" else flag)
skip_next = False
elif flag == "-T":
new_flags.append(flag)
skip_next = True
else:
new_flags.append(flag.replace("sections.ld", patched_str))
env.Replace(LINKFLAGS=new_flags)
else:
# For other platforms, put it in last
env.Append(LINKFLAGS=[linker_script])
# Assume sections.ld will be built (ESP-IDF format); add a post-action to patch it
# TODO: consider using ESP-IDF linker fragment (https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/linker-script-generation.html)
# For now, patch after building
sections_ld = Path(env.subst("$BUILD_DIR")) / "sections.ld"
def patch_sections_ld(target, source, env):
inject_before_marker(sections_ld, "_rodata_end = ABSOLUTE(.);")
env.AddPostAction(str(sections_ld), patch_sections_ld)
elif env.get("PIOPLATFORM") == "espressif8266":
# The ESP8266 framework preprocesses eagle.app.v6.common.ld.h into
# local.eagle.app.v6.common.ld in $BUILD_DIR/ld/ at build time. Register
# a post-action on that generated file so the injection happens after
# C-preprocessing but before linking.
build_ld = Path(env.subst("$BUILD_DIR")) / "ld" / "local.eagle.app.v6.common.ld"
def patch_esp8266_ld(target, source, env):
inject_before_marker(build_ld, "_irom0_text_end = ABSOLUTE(.);")
env.AddPostAction(str(build_ld), patch_esp8266_ld)

View File

@@ -1,11 +1,12 @@
import os
import re
import subprocess
from pathlib import Path # For OS-agnostic path manipulation
from pathlib import Path
from click import secho
from SCons.Script import Action, Exit
Import("env")
_ATTR = re.compile(r'\bDW_AT_(name|comp_dir)\b')
def read_lines(p: Path):
""" Read in the contents of a file for analysis """
@@ -13,86 +14,129 @@ def read_lines(p: Path):
return f.readlines()
def _get_nm_path(env) -> str:
""" Derive the nm tool path from the build environment """
if "NM" in env:
return env.subst("$NM")
# Derive from the C compiler: xtensa-esp32-elf-gcc → xtensa-esp32-elf-nm
cc = env.subst("$CC")
nm = re.sub(r'(gcc|g\+\+)$', 'nm', os.path.basename(cc))
return os.path.join(os.path.dirname(cc), nm)
def _get_readelf_path(env) -> str:
""" Derive the readelf tool path from the build environment """
# Derive from the C compiler: xtensa-esp32-elf-gcc → xtensa-esp32-elf-readelf
cc = Path(env.subst("$CC"))
return str(cc.with_name(re.sub(r'(gcc|g\+\+)$', 'readelf', cc.name)))
def check_elf_modules(elf_path: Path, env, module_lib_builders) -> set[str]:
""" Check which modules have at least one defined symbol placed in the ELF.
""" Check which modules have at least one compilation unit in the ELF.
The map file is not a reliable source for this: with LTO, original object
file paths are replaced by temporary ltrans.o partitions in all output
sections, making per-module attribution impossible from the map alone.
Instead we invoke nm --defined-only -l on the ELF, which uses DWARF debug
info to attribute each placed symbol to its original source file.
Requires usermod libraries to be compiled with -g so that DWARF sections
are present in the ELF. load_usermods.py injects -g for all WLED modules
via dep.env.AppendUnique(CCFLAGS=["-g"]).
Instead we invoke readelf --debug-dump=info --dwarf-depth=1 on the ELF,
which reads only the top-level compilation-unit DIEs from .debug_info.
Each CU corresponds to one source file; matching DW_AT_comp_dir +
DW_AT_name against the module src_dirs is sufficient to confirm a module
was compiled into the ELF. The output volume is proportional to the
number of source files, not the number of symbols.
Returns the set of build_dir basenames for confirmed modules.
"""
nm_path = _get_nm_path(env)
readelf_path = _get_readelf_path(env)
secho(f"INFO: Checking for usermod compilation units...")
try:
result = subprocess.run(
[nm_path, "--defined-only", "-l", str(elf_path)],
[readelf_path, "--debug-dump=info", "--dwarf-depth=1", str(elf_path)],
capture_output=True, text=True, errors="ignore", timeout=120,
)
nm_output = result.stdout
output = result.stdout
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
secho(f"WARNING: nm failed ({e}); skipping per-module validation", fg="yellow", err=True)
secho(f"WARNING: readelf failed ({e}); skipping per-module validation", fg="yellow", err=True)
return {Path(b.build_dir).name for b in module_lib_builders} # conservative pass
# Match placed symbols against builders as we parse nm output, exiting early
# once all builders are accounted for.
# nm --defined-only still includes debugging symbols (type 'N') such as the
# per-CU markers GCC emits in .debug_info (e.g. "usermod_example_cpp_6734d48d").
# These live at address 0x00000000 in their debug section — not in any load
# segment — so filtering them out leaves only genuinely placed symbols.
# nm -l appends a tab-separated "file:lineno" location to each symbol line.
remaining = {Path(str(b.src_dir)): Path(b.build_dir).name for b in module_lib_builders}
found = set()
project_dir = Path(env.subst("$PROJECT_DIR"))
for line in nm_output.splitlines():
def _flush_cu(comp_dir: str | None, name: str | None) -> None:
"""Match one completed CU against remaining builders."""
if not name or not remaining:
return
p = Path(name)
src_path = (Path(comp_dir) / p) if (comp_dir and not p.is_absolute()) else p
# In arduino+espidf dual-framework builds the IDF toolchain sets DW_AT_comp_dir
# to the virtual path "/IDF_PROJECT" rather than the real project root, so
# src_path won't match. Pre-compute a fallback using $PROJECT_DIR and check
# both candidates in a single pass.
use_fallback = not p.is_absolute() and comp_dir and Path(comp_dir) != project_dir
src_path_real = project_dir / p if use_fallback else None
for src_dir in list(remaining):
if src_path.is_relative_to(src_dir) or (src_path_real and src_path_real.is_relative_to(src_dir)):
found.add(remaining.pop(src_dir))
return
# readelf emits one DW_TAG_compile_unit DIE per source file. Attributes
# of interest:
# DW_AT_name — source file (absolute, or relative to comp_dir)
# DW_AT_comp_dir — compile working directory
# Both appear as either a direct string or an indirect string:
# DW_AT_name : foo.cpp
# DW_AT_name : (indirect string, offset: 0x…): foo.cpp
# Taking the portion after the *last* ": " on the line handles both forms.
comp_dir = name = None
for line in output.splitlines():
if 'Compilation Unit @' in line:
_flush_cu(comp_dir, name)
comp_dir = name = None
continue
if not remaining:
break # all builders matched
addr, _, _ = line.partition(' ')
if not addr.lstrip('0'):
continue # zero address — skip debug-section marker
if '\t' not in line:
continue
loc = line.rsplit('\t', 1)[1]
# Strip trailing :lineno (e.g. "/path/to/foo.cpp:42" → "/path/to/foo.cpp")
src_path = Path(loc.rsplit(':', 1)[0])
# Path.is_relative_to() handles OS-specific separators correctly without
# any regex, avoiding Windows path escaping issues.
for src_dir in list(remaining):
if src_path.is_relative_to(src_dir):
found.add(remaining.pop(src_dir))
break
m = _ATTR.search(line)
if m:
_, _, val = line.rpartition(': ')
val = val.strip()
if m.group(1) == 'name':
name = val
else:
comp_dir = val
_flush_cu(comp_dir, name) # flush the last CU
return found
DYNARRAY_SECTION = ".dtors" if env.get("PIOPLATFORM") == "espressif8266" else ".dynarray"
USERMODS_SECTION = f"{DYNARRAY_SECTION}.usermods.1"
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 USERMODS_SECTION in x])
""" Returns the number of usermod objects in the usermod list.
Computes the count from the address span between the .dynarray.usermods.0
and .dynarray.usermods.99999 sentinel sections. This mirrors the
DYNARRAY_LENGTH macro and is reliable under LTO, where all entries are
merged into a single ltrans partition so counting section occurrences
always yields 1 regardless of the true count.
"""
ENTRY_SIZE = 4 # sizeof(Usermod*) on 32-bit targets
addr_begin = None
addr_end = None
for i, line in enumerate(map_file):
stripped = line.strip()
if stripped == '.dynarray.usermods.0':
if i + 1 < len(map_file):
m = re.search(r'0x([0-9a-fA-F]+)', map_file[i + 1])
if m:
addr_begin = int(m.group(1), 16)
elif stripped == '.dynarray.usermods.99999':
if i + 1 < len(map_file):
m = re.search(r'0x([0-9a-fA-F]+)', map_file[i + 1])
if m:
addr_end = int(m.group(1), 16)
if addr_begin is not None and addr_end is not None:
break
if addr_begin is None or addr_end is None:
return 0
return (addr_end - addr_begin) // ENTRY_SIZE
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")
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)
@@ -111,6 +155,7 @@ def validate_map_file(source, target, env):
secho(f"INFO: {usermod_object_count} usermod object entries")
elf_path = build_dir / env.subst("${PROGNAME}.elf")
confirmed_modules = check_elf_modules(elf_path, env, module_lib_builders)
missing_modules = [modname for mdir, modname in modules.items() if mdir not in confirmed_modules]
@@ -120,7 +165,6 @@ def validate_map_file(source, target, env):
fg="red",
err=True)
Exit(1)
return None
env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")])
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked optional modules (usermods) in map file'))

View File

@@ -1,10 +0,0 @@
/* ESP32 linker script fragment to add dynamic array section to binary */
SECTIONS
{
.dynarray :
{
. = ALIGN(0x10);
KEEP(*(SORT_BY_INIT_PRIORITY(.dynarray.*)))
} > default_rodata_seg
}
INSERT AFTER .flash.rodata;

View File

@@ -296,17 +296,17 @@ void FFTcode(void * parameter)
ArduinoFFT<float> FFT = ArduinoFFT<float>(valFFT, vImag, samplesFFT, SAMPLE_RATE, true);
#elif !defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT)
// allocate and initialize FFT buffers on first call
// note: free() is never used on these pointers. If it ever is implemented, this implementation can cause memory leaks (need to free raw pointers)
if (valFFT == nullptr) {
float* raw_buffer = (float*)heap_caps_malloc((2 * samplesFFT * sizeof(float)) + 16, MALLOC_CAP_8BIT);
if ((raw_buffer == nullptr)) return; // something went wrong
valFFT = (float*)(((uintptr_t)raw_buffer + 15) & ~15); // SIMD requires aligned memory to 16-byte boundary. note in IDF5 there is MALLOC_CAP_SIMD available
valFFT = (float*)heap_caps_aligned_calloc(16, 2 * samplesFFT, sizeof(float), MALLOC_CAP_8BIT); // SIMD requires aligned memory to 16-byte boundary. note in IDF5 there is MALLOC_CAP_SIMD available
if ((valFFT == nullptr)) return; // something went wrong
}
// create window
if (windowFFT == nullptr) {
float* raw_buffer = (float*)heap_caps_malloc((samplesFFT * sizeof(float)) + 16, MALLOC_CAP_8BIT);
if ((raw_buffer == nullptr)) return; // something went wrong
windowFFT = (float*)(((uintptr_t)raw_buffer + 15) & ~15); // SIMD requires aligned memory to 16-byte boundary
windowFFT = (float*)heap_caps_aligned_calloc(16, samplesFFT, sizeof(float), MALLOC_CAP_8BIT); // SIMD requires aligned memory to 16-byte boundary. note in IDF5 there is MALLOC_CAP_SIMD available
if ((windowFFT == nullptr)) {
heap_caps_free(valFFT); valFFT = nullptr;
return; // something went wrong
}
}
if (dsps_fft2r_init_fc32(NULL, samplesFFT) != ESP_OK) return; // initialize FFT tables
// create window function for FFT
@@ -316,16 +316,20 @@ void FFTcode(void * parameter)
dsps_wind_flat_top_f32(windowFFT, samplesFFT);
#endif
#else
// allocate and initialize integer FFT buffers on first call
if (valFFT == nullptr) valFFT = (int16_t*) calloc(sizeof(int16_t), samplesFFT * 2);
if ((valFFT == nullptr)) return; // something went wrong
// use integer FFT - allocate and initialize integer FFT buffers on first call, 4 bytes aligned (just in case, even if not strictly needed for int16_t)
if (valFFT == nullptr) valFFT = (int16_t*) heap_caps_aligned_calloc(4, samplesFFT * 2, sizeof(int16_t), MALLOC_CAP_8BIT);
// create window
if (windowFFT == nullptr) windowFFT = (int16_t*) calloc(sizeof(int16_t), samplesFFT);
if ((windowFFT == nullptr)) return; // something went wrong
if (dsps_fft2r_init_sc16(NULL, samplesFFT) != ESP_OK) return; // initialize FFT tables
if (windowFFT == nullptr) windowFFT = (int16_t*) heap_caps_aligned_calloc(4, samplesFFT, sizeof(int16_t), MALLOC_CAP_8BIT);
// create window function for FFT
float *windowFloat = (float*) calloc(sizeof(float), samplesFFT); // temporary buffer for window function
if ((windowFloat == nullptr)) return; // something went wrong
float *windowFloat = (float*) heap_caps_aligned_calloc(4, samplesFFT, sizeof(float), MALLOC_CAP_8BIT); // temporary buffer for window function
if (windowFloat == nullptr || windowFFT == nullptr || valFFT == nullptr) { // something went wrong
if (windowFloat) heap_caps_free(windowFloat);
if (windowFFT) heap_caps_free(windowFFT); windowFFT = nullptr;
if (valFFT) heap_caps_free(valFFT); valFFT = nullptr;
return;
}
if (dsps_fft2r_init_sc16(NULL, samplesFFT) != ESP_OK) return; // initialize FFT tables
#ifdef FFT_PREFER_EXACT_PEAKS
dsps_wind_blackman_harris_f32(windowFloat, samplesFFT);
#else
@@ -335,7 +339,7 @@ void FFTcode(void * parameter)
for (int i = 0; i < samplesFFT; i++) {
windowFFT[i] = (int16_t)(windowFloat[i] * 32767.0f);
}
free(windowFloat); // free temporary buffer
heap_caps_free(windowFloat); // free temporary buffer
#endif
// see https://www.freertos.org/vtaskdelayuntil.html
@@ -468,7 +472,6 @@ void FFTcode(void * parameter)
}
FFT_Magnitude = FFT_Magnitude_int * 512; // scale to match raw float value
FFT_MajorPeak = FFT_MajorPeak_int;
FFT_Magnitude = FFT_Magnitude_int;
#endif
#endif
FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects

View File

@@ -147,7 +147,7 @@ p {
font-size: 16px;
}
.fs1 {
font-size: 48px;
font-size: 32px;
}
.fs2 {
font-size: 28px;

View File

@@ -9,10 +9,24 @@
<link rel="stylesheet" href="style.css"></head>
<body>
<div class="bgc1 clearfix">
<h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> wled122 <small class="fgc1">(Glyphs:&nbsp;25)</small></h1>
<h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> wled122 <small class="fgc1">(Glyphs:&nbsp;26)</small></h1>
</div>
<div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: 16</h1>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="i-search"></span>
<span class="mls"> i-search</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e0a1" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe0a1;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="search, magnifier" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="i-pixelforge"></span>

View File

@@ -11,6 +11,7 @@
<glyph unicode="&#xe04c;" glyph-name="presets" d="M704 704c131.413 0 234.667-103.253 234.667-234.667 0-161.28-145.067-292.693-364.8-491.946l-61.867-56.32-61.867 55.893c-219.733 199.68-364.8 331.093-364.8 492.373 0 131.414 103.254 234.667 234.667 234.667 74.24 0 145.493-34.56 192-89.173 46.507 54.613 117.76 89.173 192 89.173zM516.267 40.533c203.093 183.894 337.066 305.494 337.066 428.8 0 85.334-64 149.334-149.333 149.334-65.707 0-129.707-42.24-151.893-100.694h-79.787c-22.613 58.454-86.613 100.694-152.32 100.694-85.333 0-149.333-64-149.333-149.334 0-123.306 133.973-244.906 337.066-428.8l4.267-4.266z" />
<glyph unicode="&#xe066;" glyph-name="info" d="M512 746.667c235.52 0 426.667-191.147 426.667-426.667s-191.147-426.667-426.667-426.667-426.667 191.147-426.667 426.667 191.147 426.667 426.667 426.667zM554.667 106.667v256h-85.334v-256h85.334zM554.667 448v85.333h-85.334v-85.333h85.334z" />
<glyph unicode="&#xe08f;" glyph-name="power" d="M554.667 704v-426.667h-85.334v426.667h85.334zM760.747 611.413c82.773-70.4 135.253-174.506 135.253-291.413 0-212.053-171.947-384-384-384s-384 171.947-384 384c0 116.907 52.48 221.013 135.253 291.413l60.16-60.16c-66.986-54.613-110.080-137.813-110.080-231.253 0-165.12 133.547-298.667 298.667-298.667s298.667 133.547 298.667 298.667c0 93.44-43.094 176.64-110.507 230.827z" />
<glyph unicode="&#xe0a1;" glyph-name="search" d="M902.213 27.99l-197.073 167.615c-20.373 18.336-42.161 26.752-59.761 25.941 46.52 54.492 74.622 125.188 74.622 202.455 0 172.313-139.687 312-312 312-172.311 0-312-139.687-312-312s139.687-312 312-312c77.266 0 147.962 28.1 202.455 74.624-0.812-17.6 7.605-39.388 25.941-59.761l167.615-197.073c28.698-31.887 75.58-34.574 104.178-5.976s25.913 75.48-5.974 104.178zM408 216c-114.874 0-208 93.125-208 208s93.125 208 208 208 208-93.125 208-208-93.125-208-208-208z" />
<glyph unicode="&#xe0a2;" glyph-name="settings" d="M816.64 280.064l85.504-67.584c8.192-6.144 10.24-16.896 5.12-26.112l-81.92-141.824c-5.12-9.216-15.872-12.8-25.088-9.216l-101.888 40.96c-20.992-15.872-44.032-29.696-69.12-39.936l-15.36-108.544c-1.024-10.24-9.728-17.408-19.968-17.408h-163.84c-10.24 0-18.432 7.168-20.48 17.408l-15.36 108.544c-25.088 10.24-47.616 23.552-69.12 39.936l-101.888-40.96c-9.216-3.072-19.968 0-25.088 9.216l-81.92 141.824c-4.608 8.704-2.56 19.968 5.12 26.112l86.528 67.584c-2.048 12.8-3.072 26.624-3.072 39.936s1.536 27.136 3.584 39.936l-86.528 67.584c-8.192 6.144-10.24 16.896-5.12 26.112l81.92 141.824c5.12 9.216 15.872 12.8 25.088 9.216l101.888-40.96c20.992 15.872 44.032 29.696 69.12 39.936l15.36 108.544c1.536 10.24 9.728 17.408 19.968 17.408h163.84c10.24 0 18.944-7.168 20.48-17.408l15.36-108.544c25.088-10.24 47.616-23.552 69.12-39.936l101.888 40.96c9.216 3.072 19.968 0 25.088-9.216l81.92-141.824c4.608-8.704 2.56-19.968-5.12-26.112l-86.528-67.584c2.048-12.8 3.072-26.112 3.072-39.936s-1.024-27.136-2.56-39.936zM512 166.4c84.48 0 153.6 69.12 153.6 153.6s-69.12 153.6-153.6 153.6-153.6-69.12-153.6-153.6 69.12-153.6 153.6-153.6z" />
<glyph unicode="&#xe0e8;" glyph-name="eye" d="M512 640c213.333 0 395.52-132.693 469.333-320-73.813-187.307-256-320-469.333-320s-395.52 132.693-469.333 320c73.813 187.307 256 320 469.333 320zM512 106.667c117.76 0 213.333 95.573 213.333 213.333s-95.573 213.333-213.333 213.333-213.333-95.573-213.333-213.333 95.573-213.333 213.333-213.333zM512 448c70.827 0 128-57.173 128-128s-57.173-128-128-128-128 57.173-128 128 57.173 128 128 128z" />
<glyph unicode="&#xe116;" glyph-name="sync" d="M512 661.333c188.587 0 341.333-152.746 341.333-341.333 0-66.987-19.626-129.28-52.906-181.76l-62.294 62.293c19.2 35.414 29.867 76.374 29.867 119.467 0 141.227-114.773 256-256 256v-128l-170.667 170.667 170.667 170.666v-128zM512 64v128l170.667-170.667-170.667-170.666v128c-188.587 0-341.333 152.746-341.333 341.333 0 66.987 19.626 129.28 52.906 181.76l62.294-62.293c-19.2-35.414-29.867-76.374-29.867-119.467 0-141.227 114.773-256 256-256z" />

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,9 @@
@font-face {
font-family: 'wled122';
src:
url('fonts/wled122.ttf?yzxblb') format('truetype'),
url('fonts/wled122.woff?yzxblb') format('woff'),
url('fonts/wled122.svg?yzxblb#wled122') format('svg');
url('fonts/wled122.ttf?2tjc6') format('truetype'),
url('fonts/wled122.woff?2tjc6') format('woff'),
url('fonts/wled122.svg?2tjc6#wled122') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
@@ -24,6 +24,9 @@
-moz-osx-font-smoothing: grayscale;
}
.i-search:before {
content: "\e0a1";
}
.i-pixelforge:before {
content: "\e900";
}

View File

@@ -1,6 +1,6 @@
@font-face {
font-family: "WIcons";
src: url(data:font/woff2;charset=utf-8;base64,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);
src: url(data:font/woff2;charset=utf-8;base64,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);
}
:root {

View File

@@ -20,15 +20,4 @@ Macros for generating a "dynamic array", a static array of objects declared in d
#define DYNARRAY_END(array_name) array_name##_end
#define DYNARRAY_LENGTH(array_name) (&DYNARRAY_END(array_name)[0] - &DYNARRAY_BEGIN(array_name)[0])
#ifdef ESP8266
// ESP8266 linker script cannot be extended with a unique section for dynamic arrays.
// We instead pack them in the ".dtors" section, as it's sorted and uploaded to the flash
// (but will never be used in the embedded system)
#define DYNARRAY_SECTION ".dtors"
#else /* ESP8266 */
// Use a unique named section; the linker script must be extended to ensure it's correctly placed.
#define DYNARRAY_SECTION ".dynarray"
#endif

View File

@@ -22,7 +22,8 @@ static void handleDDPPacket(e131_packet_t* p) {
int lastPushSeq = e131LastSequenceNumber[0];
// reject unsupported color data types (only RGB and RGBW are supported)
if (p->dataType != DDP_TYPE_RGB24 && p->dataType != DDP_TYPE_RGBW32) return;
uint8_t maskedType = p->dataType & 0x3F; // mask out custom and reserved flags, only type bits are relevant
if (maskedType != DDP_TYPE_RGB24 && maskedType != DDP_TYPE_RGBW32) return;
// reject status and config packets (not implemented)
if (p->destination == DDP_ID_STATUS || p->destination == DDP_ID_CONFIG) return;