-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Add Chase Race usermod effect #5085
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: 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:
Already used extensively in WLED:Lambda functions are already used throughout the WLED codebase. For example, server.on(F("/version"), HTTP_GET, [](AsyncWebServerRequest *request){
// handler code
});Platform compatibility:
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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @coderabbitai Can the code be simplified to avoid the lambda?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: 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 Recommendation:Keep the lambda ✅ because:
The lambda is actually the most elegant solution here for drawing three similar elements with different positions and colors. 👍 🧠 Learnings used
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @softhack007 see #4566 (comment)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" 🤔
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| 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": {} | ||
| } |
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainFix ConfigureProjectLibBuilder wrapper:
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. 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 Please re‑run a few representative 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 -5Length of output: 69 🏁 Script executed: #!/bin/bash
# Search for ConfigureProjectLibBuilder usage patterns in the codebase
rg -n "ConfigureProjectLibBuilder" -C 3 --type pyLength 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 -40Length of output: 678 🌐 Web query: 💡 Result: The ConfigureProjectLibBuilder API is implemented in PlatformIO Core; see:
References
I need more specific information about how ConfigureProjectLibBuilder is typically called. Let me search for examples and SCons documentation. [/web_search] 🌐 Web query: 💡 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') You can also pass tools or parse_flags: Docs: SCons user guide and API reference. [1][2] Sources: 🌐 Web query: 💡 Result: Short answer — call it from a PlatformIO extra Python build script (the SCons/PlatformIO env). Example:
References: PlatformIO extraScript / builder docs and examples, and PlatformIO builder code showing env.ConfigureProjectLibBuilder() usage. [1][2][3] Sources
[1] https://docs.platformio.org/en/latest/manifests/library-json/fields/build/builder.html Fix ConfigureProjectLibBuilder wrapper: 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 Calling 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 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| # 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") | ||||||||||||||||
| 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. | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add some short video or screenshots to explain your effect? |
||
| ## 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. | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 👍