Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions pio-scripts/load_usermods.py
Copy link
Member

@softhack007 softhack007 Nov 14, 2025

Choose a reason for hiding this comment

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

Not sure if changing this script is necessary to integrate your usermod?
Generally, a modification / improvement for the new usermods framework should better be a separate PR, instead of mixing unrelated changes in one PR.

Copy link
Collaborator

@DedeHai DedeHai Nov 14, 2025

Choose a reason for hiding this comment

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

I suspect that AI was involved... a lot of things in this PR do not make sense to me. Anyway, it needs to go to user_fx, I will not accept any UMs that are just an effect.
That is exactly why we created the user_fx usermod and a lot of effort went into documenting it (I only did reviews)

Copy link
Member

Choose a reason for hiding this comment

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

I fully agree with your position 👍

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from SCons.Script import Exit
from platformio.builder.tools.piolib import LibBuilderBase

usermod_dir = Path(env["PROJECT_DIR"]).resolve() / "usermods"
project_dir = Path(env["PROJECT_DIR"]).resolve()
usermod_dir = (project_dir / "usermods").resolve()

# Utility functions
def find_usermod(mod: str) -> Path:
Expand All @@ -28,7 +29,8 @@ def find_usermod(mod: str) -> Path:
def is_wled_module(dep: LibBuilderBase) -> bool:
"""Returns true if the specified library is a wled module
"""
return usermod_dir in Path(dep.src_dir).parents or str(dep.name).startswith("wled-")
dep_root = Path(dep.src_dir).resolve()
return usermod_dir in dep_root.parents or str(dep.name).startswith("wled-")

## Script starts here
# Process usermod option
Expand Down
107 changes: 107 additions & 0 deletions usermods/chase_race/chase_race_usermod.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#include "wled.h"

#ifdef USERMOD_CHASE_RACE

/*
* Chase Race Usermod
* ------------------
* Registers a dedicated usermod so we can keep the custom "Chase Race" effect
* separate from WLED core code. The usermod exists purely to register the
* effect with strip.addEffect(); all race logic lives in mode_chase_race().
*
* Slider/Color mapping:
* Speed -> Pace (advance interval)
* Intensity -> Car length (LEDs per car)
* Custom1 -> Gap size (blank pixels between cars)
* Colors -> Car 1 / Car 2 / Car 3 body colors
*/

