Skip to content

Usermods on Steroids: A Plugin Framework#5319

Open
JoaDick wants to merge 4 commits intowled:mainfrom
JoaDick:squash/Usermod
Open

Usermods on Steroids: A Plugin Framework#5319
JoaDick wants to merge 4 commits intowled:mainfrom
JoaDick:squash/Usermod

Conversation

@JoaDick
Copy link

@JoaDick JoaDick commented Jan 25, 2026

This PR implements the feature requests from #5290 - and even a bit more.

  • Usermods can allocate GPIO pins, without having to modify existing WLED code (like the PinOwner enum in pin_manager.h).
  • They can interact with each other directly through function calls.
  • They can detect at runtime if an optionally wanted other usermod is compiled into WLED, and react accordingly if not.
  • No lookup of usermod names used internally, neither any kind of scripting magic; everything is implemented in plain C++ based on pointers.
  • Usermods don't have to override getId() and specify a custom ID in const.h.
    • Actually that is unrelated to this PR and was already possible before (I'm just mentioning this for completeness)
    • ... under the precondition that UsermodManager::getUMData() is not used
    • ... which might eventually become obsolete with this PR
    • ... and they do not interact with the UI through Usermod::addToJsonInfo() - which might just need minimal polishing (but thats out of scope of this PR)

In a nutshell, you can maintain your own usermods inside a private branch, only by adding the corresponding files. WLED will integrate them automatically without the need for any modifications inside existing WLED code. And these private usermods can even interact with each other (or with existing usermods) via simple function calls.

All this was implemented with absolute minimal changes of existing WLED code; everything is built on top of current WLED features. DHT and Temperature usermods have just been extended for demonstration.

For more details, please look at the readme in the PluginAPI directory and the comments inside the new sourcefiles there. Also have a look at the development notes, which address some questions that will surely come up during the review.
Examples for using these new features are implemented in the UM_PluginDemo and UM_DummySensor usermods.

Have fun and Enjoy!

Summary by CodeRabbit

  • New Features

    • Plugin framework with a Plugin Manager for registering sensors, managing plugin pins, and exposing plugin APIs
    • New DummySensor and PluginDemo usermods, including a thermometer LED effect that uses sensor data
  • Updates

    • Temperature and DHT usermods now register with the plugin system and report sensor validity/state
    • New TemperatureSensor and HumiditySensor interfaces and pin-management APIs; plugin info added to system UI
  • Documentation

    • Added PluginAPI docs, examples, and developer notes for plugin authors

✏️ Tip: You can customize this high-level summary in your review settings.

Plugins: MVP for effects to get data from unknown usermods

Plugins: implemented class PluginManager + plugin API PinUser and HumiditySensor

Plugins: plugins can be accessed directly from other plugins and effects

Plugins: optimized datastructures inside PluginManager

Plugins: documentation

Plugins: added PluginManager to UI info page

Plugins: added development notes

... and some final polishing
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 25, 2026

Walkthrough

Adds a Plugin API and PluginManager with sensor and pin interfaces, plugin macros and docs; integrates plugin info into WLED JSON; and updates/introduces usermods (DHT, Temperature) plus new example usermods (UM_DummySensor, UM_PluginDemo) to use the new APIs.

Changes

Cohort / File(s) Summary
Plugin Manager
wled00/PluginAPI/PluginManager.h, wled00/PluginAPI/PluginManager.cpp
New PluginManager implementation and header: register/unregister PinUsers, allocate/rollback pins, register/unregister temperature & humidity sensors, sensor lookup, plugin naming, UI JSON builders, global pluginManager.
Sensor Interfaces
wled00/PluginAPI/TemperatureSensor.h, wled00/PluginAPI/HumiditySensor.h
New TemperatureSensor and HumiditySensor interfaces: readiness flags, accessors (C/F conversion), protected pure-virtual do_get*() hooks, and internal validity state.
Pin API
wled00/PluginAPI/PinUser.h, wled00/pin_manager.h
New PinUser API types: PinType, PinConfig, PinConfigs alias and optional callback placeholder; added PluginMgr owner enum value in pin_manager.h.
Plugin Macros & Docs
wled00/PluginAPI/PluginMacros.h, wled00/PluginAPI/DevNotes.md, wled00/PluginAPI/README.md
Macros to declare/define plugin API pointers, plus design/dev documentation and README describing PluginAPI usage and architecture.
Custom DummySensor API
wled00/PluginAPI/custom/DummySensor/DummySensor.h, .../DummySensor/DummySensor.cpp, wled00/PluginAPI/custom/README.md
New DummySensor plugin API header, CPP glue (CPPFILE_PLUGIN_API), and README for custom plugin APIs.
Core integration
wled00/wled.h, wled00/um_manager.cpp
Include PluginManager in core header; call pluginManager.addToJsonInfo(...) during usermod JSON assembly to expose plugin info.
DHT usermod updates
usermods/DHT/DHT.cpp
DHT usermod now inherits TemperatureSensor and HumiditySensor, registers sensors in setup(), implements do_getTemperatureC()/do_getHumidity(), and updates internal validity flags on measurements/timeouts.
Temperature usermod updates
usermods/Temperature/UsermodTemperature.h, usermods/Temperature/Temperature.cpp
Temperature usermod now inherits TemperatureSensor, registers with pluginManager, implements do_getTemperatureC(), and sets _isTemperatureValid appropriately on success/failure.
DummySensor example usermod
usermods/UM_DummySensor/UM_DummySensor.cpp, usermods/UM_DummySensor/library.json, usermods/UM_DummySensor/README.md
New example UM_DummySensor usermod implementing DummySensor API plus TemperatureSensor/HumiditySensor, registers sensors, simulates temperature/humidity readings, and exposes plugin API (REGISTER_USERMOD, DEFINE_PLUGIN_API).
Plugin demo usermod
usermods/UM_PluginDemo/UM_PluginDemo.cpp, usermods/UM_PluginDemo/library.json, usermods/UM_PluginDemo/README.md
New UM_PluginDemo usermod with a thermometer FX, pin registration via PluginManager, optional integration with Temperature/Humidity and DummySensor APIs, and a relay PinConfig.
Plugin API utilities & examples
wled00/PluginAPI/...
Supporting macro utilities and example/demo files to wire plugin APIs and document usage.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • blazoncek
  • willmmiles
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.03% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Usermods on Steroids: A Plugin Framework' directly and accurately summarizes the main change: implementation of a plugin framework for WLED usermods.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Fix all issues with AI agents
In `@usermods/UM_DummySensor/UM_DummySensor.cpp`:
- Around line 20-28: The setup() function currently calls
pluginManager.registerTemperatureSensor(*this, _name) and then
enableTemperatureSensor(), which leads to double registration because
enableTemperatureSensor() will register again when _isTemperatureSensorEnabled
is false; remove the explicit call to
pluginManager.registerTemperatureSensor(*this, _name) from setup() and rely on
enableTemperatureSensor() to perform registration, keeping the existing
_isTemperatureValid/_isHumidityValid assignments unchanged (alternatively, if
you prefer the other approach, set _isTemperatureSensorEnabled = true before
calling enableTemperatureSensor() so it doesn't re-register).

In `@usermods/UM_PluginDemo/README.md`:
- Around line 1-3: Update the top-level heading text "Examples custom plugins."
to "Examples of custom plugins." in the README: replace the exact string
"Examples custom plugins" with "Examples of custom plugins" (ensure the period
and capitalization match the surrounding lines) so the heading reads correctly
and improves grammar.

In `@usermods/UM_PluginDemo/UM_PluginDemo.cpp`:
- Around line 140-148: The code reads TemperatureSensor::temperatureC() without
checking readiness, so guard the relay logic by first verifying the sensor is
ready via TemperatureSensor::isReady() (after
pluginManager.getTemperatureSensor() returns non-null); only call temperatureC()
and compute isHot when tempSensor->isReady() is true, otherwise skip or abort
relay control (e.g., return or leave the relay unchanged) to avoid toggling on
invalid startup/failure readings.

In `@wled00/PluginAPI/custom/DummySensor/DummySensor.h`:
- Around line 12-15: The comment in DummySensor.h contains a typo ("runtinme");
update the top-of-file API comment in DummySensor.h (the block describing the
plugin API for DummySensor) to replace "runtinme" with "runtime" so the sentence
reads "...They can determine at runtime if the usermod is included in the WLED
binary or not." Ensure only the spelling is corrected and no other text is
changed.

In `@wled00/PluginAPI/DevNotes.md`:
- Around line 11-42: Tighten wording and fix typos across DevNotes.md: run a
pass to correct misspellings and grammar (e.g. "accesible"→"accessible",
"happiliy"→"happily", "demontration"→"demonstration", "bloatyness"→"bloatiness"
or reword to "bloat", "Regardles"→"Regardless", "PluginManger"→"PluginManager",
change "I2C-driver" to "I2C driver"), normalize spacing/phrasing (e.g. "user
mod" vs "usermod") and simplify long sentences for clarity in paragraphs
discussing dependency issues, weak symbols, and the PluginManager info section;
ensure consistent term usage and run a spell/grammar check before committing.

In `@wled00/PluginAPI/PluginManager.cpp`:
- Around line 109-117: The code currently assigns synthetic pin numbers when
itr->isPinValid() is false (setting itr->pinNr to ++counter) which can collide
with real pins; instead, remove the synthetic assignment and make the function
fail fast by calling rollbackPinRegistration(user, pinCount, pinConfig) when
itr->isPinValid() returns false so no unallocated pins are registered; locate
the check around itr->isPinValid() and replace the auto-assign branch with a
rollback path that relies on the PinManager allocation flow rather than creating
phantom pins.
- Around line 139-153: The current loop in PluginManager.cpp deallocates all
pins from _pinUserConfigs before removing the target user, freeing other
plugins' pins; change the logic to only deallocate pins whose owner matches the
unregistering user: iterate _pinUserConfigs and for each entry check entry.first
== &user before calling PinManager::deallocatePin(entry.second.pinNr,
PinOwner::PluginMgr), then erase only those entries (the same predicate used for
_pinUserConfigs removal). Apply the same owner-check approach when removing from
_pinUsers (use pred1/pred2 style lambdas but ensure they test entry.first ==
&user) so only the target user's data and pins are removed.
- Around line 6-38: The map DataMap in InfoDataBuilder currently uses
std::map<const char*, String> which orders by pointer value, not by the
pointed-to plugin names; change DataMap to use a string-comparing comparator
(e.g., define a comparator struct that calls strcmp/strncmp on the char* keys)
so _dict orders entries by name, and update the typedef/using for DataMap
accordingly (ensure the comparator type is passed as the third template
parameter to std::map and include <cstring> if needed); keep the rest of
InfoDataBuilder (add, begin, end, _dict) unchanged.

In `@wled00/PluginAPI/README.md`:
- Around line 8-174: Correct spelling and grammar issues in the README text that
documents plugin design and usage: edit the descriptive paragraphs and headings
(references: "Upgrading Usermods to Plugins", "Design Description",
"Administrated Plugin APIs", "Custom Plugin APIs", "GPIO Pin Handling for
Plugins") and the example prose mentioning UM_PluginDemo, TemperatureSensor,
DHT, UM_DummySensor, DummySensor, PluginManager, PinUser, PinConfig, setup(),
PinManager and pin_manager.h to fix typos (e.g., "usermods aquire" -> "usermods
acquire", "is is defined" -> "is defined", remove awkward phrasing like "Please,
anyone??", ensure consistent capitalization and punctuation) while preserving
technical terms and code identifiers and applying the same fixes to the other
affected sections noted (around the other indicated ranges).
🧹 Nitpick comments (5)
wled00/PluginAPI/custom/DummySensor/DummySensor.h (1)

16-26: Virtual destructor is not necessary for this pattern.

DummySensor is accessed via a raw pointer (GET_PLUGIN_API) that points to a static instance managed by the plugin system, which lives for the entire program duration. The pointer is never owned by the caller and is never deleted. While adding virtual ~DummySensor() = default; is a reasonable defensive practice for interface classes in general, it is not required in this specific use case.

usermods/Temperature/Temperature.cpp (1)

144-144: Consider gating registration on sensor availability.

The sensor is registered with pluginManager.registerTemperatureSensor() unconditionally, even when sensorFound is 0 (no sensor detected). Other usermods like DHT and UM_DummySensor also register unconditionally, but for a real hardware sensor like DS18B20, registering a non-existent sensor may expose invalid readings to consumers.

Suggested conditional registration
   lastMeasurement = millis() - readingInterval + 10000;
   initDone = true;
-  pluginManager.registerTemperatureSensor(*this, _name);
+  if (sensorFound) {
+    pluginManager.registerTemperatureSensor(*this, _name);
+  }
usermods/DHT/DHT.cpp (1)

62-66: Consider initializing tempC.

The new tempC variable is not explicitly initialized. While temperature and humidity are initialized to 0, tempC relies on default initialization. For consistency and to avoid potential undefined behavior on first do_getTemperatureC() call before measurement, consider explicit initialization.

Suggested fix
-    float tempC, humidity, temperature = 0;
+    float tempC = 0, humidity = 0, temperature = 0;
usermods/UM_DummySensor/UM_DummySensor.cpp (1)

73-74: Typo in documentation comment.

The @copydoc reference should be HumiditySensor::do_getHumidity(), not do_getHumidityC().

Suggested fix
-  /// `@copydoc` HumiditySensor::do_getHumidityC()
+  /// `@copydoc` HumiditySensor::do_getHumidity()
usermods/UM_PluginDemo/UM_PluginDemo.cpp (1)

68-70: Clamp sensor-derived pixel positions to segment bounds.

At Line 68 and Line 84, computed indices can exceed [0, SEGLEN-1] if sensor values are out of range. Clamping avoids potential out-of-range writes.

♻️ Proposed safe clamping
-    int tempPos = temp * (SEGLEN - 1) / 40.0f; // 20° shall be in the middle
+    int tempPos = temp * (SEGLEN - 1) / 40.0f; // 20° shall be in the middle
+    if (tempPos < 0) tempPos = 0;
+    else if (tempPos > (int)SEGLEN - 1) tempPos = (int)SEGLEN - 1;
@@
-    const int humPos = hum * (SEGLEN - 1) / 100.0f;
-    SEGMENT.setPixelColor(humPos, 0xFF00FF);
+    int humPos = hum * (SEGLEN - 1) / 100.0f;
+    if (humPos < 0) humPos = 0;
+    else if (humPos > (int)SEGLEN - 1) humPos = (int)SEGLEN - 1;
+    SEGMENT.setPixelColor(humPos, 0xFF00FF);

Also applies to: 84-85

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@usermods/UM_PluginDemo/UM_PluginDemo.cpp`:
- Around line 62-86: The temp and humidity positions can fall outside [0,
SEGLEN-1], causing invalid indices or long loops; clamp tempPos and humPos after
computing them (from tempSensor->temperatureC() and humSensor->humidity()) to
the range 0..SEGLEN-1 and replace the risky while(tempPos >= 0) pattern with a
bounded decrement loop (e.g., for or a guarded while) that uses the clamped
tempPos, then call SEGMENT.setPixelColor safely; likewise clamp humPos before
calling SEGMENT.setPixelColor(humPos, ...). Use SEGLEN and the existing symbols
(tempSensor->temperatureC, humSensor->humidity, tempPos, humPos,
SEGMENT.setPixelColor, fast_color_scale, SEGCOLOR) to locate and update the
logic.

In `@wled00/PluginAPI/PluginManager.cpp`:
- Around line 99-105: In registerPinUser(PinUser &user, uint8_t pinCount,
PinConfig *pinConfig, const char *pluginName) guard against null or zero inputs
before performing pointer arithmetic: check that pinCount > 0 and pinConfig !=
nullptr (or treat pinCount == 0 as a no-op) and return/exit early (e.g., return
false) if the inputs are invalid; only then compute begin/end and iterate over
pinConfig. This prevents undefined behavior when dereferencing pinConfig or
doing begin + pinCount.
♻️ Duplicate comments (3)
wled00/PluginAPI/DevNotes.md (1)

11-33: Polish remaining typos/wording in DevNotes.
A few spelling/wording issues remain; please clean these for readability.

✏️ Suggested edits
- The class definition (yet not the implementation) of that other usermod would have to be inside its headerfile, which your usermod must include. <br>
+ The class definition (yet not the implementation) of that other usermod would have to be inside its header file, which your usermod must include. <br>
- ... and have an 'I2C-driver' as member variable.
+ ... and have an 'I2C driver' as member variable.
- You'll end up with a compiler error: your usermod includes the other's headerfile, which in turn includes the I2C driver's headerfile.
+ You'll end up with a compiler error: your usermod includes the other's header file, which in turn includes the I2C driver's header file.
- Whenever your sourcecode includes a headerfile from the directory of another usermod, that usermod's cpp files will be compiled automatically.
+ Whenever your source code includes a header file from the directory of another usermod, that usermod's cpp files will be compiled automatically.
- Nevertheless, this idea was with weak linking was brought up by `@softhack007` in the issue's thread
+ Nevertheless, this idea of weak linking was brought up by `@softhack007` in the issue's thread
wled00/PluginAPI/README.md (1)

4-8: Quick doc polish pass still needed.
A few wording/typo issues remain; please run a spell/grammar pass.

✏️ Suggested edits
- These interfaces here are very high-level by explicit design choice - and they are administrated
+ These interfaces here are very high-level by explicit design choice - and they are administered
- ... can draw whatever it feels to, based on the data from the sensor.
+ ... can draw whatever it feels like, based on the data from the sensor.
- Usermods can acquire their desired GPIO pins through the `PluginManager`. For an example, have a
+ Usermods can acquire their desired GPIO pins through the `PluginManager`. For example, take a
- Project Environment selection list of the statusbar.
+ Project Environment selection list of the status bar.

Also applies to: 47-49, 162-185

usermods/UM_DummySensor/UM_DummySensor.cpp (1)

20-28: Ensure enabled flags match initial registration.
setup() registers sensors but leaves _isTemperatureSensorEnabled/_isHumiditySensorEnabled false, so is*Enabled() reports disabled and disable*() won’t unregister until enable*() is called. Prefer enabling via the helpers (or set the flags true here).

🐛 Proposed fix (keep flags in sync)
   void setup() override
   {
-    // register sensor plugins
-    pluginManager.registerTemperatureSensor(*this, _name);
-    pluginManager.registerHumiditySensor(*this, _name);
+    // register sensor plugins
+    enableTemperatureSensor();
+    enableHumiditySensor();
     // this dummy can deliver readings instantly
     _isTemperatureValid = true;
     _isHumidityValid = true;
   }
🧹 Nitpick comments (1)
wled00/PluginAPI/PinUser.h (1)

42-64: Avoid a magic number for invalid pins.
Consider centralizing the sentinel value so it’s self-documenting and consistent across helpers.

♻️ Suggested tweak
 struct PinConfig
 {
+  static constexpr uint8_t kInvalidPin = 0xFF;
   PinConfig(PinType pinType_ = PinType::undefined, const char *pinName_ = nullptr)
       : pinType{pinType_}, pinName{pinName_} {}

   /// The designated pin number.
-  uint8_t pinNr = 0xFF;
+  uint8_t pinNr = kInvalidPin;
@@
-  bool isPinValid() const { return pinNr != 0xFF; }
-  void invalidatePin() { pinNr = 0xFF; }
+  bool isPinValid() const { return pinNr != kInvalidPin; }
+  void invalidatePin() { pinNr = kInvalidPin; }
 };

Based on learnings, consider replacing meaningful magic numbers with named constants.

@blazoncek
Copy link
Contributor

I'm not sure if "plugin API" is required as there is UsermodManager for such purposes. If you think something is lacking in UsermodManager it might be better to enhance it rather than supplement or replace it. The core issue with #5290 is the necessity for harcoded "owner IDs". The solution to that may simply be to let PinManager allocate new owner ID when new registration happens (i.e. ID == -1/255) and let usermod store that value for later purposes.

FYI getUMData() is an abomination I had to come up with when I ported WLED SR "soundreactive" effects and audio part to upstream WLED AC (into proper separation between usermod and effects). Since then I've made changes that allow usermods to add effect hosted in their code and since @willmmiles upgraded usermods to be libraries there is no need for that abomination.

BTW I suggested to move all audio effects (that need getUMData()) out into Audioreactive usermod and get rid of getUMData() but was unfortunately faced with strong opposition. So it looks like getUMData() (and its use) will remain.

@softhack007
Copy link
Member

softhack007 commented Jan 26, 2026

BTW I suggested to move all audio effects (that need getUMData()) out into Audioreactive usermod and get rid of getUMData() but was unfortunately faced with strong opposition. So it looks like getUMData() (and its use) will remain.

@blazoncek I think these topics are two but not one. getUMData() is a hack to export local variables from AR into the "effect" world. If this can be replaced with a different method, we should try it. The "strong opposition" (imho) was more about moving all AR effects (and AR palettes, etc) into the usermod.

We need some code in the core that reacts on #ifdef USERMOD_AUDIOREACTIVE - or a similar method. For example when the AR usermod is included, I2S#0 is not available for LED pin driving. Maybe this information can be stored in the pinManager in future.

@softhack007
Copy link
Member

softhack007 commented Jan 26, 2026

@JoaDick I like your proposal of a "Usermod OS" that provides a generic infrastructure for usermods, and exchange of data between usermods or between usermods and the WLED core.

Even when not everybody says "its perfect", I'm happy that you contributed your PR to support discussions about the future design of WLED.

@DedeHai
Copy link
Collaborator

DedeHai commented Jan 26, 2026

We need some code in the core that reacts on #ifdef USERMOD_AUDIOREACTIVE - or a similar method. For example when the AR usermod is included, I2S#0 is not available for LED pin driving. Maybe this information can be stored in the pinManager in future.

or even have a "resources manager" it is not only I2S but also SPI, ADC, DMA, RMT etc. that currently have a single purpose but might not in the future, for example I have written a driver that uses SPI in a "parralel I2S output" fashion with 4 outputs (C3) as well as the DMA-ADC driver for AR (C3, S2, S3) which may clash with "analog read" of some usermods etc.

@JoaDick I agree with @softhack007 , this is a push in the right direction exploring some concepts - I also agree with @blazoncek that this sounds like a task for the usermod-manager.
I did not check the code in great detail, something that kind of jumped out at me was that the manager is kind of entangled with the usermods where I would expect it to be fully modular. Think of this scenario: I am adding a magnetic field sensor UM and want to use it's data to drive "AR effects" instead of audio data. Would this be something that is possible? (you do not really have to consider it in detail, just as a example of what might be possible if truly modular)

@blazoncek
Copy link
Contributor

If this can be replaced with a different method, we should try it. The "strong opposition" (imho) was more about moving all AR effects (and AR palettes, etc) into the usermod.

Palettes are already part of usermod itself and are added upon its start.

And yes, move all audio effects into usermod. It has no implications on anything if usermod is included and reduces the overal code in both cases. Done that quite a while ago in my fork with no ill effect.

@blazoncek
Copy link
Contributor

We need some code in the core that reacts on #ifdef USERMOD_AUDIOREACTIVE

Why would USERMOD_AUDIOREACTIVE be defined if usermod is moved out of tree as is the purpose of library-like usermods?
This statement directly contradicts the purpose of such move.

You should not have any #ifdef USERMOD_... in WLED source.

@JoaDick
Copy link
Author

JoaDick commented Jan 26, 2026

Thank you guys, I really appreciate your feedback! It's great to see that you're interested in this PR and already started a discussion.
In particular @blazoncek already made my day for explaining the "getUMData() abomination" -- admittedly, I had to look up that word, but then couldn't stop laughing... brilliant description!

Regarding the suggestion from @blazoncek and @DedeHai that this new PluginManager stuff should be handled by the UsermodManager: Actually... Yes and No; both options have their own pros and cons which should be regarded. Indeed, I initially started that way, but rather soon stepped back and thought again. Let me just drop some of these thoughts, so you can decide on your own if that's rather pro or con:

  • "The UsermodManager" as en entity doesn't exist. It's not class, it is just a namespace with some free functions and a bunch of static variables inside the cpp file.
  • There are good reasons for that design choice; the way usermods are detected via linker magic requires global state, and isn't really compatible with class objects.
  • Extending that non-object-oriented design would easily be possible.
  • ... but it is prone to become a really big obstacle in case it shall be developed further into a full-fledged plugin system.
    • With that, I mean for example a port of WLED to Linux (RasPi), where usermods can be dropped in as a .so library. See the MM discussion about the future of WLED...
    • The easiest and straightforward way for implementing such a thing is done via interfaces (in terms of abstract base classes). Free functions for that will become a nightmare wrt. maintenance!
    • I already learned my lessons in a former job project...
  • Putting all that new stuff directly into the UsermodManager has a great risk of turning it into an(other) "God Object" (although it isn't a class - that anti-pattern still matches). The Segment-God is already hard enough to handle...
    • Also, that approach would violate the Single-Responsibility-Principle and the Open-Closed-Principle. And don't forget the Interface-Segregation-Principle...
    • Side note: In case that sounds like bullshit-bingo - search for the S.O.L.I.D. principles of object oriented design. These are the main guidelines for me when developing software designs - which already saved me from lots of trouble many times. Admittedly, these are challenging to grasp on the first time.

So I decided to make it this way:

  • The UsermodManager remains the rock-solid and mandatory foundation, only with the absolute unavoidable modifications.
  • The new PluginManager contributes the optional stuff on top of that, which the usermods may or may not use.
  • This also gave me the freedom for dedicated header files, where a lot of helpful documentation can be placed for potential future usermod-developers. Separated by domain, and not all-in-one to avoid cluttering.

From a technical perspective, it doesn't really matter. We can decide to keep the PluginManager as a new feature with a dedicated interface for the users. Or we can make it an internal member inside UsermodManager's cpp file, and just add forwarding functions. Or we can copy its code into UsermodManager.
That's just an implementation detail which shouldn't be the main discussion topic.

@JoaDick
Copy link
Author

JoaDick commented Jan 26, 2026

or even have a "resources manager" it is not only I2S but also SPI, ADC, DMA, RMT etc. that currently have a single purpose but might not in the future, for example I have written a driver that uses SPI in a "parralel I2S output" fashion with 4 outputs (C3) as well as the DMA-ADC driver for AR (C3, S2, S3) which may clash with "analog read" of some usermods etc.

@DedeHai Please have a look at the note inside PinUser.h about the possible optimization. This is exactly the same idea - so the pin requests would be forwarded from the PluginManager to that "resource manger".

@JoaDick
Copy link
Author

JoaDick commented Jan 26, 2026

... something that kind of jumped out at me was that the manager is kind of entangled with the usermods where I would expect it to be fully modular.

@DedeHai That is the core purpose of the PluginManager: to serve as one central interface from the (external) plugins into the WLED framework. That doesn't necessarily mean that it must implement all functionality by itself - it can of course delegate to other internal components. Those are doing the real work, but shall not be exposed to the usermods directly.
Just see the PluginManager like a facade or a firewall between WLED and the usermods.
Such a design choice is an essential enabler for potential drop-in usermods as I mentioned before.

Think of this scenario: I am adding a magnetic field sensor UM and want to use it's data to drive "AR effects" instead of audio data. Would this be something that is possible? (you do not really have to consider it in detail, just as a example of what might be possible if truly modular)

Let me respond with an example. Imagine such an interface:

class AudioReactiveData
{
public:
  /** Smoothed sample.
   * Range: 0.0 .. 255.0
   * @note Use this instead of: float volumeSmth = *(float *)um_data->u_data[0];
   */
  virtual float volumeSmth() = 0;

  /** Frequency (Hz) of largest FFT result.
   * @note Use this instead of: float FFT_MajorPeak = *(float *)um_data->u_data[4];
   */
  virtual float fftMajorPeak() = 0;
};

The AudioReactive usermod of course would implement this interface.
Then effects can "somehow ask WLED" for someone who provides that interface. Usually they would get AudioReactive - or NULL if it's not included in the build.

Now, would your magnetic field sensor be able to provide the data that is specified by that hypothetical interface? Could it provide something like a "volumeSmth"? If yes, then your sensor could hypothetically implement that interface.
And the effect that is asking WLED for audio data would get... your sensor!

@JoaDick
Copy link
Author

JoaDick commented Jan 26, 2026

Just out of curiosity: what are your intentions, @DedeHai ?

  • Do you want to "feed" existing audioreactive effects with our sensor data?
  • Or do you want to make new effects, which react on your sensor data - thereby "abusing" the audio-related interface just as transport medium?

If you plan the latter, please follow the example with the specific DummySensor-interface from DummySensor.h, and see how that is used in the demo usermods.

@willmmiles
Copy link
Member

I strongly agree with the API design concerns about having multiple apparent "component" interfaces. There should be one and exactly one place for defining new components. While it might seem a trivial implementation detail to an implementor, naming and code organization are of paramount importance for long term project maintenance, and make a huge difference to novice users.

I haven't had time to thoroughly digest the code, but from a quick review standpoint:

  • Usermod data flow is definitely a current pain point; thank you for taking the time to consider it.
  • For "sensor" usermods, we intend to work towards a general framework for routing these reports in a structured way that allows central handling of (at least) the three common tasks:
    • Reporting via MQTT
    • Reading by "control" usermods
    • Routing to effect controls or inputs (OK, this doesn't exist yet, but it's on the agenda)
      The key contention is that there are consumer use cases that want to be able to access any available sensor data without requiring them to know in advance about the "meaning" of the value -- merely the format. I hope to be able to route temperature, or humidity, or gyroscope measurements, or subspace ether fluctuations in to the effect inputs -- if it can be represented by a number, it should be usable without needing to know a unique type at compile time.
  • As a design rule, runtime registration systems should be avoided in favour of build-time approaches. This is principally because, in an embedded system such as this, runtime registration wastes precious system RAM to store information that was available statically at build time and could instead be placed in ROM. It may not seem like much, but every allocation can be the one responsible for heap fragmentation breaking someone's setup.
  • Re pin management: we do not want a two-layer system. It is our intent to make usermods first-class citizens of PinManager instead. The next step in that process is to adopt the usermod name standards from the MoonModules fork.

If you're interested in pursuing this general idea further, I'd suggest trying to construct a "SensorValue" base class that can be instantiated/used by usermods:

  • Templated on a data type, eg. float, int, array
  • Accepts a sensor name, for runtime introspection
  • Is added to compile-time static lists by data type
  • Can also be subclassed and added to a second compile-time static list for type safe semantics for specialized sensor types
  • Provides accessor function templates for those compile-time lists

I've got a generalization of the usermod static list code in another branch - I'll link it here when I get a moment later.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@wled00/PluginAPI/PluginManager.cpp`:
- Around line 305-320: The dead `#if` (0) alternative for TemperatureSensor (and
the similar humidity block) references val before it's declared, causing compile
errors if enabled; either remove these dead blocks or fix them by declaring a
String val (and ensure notAvailable exists) before it's used (as done in the
active implementation around user.createNestedArray and
sensor.temperature()/sensor.humidity()), and mirror that same fix for the
humidity block so both alternative implementations compile cleanly if toggled
on.

@willmmiles
Copy link
Member

Dynamic compile-time array generalization is here: https://github.com/willmmiles/WLED/blob/dynamic-array-linker-section/wled00/dynarray.h

(I originally wrote that to extend compile-time table support to ESP-IDF v5+ and other platforms; sorry I haven't got around to PR'ing it yet.)

That particular implementation still depends on some single source file "instantiating" the array. While thinking about it today, it occurred to me that it's possible to redesign it so that array accessors use locally defined symbols, as they take no space; so that no translation unit need explicitly instantiate anything. I'll follow up with that later.

A more concrete example of why I'm postulating a general sensor framework, to further the discussion: consider the Scrolling Text effect. I think it would be cool let it be able to embed sensor data, eg. "$TEMP.2f" or "$HUM.d". To do that, we'd need some kind of "name lookup for sensor data" API. Given such, it would also cover the use case of "I'm a fan control module and I want a temperature measurement".

@DedeHai
Copy link
Collaborator

DedeHai commented Jan 27, 2026

  • Do you want to "feed" existing audioreactive effects with our sensor data?
  • Or do you want to make new effects, which react on your sensor data

exactly this:

The key contention is that there are consumer use cases that want to be able to access any available sensor data without requiring them to know in advance about the "meaning" of the value -- merely the format. I hope to be able to route temperature, or humidity, or gyroscope measurements, or subspace ether fluctuations in to the effect inputs -- if it can be represented by a number, it should be usable without needing to know a unique type at compile time.

@netmindz
Copy link
Member

BTW I suggested to move all audio effects (that need getUMData()) out into Audioreactive usermod and get rid of getUMData() but was unfortunately faced with strong opposition. So it looks like getUMData() (and its use) will remain.

@blazoncek I think these topics are two but not one. getUMData() is a hack to export local variables from AR into the "effect" world. If this can be replaced with a different method, we should try it. The "strong opposition" (imho) was more about moving all AR effects (and AR palettes, etc) into the usermod.

Correct. UMData is a poor design, but that principle of allowing data to be shared between usermods and the core or between usermods is a fair requirement. Forcing Audio Reactive effects to only live inside the AR usermod is an unnecessary restriction that goes against our overall design of a modular system and doesn't provide a solution for when one usermod needs to exchange dara with another

We need some code in the core that reacts on #ifdef USERMOD_AUDIOREACTIVE - or a similar method. For example when the AR usermod is included, I2S#0 is not available for LED pin driving. Maybe this information can be stored in the pinManager in future.

Yeah now we don't have that define, we do need to replace pinmanager with a more general resource manager that can handle tracking I2S and also HardwareSerial (needed for dmx)

@netmindz
Copy link
Member

We need some code in the core that reacts on #ifdef USERMOD_AUDIOREACTIVE

Why would USERMOD_AUDIOREACTIVE be defined if usermod is moved out of tree as is the purpose of library-like usermods? This statement directly contradicts the purpose of such move.

You should not have any #ifdef USERMOD_... in WLED source.

He's meaning we need a replacement for that, if you actually read what he's talking about. It's just a reference to how historically we handled knowing if it was included

@JoaDick
Copy link
Author

JoaDick commented Jan 27, 2026

Please let me first clarify some topics that came up already, so that this discussion doesn't get too much confused:

Topics about the design itself and possible optimizations:
I'd suggest that we first clarify what can be done with this PR, and what can not (yet) be done. Let's postpone ideas about potential optimizations until we all have the same understanding about the features.

Be aware of the difference between usermod interfaces that are administrated by WLED and custom usermod interfaces, as they are handled completely different.

  • The latter ones can easily be described: usermods can define whatever kind of interface they like. WLED core components aren't involved at all; everything is done imlicitly by the linker (supported by some simple macros). These custom interfaces can be accessed like any global variable; just include the corresponding headerfile.
  • Let's focus on the administrated interfaces in the following; these can very likely provide the features you asked for so far.

Potential usecases of your desired "GenericSensor" interface:

  1. None or exactly one usermods that offer such an interface are compiled into WLED.
    --> Let's call this the "simple usecase". It is fully supported by this PR, without any needed lookup.
  2. More than one such usermods are compiled into WLED.
    --> I'd call this the "advanced usecase". It is also fully supported by this PR, however some kind of selection of a specific sensor instance will be required. That selection is essentially supported, but the rest is out of scope of this PR. Possibilites are: lookup by name, select a "default sensor" via UI, hard-code a sensor's name inside the effect, ...
  3. A Sensor can provide an array of readings (instead of just one single value).
    --> Let's discuss this "expert usecase" after the other topics have been clarified.

Dynamic compile-time array generalization is here: https://github.com/willmmiles/WLED/blob/dynamic-array-linker-section/wled00/dynarray.h

@willmmiles This looks really interesting, i'll definitely have a closer look at that later.

consider the Scrolling Text effect. I think it would be cool let it be able to embed sensor data, eg. "$TEMP.2f" or "$HUM.d". To do that, we'd need some kind of "name lookup for sensor data" API.

Exactly that usecase is implemented in the examples. The distinction is done via C++ interface type; no string/name "$TEMP.2f" or "$HUM.d" lookup required. Look at the "Thermometer-effect". Imaginary replace the "drawing a bar" with "insert into scrolling text".

Given such, it would also cover the use case of "I'm a fan control module and I want a temperature measurement".

Exactly that usecase is also implemented in the examples. Look at the "fan-control" method.

@JoaDick
Copy link
Author

JoaDick commented Jan 27, 2026

The key contention is that there are consumer use cases that want to be able to access any available sensor data without requiring them to know in advance about the "meaning" of the value -- merely the format. I hope to be able to route temperature, or humidity, or gyroscope measurements, or subspace ether fluctuations in to the effect inputs -- if it can be represented by a number, it should be usable without needing to know a unique type at compile time.

@DedeHai Should be possible; just give me some time to extend the examples. There are some more aspects to consider; it is not as simple as "just a number"...

@willmmiles
Copy link
Member

willmmiles commented Jan 27, 2026

consider the Scrolling Text effect. I think it would be cool let it be able to embed sensor data, eg. "$TEMP.2f" or "$HUM.d". To do that, we'd need some kind of "name lookup for sensor data" API.

Exactly that usecase is implemented in the examples. The distinction is done via C++ interface type; no string/name "$TEMP.2f" or "$HUM.d" lookup required. Look at the "Thermometer-effect". Imaginary replace the "drawing a bar" with "insert into scrolling text".

Your example does not implement what I want at all: it requires the "reader" to know at compile time about all possible sensor types it may access. The intent is that the FX or other reader does not know at compile time about what sensors types are possible - it should be able to draw data from any sensor with a compatible numeric type, with no changes to its source code, even ones that don't exist at the time it was written (eg. subspace ether fluctuations). The implication is that "temperature" and "humidity" must be a single, common C++ type to such code; and they must have a "name" field so that readers can find them.

At this point, one might think: OK, I can create a common base class for all of these sensors, so generic readers can access any of them in a common way. Say something like:

template<typename data_t> class SensorData {
 public:
   const char* getName() const;
   const data_t& getValue() const;   // explicit specializations for POD types can return just data_t
};

template<typename data_t> std::span<const SensorData<data_t>> getSensorsByName(const char* name);
template<typename data_t> std::span<const SensorData<data_t>> getAllSensors();
// etc.

(apologies for modern C++ usage that isn't yet supported by our ESP32 toolchain)

However: given such an interface, the point I and my colleagues are trying to make is that we don't expect to need any other API for cross-usermod data access. That API is sufficient for the overwhelming majority of control-type usermods; and, as you've noted, other more specialized use cases are well handled by #includeing the partner usermod's header and performing direct object access. From an architectural level, creating additional lists to distinguish "Temperature" and "Humidity" by a C++ type isn't worth the extra cost, given the existence of a "by name" lookup system.

For a convincing demo, try implementing a global system to replace Usermod::publishMQTT() that publishes the sensor measurements from any and all sensors as an API consumer.

@JoaDick
Copy link
Author

JoaDick commented Jan 28, 2026

@willmmiles , @DedeHai Well... what you want is something completely different than this PR was aimed at.
My main driver was to support potential future FX and usermod developers, by providing simple and well documented interfaces for realizing their ideas. Obviously I am also one of them, and also had a really hard time getting started. It took multiple attempts until I eventually decided to give WLED really a try. That was not because programming my ideas would've been difficult - it was because the API that WLED is presenting to such hobby developers is just a PITA. Sorry for such harsh words, but that's my honest opinion since my first contact with the sources back in 2020. The macro-based SEGMENT API is still a blast from the past, having the same look and feel as the origin from kitesurfer1404 back then.
While digging through the code, I saw possibilities for improvements many times - so that implementing an FX could become so much easier and more powerful. Hence this question from me lately, which obviously didn't really attract much attention. Feel free to share your thoughts there as well; I'd highly appreciate your feedback!

But I'm going off topic; enough rambling. Although this PR most likely won't make it, I am still curious about that generic sensor handling idea, and would like to know more about that.
In a former job project, we developed something comparable: It was about an executable for test automation in C++, and a GUI for that in Python. The communication between them was via json files, where the executable specified input fields of all kind (int, float, etc. plus arrays and structs thereof). The GUI then rendered those generically specified elements, and returned the entered values from the user back to the executable in a similar way. None of the involved data types were known at compile time; just the primitives were used as building blocks.

So, if you're seriously interested in contributions on that topic, please share a bit more details about your usecases with those generic sensors, and also your expectations or ideas about its design. Or is it already in such a mature state that just the coding is missing?
I can't promise anything about a PoC or concrete implementation (time is sparse, especially during upcoming summertime) - but at least i could share my my experiences from that project if you're interested.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@wled00/PluginAPI/PluginManager.cpp`:
- Around line 99-185: Several registration methods store pluginName unchecked
(e.g., registerPinUser, registerTemperatureSensor, registerHumiditySensor) which
can lead to null JSON/map keys; guard pluginName against nullptr and normalize
it to a safe literal like "unknownName" before storing. Update each emplace_back
call to use a local const char* normalizedName = (pluginName != nullptr) ?
pluginName : "unknownName" (or equivalent) and pass normalizedName to
_pinUsers.emplace_back, _temperatureSensors.emplace_back and
_humiditySensors.emplace_back so no nullptr is stored.
- Around line 111-118: The demo auto-assignment uses a static uint8_t counter in
the PluginManager loop (see itr->isPinValid() and itr->pinNr) which will wrap
and create pin collisions; change the counter to a wider type (e.g.,
uint16_t/uint32_t) and add a guard/cutoff so it never wraps into the valid GPIO
range—if the counter exceeds a defined max or would wrap, call the existing
rollbackPinRegistration(user, pinCount, pinConfig) (or return an error) instead
of assigning an invalid pin; ensure the fix references the same static counter
and the rollback path in PluginManager.cpp.

@willmmiles
Copy link
Member

Hence this question from me lately, which obviously didn't really attract much attention. Feel free to share your thoughts there as well; I'd highly appreciate your feedback!

My apologies, I'm a very far behind on catching up with PRs and questions; I've been picking away at a few (oldest and newest) as time permits.

I am still curious about that generic sensor handling idea, and would like to know more about that.

The ultimate goal is to provide a system to route sensor data in to FX parameters. An example would be using a temperature measurement as the "intensity" value for the Percentage effect, to create a fancy thermometer display. At the limit, any value that is represented by a number -- effect parameters, colors, brightness -- could in theory be routed to accept an expression based on real-time sensor value(s). (I say expression because to be practical such an engine would need to be able to perform some math: the thermometer example might need to be configured as something like clamp(temperature_c, -30, 40) to specify the target range, or the like.) Another example I used earlier would be printing out a sensor value in Scrolling Text.

There are a lot of open questions about the right shape of such an expression system -- expression languages are cumbersome, so what would the UI want to look like? How would someone configure this? Right now that's all still up in the air.

What we do know is that, at a minimum, we'd need some approach for tying a measurement name to a value readable in a standard format. In practice, there are already two such implementations in the current code, though sadly both use ASCII text as that standard: add_json_info() and the MQTT client. Both are already used by pretty much every sensor usermod (it seems everyone wants their sensor data routed to HomeAssistant, who would've thought? ;)

So what we had in mind was adding some common "sensor" registry API that would simplify sensor usermods by (a) wrapping up the common tasks of publishing measurements to info and MQTT, and (b) introducing an internal API to get sensor data that can be used by both other usermods to retrieve sensor data in a source-agnostic way, as well as a future dynamic FX system.

(And of course "Scrolling Text" is low hanging fruit for such a system, as it's already doing text substitution; given an ability to look up sensor data by name, it's an easy feature to add.)

I haven't personally written any code in this direction yet; it's pretty far down my priority list (I'm mostly busy these days with system stability problems). Contributions in this direction would be welcome. There's a fair number of details to pin down how to wrap up MQTT data formatting and topic assignment in a common way while still preserving an "a number is a number" API for future effect integration.

@JoaDick
Copy link
Author

JoaDick commented Jan 31, 2026

The next step in that process is to adopt the usermod name standards from the MoonModules fork.

@willmmiles Are these name standards already documented somewhere? Or can you point me to some examples?
When trying out some ideas about these generic sensors, I'd make the code accordingly from the get-go.

@willmmiles
Copy link
Member

The next step in that process is to adopt the usermod name standards from the MoonModules fork.

@willmmiles Are these name standards already documented somewhere? Or can you point me to some examples? When trying out some ideas about these generic sensors, I'd make the code accordingly from the get-go.

Specifically I mean that _name is a required member variable in usermods in MM -- so it's "standard" in that all usermods have a common place to define a name, and code referencing usermods is guaranteed to have somewhere to look.

MM uses a null pointer as the default for compatibility if a usermod doesn't supply it; ideally what I think we want here is a separate [deprecated] constructor for API compatibility, to encourage usermod authors to update, that uses some default name ("Usermod" or the like).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants