Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1435d03
Add WiFi-only Matter (CHIP) usermod for WLED-MM
Copilot Mar 27, 2026
739cad3
Address code review: clamp hue output, remove watchdog disable, clari…
Copilot Mar 27, 2026
e7949d6
Update usermod for v16 style
netmindz Mar 27, 2026
e213ce5
Work in progress on trying to fix the missing dependency on the esp_m…
netmindz Mar 27, 2026
cc4ed69
Work in progress on matter support
netmindz Mar 27, 2026
f765215
Work in progress on matter support
netmindz Mar 27, 2026
bb2c203
Working Matter-over-WIFI for RGB
netmindz Mar 27, 2026
f6176e2
Add factory reset API call
netmindz Mar 27, 2026
ca6d75d
fix REGISTER_USERMOD for matter usermod
netmindz Mar 27, 2026
8ad3035
fix REGISTER_USERMOD for matter usermod
netmindz Mar 27, 2026
6331729
Update docs for Matter usermod
netmindz Mar 27, 2026
99e7ca4
Update to allow externalWiFiManager for compatibility with the matter…
netmindz Mar 27, 2026
441286c
Fix for espidf+arduino builds
netmindz Mar 27, 2026
a5c75d7
CONFIG_MBEDTLS_SHA1_C check
netmindz Mar 27, 2026
7df1242
build only esp32s3_matter_wifi
netmindz Mar 27, 2026
6939821
Add CMake files
netmindz Mar 27, 2026
217eb4b
upgrade platformio
netmindz Mar 27, 2026
201ee39
no dmx input
netmindz Mar 27, 2026
f0e02e6
Add fix_nodelist.py to prevent crashes in dual-framework builds and u…
netmindz Mar 27, 2026
359eb54
do not keep prompting if there was an error, e.g no wifi
netmindz Mar 28, 2026
b3b0754
rename to matter_over_wifi
netmindz Mar 29, 2026
4e5391d
Move matter_over_wifi specifc scripts to usermod
netmindz Mar 29, 2026
4f4983c
Fix CI build: run generate_embed_files.py as post: script so managed_…
netmindz Mar 29, 2026
11670d4
Merge branch 'V5' into matter-over-wifi
netmindz Mar 29, 2026
5d513f8
platformio.ini optimizations (matter buildenv)
softhack007 Mar 31, 2026
ab6770a
Add esp32s3_matter_wifi to default buildenvs (experimental)
softhack007 Mar 31, 2026
ed60de5
remove sdkconfig.esp32s3_matter_wifi (auto-generated)
softhack007 Mar 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.16.0)
set(EXECUTABLE_COMPONENT_NAME "wled00")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)

# ── Matter / GCC 14 compatibility fixups ──────────────────────────────────────
# The CHIP SDK's TypeTraits.h has a C++23 branch with invalid code:
# using to_underlying = std::to_underlying; (type alias to function template)
# Downgrade from gnu++2b to gnu++20 so the correct #else polyfill is used.
# Guard: only apply when the Matter usermod is present.
if(EXISTS "${CMAKE_SOURCE_DIR}/usermods/usermod_v2_matter")
idf_build_replace_option_from_property(CXX_COMPILE_OPTIONS
"-std=gnu++2b" "-std=gnu++20")
endif()

project(WLED)
9 changes: 8 additions & 1 deletion pio-scripts/dynarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,20 @@ def inject_before_marker(path, marker):


if env.get("PIOPLATFORM") == "espressif32":
# Find sections.ld on the linker search path (LIBPATH).
# Find sections.ld on the linker search path (LIBPATH), or fall back to
# BUILD_DIR (the dual-framework espidf+arduino build generates sections.ld
# directly in BUILD_DIR and passes it as a bare filename in LINKFLAGS).
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 None:
build_dir = Path(env.subst("$BUILD_DIR"))
candidate = build_dir / "sections.ld"
if candidate.exists():
sections_ld_path = candidate
Comment on lines +34 to +38
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the fix needed for when doing an espidf+arduino build @willmmiles

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweet! Thanks for the tip.


if sections_ld_path is not None:
# Inject inside the existing .flash.rodata output section, just before
Expand Down
125 changes: 125 additions & 0 deletions pio-scripts/fix_nodelist.py
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clever. I wasn't able to get a good solution last time I tried to (get the AI to) dig in to this. I guess we're getting it used to monkey-patching platformio now ... ;)