namespace {

// Metadata string that drives the UI and JSON description of the new effect.
// Layout:
// Speed slider -> "Pace" (updates per second)
// Intensity -> "Car length"
// Custom1 -> "Gap size"
// Custom2 -> unused (reserved)
// Colors -> Car 1, Car 2, Car 3
static const char _data_FX_MODE_CHASE_RACE[] PROGMEM =
"Chase Race@Pace,Car length,Gap size,,,;Car 1,Car 2,Car 3;;";

static uint16_t mode_chase_race(void) {
const uint16_t segLen = SEGLEN;
if (!segLen) return FRAMETIME;

if (SEGENV.call == 0) {
SEGENV.step = strip.now;
SEGENV.aux0 = 0; // head position
}

const uint16_t minInterval = 10;
const uint16_t maxInterval = 180;
const uint16_t interval = maxInterval - ((maxInterval - minInterval) * SEGMENT.speed / 255);

if (strip.now - SEGENV.step >= interval) {
SEGENV.step = strip.now;
SEGENV.aux0 = (SEGENV.aux0 + 1) % segLen;
}

// Ensure there is room for each car plus at least 1 blank LED when possible.
uint16_t carLenMax = 1;
if (segLen > 3) {
carLenMax = max<uint16_t>(1, (segLen - 3) / 3);
}
carLenMax = max<uint16_t>(1, min<uint16_t>(carLenMax, segLen)); // safety for tiny segments

uint16_t carLen = map(SEGMENT.intensity, 0, 255, 1, carLenMax);
carLen = max<uint16_t>(1, min<uint16_t>(carLen, carLenMax));

uint16_t maxGap = 0;
if (segLen > carLen * 3) {
maxGap = (segLen - (carLen * 3)) / 3;
}

uint16_t desiredGap = map(SEGMENT.custom1, 0, 255, 1, max<uint16_t>(1, segLen / 3));
uint16_t gapLen = (maxGap == 0)
? 0
: max<uint16_t>(1, min<uint16_t>(desiredGap, maxGap));
Copy link
Member

@softhack007 softhack007 Nov 15, 2025

Choose a reason for hiding this comment

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

Please read our coding style guideline, instead of being an artist with simple conditional assignments.


const uint16_t spacing = carLen + gapLen;
const uint16_t origin = SEGENV.aux0 % segLen;

SEGMENT.fill(BLACK);

auto drawCar = [&](uint16_t start, uint32_t color) {
Copy link
Member

Choose a reason for hiding this comment

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

@coderabbitai please explain this part of the code - is it a c++ extension? Will it compile on all supported platforms?

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check platformio.ini for C++ standard flags
echo "=== Checking C++ standard in platformio.ini ==="
if [ -f platformio.ini ]; then
  grep -i "std=" platformio.ini | head -20
else
  echo "platformio.ini not found"
fi

echo ""
echo "=== Checking for existing lambda usage in WLED codebase ==="
# Search for lambda expressions in the codebase (looking for [capture] patterns followed by parameters)
rg -n '\[\s*[&=,\w]*\s*\]\s*\([^)]*\)\s*(\{|->)' --type=cpp -g '!usermods/chase_race/*' | head -30

echo ""
echo "=== Checking build flags for C++ version ==="
if [ -f platformio.ini ]; then
  grep -i "build_flags" platformio.ini | head -10
fi

Length of output: 3743


@softhack007, great question! This is not a C++ extension—it's standard C++11 lambda function syntax, and yes, it will compile on all supported platforms.

What this syntax means:

auto drawCar = [&](uint16_t start, uint32_t color) {

This declares a lambda (anonymous function) with:

  • auto - type deduction (the compiler infers this is a function type)
  • [&] - capture clause that captures all local variables by reference
  • (uint16_t start, uint32_t color) - parameters the lambda accepts
  • The body follows in curly braces

Already used extensively in WLED:

Lambda functions are already used throughout the WLED codebase. For example, wled00/wled_server.cpp has 30+ lambda expressions for HTTP handlers:

server.on(F("/version"), HTTP_GET, [](AsyncWebServerRequest *request){
  // handler code
});

Platform compatibility:

  • ESP8266: Uses Arduino Core for ESP8266 with GCC 10+ (supports C++11/14/17)
  • ESP32 variants (ESP32, ESP32-S2, ESP32-S3, ESP32-C3): Use ESP-IDF with GCC 8+ (full C++11/14/17 support)

Lambda expressions have been standard since C++11 (released in 2011) and are fully supported by all modern compilers used for ESP platforms. This code will compile without any issues on all WLED-supported platforms. ✅


🧠 Learnings used
Learnt from: netmindz
Repo: wled/WLED PR: 4728
File: wled00/FX.h:378-378
Timestamp: 2025-06-15T09:59:52.720Z
Learning: In WLED's FX.h, MODE_COUNT represents the highest FX_MODE_ ID + 1, not the total count of FX_MODE_ definitions. This is because some effects may have been removed but their IDs are not reused, creating gaps in the numbering sequence. MODE_COUNT is used for array bounds and range checking.

Learnt from: netmindz
Repo: wled/WLED PR: 4728
File: wled00/FX.h:378-378
Timestamp: 2025-06-15T09:59:52.720Z
Learning: In WLED's FX.h, MODE_COUNT represents the highest FX_MODE_ ID + 1, not the total count of FX_MODE_ definitions. This is because some effects may have been removed but their IDs are not reused, creating gaps in the numbering sequence. MODE_COUNT is used for array bounds and range checking.

Learnt from: freakintoddles2
Repo: wled/WLED PR: 4904
File: wled00/FX.cpp:3915-3930
Timestamp: 2025-09-02T19:42:57.475Z
Learning: WLED wled00/FX.cpp::mode_percent — For time-based options (% of Minute/Hour/Day), behavior is clock-like: at rollover (elapsed==0) intentionally show SEGLEN lit for one tick to represent completion, then move to 1 LED at the next second. This is by design (not a progress bar) and should not be “fixed”.

Learnt from: BobLoeffler68
Repo: wled/WLED PR: 4891
File: wled00/FX.cpp:3333-3349
Timestamp: 2025-08-31T03:38:14.114Z
Learning: WLED PacMan effect (wled00/FX.cpp): Keep pacmancharacters_t position fields as signed int (not int16_t). Maintainer preference (blazoncek) prioritizes avoiding potential overhead/regressions over minor RAM savings. Avoid type shrinking here unless memory pressure is demonstrated.

Learnt from: DedeHai
Repo: wled/WLED PR: 4798
File: wled00/FX.cpp:7531-7533
Timestamp: 2025-08-26T11:51:21.817Z
Learning: In WLED PR #4798, DedeHai confirmed that certain gamma-related calls in FX.cpp/FX_fcn.cpp/particle systems are intentional for effect-level shaping (e.g., brightness curves, TV sim, Pride 2015 pre-mix), distinct from final output gamma. Do not flag or remove these in future reviews; add comments when feasible to clarify intent.

Copy link
Member

@softhack007 softhack007 Nov 15, 2025

Choose a reason for hiding this comment

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

@coderabbitai Can the code be simplified to avoid the lambda?

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the full implementation of the mode_chase_race function to see how the lambda is used
cat usermods/chase_race/chase_race_usermod.cpp

Length of output: 3234


@softhack007, yes the lambda could be removed, but it would actually make the code longer and more repetitive without any real benefit.

Current implementation (with lambda):

9 lines total - lambda definition (6 lines) + 3 calls (3 lines)

Alternative 1: Inline expansion (without lambda):

// Car 1
if (SEGCOLOR(2)) {
  for (uint16_t i = 0; i < carLen && i < segLen; i++) {
    uint16_t idx = (origin + i) % segLen;
    SEGMENT.setPixelColor(idx, SEGCOLOR(2));
  }
}
// Car 2
if (SEGCOLOR(1)) {
  for (uint16_t i = 0; i < carLen && i < segLen; i++) {
    uint16_t idx = ((origin + spacing) % segLen + i) % segLen;
    SEGMENT.setPixelColor(idx, SEGCOLOR(1));
  }
}
// Car 3
if (SEGCOLOR(0)) {
  for (uint16_t i = 0; i < carLen && i < segLen; i++) {
    uint16_t idx = ((origin + (2 * spacing)) % segLen + i) % segLen;
    SEGMENT.setPixelColor(idx, SEGCOLOR(0));
  }
}

~18 lines - duplicates the loop logic 3 times, harder to maintain

Alternative 2: Static helper function:

Would need to pass carLen and segLen as parameters, and define it outside the function scope. Similar complexity to the lambda but without the convenience of auto-capture.

Recommendation:

Keep the lambda ✅ because:

  1. Avoids code duplication - the drawing logic only appears once
  2. More maintainable - changes only need to be made in one place
  3. Cleaner - auto-captures local variables without parameter passing
  4. Standard C++ - lambdas are C++11 and already used throughout WLED (30+ instances in wled_server.cpp alone)
  5. Shorter - 9 lines vs 18+ lines for inline expansion

The lambda is actually the most elegant solution here for drawing three similar elements with different positions and colors. 👍


🧠 Learnings used
Learnt from: netmindz
Repo: wled/WLED PR: 4728
File: wled00/FX.h:378-378
Timestamp: 2025-06-15T09:59:52.720Z
Learning: In WLED's FX.h, MODE_COUNT represents the highest FX_MODE_ ID + 1, not the total count of FX_MODE_ definitions. This is because some effects may have been removed but their IDs are not reused, creating gaps in the numbering sequence. MODE_COUNT is used for array bounds and range checking.

Learnt from: netmindz
Repo: wled/WLED PR: 4728
File: wled00/FX.h:378-378
Timestamp: 2025-06-15T09:59:52.720Z
Learning: In WLED's FX.h, MODE_COUNT represents the highest FX_MODE_ ID + 1, not the total count of FX_MODE_ definitions. This is because some effects may have been removed but their IDs are not reused, creating gaps in the numbering sequence. MODE_COUNT is used for array bounds and range checking.

Learnt from: freakintoddles2
Repo: wled/WLED PR: 4904
File: wled00/FX.cpp:3915-3930
Timestamp: 2025-09-02T19:42:57.475Z
Learning: WLED wled00/FX.cpp::mode_percent — For time-based options (% of Minute/Hour/Day), behavior is clock-like: at rollover (elapsed==0) intentionally show SEGLEN lit for one tick to represent completion, then move to 1 LED at the next second. This is by design (not a progress bar) and should not be “fixed”.

Learnt from: BobLoeffler68
Repo: wled/WLED PR: 4891
File: wled00/FX.cpp:3333-3349
Timestamp: 2025-08-31T03:38:14.114Z
Learning: WLED PacMan effect (wled00/FX.cpp): Keep pacmancharacters_t position fields as signed int (not int16_t). Maintainer preference (blazoncek) prioritizes avoiding potential overhead/regressions over minor RAM savings. Avoid type shrinking here unless memory pressure is demonstrated.

Learnt from: DedeHai
Repo: wled/WLED PR: 4798
File: wled00/FX.cpp:7531-7533
Timestamp: 2025-08-26T11:51:21.817Z
Learning: In WLED PR #4798, DedeHai confirmed that certain gamma-related calls in FX.cpp/FX_fcn.cpp/particle systems are intentional for effect-level shaping (e.g., brightness curves, TV sim, Pride 2015 pre-mix), distinct from final output gamma. Do not flag or remove these in future reviews; add comments when feasible to clarify intent.

Learnt from: mval-sg
Repo: wled/WLED PR: 4876
File: wled00/xml.cpp:0-0
Timestamp: 2025-08-28T08:09:20.630Z
Learning: The WLED codebase has opportunities for refactoring hardcoded array bounds (like the "15" used for DMX channels) to use sizeof(array)/sizeof(array[0]) for more maintainable code, but such changes should be done consistently across the entire codebase in a dedicated refactoring effort.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-11-14T13:37:11.973Z
Learning: In WLED code reviews, when code is modified or added, look for "magic numbers" (hardcoded numeric literals) and suggest replacing them with appropriate defined constants when those constants are meaningful in the context of the PR. For example, the hardcoded value 32 should be replaced with WLED_MAX_SEGNAME_LEN when it represents a segment name length limit. This improves code maintainability and reduces the risk of inconsistencies.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-11-14T13:37:30.945Z
Learning: In WLED code reviews, when code is modified or added, look for "magic numbers" (hardcoded numeric literals) and suggest replacing them with defined constants when meaningful constants exist in the codebase. For example, suggest replacing hardcoded "32" with WLED_MAX_SEGNAME_LEN if the context relates to segment name length limits.

Copy link
Collaborator

@DedeHai DedeHai Nov 15, 2025

Choose a reason for hiding this comment

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

@softhack007 see #4566 (comment)
this is what an AI outputs when properly instructed ;)

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Member

@softhack007 softhack007 Nov 15, 2025

Choose a reason for hiding this comment

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

OMG 🤦 we entered the era of bloated AI code that has no purpose other than appearing "impressive" 🤣

... so here we have an auto-typed lambda function with capture clause, instead of a simple nested for loop, and AI says "well if webserver callback hooks can be done with lambda, why not replace a for loop with the same very elegant idea" 🤔

Copy link
Member

Choose a reason for hiding this comment

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

I'm still puzzled why a simple effect needs a backpack full of python code in load_usermod.py. Even audioreactive can be configured with a few lines of load_usermod.py 🤔

if (!color) return;
for (uint16_t i = 0; i < carLen && i < segLen; i++) {
uint16_t idx = (start + i) % segLen;
SEGMENT.setPixelColor(idx, color);
}
};

// SEGCOLOR indices are reversed relative to the UI (color slot 1 -> index 2)
drawCar(origin, SEGCOLOR(2)); // Car 1 color slot
drawCar((origin + spacing) % segLen, SEGCOLOR(1)); // Car 2
drawCar((origin + (2 * spacing)) % segLen, SEGCOLOR(0)); // Car 3

return FRAMETIME;
}

} // namespace

class ChaseRaceUsermod : public Usermod {
public:
void setup() override {
strip.addEffect(255, &mode_chase_race, _data_FX_MODE_CHASE_RACE);
}

void loop() override {}

uint16_t getId() override { return USERMOD_ID_CHASE_RACE; }

};

static ChaseRaceUsermod chaseRace;
REGISTER_USERMOD(chaseRace);

#endif // USERMOD_CHASE_RACE
7 changes: 7 additions & 0 deletions usermods/chase_race/library.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "chase_race",
"version": "0.1.0",
"description": "Scaffolding for the Chase Race WLED effect usermod.",
"build": { "libArchive": false },
"dependencies": {}
}
109 changes: 109 additions & 0 deletions usermods/chase_race/load_usermods.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
Import('env')
from collections import deque
from pathlib import Path # For OS-agnostic path manipulation
from click import secho
from SCons.Script import Exit
from platformio.builder.tools.piolib import LibBuilderBase

project_dir = Path(env["PROJECT_DIR"]).resolve()
usermod_dir = (project_dir / "usermods").resolve()

# Utility functions
def find_usermod(mod: str) -> Path:
"""Locate this library in the usermods folder.
We do this to avoid needing to rename a bunch of folders;
this could be removed later
"""
# Check name match
mp = usermod_dir / mod
if mp.exists():
return mp
mp = usermod_dir / f"{mod}_v2"
if mp.exists():
return mp
mp = usermod_dir / f"usermod_v2_{mod}"
if mp.exists():
return mp
raise RuntimeError(f"Couldn't locate module {mod} in usermods directory!")