We might want to consider offering this back up to pioarduino; there's another complex middleware workaround in the framework build scripts. Really I'm coming to believe that platformio's middleware API is a minefield...

Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
Import("env")
# ── fix_nodelist.py ───────────────────────────────────────────────────────────
# Pre-build monkey-patch that prevents a clean-build crash in dual-framework
# (arduino + espidf) PlatformIO builds.
#
# Root cause
# ----------
# platformio/builder/tools/piobuild.py::CollectBuildFiles() calls
# env.File(os.path.join(_var_dir, os.path.basename(item)))
# for each matched source file. When multiple VariantDir mappings overlap
# (which happens with libraries that have sub-directories such as the
# Arduino WebServer library's detail/, middleware/, uri/ sub-trees), SCons
# can return a NodeList instead of a single Node.
#
# Additionally, middlewares such as arduino.py's smart_include_length_shorten
# call env.Object(node) which also returns a NodeList (SCons Object builder
# returns a list). When the next middleware (_skip_prj_source_files from
# espidf.py) receives a NodeList, it calls node.srcnode().get_path() which
# fails with:
# AttributeError: 'NodeList' object has no attribute 'srcnode'
# (or produces a wrong result when NodeList.__getattr__ proxies the call).
#
# Fix
# ---
# We monkey-patch CollectBuildFiles to:
# 1. Flatten NodeList items produced by env.File() BEFORE the middleware loop.
# 2. Flatten NodeList values returned BY middleware callbacks during the loop.
# When a callback returns a NodeList, all elements pass the remaining
# middlewares individually.
# ─────────────────────────────────────────────────────────────────────────────

import fnmatch as _fnmatch
import os as _os

from platformio.builder.tools import piobuild as _piobuild
from platformio.builder.tools.piobuild import SRC_BUILD_EXT


def _is_nodelist(node):
"""Return True if node is a SCons NodeList (a UserList subclass that
proxies attribute access but does not have get_path() directly)."""
return hasattr(node, 'data') and not hasattr(node, 'get_path')
Comment on lines +39 to +42
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cd /dev/null && find . -name "fix_nodelist.py" -type f 2>/dev/null | head -5

Repository: wled/WLED

Length of output: 102


🏁 Script executed:

git ls-files | grep -i nodelist

Repository: wled/WLED

Length of output: 80


🏁 Script executed:

git ls-files "pio-scripts/"

Repository: wled/WLED

Length of output: 440


🏁 Script executed:

find . -name "fix_nodelist.py" -o -name "*nodelist*" 2>/dev/null | head -20

Repository: wled/WLED

Length of output: 82


🏁 Script executed:

git ls-files | head -20

Repository: wled/WLED

Length of output: 561


🏁 Script executed:

find . -type f -name "*.py" | grep -i node | head -10

Repository: wled/WLED

Length of output: 82


🏁 Script executed:

cat -n pio-scripts/fix_nodelist.py 2>/dev/null || find . -type f -name "fix_nodelist.py" -exec cat -n {} \;

Repository: wled/WLED

Length of output: 5889


🌐 Web query:

SCons NodeList __getattr__ attribute proxy behavior

💡 Result:

The SCons NodeList class (from SCons.Util.NodeList, subclass of UserList) implements attribute proxy behavior via its getattr method. When an attribute is accessed on a NodeList instance that it does not have locally, getattr(name) is called. This method retrieves the specified attribute from every item in the list using getattr(x, name) for each x in self.data, collects these values into a new list, and returns a new NodeList containing those results. This enables uniform attribute access across all elements, such as calling methods or accessing properties on all nodes simultaneously. Example from official documentation: someList = NodeList([' foo ', ' bar ']) someList.strip # Returns NodeList(['foo', 'bar']) The implementation ensures the result is another NodeList, preserving the proxy behavior recursively. It also supports call(*args, kwargs) by applying the call to each item and returning a NodeList of results. This behavior is consistent across SCons versions, including up to 4.10.1, as shown in API docs and source snippets.

Citations:


🏁 Script executed:

python3 << 'EOF'
# Check SCons NodeList implementation details
try:
    from SCons.Util import NodeList
    import inspect
    
    print("=== NodeList class info ===")
    print(f"NodeList base classes: {NodeList.__bases__}")
    print(f"\nNodeList has __getattr__: {hasattr(NodeList, '__getattr__')}")
    print(f"NodeList has get_path: {hasattr(NodeList, 'get_path')}")
    
    # Check if it's a UserList subclass
    from collections import UserList
    print(f"\nNodeList is UserList subclass: {issubclass(NodeList, UserList)}")
    
    # Check the MRO
    print(f"\nMethod Resolution Order:")
    for cls in NodeList.__mro__:
        print(f"  - {cls}")
        
except ImportError as e:
    print(f"SCons not available: {e}")
EOF

Repository: wled/WLED

Length of output: 97


🏁 Script executed:

rg "SCons" pio-scripts/ --type py -n

Repository: wled/WLED

Length of output: 1013


🏁 Script executed:

rg "NodeList" . --type py -n

Repository: wled/WLED

Length of output: 1572