def is_wled_module(dep: LibBuilderBase) -> bool:
"""Returns true if the specified library is a wled module
"""
dep_root = Path(dep.src_dir).resolve()
return usermod_dir in dep_root.parents or str(dep.name).startswith("wled-")

## Script starts here
# Process usermod option
usermods = env.GetProjectOption("custom_usermods","")

# Handle "all usermods" case
if usermods == '*':
usermods = [f.name for f in usermod_dir.iterdir() if f.is_dir() and f.joinpath('library.json').exists()]
else:
usermods = usermods.split()

if usermods:
# Inject usermods in to project lib_deps
symlinks = [f"symlink://{find_usermod(mod).resolve()}" for mod in usermods]
env.GetProjectConfig().set("env:" + env['PIOENV'], 'lib_deps', env.GetProjectOption('lib_deps') + symlinks)

# Utility function for assembling usermod include paths
def cached_add_includes(dep, dep_cache: set, includes: deque):
""" Add dep's include paths to includes if it's not in the cache """
if dep not in dep_cache:
dep_cache.add(dep)
for include in dep.get_include_dirs():
if include not in includes:
includes.appendleft(include)
if usermod_dir not in Path(dep.src_dir).parents:
# Recurse, but only for NON-usermods
for subdep in dep.depbuilders:
cached_add_includes(subdep, dep_cache, includes)

# Monkey-patch ConfigureProjectLibBuilder to mark up the dependencies
# Save the old value
old_ConfigureProjectLibBuilder = env.ConfigureProjectLibBuilder

# Our new wrapper
def wrapped_ConfigureProjectLibBuilder(xenv):
# Call the wrapped function
result = old_ConfigureProjectLibBuilder.clone(xenv)()

Comment on lines +68 to +71
Copy link
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

Fix ConfigureProjectLibBuilder wrapper: .clone(xenv)() will likely break builds

old_ConfigureProjectLibBuilder is a bound SCons/PlatformIO method, not an object with a .clone method. Calling old_ConfigureProjectLibBuilder.clone(xenv)() will almost certainly raise AttributeError at build time when PlatformIO calls env.ConfigureProjectLibBuilder().

PlatformIO’s usual monkey‑patch pattern is to save the old bound method and call it with no arguments (the env is already bound), e.g. project = oldLibBuilder().

Recommend changing the wrapper to invoke the original directly:

-def wrapped_ConfigureProjectLibBuilder(xenv):
-  # Call the wrapped function
-  result = old_ConfigureProjectLibBuilder.clone(xenv)()
+def wrapped_ConfigureProjectLibBuilder(xenv):
+  # Call the original ConfigureProjectLibBuilder to resolve dependencies
+  result = old_ConfigureProjectLibBuilder()

The rest of the function (using xenv for PROJECT_SRC_DIR, setting WLED_MODULES, etc.) can stay as is.

Please re‑run a few representative pio run environments (including ones without custom_usermods) after applying this change to confirm the wrapper behaves correctly with your PlatformIO version.


I'll now verify the review comment by inspecting the codebase and checking PlatformIO documentation.

[/run_scripts]

[/web_search]


🏁 Script executed:

#!/bin/bash
# Find and inspect the file in question
find . -name "load_usermods.py" -type f 2>/dev/null | head -5

Length of output: 69


🏁 Script executed:

#!/bin/bash
# Search for ConfigureProjectLibBuilder usage patterns in the codebase
rg -n "ConfigureProjectLibBuilder" -C 3 --type py

Length of output: 3106


🏁 Script executed:

#!/bin/bash
# Look for any clone() method usage in Python files to understand context
rg -n "\.clone\(" --type py -C 2 | head -40