_is_nodelist() detection is unreliable due to SCons NodeList's attribute proxy behavior.

When hasattr() checks for get_path(), it triggers the NodeList's __getattr__ proxy, which collects the attribute from all contained nodes and returns it. If any node in the list has get_path(), the check fails and the NodeList goes undetected, reaching line 62 where srcnode().get_path() still causes the AttributeError this patch aims to prevent.

Use isinstance() with the actual NodeList type for reliable detection:

Recommended fix
+from SCons.Util import NodeList as _SConsNodeList
+
 def _is_nodelist(node):
-    """Return True if node is a SCons NodeList (a UserList subclass that
-    proxies attribute access but does not have get_path() directly)."""
-    return hasattr(node, 'data') and not hasattr(node, 'get_path')
+    """Return True if node is a SCons NodeList."""
+    return isinstance(node, _SConsNodeList)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _is_nodelist(node):
"""Return True if node is a SCons NodeList (a UserList subclass that
proxies attribute access but does not have get_path() directly)."""
return hasattr(node, 'data') and not hasattr(node, 'get_path')
from SCons.Util import NodeList as _SConsNodeList
def _is_nodelist(node):
"""Return True if node is a SCons NodeList."""
return isinstance(node, _SConsNodeList)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pio-scripts/fix_nodelist.py` around lines 39 - 42, The _is_nodelist detection
currently uses hasattr(node, 'get_path') which invokes NodeList's attribute
proxy and yields false negatives; change _is_nodelist to use isinstance(node,
NodeList) for reliable detection by importing the real NodeList type (e.g.
import NodeList from SCons.Node or the appropriate SCons module) and return
isinstance(node, NodeList) (keep the existing check for 'data' if still needed),
so the function uses the concrete NodeList class instead of attribute probing.



def _iter_nodes(node):
"""Yield individual SCons nodes from node, recursively unwrapping NodeList."""
if _is_nodelist(node):
for child in node.data:
yield from _iter_nodes(child)
else:
yield node


def _run_middlewares(env, node, middlewares):
"""Run all middlewares on a single node. Returns a list of result nodes
(empty if the node was filtered out, >1 if a middleware returned a NodeList).
"""
current_nodes = [node]
for callback, pattern in middlewares:
next_nodes = []
for n in current_nodes:
if pattern and not _fnmatch.fnmatch(n.srcnode().get_path(), pattern):
next_nodes.append(n) # pattern didn't match – pass through unchanged
continue
if callback.__code__.co_argcount == 2:
result = callback(env, n)
else:
result = callback(n)
if result is None:
pass # filtered out
elif _is_nodelist(result):
next_nodes.extend(_iter_nodes(result))
else:
next_nodes.append(result)
current_nodes = next_nodes
if not current_nodes:
break # all nodes filtered out
return current_nodes


def _patched_CollectBuildFiles(env, variant_dir, src_dir, src_filter=None, duplicate=False):
"""Drop-in replacement for piobuild.CollectBuildFiles that handles NodeList
nodes both from env.File() and from middleware callbacks."""
from platformio.builder.tools.piobuild import SRC_BUILD_EXT as _SRC_BUILD_EXT

sources = []
variants = []

src_dir = env.subst(src_dir)
if src_dir.endswith(_os.sep):
src_dir = src_dir[:-1]

for item in env.MatchSourceFiles(src_dir, src_filter, _SRC_BUILD_EXT):
_reldir = _os.path.dirname(item)
_src_dir = _os.path.join(src_dir, _reldir) if _reldir else src_dir
_var_dir = _os.path.join(variant_dir, _reldir) if _reldir else variant_dir

if _var_dir not in variants:
variants.append(_var_dir)
env.VariantDir(_var_dir, _src_dir, duplicate)

raw_node = env.File(_os.path.join(_var_dir, _os.path.basename(item)))
# Flatten: a NodeList becomes individual nodes; a plain Node stays as-is
for n in _iter_nodes(raw_node):
sources.append(n)

middlewares = env.get("__PIO_BUILD_MIDDLEWARES")
if not middlewares:
return sources

new_sources = []
for node in sources:
for result_node in _run_middlewares(env, node, middlewares):
new_sources.append(result_node)

return new_sources


# Replace the function in the piobuild module so all subsequent callers
# (espidf.py, etc.) pick up the patched version.
_piobuild.CollectBuildFiles = _patched_CollectBuildFiles
# Also replace the env-bound method so env.CollectBuildFiles() uses our version.
env.AddMethod(_patched_CollectBuildFiles, "CollectBuildFiles")

print("[fix_nodelist] CollectBuildFiles patched – NodeList flattening enabled")
96 changes: 96 additions & 0 deletions pio-scripts/generate_embed_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
Import("env")
from pathlib import Path

# ── generate_embed_files.py ───────────────────────────────────────────────────
# Pre-build script for the esp32s3_matter_wifi environment.
#
# Generates .S assembly files for binary data that ESP-IDF components
# expect to be embedded via CMake's target_add_binary_data().
# PlatformIO's SCons build doesn't execute those CMake commands,
# so we replicate the output here.
#
# The .S files are generated at BUILD_DIR/<name>.S – exactly the path that
# CMake's project_description.json records as the source path for these
# generated files. espidf.py's compile_source_files() will then find and
# compile them correctly.
#
# DO NOT place them in a sub-directory: the CMake code model records relative
# paths (e.g. ".pio/build/esp32s3_matter_wifi/https_server.crt.S") and
# espidf.py resolves those relative to BUILD_DIR.
# ─────────────────────────────────────────────────────────────────────────────

project_dir = Path(env["PROJECT_DIR"]).resolve()
build_dir = Path(env.subst("$BUILD_DIR")).resolve()

# Map of (source cert path, symbol name) pairs that need embedding.
# These correspond to all target_add_binary_data() calls in the managed
# components' CMakeLists.txt files (which PlatformIO's SCons build skips).
EMBED_FILES = [
(
project_dir / "managed_components" / "espressif__esp_insights" / "server_certs" / "https_server.crt",
"https_server_crt",
),
(
project_dir / "managed_components" / "espressif__esp_insights" / "server_certs" / "mqtt_server.crt",
"mqtt_server_crt",
),
(
project_dir / "managed_components" / "espressif__esp_rainmaker" / "server_certs" / "rmaker_mqtt_server.crt",
"rmaker_mqtt_server_crt",
),
(
project_dir / "managed_components" / "espressif__esp_rainmaker" / "server_certs" / "rmaker_claim_service_server.crt",
"rmaker_claim_service_server_crt",
),
(
project_dir / "managed_components" / "espressif__esp_rainmaker" / "server_certs" / "rmaker_ota_server.crt",
"rmaker_ota_server_crt",
),
]


def generate_asm(src_path: Path, symbol: str, out_dir: Path):
"""Generate an assembly .S file that embeds binary data, matching the
format produced by ESP-IDF's ``target_add_binary_data()``."""
if not src_path.exists():
print(f" [embed] WARNING: {src_path} not found – skipping")
return None

out_file = out_dir / f"{src_path.name}.S"
data = src_path.read_bytes()

lines = [
f"/* Data converted from {src_path} */",
".data",
"#if !defined (__APPLE__) && !defined (__linux__)",
".section .rodata.embedded",
"#endif",
"",
f".global {symbol}",
f"{symbol}:",
"",
f".global _binary_{symbol}_start",
f"_binary_{symbol}_start: /* for objcopy compatibility */",
]

for i in range(0, len(data), 16):
chunk = data[i : i + 16]
hexvals = ", ".join(f"0x{b:02x}" for b in chunk)
lines.append(f".byte {hexvals}")

lines += [
"",
f".global _binary_{symbol}_end",
f"_binary_{symbol}_end: /* for objcopy compatibility */",
".byte 0x00", # null terminator for TEXT mode
]

out_dir.mkdir(parents=True, exist_ok=True)
out_file.write_text("\n".join(lines) + "\n")
print(f" [embed] Generated {out_file} ({len(data)} bytes)")
return str(out_file)


for src_path, symbol in EMBED_FILES:
generate_asm(src_path, symbol, build_dir)

34 changes: 34 additions & 0 deletions pio-scripts/setup_matter_component.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
Import("env")
import shutil
from pathlib import Path
from SCons.Script import Exit

# ── setup_matter_component.py ─────────────────────────────────────────────────
# Pre-build script for the esp32s3_matter_wifi environment.
#
# Copies the idf_component.yml manifest from the Matter usermod into wled00/
# so that pioarduino's built-in ComponentManager (and the IDF Component Manager)
# can resolve the espressif/esp_matter dependency.
#
# The destination file is listed in .gitignore and must NOT be committed.
# ─────────────────────────────────────────────────────────────────────────────

project_dir = Path(env["PROJECT_DIR"]).resolve()
src_yml = project_dir / "usermods" / "usermod_v2_matter" / "idf_component.yml"
dst_yml = project_dir / "wled00" / "idf_component.yml"

if not src_yml.exists():
print(
"\033[0;31;43m"
f"Matter: idf_component.yml not found at {src_yml} – "
"cannot resolve the esp_matter component."
"\033[0m"
)
Exit(1)

shutil.copy2(str(src_yml), str(dst_yml))
print(
"\033[6;33;42m"
"Matter: copied idf_component.yml → wled00/idf_component.yml"
"\033[0m"
)
Loading
Loading