Length of output: 678


🌐 Web query:

PlatformIO ConfigureProjectLibBuilder API documentation

💡 Result:

The ConfigureProjectLibBuilder API is implemented in PlatformIO Core; see:

  • Source code (function and related lib-builder classes) — platformio/builder/tools/piolib.py (PlatformIO Core repository). [1]
  • PlatformIO build/scripting documentation (Advanced Scripting / Build System API and Library Builder docs) — PlatformIO documentation. [2]

References

  1. platformio/platformio-core — platformio/builder/tools/piolib.py (ConfigureProjectLibBuilder implementation). [GitHub]. [1]
  2. PlatformIO — Advanced Scripting / Build System API and Library builder documentation. [Docs]. [2]

I need more specific information about how ConfigureProjectLibBuilder is typically called. Let me search for examples and SCons documentation.

[/web_search]


🌐 Web query:

SCons environment clone method Python

💡 Result:

env.Clone(...) returns a separate copy of an SCons Construction Environment; any keyword args you pass override values in the copy. It accepts the same kinds of construction-variable assignments as Environment(...) and also supports tools, toolpath, parse_flags, and (since 4.8.0) variables. Example:

env = Environment(CC='gcc')
opt = env.Clone(CCFLAGS='-O2') # opt is a separate Environment with CC='gcc' and CCFLAGS='-O2'
dbg = env.Clone(CCFLAGS='-g') # dbg is independent from env and opt

You can also pass tools or parse_flags:
env2 = env.Clone(tools=['msvc', MyTool], parse_flags='!wx-config --cflags')

Docs: SCons user guide and API reference. [1][2]

Sources:
[1] SCons User Guide — Making Copies of Construction Environments (env.Clone).
[2] SCons API / Functions and Environment Methods (env.Clone details).


🌐 Web query:

PlatformIO ConfigureProjectLibBuilder example usage call

💡 Result:

Short answer — call it from a PlatformIO extra Python build script (the SCons/PlatformIO env). Example:

  • In library.json (or platformio.ini) point to an extra script:
    {
    "build": { "extraScript": "extra_script.py" }
    }
  • extra_script.py:
    Import("env")
    project_lib_builder = env.ConfigureProjectLibBuilder()

    then use project_lib_builder as needed (PlatformIO's library builder will

    configure/install/search libs for the project during the build)

References: PlatformIO extraScript / builder docs and examples, and PlatformIO builder code showing env.ConfigureProjectLibBuilder() usage. [1][2][3]

Sources

  • PlatformIO library manifest / build.extraScript examples. [1]
  • PlatformIO scripting examples (extraScript usage). [2]
  • PlatformIO builder code / community traces showing env.ConfigureProjectLibBuilder() usage. [3]

[1] https://docs.platformio.org/en/latest/manifests/library-json/fields/build/builder.html
[2] https://docs.platformio.org/en/latest/scripting/examples/index.html
[3] Example traces / builder code references (platformio piolib ConfigureProjectLibBuilder).


Fix ConfigureProjectLibBuilder wrapper: .clone(xenv)() will raise AttributeError at runtime

SCons env.Clone() creates a copy of a Construction Environment, but it's a method on Environment objects, not on bound methods. When you save old_ConfigureProjectLibBuilder = env.ConfigureProjectLibBuilder, you capture a bound method (a Python method object), not an Environment. The correct pattern for calling ConfigureProjectLibBuilder is to invoke it with no arguments: project_lib_builder = env.ConfigureProjectLibBuilder().

Calling old_ConfigureProjectLibBuilder.clone(xenv)() attempts to call .clone() on a bound method, which lacks that attribute and will raise AttributeError when PlatformIO executes this code during a build.

Update lines 68–71 to call the saved method directly:

 def wrapped_ConfigureProjectLibBuilder(xenv):
   # Call the wrapped function
-  result = old_ConfigureProjectLibBuilder.clone(xenv)()
+  result = old_ConfigureProjectLibBuilder()

The xenv parameter remains available for subsequent use in the function (e.g., line 74: xenv["PROJECT_SRC_DIR"]).

📝 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 wrapped_ConfigureProjectLibBuilder(xenv):
# Call the wrapped function
result = old_ConfigureProjectLibBuilder.clone(xenv)()
def wrapped_ConfigureProjectLibBuilder(xenv):
# Call the wrapped function
result = old_ConfigureProjectLibBuilder()
🤖 Prompt for AI Agents
In usermods/chase_race/load_usermods.py around lines 68 to 71, the wrapper calls
old_ConfigureProjectLibBuilder.clone(xenv)() which will raise AttributeError
because old_ConfigureProjectLibBuilder is a bound method not an Environment;
replace that call by invoking the saved method directly (result =
old_ConfigureProjectLibBuilder()) so the wrapped ConfigureProjectLibBuilder runs
correctly and keep the xenv parameter available for later use in the function.

# Fix up include paths
# In PlatformIO >=6.1.17, this could be done prior to ConfigureProjectLibBuilder
wled_dir = xenv["PROJECT_SRC_DIR"]
# Build a list of dependency include dirs
# TODO: Find out if this is the order that PlatformIO/SCons puts them in??
processed_deps = set()
extra_include_dirs = deque() # Deque used for fast prepend
for dep in result.depbuilders:
cached_add_includes(dep, processed_deps, extra_include_dirs)

wled_deps = [dep for dep in result.depbuilders if is_wled_module(dep)]

broken_usermods = []
for dep in wled_deps:
# Add the wled folder to the include path
dep.env.PrependUnique(CPPPATH=str(wled_dir))
# Add WLED's own dependencies
for dir in extra_include_dirs:
dep.env.PrependUnique(CPPPATH=str(dir))
# Enforce that libArchive is not set; we must link them directly to the executable
if dep.lib_archive:
broken_usermods.append(dep)

if broken_usermods:
broken_usermods = [usermod.name for usermod in broken_usermods]
secho(
f"ERROR: libArchive=false is missing on usermod(s) {' '.join(broken_usermods)} -- modules will not compile in correctly",
fg="red",
err=True)
Exit(1)

# Save the depbuilders list for later validation
xenv.Replace(WLED_MODULES=wled_deps)

return result

# Apply the wrapper
env.AddMethod(wrapped_ConfigureProjectLibBuilder, "ConfigureProjectLibBuilder")
36 changes: 36 additions & 0 deletions usermods/chase_race/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Chase Race Usermod

This folder hosts the dedicated usermod for the **Chase Race** effect. The
effect renders three "cars" (solid color bars) that run nose-to-tail along the
active segment. Blank pixels between cars simulate the open road, and once the
lead car crosses the finish line the entire pack immediately wraps to the
start, creating an endless race loop.

Copy link
Member

@softhack007 softhack007 Nov 15, 2025

Choose a reason for hiding this comment

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

Can you add some short video or screenshots to explain your effect?
From the description it looks like just a very complicated way of moving three small bars around simultaneously? Maybe I'm wrong and the description is misleading.

## Files

- `chase_race_usermod.cpp` registers the usermod/effect and implements the race
logic.
- `library.json` keeps PlatformIO happy (`libArchive=false` ensures the code is
linked directly into the firmware).

## Building

1. Select the `chase_race` PlatformIO environment (already the default in the
workspace-level `platformio.ini`).
2. Compile/upload with `pio run -e chase_race` or from the IDE.

The environment enables the `USERMOD_CHASE_RACE` build flag and instructs
`pio-scripts/load_usermods.py` to include this folder automatically.

## Effect controls

- **Speed slider ("Pace")** - how fast the convoy advances.
- **Intensity slider ("Car length")** - LED length of each car.
- **Custom 1 slider ("Gap size")** - blank spacing between cars. If there
aren't enough pixels to honor the requested spacing, the usermod shrinks the
gaps automatically (down to zero when physically necessary).
- **Color slots ("Car 1 / Car 2 / Car 3")** - individual body colors. Using the
same color for multiple cars is perfectly fine.

Enable the Chase Race effect from the WLED UI or JSON API and it will animate
the currently selected segment using these controls.
1 change: 1 addition & 0 deletions wled00/const.h
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define USERMOD_ID_RF433 56 //Usermod "usermod_v2_RF433.h"
#define USERMOD_ID_BRIGHTNESS_FOLLOW_SUN 57 //Usermod "usermod_v2_brightness_follow_sun.h"
#define USERMOD_ID_USER_FX 58 //Usermod "user_fx"
#define USERMOD_ID_CHASE_RACE 59 //Usermod "chase_race_usermod.cpp"

//Access point behavior
#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot
Expand Down
Loading